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.TimeInterpolator; 18 import android.animation.ValueAnimator; 19 import android.annotation.NonNull; 20 import android.util.Log; 21 import android.util.Pair; 22 import android.util.SparseArray; 23 import android.view.View; 24 import android.view.View.OnAttachStateChangeListener; 25 import android.view.View.OnLayoutChangeListener; 26 27 import androidx.annotation.Nullable; 28 29 import com.android.app.animation.Interpolators; 30 import com.android.systemui.dagger.qualifiers.Main; 31 import com.android.systemui.plugins.qs.QS; 32 import com.android.systemui.plugins.qs.QSTile; 33 import com.android.systemui.plugins.qs.QSTileView; 34 import com.android.systemui.qs.QSPanel.QSTileLayout; 35 import com.android.systemui.qs.TouchAnimator.Builder; 36 import com.android.systemui.qs.dagger.QSScope; 37 import com.android.systemui.qs.tileimpl.HeightOverrideable; 38 import com.android.systemui.tuner.TunerService; 39 40 import java.util.ArrayList; 41 import java.util.Collection; 42 import java.util.List; 43 import java.util.concurrent.Executor; 44 45 import javax.inject.Inject; 46 47 /** 48 * Performs the animated transition between the QQS and QS views. 49 * 50 * <p>The transition is driven externally via {@link #setPosition(float)}, where 0 is a fully 51 * collapsed QQS and one a fully expanded QS. 52 * 53 * <p>This implementation maintains a set of {@code TouchAnimator} to transition the properties of 54 * views both in QQS and QS. These {@code TouchAnimator} are re-created lazily if contents of either 55 * view change, see {@link #requestAnimatorUpdate()}. 56 * 57 * <p>During the transition, both QS and QQS are visible. For overlapping tiles (Whenever the QS 58 * shows the first page), the corresponding QS tiles are hidden until QS is fully expanded. 59 */ 60 @QSScope 61 public class QSAnimator implements QSHost.Callback, PagedTileLayout.PageListener, 62 TouchAnimator.Listener, OnLayoutChangeListener, 63 OnAttachStateChangeListener { 64 65 private static final String TAG = "QSAnimator"; 66 67 private static final float EXPANDED_TILE_DELAY = .86f; 68 //Non first page delays 69 private static final float QS_TILE_LABEL_FADE_OUT_START = 0.15f; 70 private static final float QS_TILE_LABEL_FADE_OUT_END = 0.7f; 71 private static final float QQS_FADE_IN_INTERVAL = 0.1f; 72 73 public static final float SHORT_PARALLAX_AMOUNT = 0.1f; 74 75 /** 76 * List of all views that will be reset when clearing animation state 77 * see {@link #clearAnimationState()} } 78 */ 79 private final ArrayList<View> mAllViews = new ArrayList<>(); 80 /** 81 * List of {@link View}s representing Quick Settings that are being animated from the quick QS 82 * position to the normal QS panel. These views will only show once the animation is complete, 83 * to prevent overlapping of semi transparent views 84 */ 85 private final ArrayList<View> mAnimatedQsViews = new ArrayList<>(); 86 private final QuickQSPanel mQuickQsPanel; 87 private final QSPanelController mQsPanelController; 88 private final QuickQSPanelController mQuickQSPanelController; 89 private final QuickStatusBarHeader mQuickStatusBarHeader; 90 private final QS mQs; 91 92 @Nullable 93 private PagedTileLayout mPagedLayout; 94 95 private boolean mOnFirstPage = true; 96 private int mCurrentPage = 0; 97 private final QSExpansionPathInterpolator mQSExpansionPathInterpolator; 98 // Animator for elements in the first page, including secondary labels and qqs brightness 99 // slider, as well as animating the alpha of the QS tile layout (as we are tracking QQS tiles) 100 @Nullable 101 private TouchAnimator mFirstPageAnimator; 102 // TranslationX animator for QQS/QS tiles. Only used on the first page! 103 private TouchAnimator mTranslationXAnimator; 104 // TranslationY animator for QS tiles (and their components) in the first page 105 private TouchAnimator mTranslationYAnimator; 106 // TranslationY animator for QQS tiles (and their components) 107 private TouchAnimator mQQSTranslationYAnimator; 108 // Animates alpha of permanent views (QS tile layout, QQS tiles) when not in first page 109 private TouchAnimator mNonfirstPageAlphaAnimator; 110 // This animates fading of media player 111 private TouchAnimator mAllPagesDelayedAnimator; 112 // Brightness slider translation driver, uses mQSExpansionPathInterpolator.yInterpolator 113 @Nullable 114 private TouchAnimator mBrightnessTranslationAnimator; 115 // Brightness slider opacity driver. Uses linear interpolator. 116 @Nullable 117 private TouchAnimator mBrightnessOpacityAnimator; 118 // Animator for Footer actions in QQS 119 private TouchAnimator mQQSFooterActionsAnimator; 120 // Height animator for QQS tiles (height changing from QQS size to QS size) 121 @Nullable 122 private HeightExpansionAnimator mQQSTileHeightAnimator; 123 // Height animator for QS tile in first page but not in QQS, to present the illusion that they 124 // are expanding alongside the QQS tiles 125 @Nullable 126 private HeightExpansionAnimator mOtherFirstPageTilesHeightAnimator; 127 // Pair of animators for each non first page. The creation is delayed until the user first 128 // scrolls to that page, in order to get the proper measures and layout. 129 private final SparseArray<Pair<HeightExpansionAnimator, TouchAnimator>> 130 mNonFirstPageQSAnimators = new SparseArray<>(); 131 132 private boolean mNeedsAnimatorUpdate = false; 133 private boolean mOnKeyguard; 134 135 private int mNumQuickTiles; 136 private int mLastQQSTileHeight; 137 private float mLastPosition; 138 private final QSHost mHost; 139 private final Executor mExecutor; 140 private boolean mShowCollapsedOnKeyguard; 141 private int mQQSTop; 142 143 private int[] mTmpLoc1 = new int[2]; 144 private int[] mTmpLoc2 = new int[2]; 145 146 @Inject QSAnimator(QS qs, QuickQSPanel quickPanel, QuickStatusBarHeader quickStatusBarHeader, QSPanelController qsPanelController, QuickQSPanelController quickQSPanelController, QSHost qsTileHost, @Main Executor executor, TunerService tunerService, QSExpansionPathInterpolator qsExpansionPathInterpolator)147 public QSAnimator(QS qs, QuickQSPanel quickPanel, QuickStatusBarHeader quickStatusBarHeader, 148 QSPanelController qsPanelController, 149 QuickQSPanelController quickQSPanelController, QSHost qsTileHost, 150 @Main Executor executor, TunerService tunerService, 151 QSExpansionPathInterpolator qsExpansionPathInterpolator) { 152 mQs = qs; 153 mQuickQsPanel = quickPanel; 154 mQsPanelController = qsPanelController; 155 mQuickQSPanelController = quickQSPanelController; 156 mQuickStatusBarHeader = quickStatusBarHeader; 157 mHost = qsTileHost; 158 mExecutor = executor; 159 mQSExpansionPathInterpolator = qsExpansionPathInterpolator; 160 mHost.addCallback(this); 161 mQsPanelController.addOnAttachStateChangeListener(this); 162 qs.getView().addOnLayoutChangeListener(this); 163 if (mQsPanelController.isAttachedToWindow()) { 164 onViewAttachedToWindow(null); 165 } 166 QSTileLayout tileLayout = mQsPanelController.getTileLayout(); 167 if (tileLayout instanceof PagedTileLayout) { 168 mPagedLayout = ((PagedTileLayout) tileLayout); 169 } else { 170 Log.w(TAG, "QS Not using page layout"); 171 } 172 mQsPanelController.setPageListener(this); 173 } 174 onRtlChanged()175 public void onRtlChanged() { 176 updateAnimators(); 177 } 178 179 /** 180 * Request an update to the animators. This will update them lazily next time the position 181 * is changed. 182 */ requestAnimatorUpdate()183 public void requestAnimatorUpdate() { 184 mNeedsAnimatorUpdate = true; 185 } 186 setOnKeyguard(boolean onKeyguard)187 public void setOnKeyguard(boolean onKeyguard) { 188 mOnKeyguard = onKeyguard; 189 updateQQSVisibility(); 190 if (mOnKeyguard) { 191 clearAnimationState(); 192 } 193 } 194 195 /** 196 * Sets whether or not the keyguard is currently being shown with a collapsed header. 197 */ setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard)198 void setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard) { 199 mShowCollapsedOnKeyguard = showCollapsedOnKeyguard; 200 updateQQSVisibility(); 201 setCurrentPosition(); 202 } 203 setCurrentPosition()204 private void setCurrentPosition() { 205 setPosition(mLastPosition); 206 } 207 updateQQSVisibility()208 private void updateQQSVisibility() { 209 mQuickQsPanel.setVisibility(mOnKeyguard 210 && !mShowCollapsedOnKeyguard ? View.INVISIBLE : View.VISIBLE); 211 } 212 213 @Override onViewAttachedToWindow(@onNull View view)214 public void onViewAttachedToWindow(@NonNull View view) { 215 updateAnimators(); 216 } 217 218 @Override onViewDetachedFromWindow(@onNull View v)219 public void onViewDetachedFromWindow(@NonNull View v) { 220 mHost.removeCallback(this); 221 } 222 addNonFirstPageAnimators(int page)223 private void addNonFirstPageAnimators(int page) { 224 Pair<HeightExpansionAnimator, TouchAnimator> pair = createSecondaryPageAnimators(page); 225 if (pair != null) { 226 // pair is null in one of two cases: 227 // * mPagedTileLayout is null, meaning we are still setting up. 228 // * the page has no tiles 229 // In either case, don't add the animators to the map. 230 mNonFirstPageQSAnimators.put(page, pair); 231 } 232 } 233 234 @Override onPageChanged(boolean isFirst, int currentPage)235 public void onPageChanged(boolean isFirst, int currentPage) { 236 if (currentPage != INVALID_PAGE && mCurrentPage != currentPage) { 237 mCurrentPage = currentPage; 238 if (!isFirst && !mNonFirstPageQSAnimators.contains(currentPage)) { 239 addNonFirstPageAnimators(currentPage); 240 } 241 } 242 if (mOnFirstPage == isFirst) return; 243 if (!isFirst) { 244 clearAnimationState(); 245 } 246 mOnFirstPage = isFirst; 247 } 248 translateContent( View qqsView, View qsView, View commonParent, int xOffset, int yOffset, int[] temp, TouchAnimator.Builder animatorBuilderX, TouchAnimator.Builder animatorBuilderY, TouchAnimator.Builder qqsAnimatorBuilderY )249 private void translateContent( 250 View qqsView, 251 View qsView, 252 View commonParent, 253 int xOffset, 254 int yOffset, 255 int[] temp, 256 TouchAnimator.Builder animatorBuilderX, 257 TouchAnimator.Builder animatorBuilderY, 258 TouchAnimator.Builder qqsAnimatorBuilderY 259 ) { 260 getRelativePosition(temp, qqsView, commonParent); 261 int qqsPosX = temp[0]; 262 int qqsPosY = temp[1]; 263 getRelativePosition(temp, qsView, commonParent); 264 int qsPosX = temp[0]; 265 int qsPosY = temp[1]; 266 267 int xDiff = qsPosX - qqsPosX - xOffset; 268 animatorBuilderX.addFloat(qqsView, "translationX", 0, xDiff); 269 animatorBuilderX.addFloat(qsView, "translationX", -xDiff, 0); 270 int yDiff = qsPosY - qqsPosY - yOffset; 271 qqsAnimatorBuilderY.addFloat(qqsView, "translationY", 0, yDiff); 272 animatorBuilderY.addFloat(qsView, "translationY", -yDiff, 0); 273 mAllViews.add(qqsView); 274 mAllViews.add(qsView); 275 } 276 updateAnimators()277 private void updateAnimators() { 278 mNeedsAnimatorUpdate = false; 279 TouchAnimator.Builder firstPageBuilder = new Builder(); 280 TouchAnimator.Builder translationYBuilder = new Builder(); 281 TouchAnimator.Builder qqsTranslationYBuilder = new Builder(); 282 TouchAnimator.Builder translationXBuilder = new Builder(); 283 TouchAnimator.Builder nonFirstPageAlphaBuilder = new Builder(); 284 TouchAnimator.Builder quadraticInterpolatorBuilder = new Builder() 285 .setInterpolator(Interpolators.ACCELERATE); 286 287 Collection<QSTile> tiles = mHost.getTiles(); 288 int count = 0; 289 290 clearAnimationState(); 291 mNonFirstPageQSAnimators.clear(); 292 mAllViews.clear(); 293 mAnimatedQsViews.clear(); 294 mQQSTileHeightAnimator = null; 295 mOtherFirstPageTilesHeightAnimator = null; 296 297 mNumQuickTiles = mQuickQsPanel.getNumQuickTiles(); 298 299 QSTileLayout tileLayout = mQsPanelController.getTileLayout(); 300 mAllViews.add((View) tileLayout); 301 302 mLastQQSTileHeight = 0; 303 304 if (mQsPanelController.areThereTiles()) { 305 for (QSTile tile : tiles) { 306 QSTileView tileView = mQsPanelController.getTileView(tile); 307 308 if (tileView == null) { 309 Log.e(TAG, "tileView is null " + tile.getTileSpec()); 310 continue; 311 } 312 // Only animate tiles in the first page 313 if (mPagedLayout != null && count >= mPagedLayout.getNumTilesFirstPage()) { 314 break; 315 } 316 317 final View tileIcon = tileView.getIcon().getIconView(); 318 View view = mQs.getView(); 319 320 // This case: less tiles to animate in small displays. 321 if (count < mQuickQSPanelController.getTileLayout().getNumVisibleTiles()) { 322 // Quick tiles. 323 QSTileView quickTileView = mQuickQSPanelController.getTileView(tile); 324 if (quickTileView == null) continue; 325 326 getRelativePosition(mTmpLoc1, quickTileView, view); 327 getRelativePosition(mTmpLoc2, tileView, view); 328 int yOffset = mTmpLoc2[1] - mTmpLoc1[1]; 329 int xOffset = mTmpLoc2[0] - mTmpLoc1[0]; 330 331 // Offset the translation animation on the views 332 // (that goes from 0 to getOffsetTranslation) 333 qqsTranslationYBuilder.addFloat(quickTileView, "translationY", 0, yOffset); 334 translationYBuilder.addFloat(tileView, "translationY", -yOffset, 0); 335 336 translationXBuilder.addFloat(quickTileView, "translationX", 0, xOffset); 337 translationXBuilder.addFloat(tileView, "translationX", -xOffset, 0); 338 339 if (mQQSTileHeightAnimator == null) { 340 mQQSTileHeightAnimator = new HeightExpansionAnimator(this, 341 quickTileView.getMeasuredHeight(), tileView.getMeasuredHeight()); 342 mLastQQSTileHeight = quickTileView.getMeasuredHeight(); 343 } 344 345 mQQSTileHeightAnimator.addView(quickTileView); 346 347 // Icons 348 translateContent( 349 quickTileView.getIcon(), 350 tileView.getIcon(), 351 view, 352 xOffset, 353 yOffset, 354 mTmpLoc1, 355 translationXBuilder, 356 translationYBuilder, 357 qqsTranslationYBuilder 358 ); 359 360 // Label containers 361 translateContent( 362 quickTileView.getLabelContainer(), 363 tileView.getLabelContainer(), 364 view, 365 xOffset, 366 yOffset, 367 mTmpLoc1, 368 translationXBuilder, 369 translationYBuilder, 370 qqsTranslationYBuilder 371 ); 372 373 // Secondary icon 374 translateContent( 375 quickTileView.getSecondaryIcon(), 376 tileView.getSecondaryIcon(), 377 view, 378 xOffset, 379 yOffset, 380 mTmpLoc1, 381 translationXBuilder, 382 translationYBuilder, 383 qqsTranslationYBuilder 384 ); 385 386 // Secondary labels on tiles not in QQS have two alpha animation applied: 387 // * on the tile themselves 388 // * on TileLayout 389 // Therefore, we use a quadratic interpolator animator to animate the alpha 390 // for tiles in QQS to match. 391 quadraticInterpolatorBuilder 392 .addFloat(quickTileView.getSecondaryLabel(), "alpha", 0, 1); 393 nonFirstPageAlphaBuilder 394 .addFloat(quickTileView.getSecondaryLabel(), "alpha", 0, 0); 395 396 mAnimatedQsViews.add(tileView); 397 mAllViews.add(quickTileView); 398 mAllViews.add(quickTileView.getSecondaryLabel()); 399 } else if (!isIconInAnimatedRow(count)) { 400 // Pretend there's a corresponding QQS tile (for the position) that we are 401 // expanding from. 402 SideLabelTileLayout qqsLayout = 403 (SideLabelTileLayout) mQuickQsPanel.getTileLayout(); 404 getRelativePosition(mTmpLoc1, qqsLayout, view); 405 mQQSTop = mTmpLoc1[1]; 406 getRelativePosition(mTmpLoc2, tileView, view); 407 int diff = mTmpLoc2[1] - (mTmpLoc1[1] + qqsLayout.getPhantomTopPosition(count)); 408 translationYBuilder.addFloat(tileView, "translationY", -diff, 0); 409 if (mOtherFirstPageTilesHeightAnimator == null) { 410 mOtherFirstPageTilesHeightAnimator = 411 new HeightExpansionAnimator( 412 this, mLastQQSTileHeight, tileView.getMeasuredHeight()); 413 } 414 mOtherFirstPageTilesHeightAnimator.addView(tileView); 415 tileView.setClipChildren(true); 416 tileView.setClipToPadding(true); 417 firstPageBuilder.addFloat(tileView.getSecondaryLabel(), "alpha", 0, 1); 418 mAllViews.add(tileView.getSecondaryLabel()); 419 } 420 421 mAllViews.add(tileView); 422 count++; 423 } 424 if (mCurrentPage != 0) { 425 addNonFirstPageAnimators(mCurrentPage); 426 } 427 } 428 429 animateBrightnessSlider(); 430 431 mFirstPageAnimator = firstPageBuilder 432 // Fade in the tiles/labels as we reach the final position. 433 .addFloat(tileLayout, "alpha", 0, 1) 434 .addFloat(quadraticInterpolatorBuilder.build(), "position", 0, 1) 435 .setListener(this) 436 .build(); 437 438 // Fade in the media player as we reach the final position 439 Builder builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY); 440 if (mQsPanelController.shouldUseHorizontalLayout() 441 && mQsPanelController.mMediaHost.hostView != null) { 442 builder.addFloat(mQsPanelController.mMediaHost.hostView, "alpha", 0, 1); 443 } else { 444 // In portrait, media view should always be visible 445 mQsPanelController.mMediaHost.hostView.setAlpha(1.0f); 446 } 447 mAllPagesDelayedAnimator = builder.build(); 448 translationYBuilder.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator()); 449 qqsTranslationYBuilder.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator()); 450 translationXBuilder.setInterpolator(mQSExpansionPathInterpolator.getXInterpolator()); 451 if (mOnFirstPage) { 452 // Only recreate this animator if we're in the first page. That way we know that 453 // the first page is attached and has the proper positions/measures. 454 mQQSTranslationYAnimator = qqsTranslationYBuilder.build(); 455 } 456 mTranslationYAnimator = translationYBuilder.build(); 457 mTranslationXAnimator = translationXBuilder.build(); 458 if (mQQSTileHeightAnimator != null) { 459 mQQSTileHeightAnimator.setInterpolator( 460 mQSExpansionPathInterpolator.getYInterpolator()); 461 } 462 if (mOtherFirstPageTilesHeightAnimator != null) { 463 mOtherFirstPageTilesHeightAnimator.setInterpolator( 464 mQSExpansionPathInterpolator.getYInterpolator()); 465 } 466 mNonfirstPageAlphaAnimator = nonFirstPageAlphaBuilder 467 .addFloat(mQuickQsPanel, "alpha", 1, 0) 468 .addFloat(tileLayout, "alpha", 0, 1) 469 .setListener(mNonFirstPageListener) 470 .setEndDelay(1 - QQS_FADE_IN_INTERVAL) 471 .build(); 472 } 473 createSecondaryPageAnimators(int page)474 private Pair<HeightExpansionAnimator, TouchAnimator> createSecondaryPageAnimators(int page) { 475 if (mPagedLayout == null) return null; 476 HeightExpansionAnimator animator = null; 477 TouchAnimator.Builder builder = new Builder() 478 .setInterpolator(mQSExpansionPathInterpolator.getYInterpolator()); 479 TouchAnimator.Builder alphaDelayedBuilder = new Builder() 480 .setStartDelay(QS_TILE_LABEL_FADE_OUT_START) 481 .setEndDelay(QS_TILE_LABEL_FADE_OUT_END); 482 SideLabelTileLayout qqsLayout = (SideLabelTileLayout) mQuickQsPanel.getTileLayout(); 483 View view = mQs.getView(); 484 List<String> specs = mPagedLayout.getSpecsForPage(page); 485 if (specs.isEmpty()) { 486 // specs should not be empty in a valid secondary page, as we scrolled to it. 487 // We may crash later on because there's a null animator. 488 specs = mHost.getSpecs(); 489 Log.e(TAG, "Trying to create animators for empty page " + page + ". Tiles: " + specs); 490 // return null; 491 } 492 493 int row = -1; 494 int lastTileTop = -1; 495 496 for (int i = 0; i < specs.size(); i++) { 497 QSTileView tileView = mQsPanelController.getTileView(specs.get(i)); 498 getRelativePosition(mTmpLoc2, tileView, view); 499 int diff = mTmpLoc2[1] - (mQQSTop + qqsLayout.getPhantomTopPosition(i)); 500 builder.addFloat(tileView, "translationY", -diff, 0); 501 // The different elements in the tile should be centered, so maintain them centered 502 int centerDiff = (tileView.getMeasuredHeight() - mLastQQSTileHeight) / 2; 503 builder.addFloat(tileView.getIcon(), "translationY", -centerDiff, 0); 504 builder.addFloat(tileView.getSecondaryIcon(), "translationY", -centerDiff, 0); 505 // The labels have different apparent size in QQS vs QS (no secondary label), so the 506 // translation needs to account for that. 507 int secondaryLabelOffset = 0; 508 if (tileView.getSecondaryLabel().getVisibility() == View.VISIBLE) { 509 secondaryLabelOffset = tileView.getSecondaryLabel().getMeasuredHeight() / 2; 510 } 511 int labelDiff = centerDiff - secondaryLabelOffset; 512 builder.addFloat(tileView.getLabelContainer(), "translationY", -labelDiff, 0); 513 builder.addFloat(tileView.getSecondaryLabel(), "alpha", 0, 0.3f, 1); 514 515 alphaDelayedBuilder.addFloat(tileView.getLabelContainer(), "alpha", 0, 1); 516 alphaDelayedBuilder.addFloat(tileView.getIcon(), "alpha", 0, 1); 517 alphaDelayedBuilder.addFloat(tileView.getSecondaryIcon(), "alpha", 0, 1); 518 519 final int tileTop = tileView.getTop(); 520 if (tileTop != lastTileTop) { 521 row++; 522 lastTileTop = tileTop; 523 } 524 if (i >= mQuickQsPanel.getTileLayout().getNumVisibleTiles() && row >= 2) { 525 // Fade completely the tiles in rows below the ones that will merge into QQS. 526 // args is an array of 0s where the length is the current row index (at least third 527 // row) 528 final float[] args = new float[row]; 529 args[args.length - 1] = 1f; 530 builder.addFloat(tileView, "alpha", args); 531 } else { 532 // For all the other rows, fade them a bit 533 builder.addFloat(tileView, "alpha", 0.6f, 1); 534 } 535 536 if (animator == null) { 537 animator = new HeightExpansionAnimator( 538 this, mLastQQSTileHeight, tileView.getMeasuredHeight()); 539 animator.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator()); 540 } 541 animator.addView(tileView); 542 543 tileView.setClipChildren(true); 544 tileView.setClipToPadding(true); 545 mAllViews.add(tileView); 546 mAllViews.add(tileView.getSecondaryLabel()); 547 mAllViews.add(tileView.getIcon()); 548 mAllViews.add(tileView.getSecondaryIcon()); 549 mAllViews.add(tileView.getLabelContainer()); 550 } 551 builder.addFloat(alphaDelayedBuilder.build(), "position", 0, 1); 552 return new Pair<>(animator, builder.build()); 553 } 554 animateBrightnessSlider()555 private void animateBrightnessSlider() { 556 mBrightnessTranslationAnimator = null; 557 mBrightnessOpacityAnimator = null; 558 View qsBrightness = mQsPanelController.getBrightnessView(); 559 View qqsBrightness = mQuickQSPanelController.getBrightnessView(); 560 if (qqsBrightness != null && qqsBrightness.getVisibility() == View.VISIBLE) { 561 // animating in split shade mode 562 mAnimatedQsViews.add(qsBrightness); 563 mAllViews.add(qqsBrightness); 564 int translationY = getRelativeTranslationY(qsBrightness, qqsBrightness); 565 mBrightnessTranslationAnimator = new Builder() 566 // we need to animate qs brightness even if animation will not be visible, 567 // as we might start from sliderScaleY set to 0.3 if device was in collapsed QS 568 // portrait orientation before 569 .addFloat(qsBrightness, "sliderScaleY", 0.3f, 1) 570 .addFloat(qqsBrightness, "translationY", 0, translationY) 571 .setInterpolator(mQSExpansionPathInterpolator.getYInterpolator()) 572 .build(); 573 } else if (qsBrightness != null) { 574 // The brightness slider's visible bottom edge must maintain a constant margin from the 575 // QS tiles during transition. Thus the slider must (1) perform the same vertical 576 // translation as the tiles, and (2) compensate for the slider scaling. 577 578 // For (1), compute the distance via the vertical distance between QQS and QS tile 579 // layout top. 580 View quickSettingsRootView = mQs.getView(); 581 View qsTileLayout = (View) mQsPanelController.getTileLayout(); 582 View qqsTileLayout = (View) mQuickQSPanelController.getTileLayout(); 583 getRelativePosition(mTmpLoc1, qsTileLayout, quickSettingsRootView); 584 getRelativePosition(mTmpLoc2, qqsTileLayout, quickSettingsRootView); 585 int tileMovement = mTmpLoc2[1] - mTmpLoc1[1]; 586 587 // For (2), the slider scales to the vertical center, so compensate with half the 588 // height at full collapse. 589 float scaleCompensation = qsBrightness.getMeasuredHeight() * 0.5f; 590 mBrightnessTranslationAnimator = new Builder() 591 .addFloat(qsBrightness, "translationY", scaleCompensation + tileMovement, 0) 592 .addFloat(qsBrightness, "sliderScaleY", 0, 1) 593 .setInterpolator(mQSExpansionPathInterpolator.getYInterpolator()) 594 .build(); 595 596 // While the slider's position and unfurl is animated throughouth the motion, the 597 // fade in happens independently. 598 mBrightnessOpacityAnimator = new Builder() 599 .addFloat(qsBrightness, "alpha", 0, 1) 600 .setStartDelay(0.2f) 601 .setEndDelay(1 - 0.5f) 602 .build(); 603 mAllViews.add(qsBrightness); 604 } 605 } 606 getRelativeTranslationY(View view1, View view2)607 private int getRelativeTranslationY(View view1, View view2) { 608 int[] qsPosition = new int[2]; 609 int[] qqsPosition = new int[2]; 610 View commonView = mQs.getView(); 611 getRelativePositionInt(qsPosition, view1, commonView); 612 getRelativePositionInt(qqsPosition, view2, commonView); 613 return qsPosition[1] - qqsPosition[1]; 614 } 615 isIconInAnimatedRow(int count)616 private boolean isIconInAnimatedRow(int count) { 617 if (mPagedLayout == null) { 618 return false; 619 } 620 final int columnCount = mPagedLayout.getColumnCount(); 621 return count < ((mNumQuickTiles + columnCount - 1) / columnCount) * columnCount; 622 } 623 getRelativePosition(int[] loc1, View view, View parent)624 private void getRelativePosition(int[] loc1, View view, View parent) { 625 loc1[0] = 0 + view.getWidth() / 2; 626 loc1[1] = 0; 627 getRelativePositionInt(loc1, view, parent); 628 } 629 getRelativePositionInt(int[] loc1, View view, View parent)630 private void getRelativePositionInt(int[] loc1, View view, View parent) { 631 if (view == parent || view == null) return; 632 // Ignore tile pages as they can have some offset we don't want to take into account in 633 // RTL. 634 if (!isAPage(view)) { 635 loc1[0] += view.getLeft(); 636 loc1[1] += view.getTop(); 637 } 638 if (!(view instanceof PagedTileLayout)) { 639 // Remove the scrolling position of all scroll views other than the viewpager 640 loc1[0] -= view.getScrollX(); 641 loc1[1] -= view.getScrollY(); 642 } 643 getRelativePositionInt(loc1, (View) view.getParent(), parent); 644 } 645 646 // Returns true if the view is a possible page in PagedTileLayout isAPage(View view)647 private boolean isAPage(View view) { 648 return view.getClass().equals(SideLabelTileLayout.class); 649 } 650 setPosition(float position)651 public void setPosition(float position) { 652 if (mNeedsAnimatorUpdate) { 653 updateAnimators(); 654 } 655 if (mFirstPageAnimator == null) return; 656 if (mOnKeyguard) { 657 if (mShowCollapsedOnKeyguard) { 658 position = 0; 659 } else { 660 position = 1; 661 } 662 } 663 mLastPosition = position; 664 if (mOnFirstPage) { 665 mQuickQsPanel.setAlpha(1); 666 mFirstPageAnimator.setPosition(position); 667 mTranslationYAnimator.setPosition(position); 668 mTranslationXAnimator.setPosition(position); 669 if (mOtherFirstPageTilesHeightAnimator != null) { 670 mOtherFirstPageTilesHeightAnimator.setPosition(position); 671 } 672 } else { 673 mNonfirstPageAlphaAnimator.setPosition(position); 674 } 675 for (int i = 0; i < mNonFirstPageQSAnimators.size(); i++) { 676 Pair<HeightExpansionAnimator, TouchAnimator> pair = mNonFirstPageQSAnimators.valueAt(i); 677 if (pair != null) { 678 pair.first.setPosition(position); 679 pair.second.setPosition(position); 680 } 681 } 682 if (mQQSTileHeightAnimator != null) { 683 mQQSTileHeightAnimator.setPosition(position); 684 } 685 mQQSTranslationYAnimator.setPosition(position); 686 mAllPagesDelayedAnimator.setPosition(position); 687 if (mBrightnessOpacityAnimator != null) { 688 mBrightnessOpacityAnimator.setPosition(position); 689 } 690 if (mBrightnessTranslationAnimator != null) { 691 mBrightnessTranslationAnimator.setPosition(position); 692 } 693 if (mQQSFooterActionsAnimator != null) { 694 mQQSFooterActionsAnimator.setPosition(position); 695 } 696 } 697 698 @Override onAnimationAtStart()699 public void onAnimationAtStart() { 700 mQuickQsPanel.setVisibility(View.VISIBLE); 701 } 702 703 @Override onAnimationAtEnd()704 public void onAnimationAtEnd() { 705 mQuickQsPanel.setVisibility(View.INVISIBLE); 706 final int N = mAnimatedQsViews.size(); 707 for (int i = 0; i < N; i++) { 708 mAnimatedQsViews.get(i).setVisibility(View.VISIBLE); 709 } 710 } 711 712 @Override onAnimationStarted()713 public void onAnimationStarted() { 714 updateQQSVisibility(); 715 if (mOnFirstPage) { 716 final int N = mAnimatedQsViews.size(); 717 for (int i = 0; i < N; i++) { 718 mAnimatedQsViews.get(i).setVisibility(View.INVISIBLE); 719 } 720 } 721 } 722 clearAnimationState()723 private void clearAnimationState() { 724 final int N = mAllViews.size(); 725 mQuickQsPanel.setAlpha(0); 726 for (int i = 0; i < N; i++) { 727 View v = mAllViews.get(i); 728 v.setAlpha(1); 729 v.setTranslationX(0); 730 v.setTranslationY(0); 731 v.setScaleY(1f); 732 if (v instanceof SideLabelTileLayout) { 733 ((SideLabelTileLayout) v).setClipChildren(false); 734 ((SideLabelTileLayout) v).setClipToPadding(false); 735 } 736 } 737 if (mQQSTileHeightAnimator != null) { 738 mQQSTileHeightAnimator.resetViewsHeights(); 739 } 740 if (mOtherFirstPageTilesHeightAnimator != null) { 741 mOtherFirstPageTilesHeightAnimator.resetViewsHeights(); 742 } 743 for (int i = 0; i < mNonFirstPageQSAnimators.size(); i++) { 744 mNonFirstPageQSAnimators.valueAt(i).first.resetViewsHeights(); 745 } 746 final int N2 = mAnimatedQsViews.size(); 747 for (int i = 0; i < N2; i++) { 748 mAnimatedQsViews.get(i).setVisibility(View.VISIBLE); 749 } 750 } 751 752 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)753 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 754 int oldTop, int oldRight, int oldBottom) { 755 boolean actualChange = 756 left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom; 757 if (actualChange) mExecutor.execute(mUpdateAnimators); 758 } 759 760 @Override onTilesChanged()761 public void onTilesChanged() { 762 // Give the QS panels a moment to generate their new tiles, then create all new animators 763 // hooked up to the new views. 764 mExecutor.execute(mUpdateAnimators); 765 } 766 767 private final TouchAnimator.Listener mNonFirstPageListener = 768 new TouchAnimator.ListenerAdapter() { 769 @Override 770 public void onAnimationAtEnd() { 771 mQuickQsPanel.setVisibility(View.INVISIBLE); 772 } 773 774 @Override 775 public void onAnimationStarted() { 776 mQuickQsPanel.setVisibility(View.VISIBLE); 777 } 778 }; 779 780 private final Runnable mUpdateAnimators = () -> { 781 updateAnimators(); 782 setCurrentPosition(); 783 }; 784 785 private static class HeightExpansionAnimator { 786 private final List<View> mViews = new ArrayList<>(); 787 private final ValueAnimator mAnimator; 788 private final TouchAnimator.Listener mListener; 789 790 private final ValueAnimator.AnimatorUpdateListener mUpdateListener = 791 new ValueAnimator.AnimatorUpdateListener() { 792 float mLastT = -1; 793 794 @Override 795 public void onAnimationUpdate(ValueAnimator valueAnimator) { 796 float t = valueAnimator.getAnimatedFraction(); 797 final int viewCount = mViews.size(); 798 int height = (Integer) valueAnimator.getAnimatedValue(); 799 for (int i = 0; i < viewCount; i++) { 800 View v = mViews.get(i); 801 if (v instanceof HeightOverrideable) { 802 ((HeightOverrideable) v).setHeightOverride(height); 803 } else { 804 v.setBottom(v.getTop() + height); 805 } 806 } 807 if (t == 0f) { 808 mListener.onAnimationAtStart(); 809 } else if (t == 1f) { 810 mListener.onAnimationAtEnd(); 811 } else if (mLastT <= 0 || mLastT == 1) { 812 mListener.onAnimationStarted(); 813 } 814 mLastT = t; 815 } 816 }; 817 HeightExpansionAnimator(TouchAnimator.Listener listener, int startHeight, int endHeight)818 HeightExpansionAnimator(TouchAnimator.Listener listener, int startHeight, int endHeight) { 819 mListener = listener; 820 mAnimator = ValueAnimator.ofInt(startHeight, endHeight); 821 mAnimator.setRepeatCount(ValueAnimator.INFINITE); 822 mAnimator.setRepeatMode(ValueAnimator.REVERSE); 823 mAnimator.addUpdateListener(mUpdateListener); 824 } 825 addView(View v)826 void addView(View v) { 827 mViews.add(v); 828 } 829 setInterpolator(TimeInterpolator interpolator)830 void setInterpolator(TimeInterpolator interpolator) { 831 mAnimator.setInterpolator(interpolator); 832 } 833 setPosition(float position)834 void setPosition(float position) { 835 mAnimator.setCurrentFraction(position); 836 } 837 resetViewsHeights()838 void resetViewsHeights() { 839 final int viewsCount = mViews.size(); 840 for (int i = 0; i < viewsCount; i++) { 841 View v = mViews.get(i); 842 if (v instanceof HeightOverrideable) { 843 ((HeightOverrideable) v).resetOverride(); 844 } else { 845 v.setBottom(v.getTop() + v.getMeasuredHeight()); 846 } 847 } 848 } 849 } 850 } 851