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