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 21 import android.content.Context; 22 import android.graphics.Canvas; 23 import android.graphics.Path; 24 import android.graphics.PointF; 25 import android.util.AttributeSet; 26 import android.view.MotionEvent; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.FrameLayout; 30 31 import androidx.annotation.Nullable; 32 33 import com.android.systemui.Dumpable; 34 import com.android.systemui.qs.customize.QSCustomizer; 35 import com.android.systemui.res.R; 36 import com.android.systemui.scene.shared.flag.SceneContainerFlag; 37 import com.android.systemui.shade.LargeScreenHeaderHelper; 38 import com.android.systemui.shade.TouchLogger; 39 import com.android.systemui.util.LargeScreenUtils; 40 41 import java.io.PrintWriter; 42 43 /** 44 * Wrapper view with background which contains {@link QSPanel} and {@link QuickStatusBarHeader} 45 */ 46 public class QSContainerImpl extends FrameLayout implements Dumpable { 47 48 private int mFancyClippingLeftInset; 49 private int mFancyClippingTop; 50 private int mFancyClippingRightInset; 51 private int mFancyClippingBottom; 52 private final float[] mFancyClippingRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0}; 53 private final Path mFancyClippingPath = new Path(); 54 private int mHeightOverride = -1; 55 private QuickStatusBarHeader mHeader; 56 private float mQsExpansion; 57 private QSCustomizer mQSCustomizer; 58 private QSPanel mQSPanel; 59 private NonInterceptingScrollView mQSPanelContainer; 60 61 private int mHorizontalMargins; 62 private int mTilesPageMargin; 63 private boolean mQsDisabled; 64 private int mContentHorizontalPadding = -1; 65 private boolean mClippingEnabled; 66 private boolean mIsFullWidth; 67 68 private boolean mSceneContainerEnabled; 69 QSContainerImpl(Context context, AttributeSet attrs)70 public QSContainerImpl(Context context, AttributeSet attrs) { 71 super(context, attrs); 72 } 73 74 @Override onFinishInflate()75 protected void onFinishInflate() { 76 super.onFinishInflate(); 77 mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view); 78 mQSPanel = findViewById(R.id.quick_settings_panel); 79 mHeader = findViewById(R.id.header); 80 mQSCustomizer = findViewById(R.id.qs_customize); 81 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 82 } 83 setSceneContainerEnabled(boolean enabled)84 void setSceneContainerEnabled(boolean enabled) { 85 mSceneContainerEnabled = enabled; 86 if (enabled) { 87 mQSPanelContainer.removeAllViews(); 88 removeView(mQSPanelContainer); 89 LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 90 ViewGroup.LayoutParams.WRAP_CONTENT); 91 addView(mQSPanel, 0, lp); 92 } 93 } 94 95 @Override hasOverlappingRendering()96 public boolean hasOverlappingRendering() { 97 return false; 98 } 99 100 @Override performClick()101 public boolean performClick() { 102 // Want to receive clicks so missing QQS tiles doesn't cause collapse, but 103 // don't want to do anything with them. 104 return true; 105 } 106 107 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)108 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 109 // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the 110 // bottom and footer are inside the screen. 111 int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec); 112 113 if (!mSceneContainerEnabled) { 114 MarginLayoutParams layoutParams = 115 (MarginLayoutParams) mQSPanelContainer.getLayoutParams(); 116 int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin 117 - getPaddingBottom(); 118 int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin 119 + layoutParams.rightMargin; 120 final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding, 121 layoutParams.width); 122 mQSPanelContainer.measure(qsPanelWidthSpec, 123 MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST)); 124 int width = mQSPanelContainer.getMeasuredWidth() + padding; 125 super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 126 MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY)); 127 } else { 128 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 129 } 130 131 // QSCustomizer will always be the height of the screen, but do this after 132 // other measuring to avoid changing the height of the QS. 133 mQSCustomizer.measure(widthMeasureSpec, 134 MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY)); 135 } 136 137 @Override dispatchDraw(Canvas canvas)138 public void dispatchDraw(Canvas canvas) { 139 if (!mFancyClippingPath.isEmpty()) { 140 canvas.translate(0, -getTranslationY()); 141 canvas.clipOutPath(mFancyClippingPath); 142 canvas.translate(0, getTranslationY()); 143 } 144 super.dispatchDraw(canvas); 145 } 146 147 @Override measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)148 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 149 int parentHeightMeasureSpec, int heightUsed) { 150 if (!mSceneContainerEnabled) { 151 // Do not measure QSPanel again when doing super.onMeasure. 152 // This prevents the pages in PagedTileLayout to be remeasured with a different 153 // (incorrect) size to the one used for determining the number of rows and then the 154 // number of pages. 155 if (child != mQSPanelContainer) { 156 super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, 157 parentHeightMeasureSpec, heightUsed); 158 } 159 } else { 160 // Don't measure the customizer with all the children, it will be measured separately 161 if (child != mQSCustomizer) { 162 super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, 163 parentHeightMeasureSpec, heightUsed); 164 } 165 } 166 } 167 168 @Override dispatchTouchEvent(MotionEvent ev)169 public boolean dispatchTouchEvent(MotionEvent ev) { 170 return TouchLogger.logDispatchTouch("QS", ev, super.dispatchTouchEvent(ev)); 171 } 172 173 @Override onLayout(boolean changed, int left, int top, int right, int bottom)174 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 175 super.onLayout(changed, left, top, right, bottom); 176 updateExpansion(); 177 updateClippingPath(); 178 } 179 180 @Nullable getQSPanelContainer()181 public NonInterceptingScrollView getQSPanelContainer() { 182 return mQSPanelContainer; 183 } 184 disable(int state1, int state2, boolean animate)185 public void disable(int state1, int state2, boolean animate) { 186 final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0; 187 if (disabled == mQsDisabled) return; 188 mQsDisabled = disabled; 189 } 190 updateResources(QSPanelController qsPanelController, QuickStatusBarHeaderController quickStatusBarHeaderController)191 void updateResources(QSPanelController qsPanelController, 192 QuickStatusBarHeaderController quickStatusBarHeaderController) { 193 int topPadding = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext); 194 if (!LargeScreenUtils.shouldUseLargeScreenShadeHeader(mContext.getResources())) { 195 topPadding = LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext); 196 } 197 if (mQSPanelContainer != null) { 198 mQSPanelContainer.setPaddingRelative( 199 mQSPanelContainer.getPaddingStart(), 200 mSceneContainerEnabled ? 0 : topPadding, 201 mQSPanelContainer.getPaddingEnd(), 202 mQSPanelContainer.getPaddingBottom()); 203 } else { 204 mQSPanel.setPaddingRelative( 205 mQSPanel.getPaddingStart(), 206 mSceneContainerEnabled ? 0 : topPadding, 207 mQSPanel.getPaddingEnd(), 208 mQSPanel.getPaddingBottom()); 209 } 210 211 int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin); 212 int horizontalPadding = getResources().getDimensionPixelSize( 213 R.dimen.qs_content_horizontal_padding); 214 int tilesPageMargin = getResources().getDimensionPixelSize( 215 R.dimen.qs_tiles_page_horizontal_margin); 216 boolean marginsChanged = horizontalPadding != mContentHorizontalPadding 217 || horizontalMargins != mHorizontalMargins 218 || tilesPageMargin != mTilesPageMargin; 219 mContentHorizontalPadding = horizontalPadding; 220 mHorizontalMargins = horizontalMargins; 221 mTilesPageMargin = tilesPageMargin; 222 if (marginsChanged) { 223 updatePaddingsAndMargins(qsPanelController, quickStatusBarHeaderController); 224 } 225 } 226 227 /** 228 * Overrides the height of this view (post-layout), so that the content is clipped to that 229 * height and the background is set to that height. 230 * 231 * @param heightOverride the overridden height 232 */ setHeightOverride(int heightOverride)233 public void setHeightOverride(int heightOverride) { 234 mHeightOverride = heightOverride; 235 updateExpansion(); 236 } 237 updateExpansion()238 public void updateExpansion() { 239 int height = calculateContainerHeight(); 240 setBottom(getTop() + height); 241 } 242 calculateContainerHeight()243 protected int calculateContainerHeight() { 244 int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight(); 245 // Need to add the dragHandle height so touches will be intercepted by it. 246 return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight() 247 : Math.round(mQsExpansion * (heightOverride - mHeader.getHeight())) 248 + mHeader.getHeight(); 249 } 250 251 // These next two methods are used with Scene container to determine the size of QQS and QS . 252 253 /** 254 * Returns the size of the QQS container, regardless of the measured size of this view. 255 * @return size in pixels of QQS 256 */ getQqsHeight()257 public int getQqsHeight() { 258 SceneContainerFlag.unsafeAssertInNewMode(); 259 return mHeader.getMeasuredHeight(); 260 } 261 262 /** 263 * @return height with the squishiness fraction applied. 264 */ getSquishedQqsHeight()265 int getSquishedQqsHeight() { 266 return mHeader.getSquishedHeight(); 267 } 268 269 /** 270 * Returns the size of QS (or the QSCustomizer), regardless of the measured size of this view 271 * @return size in pixels of QS (or QSCustomizer) 272 */ getQsHeight()273 public int getQsHeight() { 274 return mQSCustomizer.isCustomizing() ? mQSCustomizer.getMeasuredHeight() 275 : mQSPanel.getMeasuredHeight(); 276 } 277 278 /** 279 * @return height with the squishiness fraction applied. 280 */ getSquishedQsHeight()281 int getSquishedQsHeight() { 282 return mQSPanel.getSquishedHeight(); 283 } 284 setExpansion(float expansion)285 public void setExpansion(float expansion) { 286 mQsExpansion = expansion; 287 if (mQSPanelContainer != null) { 288 mQSPanelContainer.setScrollingEnabled(expansion > 0f); 289 } 290 updateExpansion(); 291 } 292 updatePaddingsAndMargins(QSPanelController qsPanelController, QuickStatusBarHeaderController quickStatusBarHeaderController)293 private void updatePaddingsAndMargins(QSPanelController qsPanelController, 294 QuickStatusBarHeaderController quickStatusBarHeaderController) { 295 for (int i = 0; i < getChildCount(); i++) { 296 View view = getChildAt(i); 297 if (view == mQSCustomizer) { 298 // Some views are always full width or have dependent padding 299 continue; 300 } 301 if (view.getId() != R.id.qs_footer_actions) { 302 // Only padding for FooterActionsView, no margin. That way, the background goes 303 // all the way to the edge. 304 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 305 lp.rightMargin = mHorizontalMargins; 306 lp.leftMargin = mHorizontalMargins; 307 } 308 if (view == mQSPanelContainer || view == mQSPanel) { 309 // QS panel lays out some of its content full width 310 qsPanelController.setContentMargins(mContentHorizontalPadding, 311 mContentHorizontalPadding); 312 setPageMargins(qsPanelController); 313 } else if (view == mHeader) { 314 quickStatusBarHeaderController.setContentMargins(mContentHorizontalPadding, 315 mContentHorizontalPadding); 316 } else { 317 // Set the horizontal paddings unless the view is the Compose implementation of the 318 // footer actions. 319 if (view.getId() != R.id.qs_footer_actions) { 320 view.setPaddingRelative( 321 mContentHorizontalPadding, 322 view.getPaddingTop(), 323 mContentHorizontalPadding, 324 view.getPaddingBottom()); 325 } 326 } 327 } 328 } 329 setPageMargins(QSPanelController qsPanelController)330 private void setPageMargins(QSPanelController qsPanelController) { 331 if (SceneContainerFlag.isEnabled()) { 332 if (mHorizontalMargins == mTilesPageMargin * 2 + 1) { 333 qsPanelController.setPageMargin(mTilesPageMargin, mTilesPageMargin + 1); 334 } else { 335 qsPanelController.setPageMargin(mTilesPageMargin, mTilesPageMargin); 336 } 337 } else { 338 qsPanelController.setPageMargin(mTilesPageMargin, mTilesPageMargin); 339 } 340 } 341 342 /** 343 * Clip QS bottom using a concave shape. 344 */ setFancyClipping(int leftInset, int top, int rightInset, int bottom, int radius, boolean enabled, boolean fullWidth)345 public void setFancyClipping(int leftInset, int top, int rightInset, int bottom, int radius, 346 boolean enabled, boolean fullWidth) { 347 boolean updatePath = false; 348 if (mFancyClippingRadii[0] != radius) { 349 mFancyClippingRadii[0] = radius; 350 mFancyClippingRadii[1] = radius; 351 mFancyClippingRadii[2] = radius; 352 mFancyClippingRadii[3] = radius; 353 updatePath = true; 354 } 355 if (mFancyClippingLeftInset != leftInset) { 356 mFancyClippingLeftInset = leftInset; 357 updatePath = true; 358 } 359 if (mFancyClippingTop != top) { 360 mFancyClippingTop = top; 361 updatePath = true; 362 } 363 if (mFancyClippingRightInset != rightInset) { 364 mFancyClippingRightInset = rightInset; 365 updatePath = true; 366 } 367 if (mFancyClippingBottom != bottom) { 368 mFancyClippingBottom = bottom; 369 updatePath = true; 370 } 371 if (mClippingEnabled != enabled) { 372 mClippingEnabled = enabled; 373 updatePath = true; 374 } 375 if (mIsFullWidth != fullWidth) { 376 mIsFullWidth = fullWidth; 377 updatePath = true; 378 } 379 380 if (updatePath) { 381 updateClippingPath(); 382 } 383 } 384 385 @Override isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint)386 protected boolean isTransformedTouchPointInView(float x, float y, 387 View child, PointF outLocalPoint) { 388 // Prevent touches outside the clipped area from propagating to a child in that area. 389 if (mClippingEnabled && y + getTranslationY() > mFancyClippingTop) { 390 return false; 391 } 392 return super.isTransformedTouchPointInView(x, y, child, outLocalPoint); 393 } 394 updateClippingPath()395 private void updateClippingPath() { 396 mFancyClippingPath.reset(); 397 if (!mClippingEnabled) { 398 invalidate(); 399 return; 400 } 401 402 int clippingLeft = mIsFullWidth ? -mFancyClippingLeftInset : 0; 403 int clippingRight = mIsFullWidth ? getWidth() + mFancyClippingRightInset : getWidth(); 404 mFancyClippingPath.addRoundRect(clippingLeft, mFancyClippingTop, clippingRight, 405 mFancyClippingBottom, mFancyClippingRadii, Path.Direction.CW); 406 invalidate(); 407 } 408 409 @Override dump(PrintWriter pw, String[] args)410 public void dump(PrintWriter pw, String[] args) { 411 pw.println(getClass().getSimpleName() + " updateClippingPath: " 412 + "leftInset(" + mFancyClippingLeftInset + ") " 413 + "top(" + mFancyClippingTop + ") " 414 + "rightInset(" + mFancyClippingRightInset + ") " 415 + "bottom(" + mFancyClippingBottom + ") " 416 + "mClippingEnabled(" + mClippingEnabled + ") " 417 + "mIsFullWidth(" + mIsFullWidth + ")"); 418 } 419 } 420