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