/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.qs; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.statusbar.phone.BaseStatusBarHeader; import com.android.systemui.statusbar.phone.NotificationPanelView; import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.stack.StackStateAnimator; /** * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader} * * Also manages animations for the QS Header and Panel. */ public class QSContainer extends FrameLayout { private static final String TAG = "QSContainer"; private static final boolean DEBUG = false; private final Point mSizePoint = new Point(); private final Rect mQsBounds = new Rect(); private int mHeightOverride = -1; protected QSPanel mQSPanel; private QSDetail mQSDetail; protected BaseStatusBarHeader mHeader; protected float mQsExpansion; private boolean mQsExpanded; private boolean mHeaderAnimating; private boolean mKeyguardShowing; private boolean mStackScrollerOverscrolling; private long mDelay; private QSAnimator mQSAnimator; private QSCustomizer mQSCustomizer; private NotificationPanelView mPanelView; private boolean mListening; public QSContainer(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); mQSPanel = (QSPanel) findViewById(R.id.quick_settings_panel); mQSDetail = (QSDetail) findViewById(R.id.qs_detail); mHeader = (BaseStatusBarHeader) findViewById(R.id.header); mQSDetail.setQsPanel(mQSPanel, mHeader); mQSAnimator = new QSAnimator(this, (QuickQSPanel) mHeader.findViewById(R.id.quick_qs_panel), mQSPanel); mQSCustomizer = (QSCustomizer) findViewById(R.id.qs_customize); mQSCustomizer.setQsContainer(this); } @Override public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); mQSAnimator.onRtlChanged(); } public void setHost(QSTileHost qsh) { mQSPanel.setHost(qsh, mQSCustomizer); mHeader.setQSPanel(mQSPanel); mQSDetail.setHost(qsh); mQSAnimator.setHost(qsh); } public void setPanelView(NotificationPanelView panelView) { mPanelView = panelView; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Since we control our own bottom, be whatever size we want. // Otherwise the QSPanel ends up with 0 height when the window is only the // size of the status bar. mQSPanel.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec( MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED)); int width = mQSPanel.getMeasuredWidth(); int height = ((LayoutParams) mQSPanel.getLayoutParams()).topMargin + mQSPanel.getMeasuredHeight(); super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); // QSCustomizer is always be the height of the screen, but do this after // other measuring to avoid changing the height of the QSContainer. getDisplay().getRealSize(mSizePoint); mQSCustomizer.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mSizePoint.y, MeasureSpec.EXACTLY)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); updateBottom(); } public boolean isCustomizing() { return mQSCustomizer.isCustomizing(); } /** * Overrides the height of this view (post-layout), so that the content is clipped to that * height and the background is set to that height. * * @param heightOverride the overridden height */ public void setHeightOverride(int heightOverride) { mHeightOverride = heightOverride; updateBottom(); } /** * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that * during closing the detail panel, this already returns the smaller height. */ public int getDesiredHeight() { if (isCustomizing()) { return getHeight(); } if (mQSDetail.isClosingDetail()) { return mQSPanel.getGridHeight() + mHeader.getCollapsedHeight() + getPaddingBottom(); } else { return getMeasuredHeight(); } } public void notifyCustomizeChanged() { // The customize state changed, so our height changed. updateBottom(); mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); mHeader.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); // Let the panel know the position changed and it needs to update where notifications // and whatnot are. mPanelView.onQsHeightChanged(); } private void updateBottom() { int height = calculateContainerHeight(); setBottom(getTop() + height); mQSDetail.setBottom(getTop() + height); } protected int calculateContainerHeight() { int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight(); return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight() : (int) (mQsExpansion * (heightOverride - mHeader.getCollapsedHeight())) + mHeader.getCollapsedHeight(); } private void updateQsState() { boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating; mQSPanel.setExpanded(mQsExpanded); mQSDetail.setExpanded(mQsExpanded); mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating) ? View.VISIBLE : View.INVISIBLE); mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating) || (mQsExpanded && !mStackScrollerOverscrolling)); mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); } public BaseStatusBarHeader getHeader() { return mHeader; } public QSPanel getQsPanel() { return mQSPanel; } public QSCustomizer getCustomizer() { return mQSCustomizer; } public boolean isShowingDetail() { return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail(); } public void setHeaderClickable(boolean clickable) { if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable); mHeader.setClickable(clickable); } public void setExpanded(boolean expanded) { if (DEBUG) Log.d(TAG, "setExpanded " + expanded); mQsExpanded = expanded; mQSPanel.setListening(mListening && mQsExpanded); updateQsState(); } public void setKeyguardShowing(boolean keyguardShowing) { if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing); mKeyguardShowing = keyguardShowing; mQSAnimator.setOnKeyguard(keyguardShowing); updateQsState(); } public void setOverscrolling(boolean stackScrollerOverscrolling) { if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling); mStackScrollerOverscrolling = stackScrollerOverscrolling; updateQsState(); } public void setListening(boolean listening) { if (DEBUG) Log.d(TAG, "setListening " + listening); mListening = listening; mHeader.setListening(listening); mQSPanel.setListening(mListening && mQsExpanded); } public void setQsExpansion(float expansion, float headerTranslation) { if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation); mQsExpansion = expansion; final float translationScaleY = expansion - 1; if (!mHeaderAnimating) { setTranslationY(mKeyguardShowing ? (translationScaleY * mHeader.getHeight()) : headerTranslation); } mHeader.setExpansion(mKeyguardShowing ? 1 : expansion); mQSPanel.setTranslationY(translationScaleY * mQSPanel.getHeight()); mQSDetail.setFullyExpanded(expansion == 1); mQSAnimator.setPosition(expansion); updateBottom(); // Set bounds on the QS panel so it doesn't run over the header. mQsBounds.top = (int) (mQSPanel.getHeight() * (1 - expansion)); mQsBounds.right = mQSPanel.getWidth(); mQsBounds.bottom = mQSPanel.getHeight(); mQSPanel.setClipBounds(mQsBounds); } public void animateHeaderSlidingIn(long delay) { if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn"); // If the QS is already expanded we don't need to slide in the header as it's already // visible. if (!mQsExpanded) { mHeaderAnimating = true; mDelay = delay; getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); } } public void animateHeaderSlidingOut() { if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut"); mHeaderAnimating = true; animate().y(-mHeader.getHeight()) .setStartDelay(0) .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animate().setListener(null); mHeaderAnimating = false; updateQsState(); } }) .start(); } private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); animate() .translationY(0f) .setStartDelay(mDelay) .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .setListener(mAnimateHeaderSlidingInListener) .start(); setY(-mHeader.getHeight()); return true; } }; private final Animator.AnimatorListener mAnimateHeaderSlidingInListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mHeaderAnimating = false; updateQsState(); } }; public int getQsMinExpansionHeight() { return mHeader.getHeight(); } }