1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.launcher3;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.util.Log;
23import android.view.View;
24import android.view.ViewPropertyAnimator;
25import android.view.ViewTreeObserver;
26
27import com.android.launcher3.util.Thunk;
28
29/*
30 *  This is a helper class that listens to updates from the corresponding animation.
31 *  For the first two frames, it adjusts the current play time of the animation to
32 *  prevent jank at the beginning of the animation
33 */
34public class FirstFrameAnimatorHelper extends AnimatorListenerAdapter
35    implements ValueAnimator.AnimatorUpdateListener {
36    private static final boolean DEBUG = false;
37    private static final int MAX_DELAY = 1000;
38    private static final int IDEAL_FRAME_DURATION = 16;
39    private View mTarget;
40    private long mStartFrame;
41    private long mStartTime = -1;
42    private boolean mHandlingOnAnimationUpdate;
43    private boolean mAdjustedSecondFrameTime;
44
45    private static ViewTreeObserver.OnDrawListener sGlobalDrawListener;
46    @Thunk static long sGlobalFrameCounter;
47    private static boolean sVisible;
48
49    public FirstFrameAnimatorHelper(ValueAnimator animator, View target) {
50        mTarget = target;
51        animator.addUpdateListener(this);
52    }
53
54    public FirstFrameAnimatorHelper(ViewPropertyAnimator vpa, View target) {
55        mTarget = target;
56        vpa.setListener(this);
57    }
58
59    // only used for ViewPropertyAnimators
60    public void onAnimationStart(Animator animation) {
61        final ValueAnimator va = (ValueAnimator) animation;
62        va.addUpdateListener(FirstFrameAnimatorHelper.this);
63        onAnimationUpdate(va);
64    }
65
66    public static void setIsVisible(boolean visible) {
67        sVisible = visible;
68    }
69
70    public static void initializeDrawListener(View view) {
71        if (sGlobalDrawListener != null) {
72            view.getViewTreeObserver().removeOnDrawListener(sGlobalDrawListener);
73        }
74        sGlobalDrawListener = new ViewTreeObserver.OnDrawListener() {
75                private long mTime = System.currentTimeMillis();
76                public void onDraw() {
77                    sGlobalFrameCounter++;
78                    if (DEBUG) {
79                        long newTime = System.currentTimeMillis();
80                        Log.d("FirstFrameAnimatorHelper", "TICK " + (newTime - mTime));
81                        mTime = newTime;
82                    }
83                }
84            };
85        view.getViewTreeObserver().addOnDrawListener(sGlobalDrawListener);
86        sVisible = true;
87    }
88
89    public void onAnimationUpdate(final ValueAnimator animation) {
90        final long currentTime = System.currentTimeMillis();
91        if (mStartTime == -1) {
92            mStartFrame = sGlobalFrameCounter;
93            mStartTime = currentTime;
94        }
95
96        final long currentPlayTime = animation.getCurrentPlayTime();
97        boolean isFinalFrame = Float.compare(1f, animation.getAnimatedFraction()) == 0;
98
99        if (!mHandlingOnAnimationUpdate &&
100            sVisible &&
101            // If the current play time exceeds the duration, or the animated fraction is 1,
102            // the animation will get finished, even if we call setCurrentPlayTime -- therefore
103            // don't adjust the animation in that case
104            currentPlayTime < animation.getDuration() && !isFinalFrame) {
105            mHandlingOnAnimationUpdate = true;
106            long frameNum = sGlobalFrameCounter - mStartFrame;
107            // If we haven't drawn our first frame, reset the time to t = 0
108            // (give up after MAX_DELAY ms of waiting though - might happen, for example, if we
109            // are no longer in the foreground and no frames are being rendered ever)
110            if (frameNum == 0 && currentTime < mStartTime + MAX_DELAY && currentPlayTime > 0) {
111                // The first frame on animations doesn't always trigger an invalidate...
112                // force an invalidate here to make sure the animation continues to advance
113                mTarget.getRootView().invalidate();
114                animation.setCurrentPlayTime(0);
115            // For the second frame, if the first frame took more than 16ms,
116            // adjust the start time and pretend it took only 16ms anyway. This
117            // prevents a large jump in the animation due to an expensive first frame
118            } else if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY &&
119                       !mAdjustedSecondFrameTime &&
120                       currentTime > mStartTime + IDEAL_FRAME_DURATION &&
121                       currentPlayTime > IDEAL_FRAME_DURATION) {
122                animation.setCurrentPlayTime(IDEAL_FRAME_DURATION);
123                mAdjustedSecondFrameTime = true;
124            } else {
125                if (frameNum > 1) {
126                    mTarget.post(new Runnable() {
127                            public void run() {
128                                animation.removeUpdateListener(FirstFrameAnimatorHelper.this);
129                            }
130                        });
131                }
132                if (DEBUG) print(animation);
133            }
134            mHandlingOnAnimationUpdate = false;
135        } else {
136            if (DEBUG) print(animation);
137        }
138    }
139
140    public void print(ValueAnimator animation) {
141        float flatFraction = animation.getCurrentPlayTime() / (float) animation.getDuration();
142        Log.d("FirstFrameAnimatorHelper", sGlobalFrameCounter +
143              "(" + (sGlobalFrameCounter - mStartFrame) + ") " + mTarget + " dirty? " +
144              mTarget.isDirty() + " " + flatFraction + " " + this + " " + animation);
145    }
146}
147