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