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