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 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
21 
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.graphics.Point;
25 import android.util.AttributeSet;
26 import android.view.View;
27 import android.widget.FrameLayout;
28 
29 import androidx.dynamicanimation.animation.FloatPropertyCompat;
30 import androidx.dynamicanimation.animation.SpringForce;
31 
32 import com.android.systemui.R;
33 import com.android.systemui.qs.customize.QSCustomizer;
34 import com.android.systemui.util.animation.PhysicsAnimator;
35 
36 /**
37  * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
38  */
39 public class QSContainerImpl extends FrameLayout {
40 
41     private final Point mSizePoint = new Point();
42     private static final FloatPropertyCompat<QSContainerImpl> BACKGROUND_BOTTOM =
43             new FloatPropertyCompat<QSContainerImpl>("backgroundBottom") {
44                 @Override
45                 public float getValue(QSContainerImpl qsImpl) {
46                     return qsImpl.getBackgroundBottom();
47                 }
48 
49                 @Override
50                 public void setValue(QSContainerImpl background, float value) {
51                     background.setBackgroundBottom((int) value);
52                 }
53             };
54     private static final PhysicsAnimator.SpringConfig BACKGROUND_SPRING
55             = new PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM,
56             SpringForce.DAMPING_RATIO_LOW_BOUNCY);
57     private int mBackgroundBottom = -1;
58     private int mHeightOverride = -1;
59     private QSPanel mQSPanel;
60     private View mQSDetail;
61     private QuickStatusBarHeader mHeader;
62     private float mQsExpansion;
63     private QSCustomizer mQSCustomizer;
64     private View mDragHandle;
65     private View mQSPanelContainer;
66 
67     private View mBackground;
68     private View mBackgroundGradient;
69     private View mStatusBarBackground;
70 
71     private int mSideMargins;
72     private boolean mQsDisabled;
73     private int mContentPaddingStart = -1;
74     private int mContentPaddingEnd = -1;
75     private boolean mAnimateBottomOnNextLayout;
76 
QSContainerImpl(Context context, AttributeSet attrs)77     public QSContainerImpl(Context context, AttributeSet attrs) {
78         super(context, attrs);
79     }
80 
81     @Override
onFinishInflate()82     protected void onFinishInflate() {
83         super.onFinishInflate();
84         mQSPanel = findViewById(R.id.quick_settings_panel);
85         mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view);
86         mQSDetail = findViewById(R.id.qs_detail);
87         mHeader = findViewById(R.id.header);
88         mQSCustomizer = findViewById(R.id.qs_customize);
89         mDragHandle = findViewById(R.id.qs_drag_handle_view);
90         mBackground = findViewById(R.id.quick_settings_background);
91         mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
92         mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
93         updateResources();
94         mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> {
95             if (mHeader.getHeaderQsPanel().isShown()) {
96                 mAnimateBottomOnNextLayout = true;
97             }
98         });
99         mQSPanel.setMediaVisibilityChangedListener((visible) -> {
100             if (mQSPanel.isShown()) {
101                 mAnimateBottomOnNextLayout = true;
102             }
103         });
104 
105 
106         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
107     }
108 
setBackgroundBottom(int value)109     private void setBackgroundBottom(int value) {
110         // We're saving the bottom separately since otherwise the bottom would be overridden in
111         // the layout and the animation wouldn't properly start at the old position.
112         mBackgroundBottom = value;
113         mBackground.setBottom(value);
114     }
115 
getBackgroundBottom()116     private float getBackgroundBottom() {
117         if (mBackgroundBottom == -1) {
118             return mBackground.getBottom();
119         }
120         return mBackgroundBottom;
121     }
122 
123     @Override
onConfigurationChanged(Configuration newConfig)124     protected void onConfigurationChanged(Configuration newConfig) {
125         super.onConfigurationChanged(newConfig);
126         setBackgroundGradientVisibility(newConfig);
127         updateResources();
128         mSizePoint.set(0, 0); // Will be retrieved on next measure pass.
129     }
130 
131     @Override
performClick()132     public boolean performClick() {
133         // Want to receive clicks so missing QQS tiles doesn't cause collapse, but
134         // don't want to do anything with them.
135         return true;
136     }
137 
138     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)139     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
140         // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
141         // bottom and footer are inside the screen.
142         Configuration config = getResources().getConfiguration();
143         boolean navBelow = config.smallestScreenWidthDp >= 600
144                 || config.orientation != Configuration.ORIENTATION_LANDSCAPE;
145         MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
146 
147         // The footer is pinned to the bottom of QSPanel (same bottoms), therefore we don't need to
148         // subtract its height. We do not care if the collapsed notifications fit in the screen.
149         int maxQs = getDisplayHeight() - layoutParams.topMargin - layoutParams.bottomMargin
150                 - getPaddingBottom();
151         if (navBelow) {
152             maxQs -= getResources().getDimensionPixelSize(R.dimen.navigation_bar_height);
153         }
154 
155         int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
156                 + layoutParams.rightMargin;
157         final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
158                 layoutParams.width);
159         mQSPanelContainer.measure(qsPanelWidthSpec,
160                 MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
161         int width = mQSPanelContainer.getMeasuredWidth() + padding;
162         int height = layoutParams.topMargin + layoutParams.bottomMargin
163                 + mQSPanelContainer.getMeasuredHeight() + getPaddingBottom();
164         super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
165                 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
166         // QSCustomizer will always be the height of the screen, but do this after
167         // other measuring to avoid changing the height of the QS.
168         mQSCustomizer.measure(widthMeasureSpec,
169                 MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
170     }
171 
172 
173     @Override
measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)174     protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
175             int parentHeightMeasureSpec, int heightUsed) {
176         // Do not measure QSPanel again when doing super.onMeasure.
177         // This prevents the pages in PagedTileLayout to be remeasured with a different (incorrect)
178         // size to the one used for determining the number of rows and then the number of pages.
179         if (child != mQSPanelContainer) {
180             super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
181                     parentHeightMeasureSpec, heightUsed);
182         }
183     }
184 
185     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)186     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
187         super.onLayout(changed, left, top, right, bottom);
188         updateExpansion(mAnimateBottomOnNextLayout /* animate */);
189         mAnimateBottomOnNextLayout = false;
190     }
191 
disable(int state1, int state2, boolean animate)192     public void disable(int state1, int state2, boolean animate) {
193         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
194         if (disabled == mQsDisabled) return;
195         mQsDisabled = disabled;
196         setBackgroundGradientVisibility(getResources().getConfiguration());
197         mBackground.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
198     }
199 
updateResources()200     private void updateResources() {
201         LayoutParams layoutParams = (LayoutParams) mQSPanelContainer.getLayoutParams();
202         layoutParams.topMargin = mContext.getResources().getDimensionPixelSize(
203                 com.android.internal.R.dimen.quick_qs_offset_height);
204         mQSPanelContainer.setLayoutParams(layoutParams);
205 
206         mSideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings);
207         mContentPaddingStart = getResources().getDimensionPixelSize(
208                 com.android.internal.R.dimen.notification_content_margin_start);
209         int newPaddingEnd = getResources().getDimensionPixelSize(
210                 com.android.internal.R.dimen.notification_content_margin_end);
211         boolean marginsChanged = newPaddingEnd != mContentPaddingEnd;
212         mContentPaddingEnd = newPaddingEnd;
213         if (marginsChanged) {
214             updatePaddingsAndMargins();
215         }
216     }
217 
218     /**
219      * Overrides the height of this view (post-layout), so that the content is clipped to that
220      * height and the background is set to that height.
221      *
222      * @param heightOverride the overridden height
223      */
setHeightOverride(int heightOverride)224     public void setHeightOverride(int heightOverride) {
225         mHeightOverride = heightOverride;
226         updateExpansion();
227     }
228 
updateExpansion()229     public void updateExpansion() {
230         updateExpansion(false /* animate */);
231     }
232 
updateExpansion(boolean animate)233     public void updateExpansion(boolean animate) {
234         int height = calculateContainerHeight();
235         setBottom(getTop() + height);
236         mQSDetail.setBottom(getTop() + height);
237         // Pin the drag handle to the bottom of the panel.
238         mDragHandle.setTranslationY(height - mDragHandle.getHeight());
239         mBackground.setTop(mQSPanelContainer.getTop());
240         updateBackgroundBottom(height, animate);
241     }
242 
updateBackgroundBottom(int height, boolean animated)243     private void updateBackgroundBottom(int height, boolean animated) {
244         PhysicsAnimator<QSContainerImpl> physicsAnimator = PhysicsAnimator.getInstance(this);
245         if (physicsAnimator.isPropertyAnimating(BACKGROUND_BOTTOM) || animated) {
246             // An animation is running or we want to animate
247             // Let's make sure to set the currentValue again, since the call below might only
248             // start in the next frame and otherwise we'd flicker
249             BACKGROUND_BOTTOM.setValue(this, BACKGROUND_BOTTOM.getValue(this));
250             physicsAnimator.spring(BACKGROUND_BOTTOM, height, BACKGROUND_SPRING).start();
251         } else {
252             BACKGROUND_BOTTOM.setValue(this, height);
253         }
254 
255     }
256 
calculateContainerHeight()257     protected int calculateContainerHeight() {
258         int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight();
259         return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight()
260                 : Math.round(mQsExpansion * (heightOverride - mHeader.getHeight()))
261                 + mHeader.getHeight();
262     }
263 
setBackgroundGradientVisibility(Configuration newConfig)264     private void setBackgroundGradientVisibility(Configuration newConfig) {
265         if (newConfig.orientation == ORIENTATION_LANDSCAPE) {
266             mBackgroundGradient.setVisibility(View.INVISIBLE);
267             mStatusBarBackground.setVisibility(View.INVISIBLE);
268         } else {
269             mBackgroundGradient.setVisibility(mQsDisabled ? View.INVISIBLE : View.VISIBLE);
270             mStatusBarBackground.setVisibility(View.VISIBLE);
271         }
272     }
273 
setExpansion(float expansion)274     public void setExpansion(float expansion) {
275         mQsExpansion = expansion;
276         mDragHandle.setAlpha(1.0f - expansion);
277         updateExpansion();
278     }
279 
updatePaddingsAndMargins()280     private void updatePaddingsAndMargins() {
281         for (int i = 0; i < getChildCount(); i++) {
282             View view = getChildAt(i);
283             if (view == mStatusBarBackground || view == mBackgroundGradient
284                     || view == mQSCustomizer) {
285                 // Some views are always full width
286                 continue;
287             }
288             LayoutParams lp = (LayoutParams) view.getLayoutParams();
289             lp.rightMargin = mSideMargins;
290             lp.leftMargin = mSideMargins;
291             if (view == mQSPanelContainer) {
292                 // QS panel lays out some of its content full width
293                 mQSPanel.setContentMargins(mContentPaddingStart, mContentPaddingEnd);
294             } else if (view == mHeader) {
295                 // The header contains the QQS panel which needs to have special padding, to
296                 // visually align them.
297                 mHeader.setContentMargins(mContentPaddingStart, mContentPaddingEnd);
298             } else {
299                 view.setPaddingRelative(
300                         mContentPaddingStart,
301                         view.getPaddingTop(),
302                         mContentPaddingEnd,
303                         view.getPaddingBottom());
304             }
305         }
306     }
307 
getDisplayHeight()308     private int getDisplayHeight() {
309         if (mSizePoint.y == 0) {
310             getDisplay().getRealSize(mSizePoint);
311         }
312         return mSizePoint.y;
313     }
314 }
315