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
15package com.android.systemui.qs;
16
17import android.util.Log;
18import android.view.View;
19import android.view.View.OnAttachStateChangeListener;
20import android.view.View.OnLayoutChangeListener;
21
22import com.android.systemui.Dependency;
23import com.android.systemui.plugins.qs.*;
24import com.android.systemui.plugins.qs.QSTileView;
25import com.android.systemui.qs.PagedTileLayout.PageListener;
26import com.android.systemui.qs.QSPanel.QSTileLayout;
27import com.android.systemui.qs.QSHost.Callback;
28import com.android.systemui.qs.TouchAnimator.Builder;
29import com.android.systemui.qs.TouchAnimator.Listener;
30import com.android.systemui.tuner.TunerService;
31import com.android.systemui.tuner.TunerService.Tunable;
32
33import java.util.ArrayList;
34import java.util.Collection;
35
36public 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
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
88    public void onRtlChanged() {
89        updateAnimators();
90    }
91
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
100    public void setHost(QSTileHost qsh) {
101        mHost = qsh;
102        qsh.addCallback(this);
103        updateAnimators();
104    }
105
106    @Override
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
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
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
137    public void onPageChanged(boolean isFirst) {
138        if (mOnFirstPage == isFirst) return;
139        if (!isFirst) {
140            clearAnimationState();
141        }
142        mOnFirstPage = isFirst;
143    }
144
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
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
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
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
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
321    public void onAnimationAtStart() {
322        mQuickQsPanel.setVisibility(View.VISIBLE);
323    }
324
325    @Override
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
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
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
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
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