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