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
17package com.android.systemui.qs;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.content.Context;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.util.AttributeSet;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewTreeObserver;
28import android.widget.FrameLayout;
29import com.android.systemui.Interpolators;
30import com.android.systemui.R;
31import com.android.systemui.qs.customize.QSCustomizer;
32import com.android.systemui.statusbar.phone.BaseStatusBarHeader;
33import com.android.systemui.statusbar.phone.NotificationPanelView;
34import com.android.systemui.statusbar.phone.QSTileHost;
35import com.android.systemui.statusbar.stack.StackStateAnimator;
36
37/**
38 * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
39 *
40 * Also manages animations for the QS Header and Panel.
41 */
42public class QSContainer extends FrameLayout {
43    private static final String TAG = "QSContainer";
44    private static final boolean DEBUG = false;
45
46    private final Point mSizePoint = new Point();
47    private final Rect mQsBounds = new Rect();
48
49    private int mHeightOverride = -1;
50    protected QSPanel mQSPanel;
51    private QSDetail mQSDetail;
52    protected BaseStatusBarHeader mHeader;
53    protected float mQsExpansion;
54    private boolean mQsExpanded;
55    private boolean mHeaderAnimating;
56    private boolean mKeyguardShowing;
57    private boolean mStackScrollerOverscrolling;
58
59    private long mDelay;
60    private QSAnimator mQSAnimator;
61    private QSCustomizer mQSCustomizer;
62    private NotificationPanelView mPanelView;
63    private boolean mListening;
64
65    public QSContainer(Context context, AttributeSet attrs) {
66        super(context, attrs);
67    }
68
69    @Override
70    protected void onFinishInflate() {
71        super.onFinishInflate();
72        mQSPanel = (QSPanel) findViewById(R.id.quick_settings_panel);
73        mQSDetail = (QSDetail) findViewById(R.id.qs_detail);
74        mHeader = (BaseStatusBarHeader) findViewById(R.id.header);
75        mQSDetail.setQsPanel(mQSPanel, mHeader);
76        mQSAnimator = new QSAnimator(this, (QuickQSPanel) mHeader.findViewById(R.id.quick_qs_panel),
77                mQSPanel);
78        mQSCustomizer = (QSCustomizer) findViewById(R.id.qs_customize);
79        mQSCustomizer.setQsContainer(this);
80    }
81
82    @Override
83    public void onRtlPropertiesChanged(int layoutDirection) {
84        super.onRtlPropertiesChanged(layoutDirection);
85        mQSAnimator.onRtlChanged();
86    }
87
88    public void setHost(QSTileHost qsh) {
89        mQSPanel.setHost(qsh, mQSCustomizer);
90        mHeader.setQSPanel(mQSPanel);
91        mQSDetail.setHost(qsh);
92        mQSAnimator.setHost(qsh);
93    }
94
95    public void setPanelView(NotificationPanelView panelView) {
96        mPanelView = panelView;
97    }
98
99    @Override
100    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
101        // Since we control our own bottom, be whatever size we want.
102        // Otherwise the QSPanel ends up with 0 height when the window is only the
103        // size of the status bar.
104        mQSPanel.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(
105                MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED));
106        int width = mQSPanel.getMeasuredWidth();
107        int height = ((LayoutParams) mQSPanel.getLayoutParams()).topMargin
108                + mQSPanel.getMeasuredHeight();
109        super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
110                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
111
112        // QSCustomizer is always be the height of the screen, but do this after
113        // other measuring to avoid changing the height of the QSContainer.
114        getDisplay().getRealSize(mSizePoint);
115        mQSCustomizer.measure(widthMeasureSpec,
116                MeasureSpec.makeMeasureSpec(mSizePoint.y, MeasureSpec.EXACTLY));
117    }
118
119    @Override
120    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
121        super.onLayout(changed, left, top, right, bottom);
122        updateBottom();
123    }
124
125    public boolean isCustomizing() {
126        return mQSCustomizer.isCustomizing();
127    }
128
129    /**
130     * Overrides the height of this view (post-layout), so that the content is clipped to that
131     * height and the background is set to that height.
132     *
133     * @param heightOverride the overridden height
134     */
135    public void setHeightOverride(int heightOverride) {
136        mHeightOverride = heightOverride;
137        updateBottom();
138    }
139
140    /**
141     * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that
142     * during closing the detail panel, this already returns the smaller height.
143     */
144    public int getDesiredHeight() {
145        if (isCustomizing()) {
146            return getHeight();
147        }
148        if (mQSDetail.isClosingDetail()) {
149            return mQSPanel.getGridHeight() + mHeader.getCollapsedHeight() + getPaddingBottom();
150        } else {
151            return getMeasuredHeight();
152        }
153    }
154
155    public void notifyCustomizeChanged() {
156        // The customize state changed, so our height changed.
157        updateBottom();
158        mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
159        mHeader.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE);
160        // Let the panel know the position changed and it needs to update where notifications
161        // and whatnot are.
162        mPanelView.onQsHeightChanged();
163    }
164
165    private void updateBottom() {
166        int height = calculateContainerHeight();
167        setBottom(getTop() + height);
168        mQSDetail.setBottom(getTop() + height);
169    }
170
171    protected int calculateContainerHeight() {
172        int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight();
173        return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight()
174                : (int) (mQsExpansion * (heightOverride - mHeader.getCollapsedHeight()))
175                + mHeader.getCollapsedHeight();
176    }
177
178    private void updateQsState() {
179        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating;
180        mQSPanel.setExpanded(mQsExpanded);
181        mQSDetail.setExpanded(mQsExpanded);
182        mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating)
183                ? View.VISIBLE
184                : View.INVISIBLE);
185        mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating)
186                || (mQsExpanded && !mStackScrollerOverscrolling));
187        mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
188    }
189
190    public BaseStatusBarHeader getHeader() {
191        return mHeader;
192    }
193
194    public QSPanel getQsPanel() {
195        return mQSPanel;
196    }
197
198    public QSCustomizer getCustomizer() {
199        return mQSCustomizer;
200    }
201
202    public boolean isShowingDetail() {
203        return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail();
204    }
205
206    public void setHeaderClickable(boolean clickable) {
207        if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable);
208        mHeader.setClickable(clickable);
209    }
210
211    public void setExpanded(boolean expanded) {
212        if (DEBUG) Log.d(TAG, "setExpanded " + expanded);
213        mQsExpanded = expanded;
214        mQSPanel.setListening(mListening && mQsExpanded);
215        updateQsState();
216    }
217
218    public void setKeyguardShowing(boolean keyguardShowing) {
219        if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing);
220        mKeyguardShowing = keyguardShowing;
221        mQSAnimator.setOnKeyguard(keyguardShowing);
222        updateQsState();
223    }
224
225    public void setOverscrolling(boolean stackScrollerOverscrolling) {
226        if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling);
227        mStackScrollerOverscrolling = stackScrollerOverscrolling;
228        updateQsState();
229    }
230
231    public void setListening(boolean listening) {
232        if (DEBUG) Log.d(TAG, "setListening " + listening);
233        mListening = listening;
234        mHeader.setListening(listening);
235        mQSPanel.setListening(mListening && mQsExpanded);
236    }
237
238    public void setQsExpansion(float expansion, float headerTranslation) {
239        if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation);
240        mQsExpansion = expansion;
241        final float translationScaleY = expansion - 1;
242        if (!mHeaderAnimating) {
243            setTranslationY(mKeyguardShowing ? (translationScaleY * mHeader.getHeight())
244                    : headerTranslation);
245        }
246        mHeader.setExpansion(mKeyguardShowing ? 1 : expansion);
247        mQSPanel.setTranslationY(translationScaleY * mQSPanel.getHeight());
248        mQSDetail.setFullyExpanded(expansion == 1);
249        mQSAnimator.setPosition(expansion);
250        updateBottom();
251
252        // Set bounds on the QS panel so it doesn't run over the header.
253        mQsBounds.top = (int) (mQSPanel.getHeight() * (1 - expansion));
254        mQsBounds.right = mQSPanel.getWidth();
255        mQsBounds.bottom = mQSPanel.getHeight();
256        mQSPanel.setClipBounds(mQsBounds);
257    }
258
259    public void animateHeaderSlidingIn(long delay) {
260        if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn");
261        // If the QS is already expanded we don't need to slide in the header as it's already
262        // visible.
263        if (!mQsExpanded) {
264            mHeaderAnimating = true;
265            mDelay = delay;
266            getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
267        }
268    }
269
270    public void animateHeaderSlidingOut() {
271        if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut");
272        mHeaderAnimating = true;
273        animate().y(-mHeader.getHeight())
274                .setStartDelay(0)
275                .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD)
276                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
277                .setListener(new AnimatorListenerAdapter() {
278                    @Override
279                    public void onAnimationEnd(Animator animation) {
280                        animate().setListener(null);
281                        mHeaderAnimating = false;
282                        updateQsState();
283                    }
284                })
285                .start();
286    }
287
288    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
289            = new ViewTreeObserver.OnPreDrawListener() {
290        @Override
291        public boolean onPreDraw() {
292            getViewTreeObserver().removeOnPreDrawListener(this);
293            animate()
294                    .translationY(0f)
295                    .setStartDelay(mDelay)
296                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
297                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
298                    .setListener(mAnimateHeaderSlidingInListener)
299                    .start();
300            setY(-mHeader.getHeight());
301            return true;
302        }
303    };
304
305    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
306            = new AnimatorListenerAdapter() {
307        @Override
308        public void onAnimationEnd(Animator animation) {
309            mHeaderAnimating = false;
310            updateQsState();
311        }
312    };
313
314    public int getQsMinExpansionHeight() {
315        return mHeader.getHeight();
316    }
317}
318