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 android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.content.Context; 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.View; 27 import android.view.ViewTreeObserver; 28 import android.widget.FrameLayout; 29 import com.android.systemui.Interpolators; 30 import com.android.systemui.R; 31 import com.android.systemui.qs.customize.QSCustomizer; 32 import com.android.systemui.statusbar.phone.BaseStatusBarHeader; 33 import com.android.systemui.statusbar.phone.NotificationPanelView; 34 import com.android.systemui.statusbar.phone.QSTileHost; 35 import 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 */ 42 public 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 int panelHeight = ((LayoutParams) mQSPanel.getLayoutParams()).topMargin 150 + mQSPanel.getMeasuredHeight(); 151 return panelHeight + getPaddingBottom(); 152 } else { 153 return getMeasuredHeight(); 154 } 155 } 156 157 public void notifyCustomizeChanged() { 158 // The customize state changed, so our height changed. 159 updateBottom(); 160 mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); 161 mHeader.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); 162 // Let the panel know the position changed and it needs to update where notifications 163 // and whatnot are. 164 mPanelView.onQsHeightChanged(); 165 } 166 167 private void updateBottom() { 168 int height = calculateContainerHeight(); 169 setBottom(getTop() + height); 170 mQSDetail.setBottom(getTop() + height); 171 } 172 173 protected int calculateContainerHeight() { 174 int heightOverride = mHeightOverride != -1 ? mHeightOverride : getMeasuredHeight(); 175 return mQSCustomizer.isCustomizing() ? mQSCustomizer.getHeight() 176 : (int) (mQsExpansion * (heightOverride - mHeader.getCollapsedHeight())) 177 + mHeader.getCollapsedHeight(); 178 } 179 180 private void updateQsState() { 181 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating; 182 mQSPanel.setExpanded(mQsExpanded); 183 mQSDetail.setExpanded(mQsExpanded); 184 mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating) 185 ? View.VISIBLE 186 : View.INVISIBLE); 187 mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating) 188 || (mQsExpanded && !mStackScrollerOverscrolling)); 189 mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 190 } 191 192 public BaseStatusBarHeader getHeader() { 193 return mHeader; 194 } 195 196 public QSPanel getQsPanel() { 197 return mQSPanel; 198 } 199 200 public QSCustomizer getCustomizer() { 201 return mQSCustomizer; 202 } 203 204 public boolean isShowingDetail() { 205 return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail(); 206 } 207 208 public void setHeaderClickable(boolean clickable) { 209 if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable); 210 mHeader.setClickable(clickable); 211 } 212 213 public void setExpanded(boolean expanded) { 214 if (DEBUG) Log.d(TAG, "setExpanded " + expanded); 215 mQsExpanded = expanded; 216 mQSPanel.setListening(mListening && mQsExpanded); 217 updateQsState(); 218 } 219 220 public void setKeyguardShowing(boolean keyguardShowing) { 221 if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing); 222 mKeyguardShowing = keyguardShowing; 223 mQSAnimator.setOnKeyguard(keyguardShowing); 224 updateQsState(); 225 } 226 227 public void setOverscrolling(boolean stackScrollerOverscrolling) { 228 if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling); 229 mStackScrollerOverscrolling = stackScrollerOverscrolling; 230 updateQsState(); 231 } 232 233 public void setListening(boolean listening) { 234 if (DEBUG) Log.d(TAG, "setListening " + listening); 235 mListening = listening; 236 mHeader.setListening(listening); 237 mQSPanel.setListening(mListening && mQsExpanded); 238 } 239 240 public void setHeaderListening(boolean listening) { 241 mHeader.setListening(listening); 242 } 243 244 public void setQsExpansion(float expansion, float headerTranslation) { 245 if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation); 246 mQsExpansion = expansion; 247 final float translationScaleY = expansion - 1; 248 if (!mHeaderAnimating) { 249 setTranslationY(mKeyguardShowing ? (translationScaleY * mHeader.getHeight()) 250 : headerTranslation); 251 } 252 mHeader.setExpansion(mKeyguardShowing ? 1 : expansion); 253 mQSPanel.setTranslationY(translationScaleY * mQSPanel.getHeight()); 254 mQSDetail.setFullyExpanded(expansion == 1); 255 mQSAnimator.setPosition(expansion); 256 updateBottom(); 257 258 // Set bounds on the QS panel so it doesn't run over the header. 259 mQsBounds.top = (int) (mQSPanel.getHeight() * (1 - expansion)); 260 mQsBounds.right = mQSPanel.getWidth(); 261 mQsBounds.bottom = mQSPanel.getHeight(); 262 mQSPanel.setClipBounds(mQsBounds); 263 } 264 265 public void animateHeaderSlidingIn(long delay) { 266 if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn"); 267 // If the QS is already expanded we don't need to slide in the header as it's already 268 // visible. 269 if (!mQsExpanded) { 270 mHeaderAnimating = true; 271 mDelay = delay; 272 getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); 273 } 274 } 275 276 public void animateHeaderSlidingOut() { 277 if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut"); 278 mHeaderAnimating = true; 279 animate().y(-mHeader.getHeight()) 280 .setStartDelay(0) 281 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) 282 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 283 .setListener(new AnimatorListenerAdapter() { 284 @Override 285 public void onAnimationEnd(Animator animation) { 286 animate().setListener(null); 287 mHeaderAnimating = false; 288 updateQsState(); 289 } 290 }) 291 .start(); 292 } 293 294 private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn 295 = new ViewTreeObserver.OnPreDrawListener() { 296 @Override 297 public boolean onPreDraw() { 298 getViewTreeObserver().removeOnPreDrawListener(this); 299 animate() 300 .translationY(0f) 301 .setStartDelay(mDelay) 302 .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) 303 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 304 .setListener(mAnimateHeaderSlidingInListener) 305 .start(); 306 setY(-mHeader.getHeight()); 307 return true; 308 } 309 }; 310 311 private final Animator.AnimatorListener mAnimateHeaderSlidingInListener 312 = new AnimatorListenerAdapter() { 313 @Override 314 public void onAnimationEnd(Animator animation) { 315 mHeaderAnimating = false; 316 updateQsState(); 317 } 318 }; 319 320 public int getQsMinExpansionHeight() { 321 return mHeader.getHeight(); 322 } 323 } 324