1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs; 16 17 import android.animation.Animator; 18 import android.animation.AnimatorListenerAdapter; 19 import android.annotation.Nullable; 20 import android.app.Fragment; 21 import android.content.res.Configuration; 22 import android.graphics.Rect; 23 import android.os.Bundle; 24 import android.support.annotation.VisibleForTesting; 25 import android.util.Log; 26 import android.view.ContextThemeWrapper; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.View.OnClickListener; 30 import android.view.ViewGroup; 31 import android.view.ViewTreeObserver; 32 import android.widget.FrameLayout.LayoutParams; 33 34 import com.android.systemui.Interpolators; 35 import com.android.systemui.R; 36 import com.android.systemui.R.id; 37 import com.android.systemui.plugins.qs.QS; 38 import com.android.systemui.qs.customize.QSCustomizer; 39 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer; 40 import com.android.systemui.statusbar.stack.StackStateAnimator; 41 42 public class QSFragment extends Fragment implements QS { 43 private static final String TAG = "QS"; 44 private static final boolean DEBUG = false; 45 private static final String EXTRA_EXPANDED = "expanded"; 46 private static final String EXTRA_LISTENING = "listening"; 47 48 private final Rect mQsBounds = new Rect(); 49 private boolean mQsExpanded; 50 private boolean mHeaderAnimating; 51 private boolean mKeyguardShowing; 52 private boolean mStackScrollerOverscrolling; 53 54 private long mDelay; 55 56 private QSAnimator mQSAnimator; 57 private HeightListener mPanelView; 58 protected QuickStatusBarHeader mHeader; 59 private QSCustomizer mQSCustomizer; 60 protected QSPanel mQSPanel; 61 private QSDetail mQSDetail; 62 private boolean mListening; 63 private QSContainerImpl mContainer; 64 private int mLayoutDirection; 65 private QSFooter mFooter; 66 private int mGutterHeight; 67 68 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState)69 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 70 Bundle savedInstanceState) { 71 inflater =inflater.cloneInContext(new ContextThemeWrapper(getContext(), R.style.qs_theme)); 72 return inflater.inflate(R.layout.qs_panel, container, false); 73 } 74 75 @Override onViewCreated(View view, @Nullable Bundle savedInstanceState)76 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 77 super.onViewCreated(view, savedInstanceState); 78 mQSPanel = view.findViewById(R.id.quick_settings_panel); 79 mQSDetail = view.findViewById(R.id.qs_detail); 80 mHeader = view.findViewById(R.id.header); 81 mFooter = view.findViewById(R.id.qs_footer); 82 mContainer = view.findViewById(id.quick_settings_container); 83 mGutterHeight = getContext().getResources().getDimensionPixelSize(R.dimen.qs_gutter_height); 84 85 mQSDetail.setQsPanel(mQSPanel, mHeader, mFooter); 86 87 // If the quick settings row is not shown, then there is no need for the animation from 88 // the row to the full QS panel. 89 if (getResources().getBoolean(R.bool.config_showQuickSettingsRow)) { 90 mQSAnimator = new QSAnimator(this, 91 mHeader.findViewById(R.id.quick_qs_panel), mQSPanel); 92 } 93 94 mQSCustomizer = view.findViewById(R.id.qs_customize); 95 mQSCustomizer.setQs(this); 96 if (savedInstanceState != null) { 97 setExpanded(savedInstanceState.getBoolean(EXTRA_EXPANDED)); 98 setListening(savedInstanceState.getBoolean(EXTRA_LISTENING)); 99 } 100 } 101 102 @Override onDestroy()103 public void onDestroy() { 104 super.onDestroy(); 105 if (mListening) { 106 setListening(false); 107 } 108 } 109 110 @Override onSaveInstanceState(Bundle outState)111 public void onSaveInstanceState(Bundle outState) { 112 super.onSaveInstanceState(outState); 113 outState.putBoolean(EXTRA_EXPANDED, mQsExpanded); 114 outState.putBoolean(EXTRA_LISTENING, mListening); 115 } 116 117 @VisibleForTesting isListening()118 boolean isListening() { 119 return mListening; 120 } 121 122 @VisibleForTesting isExpanded()123 boolean isExpanded() { 124 return mQsExpanded; 125 } 126 127 @Override getHeader()128 public View getHeader() { 129 return mHeader; 130 } 131 132 @Override setHasNotifications(boolean hasNotifications)133 public void setHasNotifications(boolean hasNotifications) { 134 mContainer.setGutterEnabled(hasNotifications); 135 } 136 setPanelView(HeightListener panelView)137 public void setPanelView(HeightListener panelView) { 138 mPanelView = panelView; 139 } 140 141 @Override onConfigurationChanged(Configuration newConfig)142 public void onConfigurationChanged(Configuration newConfig) { 143 super.onConfigurationChanged(newConfig); 144 if (newConfig.getLayoutDirection() != mLayoutDirection) { 145 mLayoutDirection = newConfig.getLayoutDirection(); 146 147 if (mQSAnimator != null) { 148 mQSAnimator.onRtlChanged(); 149 } 150 } 151 } 152 153 @Override setContainer(ViewGroup container)154 public void setContainer(ViewGroup container) { 155 if (container instanceof NotificationsQuickSettingsContainer) { 156 mQSCustomizer.setContainer((NotificationsQuickSettingsContainer) container); 157 } 158 } 159 isCustomizing()160 public boolean isCustomizing() { 161 return mQSCustomizer.isCustomizing(); 162 } 163 setHost(QSTileHost qsh)164 public void setHost(QSTileHost qsh) { 165 mQSPanel.setHost(qsh, mQSCustomizer); 166 mHeader.setQSPanel(mQSPanel); 167 mFooter.setQSPanel(mQSPanel); 168 mQSDetail.setHost(qsh); 169 170 if (mQSAnimator != null) { 171 mQSAnimator.setHost(qsh); 172 } 173 } 174 updateQsState()175 private void updateQsState() { 176 final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling 177 || mHeaderAnimating; 178 mQSPanel.setExpanded(mQsExpanded); 179 mQSDetail.setExpanded(mQsExpanded); 180 mHeader.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating) 181 ? View.VISIBLE 182 : View.INVISIBLE); 183 mHeader.setExpanded((mKeyguardShowing && !mHeaderAnimating) 184 || (mQsExpanded && !mStackScrollerOverscrolling)); 185 mFooter.setVisibility((mQsExpanded || !mKeyguardShowing || mHeaderAnimating) 186 ? View.VISIBLE 187 : View.INVISIBLE); 188 mFooter.setExpanded((mKeyguardShowing && !mHeaderAnimating) 189 || (mQsExpanded && !mStackScrollerOverscrolling)); 190 mQSPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 191 } 192 getQsPanel()193 public QSPanel getQsPanel() { 194 return mQSPanel; 195 } 196 getCustomizer()197 public QSCustomizer getCustomizer() { 198 return mQSCustomizer; 199 } 200 isShowingDetail()201 public boolean isShowingDetail() { 202 return mQSPanel.isShowingCustomize() || mQSDetail.isShowingDetail(); 203 } 204 setHeaderClickable(boolean clickable)205 public void setHeaderClickable(boolean clickable) { 206 if (DEBUG) Log.d(TAG, "setHeaderClickable " + clickable); 207 mFooter.getExpandView().setClickable(clickable); 208 } 209 setExpanded(boolean expanded)210 public void setExpanded(boolean expanded) { 211 if (DEBUG) Log.d(TAG, "setExpanded " + expanded); 212 mQsExpanded = expanded; 213 mQSPanel.setListening(mListening && mQsExpanded); 214 updateQsState(); 215 } 216 setKeyguardShowing(boolean keyguardShowing)217 public void setKeyguardShowing(boolean keyguardShowing) { 218 if (DEBUG) Log.d(TAG, "setKeyguardShowing " + keyguardShowing); 219 mKeyguardShowing = keyguardShowing; 220 221 if (mQSAnimator != null) { 222 mQSAnimator.setOnKeyguard(keyguardShowing); 223 } 224 225 mFooter.setKeyguardShowing(keyguardShowing); 226 updateQsState(); 227 } 228 setOverscrolling(boolean stackScrollerOverscrolling)229 public void setOverscrolling(boolean stackScrollerOverscrolling) { 230 if (DEBUG) Log.d(TAG, "setOverscrolling " + stackScrollerOverscrolling); 231 mStackScrollerOverscrolling = stackScrollerOverscrolling; 232 updateQsState(); 233 } 234 setListening(boolean listening)235 public void setListening(boolean listening) { 236 if (DEBUG) Log.d(TAG, "setListening " + listening); 237 mListening = listening; 238 mHeader.setListening(listening); 239 mFooter.setListening(listening); 240 mQSPanel.setListening(mListening && mQsExpanded); 241 } 242 setHeaderListening(boolean listening)243 public void setHeaderListening(boolean listening) { 244 mHeader.setListening(listening); 245 mFooter.setListening(listening); 246 } 247 setQsExpansion(float expansion, float headerTranslation)248 public void setQsExpansion(float expansion, float headerTranslation) { 249 if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + headerTranslation); 250 mContainer.setExpansion(expansion); 251 final float translationScaleY = expansion - 1; 252 if (!mHeaderAnimating) { 253 int height = mHeader.getHeight() + mGutterHeight; 254 getView().setTranslationY(mKeyguardShowing ? (translationScaleY * height) 255 : headerTranslation); 256 } 257 mHeader.setExpansion(mKeyguardShowing ? 1 : expansion); 258 mFooter.setExpansion(mKeyguardShowing ? 1 : expansion); 259 int heightDiff = mQSPanel.getBottom() - mHeader.getBottom() + mHeader.getPaddingBottom() 260 + mFooter.getHeight(); 261 mQSPanel.setTranslationY(translationScaleY * heightDiff); 262 mQSDetail.setFullyExpanded(expansion == 1); 263 264 if (mQSAnimator != null) { 265 mQSAnimator.setPosition(expansion); 266 } 267 268 // Set bounds on the QS panel so it doesn't run over the header. 269 mQsBounds.top = (int) -mQSPanel.getTranslationY(); 270 mQsBounds.right = mQSPanel.getWidth(); 271 mQsBounds.bottom = mQSPanel.getHeight(); 272 mQSPanel.setClipBounds(mQsBounds); 273 } 274 animateHeaderSlidingIn(long delay)275 public void animateHeaderSlidingIn(long delay) { 276 if (DEBUG) Log.d(TAG, "animateHeaderSlidingIn"); 277 // If the QS is already expanded we don't need to slide in the header as it's already 278 // visible. 279 if (!mQsExpanded) { 280 mHeaderAnimating = true; 281 mDelay = delay; 282 getView().getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); 283 } 284 } 285 animateHeaderSlidingOut()286 public void animateHeaderSlidingOut() { 287 if (DEBUG) Log.d(TAG, "animateHeaderSlidingOut"); 288 mHeaderAnimating = true; 289 getView().animate().y(-mHeader.getHeight()) 290 .setStartDelay(0) 291 .setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD) 292 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 293 .setListener(new AnimatorListenerAdapter() { 294 @Override 295 public void onAnimationEnd(Animator animation) { 296 getView().animate().setListener(null); 297 mHeaderAnimating = false; 298 updateQsState(); 299 } 300 }) 301 .start(); 302 } 303 304 @Override setExpandClickListener(OnClickListener onClickListener)305 public void setExpandClickListener(OnClickListener onClickListener) { 306 mFooter.getExpandView().setOnClickListener(onClickListener); 307 } 308 309 @Override closeDetail()310 public void closeDetail() { 311 mQSPanel.closeDetail(); 312 } 313 notifyCustomizeChanged()314 public void notifyCustomizeChanged() { 315 // The customize state changed, so our height changed. 316 mContainer.updateExpansion(); 317 mQSPanel.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); 318 mHeader.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); 319 mFooter.setVisibility(!mQSCustomizer.isCustomizing() ? View.VISIBLE : View.INVISIBLE); 320 // Let the panel know the position changed and it needs to update where notifications 321 // and whatnot are. 322 mPanelView.onQsHeightChanged(); 323 } 324 325 /** 326 * The height this view wants to be. This is different from {@link #getMeasuredHeight} such that 327 * during closing the detail panel, this already returns the smaller height. 328 */ getDesiredHeight()329 public int getDesiredHeight() { 330 if (mQSCustomizer.isCustomizing()) { 331 return getView().getHeight(); 332 } 333 if (mQSDetail.isClosingDetail()) { 334 LayoutParams layoutParams = (LayoutParams) mQSPanel.getLayoutParams(); 335 int panelHeight = layoutParams.topMargin + layoutParams.bottomMargin + 336 + mQSPanel.getMeasuredHeight(); 337 return panelHeight + getView().getPaddingBottom() + mGutterHeight; 338 } else { 339 return getView().getMeasuredHeight() + mGutterHeight; 340 } 341 } 342 343 @Override setHeightOverride(int desiredHeight)344 public void setHeightOverride(int desiredHeight) { 345 mContainer.setHeightOverride(desiredHeight - mGutterHeight); 346 } 347 getQsMinExpansionHeight()348 public int getQsMinExpansionHeight() { 349 return mHeader.getHeight(); 350 } 351 352 @Override hideImmediately()353 public void hideImmediately() { 354 getView().animate().cancel(); 355 getView().setY(-mHeader.getHeight()); 356 } 357 358 private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn 359 = new ViewTreeObserver.OnPreDrawListener() { 360 @Override 361 public boolean onPreDraw() { 362 getView().getViewTreeObserver().removeOnPreDrawListener(this); 363 getView().animate() 364 .translationY(0f) 365 .setStartDelay(mDelay) 366 .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) 367 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 368 .setListener(mAnimateHeaderSlidingInListener) 369 .start(); 370 getView().setY(-mHeader.getHeight()); 371 return true; 372 } 373 }; 374 375 private final Animator.AnimatorListener mAnimateHeaderSlidingInListener 376 = new AnimatorListenerAdapter() { 377 @Override 378 public void onAnimationEnd(Animator animation) { 379 mHeaderAnimating = false; 380 updateQsState(); 381 } 382 }; 383 } 384