/** * Copyright (c) 2016-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.keyframes; import android.annotation.TargetApi; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.view.Choreographer; import com.facebook.keyframes.model.KFImage; import java.lang.ref.WeakReference; /** * A simple callback that when run, will call back indefinitely with progress updates until * cancelled. This will continuously feed back progress data from [0, 1] calculated by millis * per loop. */ public abstract class KeyframesDrawableAnimationCallback { /** * And interface for a class which wants to listen to progress updates. */ public interface FrameListener { void onProgressUpdate(float frameProgress); void onStop(); } private final WeakReference<FrameListener> mListener; private final int mFrameCount; private final int mMillisPerLoop; private long mStartTimeMillis; private boolean mStopAtLoopEnd; private int mCurrentLoopNumber; private long mMinimumMillisBetweenProgressUpdates = -1; private long mPreviousProgressMillis = 0; /** * Creates a KeyframesDrawableAnimationCallback appropriate for the API level of the device. * @param listener The listener that will receive callbacks on updates to the value * @return A KeyframesDrawableAnimationCallback implementation */ public static KeyframesDrawableAnimationCallback create( FrameListener listener, KFImage face) { if (hasChoreographer()) { return new FrameCallbackFaceAnimationCallback( listener, face.getFrameRate(), face.getFrameCount()); } else { return new RunnableFaceAnimationCallback( listener, face.getFrameRate(), face.getFrameCount()); } } private static boolean hasChoreographer() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; } private KeyframesDrawableAnimationCallback(FrameListener listener, int frameRate, int frameCount) { mListener = new WeakReference<>(listener); mFrameCount = frameCount; mMillisPerLoop = Math.round(1000 * ((float) frameCount / frameRate)); } /** * Set the maximum frame rate for this animation. * Consider using this for low end devices. * @param maxFrameRate */ public void setMaxFrameRate(int maxFrameRate) { mMinimumMillisBetweenProgressUpdates = 1000 / maxFrameRate; } protected abstract void postCallback(); protected abstract void cancelCallback(); /** * Starts this animation callback and resets the start time. * * !IMPORTANT! This animator will run indefinitely, so it must be cancelled via #stop() * or #pause() when no longer in use! */ public void start() { mStopAtLoopEnd = false; mStartTimeMillis = 0; mCurrentLoopNumber = -1; cancelCallback(); postCallback(); } /** * Starts the animation and plays it once */ public void playOnce() { mStopAtLoopEnd = true; mStartTimeMillis = 0; mCurrentLoopNumber = 0; cancelCallback(); postCallback(); } /** * Stops the callbacks animation and resets the start time. */ public void stop() { cancelCallback(); mStartTimeMillis = 0; mCurrentLoopNumber = -1; mListener.get().onStop(); } /** * Pauses the callbacks animation and saves start time. */ public void pause() { cancelCallback(); mStartTimeMillis *= -1; } /** * Resumes this animation callback. * * !IMPORTANT! This animator will run indefinitely, so it must be cancelled via #stop() * or #pause() when no longer in use! */ public void resume() { mStopAtLoopEnd = false; cancelCallback(); postCallback(); } /** * Stops looping the animation, but finishes the current animation. */ public void stopAtLoopEnd() { mStopAtLoopEnd = true; } protected void advanceAnimation(final long frameTimeMillis) { if (mListener.get() == null) { cancelCallback(); mStartTimeMillis = 0; mPreviousProgressMillis = 0; mCurrentLoopNumber = -1; return; } if (mStartTimeMillis == 0) { mStartTimeMillis = frameTimeMillis; } else if (mStartTimeMillis < 0) { long pausedTimeMillis = frameTimeMillis - mPreviousProgressMillis; mStartTimeMillis = mStartTimeMillis * -1 + pausedTimeMillis; mPreviousProgressMillis += pausedTimeMillis; } int currentLoopNumber = (int) (frameTimeMillis - mStartTimeMillis) / mMillisPerLoop; final boolean loopHasEnded = currentLoopNumber > mCurrentLoopNumber; if (mStopAtLoopEnd && loopHasEnded) { mListener.get().onProgressUpdate(mFrameCount); stop(); return; } long currentProgressMillis = (frameTimeMillis - mStartTimeMillis) % mMillisPerLoop; boolean shouldUpdateProgress = true; if (frameTimeMillis - mPreviousProgressMillis < mMinimumMillisBetweenProgressUpdates) { shouldUpdateProgress = false; } else { mPreviousProgressMillis = frameTimeMillis; } if (shouldUpdateProgress) { mListener.get().onProgressUpdate((float) currentProgressMillis / mMillisPerLoop * mFrameCount); } mCurrentLoopNumber = (int) (frameTimeMillis - mStartTimeMillis) / mMillisPerLoop; postCallback(); } @TargetApi(16) private static class FrameCallbackFaceAnimationCallback extends KeyframesDrawableAnimationCallback implements Choreographer.FrameCallback { private final Choreographer mChoreographer; private FrameCallbackFaceAnimationCallback( FrameListener listener, int frameRate, int frameCount) { super(listener, frameRate, frameCount); mChoreographer = Choreographer.getInstance(); } @Override public void doFrame(long frameTimeNanos) { advanceAnimation(frameTimeNanos / 1000000); // nanoseconds per millisecond } @Override protected void postCallback() { mChoreographer.postFrameCallback(this); } @Override protected void cancelCallback() { mChoreographer.removeFrameCallback(this); } } private static class RunnableFaceAnimationCallback extends KeyframesDrawableAnimationCallback implements Runnable { private static final int ANIMATION_MIN_STEP_TIME_MS = 25; // 40 fps private final Handler mHandler; private RunnableFaceAnimationCallback( FrameListener listener, int frameRate, int frameCount) { super(listener, frameRate, frameCount); mHandler = new Handler(Looper.getMainLooper()); } @Override public void run() { advanceAnimation(SystemClock.uptimeMillis()); } @Override protected void postCallback() { mHandler.postDelayed(this, ANIMATION_MIN_STEP_TIME_MS); } @Override protected void cancelCallback() { mHandler.removeCallbacks(this); } } }