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