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 = newValue == null || Integer.parseInt(newValue) != 0;
131             if (!mAllowFancy) {
132                 clearAnimationState();
133             }
134         } else if (MOVE_FULL_ROWS.equals(key)) {
135             mFullRows = newValue == null || Integer.parseInt(newValue) != 0;
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 heightDiff = height - mQs.getHeader().getBottom()
173                 + mQs.getHeader().getPaddingBottom();
174         firstPageBuilder.addFloat(tileLayout, "translationY", heightDiff, 0);
175 
176         for (QSTile tile : tiles) {
177             QSTileView tileView = mQsPanel.getTileView(tile);
178             if (tileView == null) {
179                 Log.e(TAG, "tileView is null " + tile.getTileSpec());
180                 continue;
181             }
182             final View tileIcon = tileView.getIcon().getIconView();
183             View view = mQs.getView();
184             if (count < mNumQuickTiles && mAllowFancy) {
185                 // Quick tiles.
186                 QSTileView quickTileView = mQuickQsPanel.getTileView(tile);
187                 if (quickTileView == null) continue;
188 
189                 lastX = loc1[0];
190                 getRelativePosition(loc1, quickTileView.getIcon().getIconView(), view);
191                 getRelativePosition(loc2, tileIcon, view);
192                 final int xDiff = loc2[0] - loc1[0];
193                 final int yDiff = loc2[1] - loc1[1];
194                 lastXDiff = loc1[0] - lastX;
195                 // Move the quick tile right from its location to the new one.
196                 translationXBuilder.addFloat(quickTileView, "translationX", 0, xDiff);
197                 translationYBuilder.addFloat(quickTileView, "translationY", 0, yDiff);
198 
199                 // Counteract the parent translation on the tile. So we have a static base to
200                 // animate the label position off from.
201                 //firstPageBuilder.addFloat(tileView, "translationY", mQsPanel.getHeight(), 0);
202 
203                 // Move the real tile from the quick tile position to its final
204                 // location.
205                 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0);
206                 translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0);
207 
208                 mQuickQsViews.add(tileView.getIconWithBackground());
209                 mAllViews.add(tileView.getIcon());
210                 mAllViews.add(quickTileView);
211             } else if (mFullRows && isIconInAnimatedRow(count)) {
212                 // TODO: Refactor some of this, it shares a lot with the above block.
213                 // Move the last tile position over by the last difference between quick tiles.
214                 // This makes the extra icons seems as if they are coming from positions in the
215                 // quick panel.
216                 loc1[0] += lastXDiff;
217                 getRelativePosition(loc2, tileIcon, view);
218                 final int xDiff = loc2[0] - loc1[0];
219                 final int yDiff = loc2[1] - loc1[1];
220 
221                 firstPageBuilder.addFloat(tileView, "translationY", heightDiff, 0);
222                 translationXBuilder.addFloat(tileView, "translationX", -xDiff, 0);
223                 translationYBuilder.addFloat(tileView, "translationY", -yDiff, 0);
224                 translationYBuilder.addFloat(tileIcon, "translationY", -yDiff, 0);
225 
226                 mAllViews.add(tileIcon);
227             } else {
228                 firstPageBuilder.addFloat(tileView, "alpha", 0, 1);
229                 firstPageBuilder.addFloat(tileView, "translationY", -heightDiff, 0);
230             }
231             mAllViews.add(tileView);
232             count++;
233         }
234         if (mAllowFancy) {
235             // Make brightness appear static position and alpha in through second half.
236             View brightness = mQsPanel.getBrightnessView();
237             if (brightness != null) {
238                 firstPageBuilder.addFloat(brightness, "translationY", heightDiff, 0);
239                 mBrightnessAnimator = new TouchAnimator.Builder()
240                         .addFloat(brightness, "alpha", 0, 1)
241                         .setStartDelay(.5f)
242                         .build();
243                 mAllViews.add(brightness);
244             } else {
245                 mBrightnessAnimator = null;
246             }
247             mFirstPageAnimator = firstPageBuilder
248                     .setListener(this)
249                     .build();
250             // Fade in the tiles/labels as we reach the final position.
251             mFirstPageDelayedAnimator = new TouchAnimator.Builder()
252                     .setStartDelay(EXPANDED_TILE_DELAY)
253                     .addFloat(mQsPanel.getPageIndicator(), "alpha", 0, 1)
254                     .addFloat(tileLayout, "alpha", 0, 1)
255                     .addFloat(mQsPanel.getDivider(), "alpha", 0, 1)
256                     .addFloat(mQsPanel.getFooter().getView(), "alpha", 0, 1).build();
257             mAllViews.add(mQsPanel.getPageIndicator());
258             mAllViews.add(mQsPanel.getDivider());
259             mAllViews.add(mQsPanel.getFooter().getView());
260             float px = 0;
261             float py = 1;
262             if (tiles.size() <= 3) {
263                 px = 1;
264             } else if (tiles.size() <= 6) {
265                 px = .4f;
266             }
267             PathInterpolatorBuilder interpolatorBuilder = new PathInterpolatorBuilder(0, 0, px, py);
268             translationXBuilder.setInterpolator(interpolatorBuilder.getXInterpolator());
269             translationYBuilder.setInterpolator(interpolatorBuilder.getYInterpolator());
270             mTranslationXAnimator = translationXBuilder.build();
271             mTranslationYAnimator = translationYBuilder.build();
272         }
273         mNonfirstPageAnimator = new TouchAnimator.Builder()
274                 .addFloat(mQuickQsPanel, "alpha", 1, 0)
275                 .addFloat(mQsPanel.getPageIndicator(), "alpha", 0, 1)
276                 .addFloat(mQsPanel.getDivider(), "alpha", 0, 1)
277                 .setListener(mNonFirstPageListener)
278                 .setEndDelay(.5f)
279                 .build();
280         mNonfirstPageDelayedAnimator = new TouchAnimator.Builder()
281                 .setStartDelay(.14f)
282                 .addFloat(tileLayout, "alpha", 0, 1).build();
283     }
284 
isIconInAnimatedRow(int count)285     private boolean isIconInAnimatedRow(int count) {
286         if (mPagedLayout == null) {
287             return false;
288         }
289         final int columnCount = mPagedLayout.getColumnCount();
290         return count < ((mNumQuickTiles + columnCount - 1) / columnCount) * columnCount;
291     }
292 
getRelativePosition(int[] loc1, View view, View parent)293     private void getRelativePosition(int[] loc1, View view, View parent) {
294         loc1[0] = 0 + view.getWidth() / 2;
295         loc1[1] = 0;
296         getRelativePositionInt(loc1, view, parent);
297     }
298 
getRelativePositionInt(int[] loc1, View view, View parent)299     private void getRelativePositionInt(int[] loc1, View view, View parent) {
300         if(view == parent || view == null) return;
301         // Ignore tile pages as they can have some offset we don't want to take into account in
302         // RTL.
303         if (!(view instanceof PagedTileLayout.TilePage)) {
304             loc1[0] += view.getLeft();
305             loc1[1] += view.getTop();
306         }
307         getRelativePositionInt(loc1, (View) view.getParent(), parent);
308     }
309 
setPosition(float position)310     public void setPosition(float position) {
311         if (mFirstPageAnimator == null) return;
312         if (mOnKeyguard) {
313             return;
314         }
315         mLastPosition = position;
316         if (mOnFirstPage && mAllowFancy) {
317             mQuickQsPanel.setAlpha(1);
318             mFirstPageAnimator.setPosition(position);
319             mFirstPageDelayedAnimator.setPosition(position);
320             mTranslationXAnimator.setPosition(position);
321             mTranslationYAnimator.setPosition(position);
322             if (mBrightnessAnimator != null) {
323                 mBrightnessAnimator.setPosition(position);
324             }
325         } else {
326             mNonfirstPageAnimator.setPosition(position);
327             mNonfirstPageDelayedAnimator.setPosition(position);
328         }
329     }
330 
331     @Override
onAnimationAtStart()332     public void onAnimationAtStart() {
333         mQuickQsPanel.setVisibility(View.VISIBLE);
334     }
335 
336     @Override
onAnimationAtEnd()337     public void onAnimationAtEnd() {
338         mQuickQsPanel.setVisibility(View.INVISIBLE);
339         final int N = mQuickQsViews.size();
340         for (int i = 0; i < N; i++) {
341             mQuickQsViews.get(i).setVisibility(View.VISIBLE);
342         }
343     }
344 
345     @Override
onAnimationStarted()346     public void onAnimationStarted() {
347         mQuickQsPanel.setVisibility(mOnKeyguard ? View.INVISIBLE : View.VISIBLE);
348         if (mOnFirstPage) {
349             final int N = mQuickQsViews.size();
350             for (int i = 0; i < N; i++) {
351                 mQuickQsViews.get(i).setVisibility(View.INVISIBLE);
352             }
353         }
354     }
355 
clearAnimationState()356     private void clearAnimationState() {
357         final int N = mAllViews.size();
358         mQuickQsPanel.setAlpha(0);
359         for (int i = 0; i < N; i++) {
360             View v = mAllViews.get(i);
361             v.setAlpha(1);
362             v.setTranslationX(0);
363             v.setTranslationY(0);
364         }
365         final int N2 = mQuickQsViews.size();
366         for (int i = 0; i < N2; i++) {
367             mQuickQsViews.get(i).setVisibility(View.VISIBLE);
368         }
369     }
370 
371     @Override
onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)372     public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
373             int oldTop, int oldRight, int oldBottom) {
374         mQsPanel.post(mUpdateAnimators);
375     }
376 
377     @Override
onTilesChanged()378     public void onTilesChanged() {
379         // Give the QS panels a moment to generate their new tiles, then create all new animators
380         // hooked up to the new views.
381         mQsPanel.post(mUpdateAnimators);
382     }
383 
384     private final TouchAnimator.Listener mNonFirstPageListener =
385             new TouchAnimator.ListenerAdapter() {
386                 @Override
387                 public void onAnimationAtEnd() {
388                     mQuickQsPanel.setVisibility(View.INVISIBLE);
389                 }
390 
391                 @Override
392                 public void onAnimationStarted() {
393                     mQuickQsPanel.setVisibility(View.VISIBLE);
394                 }
395             };
396 
397     private Runnable mUpdateAnimators = new Runnable() {
398         @Override
399         public void run() {
400             updateAnimators();
401             setPosition(mLastPosition);
402         }
403     };
404 }
405