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