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