/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.util.Log; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.ViewTreeObserver.OnDrawListener; /* * This is a helper class that listens to updates from the corresponding animation. * For the first two frames, it adjusts the current play time of the animation to * prevent jank at the beginning of the animation */ public class FirstFrameAnimatorHelper implements OnDrawListener, OnAttachStateChangeListener { private static final String TAG = "FirstFrameAnimatorHlpr"; private static final boolean DEBUG = false; private static final int MAX_DELAY = 1000; private View mRootView; private long mGlobalFrameCount; public FirstFrameAnimatorHelper(View target) { target.addOnAttachStateChangeListener(this); if (target.isAttachedToWindow()) { onViewAttachedToWindow(target); } } public T addTo(T anim) { anim.addUpdateListener(new MyListener()); return anim; } @Override public void onDraw() { mGlobalFrameCount ++; } @Override public void onViewAttachedToWindow(View view) { mRootView = view.getRootView(); mRootView.getViewTreeObserver().addOnDrawListener(this); } @Override public void onViewDetachedFromWindow(View view) { if (mRootView != null) { mRootView.getViewTreeObserver().removeOnDrawListener(this); mRootView = null; } } private class MyListener implements AnimatorUpdateListener { private long mStartFrame; private long mStartTime = -1; private boolean mHandlingOnAnimationUpdate; private boolean mAdjustedSecondFrameTime; @Override public void onAnimationUpdate(final ValueAnimator animation) { final long currentTime = System.currentTimeMillis(); if (mStartTime == -1) { mStartFrame = mGlobalFrameCount; mStartTime = currentTime; } final long currentPlayTime = animation.getCurrentPlayTime(); boolean isFinalFrame = Float.compare(1f, animation.getAnimatedFraction()) == 0; if (!mHandlingOnAnimationUpdate && mRootView != null && mRootView.getWindowVisibility() == View.VISIBLE && // If the current play time exceeds the duration, or the animated fraction is 1, // the animation will get finished, even if we call setCurrentPlayTime -- // therefore don't adjust the animation in that case currentPlayTime < animation.getDuration() && !isFinalFrame) { mHandlingOnAnimationUpdate = true; long frameNum = mGlobalFrameCount - mStartFrame; // If we haven't drawn our first frame, reset the time to t = 0 // (give up after MAX_DELAY ms of waiting though - might happen, for example, if we // are no longer in the foreground and no frames are being rendered ever) if (frameNum == 0 && currentTime < mStartTime + MAX_DELAY && currentPlayTime > 0) { // The first frame on animations doesn't always trigger an invalidate... // force an invalidate here to make sure the animation continues to advance mRootView.invalidate(); animation.setCurrentPlayTime(0); // For the second frame, if the first frame took more than 16ms, // adjust the start time and pretend it took only 16ms anyway. This // prevents a large jump in the animation due to an expensive first frame } else { int singleFrameMS = getSingleFrameMs(mRootView.getContext()); if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY && !mAdjustedSecondFrameTime && currentTime > mStartTime + singleFrameMS && currentPlayTime > singleFrameMS) { animation.setCurrentPlayTime(singleFrameMS); mAdjustedSecondFrameTime = true; } else { if (frameNum > 1) { mRootView.post(() -> animation.removeUpdateListener(this)); } if (DEBUG) print(animation); } } mHandlingOnAnimationUpdate = false; } else { if (DEBUG) print(animation); } } public void print(ValueAnimator animation) { float flatFraction = animation.getCurrentPlayTime() / (float) animation.getDuration(); Log.d(TAG, mGlobalFrameCount + "(" + (mGlobalFrameCount - mStartFrame) + ") " + mRootView + " dirty? " + mRootView.isDirty() + " " + flatFraction + " " + this + " " + animation); } } }