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