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