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.util.Log;
18 import android.view.View;
19 import android.view.View.OnAttachStateChangeListener;
20 import android.view.View.OnLayoutChangeListener;
21 
22 import com.android.systemui.Dependency;
23 import com.android.systemui.plugins.qs.QS;
24 import com.android.systemui.plugins.qs.QSTile;
25 import com.android.systemui.plugins.qs.QSTileView;
26 import com.android.systemui.qs.PagedTileLayout.PageListener;
27 import com.android.systemui.qs.QSHost.Callback;
28 import com.android.systemui.qs.QSPanel.QSTileLayout;
29 import com.android.systemui.qs.TouchAnimator.Builder;
30 import com.android.systemui.qs.TouchAnimator.Listener;
31 import com.android.systemui.tuner.TunerService;
32 import com.android.systemui.tuner.TunerService.Tunable;
33 
34 import java.util.ArrayList;
35 import java.util.Collection;
36 
37 public class QSAnimator implements Callback, PageListener, Listener, OnLayoutChangeListener,
38         OnAttachStateChangeListener, Tunable {
39 
40     private static final String TAG = "QSAnimator";
41 
42     private static final String ALLOW_FANCY_ANIMATION = "sysui_qs_fancy_anim";
43     private static final String MOVE_FULL_ROWS = "sysui_qs_move_whole_rows";
44 
45     public static final float EXPANDED_TILE_DELAY = .86f;
46 
47 
48     private final ArrayList<View> mAllViews = new ArrayList<>();
49     /**
50      * List of {@link View}s representing Quick Settings that are being animated from the quick QS
51      * position to the normal QS panel.
52      */
53     private final ArrayList<View> mQuickQsViews = new ArrayList<>();
54     private final QuickQSPanel mQuickQsPanel;
55     private final QSPanel mQsPanel;
56     private final QS mQs;
57 
58     private PagedTileLayout mPagedLayout;
59 
60     private boolean mOnFirstPage = true;
61     private TouchAnimator mFirstPageAnimator;
62     private TouchAnimator mFirstPageDelayedAnimator;
63     private TouchAnimator mTranslationXAnimator;
64     private TouchAnimator mTranslationYAnimator;
65     private TouchAnimator mNonfirstPageAnimator;
66     private TouchAnimator mNonfirstPageDelayedAnimator;
67     private TouchAnimator mBrightnessAnimator;
68 
69     private boolean mOnKeyguard;
70 
71     private boolean mAllowFancy;
72     private boolean mFullRows;
73     private int mNumQuickTiles;
74     private float mLastPosition;
75     private QSTileHost mHost;
76     private boolean mShowCollapsedOnKeyguard;
77 
QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel)78     public QSAnimator(QS qs, QuickQSPanel quickPanel, QSPanel panel) {
79         mQs = qs;
80         mQuickQsPanel = quickPanel;
81         mQsPanel = panel;
82         mQsPanel.addOnAttachStateChangeListener(this);
83         qs.getView().addOnLayoutChangeListener(this);
84         if (mQsPanel.isAttachedToWindow()) {
85             onViewAttachedToWindow(null);
86         }
87         QSTileLayout tileLayout = mQsPanel.getTileLayout();
88         if (tileLayout instanceof PagedTileLayout) {
89             mPagedLayout = ((PagedTileLayout) tileLayout);
90         } else {
91             Log.w(TAG, "QS Not using page layout");
92         }
93         panel.setPageListener(this);
94     }
95 
onRtlChanged()96     public void onRtlChanged() {
97         updateAnimators();
98     }
99 
setOnKeyguard(boolean onKeyguard)100     public void setOnKeyguard(boolean onKeyguard) {
101         mOnKeyguard = onKeyguard;
102         updateQQSVisibility();
103         if (mOnKeyguard) {
104             clearAnimationState();
105         }
106     }
107 
108 
109     /**
110      * Sets whether or not the keyguard is currently being shown with a collapsed header.
111      */
setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard)112     void setShowCollapsedOnKeyguard(boolean showCollapsedOnKeyguard) {
113         mShowCollapsedOnKeyguard = showCollapsedOnKeyguard;
114         updateQQSVisibility();
115         setCurrentPosition();
116     }
117 
118 
setCurrentPosition()119     private void setCurrentPosition() {
120         setPosition(mLastPosition);
121     }
122 
updateQQSVisibility()123     private void updateQQSVisibility() {
124         mQuickQsPanel.setVisibility(mOnKeyguard
125                 && !mShowCollapsedOnKeyguard ? View.INVISIBLE : View.VISIBLE);
126     }
127 
setHost(QSTileHost qsh)128     public void setHost(QSTileHost qsh) {
129         mHost = qsh;
130         qsh.addCallback(this);
131         updateAnimators();
132     }
133 
134     @Override
onViewAttachedToWindow(View v)135     public void onViewAttachedToWindow(View v) {
136         Dependency.get(TunerService.class).addTunable(this, ALLOW_FANCY_ANIMATION,
137                 MOVE_FULL_ROWS, QuickQSPanel.NUM_QUICK_TILES);
138     }
139 
140     @Override
onViewDetachedFromWindow(View v)141     public void onViewDetachedFromWindow(View v) {
142         if (mHost != null) {
143             mHost.removeCallback(this);
144         }
145         Dependency.get(TunerService.class).removeTunable(this);
146     }
147 
148     @Override
onTuningChanged(String key, String newValue)149     public void onTuningChanged(String key, String newValue) {
150         if (ALLOW_FANCY_ANIMATION.equals(key)) {
151             mAllowFancy = TunerService.parseIntegerSwitch(newValue, true);
152             if (!mAllowFancy) {
153                 clearAnimationState();
154             }
155         } else if (MOVE_FULL_ROWS.equals(key)) {
156             mFullRows = TunerService.parseIntegerSwitch(newValue, true);
157         } else if (QuickQSPanel.NUM_QUICK_TILES.equals(key)) {
158             mNumQuickTiles = mQuickQsPanel.getNumQuickTiles(mQs.getContext());
159             clearAnimationState();
160         }
161         updateAnimators();
162     }
163 
164     @Override
onPageChanged(boolean isFirst)165     public void onPageChanged(boolean isFirst) {
166         if (mOnFirstPage == isFirst) return;
167         if (!isFirst) {
168             clearAnimationState();
169         }
170         mOnFirstPage = isFirst;
171     }
172 
updateAnimators()173     private void updateAnimators() {
174         TouchAnimator.Builder firstPageBuilder = new Builder();
175         TouchAnimator.Builder translationXBuilder = new Builder();
176         TouchAnimator.Builder translationYBuilder = new Builder();
177 
178         if (mQsPanel.getHost() == null) return;
179         Collection<QSTile> tiles = mQsPanel.getHost().getTiles();
180         int count = 0;
181         int[] loc1 = new int[2];
182         int[] loc2 = new int[2];
183         int lastXDiff = 0;
184         int lastX = 0;
185 
186         clearAnimationState();
187         mAllViews.clear();
188         mQuickQsViews.clear();
189 
190         QSTileLayout tileLayout = mQsPanel.getTileLayout();
191         mAllViews.add((View) tileLayout);
192         int height = mQs.getView() != null ? mQs.getView().getMeasuredHeight() : 0;
193         int width = mQs.getView() != null ? mQs.getView().getMeasuredWidth() : 0;
194         int heightDiff = height - mQs.getHeader().getBottom()
195                 + mQs.getHeader().getPaddingBottom();
196         firstPageBuilder.addFloat(tileLayout, "translationY", heightDiff, 0);
197 
198         for (QSTile tile : tiles) {
199             QSTileView tileView = mQsPanel.getTileView(tile);
200             if (tileView == null) {
201                 Log.e(TAG, "tileView is null " + tile.getTileSpec());
202                 continue;
203             }
204             final View tileIcon = tileView.getIcon().getIconView();
205             View view = mQs.getView();
206 
207             // This case: less tiles to animate in small displays.
208             if (count < mQuickQsPanel.getTileLayout().getNumVisibleTiles() && mAllowFancy) {
209                 // Quick tiles.
210                 QSTileView quickTileView = mQuickQsPanel.getTileView(tile);
211                 if (quickTileView == null) continue;
212 
213                 lastX = loc1[0];
214                 getRelativePosition(loc1, quickTileView.getIcon().getIconView(), view);
215                 getRelativePosition(loc2, tileIcon, view);
216                 final int xDiff = loc2[0] - loc1[0];
217                 final int yDiff = loc2[1] - loc1[1];
218                 lastXDiff = loc1[0] - lastX;
219 
220                 if (count < tileLayout.getNumVisibleTiles()) {
221                     // Move the quick tile right from its location to the new one.
222                     translationXBuilder.addFloat(quickTileView, "translationX", 0, xDiff);
223                     translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff);
224 
225                     // Counteract the parent translation on the tile. So we have a static base to
226                     // animate the label position off from.
227                     //firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0);
228 
229                     // Move the real tile from the quick tile position to its final
230                     // location.
231                     translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0);
232                     translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0);
233 
234                 } else { // These tiles disappear when expanding
235                     firstPageBuilder.addFloat(quickTileView, "alpha", 1, 0);
236                     translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff);
237 
238                     // xDiff is negative here and this makes it "more" negative
239                     final int translationX = mQsPanel.isLayoutRtl() ? xDiff - width : xDiff + width;
240                     translationXBuilder.addFloat(quickTileView, "translationX", 0,
241                             translationX);
242                 }
243 
244                 mQuickQsViews.add(tileView.getIconWithBackground());
245                 mAllViews.add(tileView.getIcon());
246                 mAllViews.add(quickTileView);
247             } else if (mFullRows && isIconInAnimatedRow(count)) {
248                 // TODO: Refactor some of this, it shares a lot with the above block.
249                 // Move the last tile position over by the last difference between quick tiles.
250                 // This makes the extra icons seems as if they are coming from positions in the
251                 // quick panel.
252                 loc1[0] += lastXDiff;
253                 getRelativePosition(loc2, tileIcon, view);
254                 final int xDiff = loc2[0] - loc1[0];
255                 final int yDiff = loc2[1] - loc1[1];
256 
257                 firstPageBuilder.addFloat(tileView, "translationY", heightDiff, 0);
258                 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0);
259                 translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0);
260                 translationYBuilder.addFloat(tileIcon, "translationY", -yDiff, 0);
261 
262                 mAllViews.add(tileIcon);
263             } else {
264                 firstPageBuilder.addFloat(tileView, "alpha", 0, 1);
265                 firstPageBuilder.addFloat(tileView, "translationY", -heightDiff, 0);
266             }
267             mAllViews.add(tileView);
268             count++;
269         }
270         if (mAllowFancy) {
271             // Make brightness appear static position and alpha in through second half.
272             View brightness = mQsPanel.getBrightnessView();
273             if (brightness != null) {
274                 firstPageBuilder.addFloat(brightness, "translationY", heightDiff, 0);
275                 mBrightnessAnimator = new TouchAnimator.Builder()
276                         .addFloat(brightness, "alpha", 0, 1)
277                         .setStartDelay(.5f)
278                         .build();
279                 mAllViews.add(brightness);
280             } else {
281                 mBrightnessAnimator = null;
282             }
283             mFirstPageAnimator = firstPageBuilder
284                     .setListener(this)
285                     .build();
286             // Fade in the tiles/labels as we reach the final position.
287             mFirstPageDelayedAnimator = new TouchAnimator.Builder()
288                     .setStartDelay(EXPANDED_TILE_DELAY)
289                     .addFloat(tileLayout, "alpha", 0, 1)
290                     .addFloat(mQsPanel.getDivider(), "alpha", 0, 1)
291                     .addFloat(mQsPanel.getFooter().getView(), "alpha", 0, 1).build();
292             mAllViews.add(mQsPanel.getDivider());
293             mAllViews.add(mQsPanel.getFooter().getView());
294             float px = 0;
295             float py = 1;
296             if (tiles.size() <= 3) {
297                 px = 1;
298             } else if (tiles.size() <= 6) {
299                 px = .4f;
300             }
301             PathInterpolatorBuilder interpolatorBuilder = new PathInterpolatorBuilder(0, 0, px, py);
302             translationXBuilder.setInterpolator(interpolatorBuilder.getXInterpolator());
303             translationYBuilder.setInterpolator(interpolatorBuilder.getYInterpolator());
304             mTranslationXAnimator = translationXBuilder.build();
305             mTranslationYAnimator = translationYBuilder.build();
306         }
307         mNonfirstPageAnimator = new TouchAnimator.Builder()
308                 .addFloat(mQuickQsPanel, "alpha", 1, 0)
309                 .addFloat(mQsPanel.getDivider(), "alpha", 0, 1)
310                 .setListener(mNonFirstPageListener)
311                 .setEndDelay(.5f)
312                 .build();
313         mNonfirstPageDelayedAnimator = new TouchAnimator.Builder()
314                 .setStartDelay(.14f)
315                 .addFloat(tileLayout, "alpha", 0, 1).build();
316     }
317 
isIconInAnimatedRow(int count)318     private boolean isIconInAnimatedRow(int count) {
319         if (mPagedLayout == null) {
320             return false;
321         }
322         final int columnCount = mPagedLayout.getColumnCount();
323         return count < ((mNumQuickTiles + columnCount - 1) / columnCount) * columnCount;
324     }
325 
getRelativePosition(int[] loc1, View view, View parent)326     private void getRelativePosition(int[] loc1, View view, View parent) {
327         loc1[0] = 0 + view.getWidth() / 2;
328         loc1[1] = 0;
329         getRelativePositionInt(loc1, view, parent);
330     }
331 
getRelativePositionInt(int[] loc1, View view, View parent)332     private void getRelativePositionInt(int[] loc1, View view, View parent) {
333         if(view == parent || view == null) return;
334         // Ignore tile pages as they can have some offset we don't want to take into account in
335         // RTL.
336         if (!(view instanceof PagedTileLayout.TilePage)) {
337             loc1[0] += view.getLeft();
338             loc1[1] += view.getTop();
339         }
340         getRelativePositionInt(loc1, (View) view.getParent(), parent);
341     }
342 
setPosition(float position)343     public void setPosition(float position) {
344         if (mFirstPageAnimator == null) return;
345         if (mOnKeyguard) {
346             if (mShowCollapsedOnKeyguard) {
347                 position = 0;
348             } else {
349                 position = 1;
350             }
351         }
352         mLastPosition = position;
353         if (mOnFirstPage && mAllowFancy) {
354             mQuickQsPanel.setAlpha(1);
355             mFirstPageAnimator.setPosition(position);
356             mFirstPageDelayedAnimator.setPosition(position);
357             mTranslationXAnimator.setPosition(position);
358             mTranslationYAnimator.setPosition(position);
359             if (mBrightnessAnimator != null) {
360                 mBrightnessAnimator.setPosition(position);
361             }
362         } else {
363             mNonfirstPageAnimator.setPosition(position);
364             mNonfirstPageDelayedAnimator.setPosition(position);
365         }
366     }
367 
368     @Override
onAnimationAtStart()369     public void onAnimationAtStart() {
370         mQuickQsPanel.setVisibility(View.VISIBLE);
371     }
372 
373     @Override
onAnimationAtEnd()374     public void onAnimationAtEnd() {
375         mQuickQsPanel.setVisibility(View.INVISIBLE);
376         final int N = mQuickQsViews.size();
377         for (int i = 0; i < N; i++) {
378             mQuickQsViews.get(i).setVisibility(View.VISIBLE);
379         }
380     }
381 
382     @Override
onAnimationStarted()383     public void onAnimationStarted() {
384         updateQQSVisibility();
385         if (mOnFirstPage) {
386             final int N = mQuickQsViews.size();
387             for (int i = 0; i < N; i++) {
388                 mQuickQsViews.get(i).setVisibility(View.INVISIBLE);
389             }
390         }
391     }
392 
clearAnimationState()393     private void clearAnimationState() {
394         final int N = mAllViews.size();
395         mQuickQsPanel.setAlpha(0);
396         for (int i = 0; i < N; i++) {
397             View v = mAllViews.get(i);
398             v.setAlpha(1);
399             v.setTranslationX(0);
400             v.setTranslationY(0);
401         }
402         final int N2 = mQuickQsViews.size();
403         for (int i = 0; i < N2; i++) {
404             mQuickQsViews.get(i).setVisibility(View.VISIBLE);
405         }
406     }
407 
408     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)409     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
410             int oldTop, int oldRight, int oldBottom) {
411         mQsPanel.post(mUpdateAnimators);
412     }
413 
414     @Override
onTilesChanged()415     public void onTilesChanged() {
416         // Give the QS panels a moment to generate their new tiles, then create all new animators
417         // hooked up to the new views.
418         mQsPanel.post(mUpdateAnimators);
419     }
420 
421     private final TouchAnimator.Listener mNonFirstPageListener =
422             new TouchAnimator.ListenerAdapter() {
423                 @Override
424                 public void onAnimationAtEnd() {
425                     mQuickQsPanel.setVisibility(View.INVISIBLE);
426                 }
427 
428                 @Override
429                 public void onAnimationStarted() {
430                     mQuickQsPanel.setVisibility(View.VISIBLE);
431                 }
432             };
433 
434     private Runnable mUpdateAnimators = new Runnable() {
435         @Override
436         public void run() {
437             updateAnimators();
438             setCurrentPosition();
439         }
440     };
441 }
442