1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.qs; 18 19 import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; 20 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 21 22 import android.content.Context; 23 import android.content.res.Configuration; 24 import android.graphics.Point; 25 import android.util.AttributeSet; 26 import android.view.View; 27 import android.widget.FrameLayout; 28 29 import androidx.dynamicanimation.animation.FloatPropertyCompat; 30 import androidx.dynamicanimation.animation.SpringForce; 31 32 import com.android.systemui.R; 33 import com.android.systemui.qs.customize.QSCustomizer; 34 import com.android.systemui.util.animation.PhysicsAnimator; 35 36 /** 37 * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader} 38 */ 39 public class QSContainerImpl extends FrameLayout { 40 41 private final Point mSizePoint = new Point(); 42 private static final FloatPropertyCompat<QSContainerImpl> BACKGROUND_BOTTOM = 43 new FloatPropertyCompat<QSContainerImpl>("backgroundBottom") { 44 @Override 45 public float getValue(QSContainerImpl qsImpl) { 46 return qsImpl.getBackgroundBottom(); 47 } 48 49 @Override 50 public void setValue(QSContainerImpl background, float value) { 51 background.setBackgroundBottom((int) value); 52 } 53 }; 54 private static final PhysicsAnimator.SpringConfig BACKGROUND_SPRING 55 = new PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, 56 SpringForce.DAMPING_RATIO_LOW_BOUNCY); 57 private int mBackgroundBottom = -1; 58 private int mHeightOverride = -1; 59 private QSPanel mQSPanel; 60 private View mQSDetail; 61 private QuickStatusBarHeader mHeader; 62 private float mQsExpansion; 63 private QSCustomizer mQSCustomizer; 64 private View mDragHandle; 65 private View mQSPanelContainer; 66 67 private View mBackground; 68 private View mBackgroundGradient; 69 private View mStatusBarBackground; 70 71 private int mSideMargins; 72 private boolean mQsDisabled; 73 private int mContentPaddingStart = -1; 74 private int mContentPaddingEnd = -1; 75 private boolean mAnimateBottomOnNextLayout; 76 QSContainerImpl(Context context, AttributeSet attrs)77 public QSContainerImpl(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 } 80 81 @Override onFinishInflate()82 protected void onFinishInflate() { 83 super.onFinishInflate(); 84 mQSPanel = findViewById(R.id.quick_settings_panel); 85 mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view); 86 mQSDetail = findViewById(R.id.qs_detail); 87 mHeader = findViewById(R.id.header); 88 mQSCustomizer = findViewById(R.id.qs_customize); 89 mDragHandle = findViewById(R.id.qs_drag_handle_view); 90 mBackground = findViewById(R.id.quick_settings_background); 91 mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background); 92 mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view); 93 updateResources(); 94 mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> { 95 if (mHeader.getHeaderQsPanel().isShown()) { 96 mAnimateBottomOnNextLayout = true; 97 } 98 }); 99 mQSPanel.setMediaVisibilityChangedListener((visible) -> { 100 if (mQSPanel.isShown()) { 101 mAnimateBottomOnNextLayout = true; 102 } 103 }); 104 105 106 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 107 } 108 setBackgroundBottom(int value)109 private void setBackgroundBottom(int value) { 110 // We're saving the bottom separately since otherwise the bottom would be overridden in 111 // the layout and the animation wouldn't properly start at the old position. 112 mBackgroundBottom = value; 113 mBackground.setBottom(value); 114 } 115 getBackgroundBottom()116 private float getBackgroundBottom() { 117 if (mBackgroundBottom == -1) { 118 return mBackground.getBottom(); 119 } 120 return mBackgroundBottom; 121 } 122 123 @Override onConfigurationChanged(Configuration newConfig)124 protected void onConfigurationChanged(Configuration newConfig) { 125 super.onConfigurationChanged(newConfig); 126 setBackgroundGradientVisibility(newConfig); 127 updateResources(); 128 mSizePoint.set(0, 0); // Will be retrieved on next measure pass. 129 } 130 131 @Override performClick()132 public boolean performClick() { 133 // Want to receive clicks so missing QQS tiles doesn't cause collapse, but 134 // don't want to do anything with them. 135 return true; 136 } 137 138 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)139 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 140 // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the 141 // bottom and footer are inside the screen. 142 Configuration config = getResources().getConfiguration(); 143 boolean navBelow = config.smallestScreenWidthDp >= 600 144 || config.orientation != Configuration.ORIENTATION_LANDSCAPE; 145 MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams(); 146 147 // The footer is pinned to the bottom of QSPanel (same bottoms), therefore we don't need to 148 // subtract its height. We do not care if the collapsed notifications fit in the screen. 149 int maxQs = getDisplayHeight() - layoutParams.topMargin - layoutParams.bottomMargin 150 - getPaddingBottom(); 151 if (navBelow) { 152 maxQs -= getResources().getDimensionPixelSize(R.dimen.navigation_bar_height); 153 } 154 155 int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin 156 + layoutParams.rightMargin; 157 final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding, 158 layoutParams.width); 159 mQSPanelContainer.measure(qsPanelWidthSpec, 160 MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST)); 161 int width = mQSPanelContainer.getMeasuredWidth() + padding; 162 int height = layoutParams.topMargin + layoutParams.bottomMargin 163 + mQSPanelContainer.getMeasuredHeight() + getPaddingBottom(); 164 super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 165 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); 166 // QSCustomizer will always be the height of the screen, but do this after 167 // other measuring to avoid changing the height of the QS. 168 mQSCustomizer.measure(widthMeasureSpec, 169 MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY)); 170 } 171 172 173 @Override measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)174 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 175 int parentHeightMeasureSpec, int heightUsed) { 176 // Do not measure QSPanel again when doing super.onMeasure. 177 // This prevents the pages in PagedTileLayout to be remeasured with a different (incorrect) 178 // size to the one used for determining the number of rows and then the number of pages. 179 if (child != mQSPanelContainer) { 180 super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, 181 parentHeightMeasureSpec, heightUsed); 182 } 183 } 184 185 @Override onLayout(boolean changed, int left, int top, int right, int bottom)186 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 187 super.onLayout(changed, left, top, right, bottom); 188 updateExpansion(mAnimateBottomOnNextLayout /* animate */); 189 mAnimateBottomOnNextLayout = false; 190 } 191 disable(int state1, int state2, boolean animate)192 public void disable(int state1, int state2, boolean animate) { 193 final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; 194 if (disabled == mQsDisabled) return; 195 mQsDisabled = disabled; 196 setBackgroundGradientVisibility(getResources().getConfiguration()); 197 mBackground.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE); 198 } 199 updateResources()200 private void updateResources() { 201 LayoutParams layoutParams = (LayoutParams) mQSPanelContainer.getLayoutParams(); 202 layoutParams.topMargin = mContext.getResources().getDimensionPixelSize( 203 com.android.internal.R.dimen.quick_qs_offset_height); 204 mQSPanelContainer.setLayoutParams(layoutParams); 205 206 mSideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings); 207 mContentPaddingStart = getResources().getDimensionPixelSize( 208 com.android.internal.R.dimen.notification_content_margin_start); 209 int newPaddingEnd = getResources().getDimensionPixelSize( 210 com.android.internal.R.dimen.notification_content_margin_end); 211 boolean marginsChanged = newPaddingEnd != mContentPaddingEnd; 212 mContentPaddingEnd = newPaddingEnd; 213 if (marginsChanged) { 214 updatePaddingsAndMargins(); 215 } 216 } 217 218 /** 219 * Overrides the height of this view (post-layout), so that the content is clipped to that 220 * height and the background is set to that height. 221 * 222 * @param heightOverride the overridden height 223 */ setHeightOverride(int heightOverride)224 public void setHeightOverride(int heightOverride) { 225 mHeightOverride = heightOverride; 226 updateExpansion(); 227 } 228 updateExpansion()229 public void updateExpansion() { 230 updateExpansion(false /* animate */); 231 } 232 updateExpansion(boolean animate)233 public void updateExpansion(boolean animate) { 234 int height = calculateContainerHeight(); 235 setBottom(getTop() + height); 236 mQSDetail.setBottom(getTop() + height); 237 // Pin the drag handle to the bottom of the panel. 238 mDragHandle.setTranslationY(height - mDragHandle.getHeight()); 239 mBackground.setTop(mQSPanelContainer.getTop()); 240 updateBackgroundBottom(height, animate); 241 } 242 updateBackgroundBottom(int height, boolean animated)243 private void updateBackgroundBottom(int height, boolean animated) { 244 PhysicsAnimator<QSContainerImpl> physicsAnimator = PhysicsAnimator.getInstance(this); 245 if (physicsAnimator.isPropertyAnimating(BACKGROUND_BOTTOM) || animated) { 246 // An animation is running or we want to animate 247 // Let's make sure to set the currentValue again, since the call below might only 248 // start in the next frame and otherwise we'd flicker 249 BACKGROUND_BOTTOM.setValue(this, BACKGROUND_BOTTOM.getValue(this)); 250 physicsAnimator.spring(BACKGROUND_BOTTOM, height, BACKGROUND_SPRING).start(); 251 } else { 252 BACKGROUND_BOTTOM.setValue(this, height); 253 } 254 255 } 256 calculateContainerHeight()257 protected int calculateContainerHeight() { 258 int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight(); 259 return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight() 260 : Math.round(mQsExpansion * (heightOverride - mHeader.getHeight())) 261 + mHeader.getHeight(); 262 } 263 setBackgroundGradientVisibility(Configuration newConfig)264 private void setBackgroundGradientVisibility(Configuration newConfig) { 265 if (newConfig.orientation == ORIENTATION_LANDSCAPE) { 266 mBackgroundGradient.setVisibility(View.INVISIBLE); 267 mStatusBarBackground.setVisibility(View.INVISIBLE); 268 } else { 269 mBackgroundGradient.setVisibility(mQsDisabled ? View.INVISIBLE : View.VISIBLE); 270 mStatusBarBackground.setVisibility(View.VISIBLE); 271 } 272 } 273 setExpansion(float expansion)274 public void setExpansion(float expansion) { 275 mQsExpansion = expansion; 276 mDragHandle.setAlpha(1.0f - expansion); 277 updateExpansion(); 278 } 279 updatePaddingsAndMargins()280 private void updatePaddingsAndMargins() { 281 for (int i = 0; i < getChildCount(); i++) { 282 View view = getChildAt(i); 283 if (view == mStatusBarBackground || view == mBackgroundGradient 284 || view == mQSCustomizer) { 285 // Some views are always full width 286 continue; 287 } 288 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 289 lp.rightMargin = mSideMargins; 290 lp.leftMargin = mSideMargins; 291 if (view == mQSPanelContainer) { 292 // QS panel lays out some of its content full width 293 mQSPanel.setContentMargins(mContentPaddingStart, mContentPaddingEnd); 294 } else if (view == mHeader) { 295 // The header contains the QQS panel which needs to have special padding, to 296 // visually align them. 297 mHeader.setContentMargins(mContentPaddingStart, mContentPaddingEnd); 298 } else { 299 view.setPaddingRelative( 300 mContentPaddingStart, 301 view.getPaddingTop(), 302 mContentPaddingEnd, 303 view.getPaddingBottom()); 304 } 305 } 306 } 307 getDisplayHeight()308 private int getDisplayHeight() { 309 if (mSizePoint.y == 0) { 310 getDisplay().getRealSize(mSizePoint); 311 } 312 return mSizePoint.y; 313 } 314 } 315