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