/*
* Copyright (c) 2015-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.imagepipeline.animated.base;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.logging.FLog;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.time.MonotonicClock;
import com.facebook.drawable.base.DrawableWithCaches;
/**
* A {@link Drawable} that renders a animated image. The details of the format are abstracted by the
* {@link AnimatedDrawableBackend} interface. The drawable can work either as an {@link Animatable}
* where the client calls start/stop to animate it or it can work as a level-based drawable where
* the client drives the animation by calling {@link Drawable#setLevel}.
*/
public abstract class AbstractAnimatedDrawable extends Drawable
implements Animatable, DrawableWithCaches {
private static final Class<?> TAG = AnimatedDrawable.class;
private static final long WATCH_DOG_TIMER_POLL_INTERVAL_MS = 2000;
private static final long WATCH_DOG_TIMER_MIN_TIMEOUT_MS = 1000;
private static final int POLL_FOR_RENDERED_FRAME_MS = 5;
private static final int NO_FRAME = -1;
private final ScheduledExecutorService mScheduledExecutorServiceForUiThread;
private final AnimatedDrawableDiagnostics mAnimatedDrawableDiagnostics;
private final MonotonicClock mMonotonicClock;
private final int mDurationMs;
private final int mFrameCount;
private final int mLoopCount;
// Paint used to draw on a Canvas
private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
private final Rect mDstRect = new Rect();
private final Paint mTransparentPaint;
private volatile String mLogId;
private AnimatedDrawableCachingBackend mAnimatedDrawableBackend;
private long mStartTimeMs;
// Index of frame scheduled to be drawn. Between 0 and mFrameCount - 1
private int mScheduledFrameNumber;
// Index of frame scheduled to be drawn but never is reset to zero. Keeps growing.
private int mScheduledFrameMonotonicNumber;
// Index of frame that will be drawn next. Between 0 and mFrameCount - 1. May fall behind
// mScheduledFrameIndex if we can't keep up.
private int mPendingRenderedFrameNumber;
// Corresponds to mPendingRenderedFrameNumber but keeps growing.
private int mPendingRenderedFrameMonotonicNumber;
// Index of last frame that was drawn.
private int mLastDrawnFrameNumber = -1;
// Corresponds to mLastDrawnFrameNumber but keeps growing.
private int mLastDrawnFrameMonotonicNumber = -1;
// Bitmap for last drawn frame. Corresponds to mLastDrawnFrameNumber.
private CloseableReference<Bitmap> mLastDrawnFrame;
private boolean mWaitingForDraw;
private long mLastInvalidateTimeMs = -1;
private boolean mIsRunning;
private boolean mHaveWatchdogScheduled;
private float mSx = 1f;
private float mSy = 1f;
private boolean mApplyTransformation;
private boolean mInvalidateTaskScheduled;
private long mNextFrameTaskMs = -1;
private boolean mIsPaused = false;
private final Runnable mStartTask = new Runnable() {
@Override
public void run() {
onStart();
}
};
private final Runnable mNextFrameTask = new Runnable() {
@Override
public void run() {
FLog.v(TAG, "(%s) Next Frame Task", mLogId);
onNextFrame();
}
};
private final Runnable mInvalidateTask = new Runnable() {
@Override
public void run() {
FLog.v(TAG, "(%s) Invalidate Task", mLogId);
mInvalidateTaskScheduled = false;
doInvalidateSelf();
}
};
private final Runnable mWatchdogTask = new Runnable() {
@Override
public void run() {
FLog.v(TAG, "(%s) Watchdog Task", mLogId);
doWatchdogCheck();
}
};
public AbstractAnimatedDrawable(
ScheduledExecutorService scheduledExecutorServiceForUiThread,
AnimatedDrawableCachingBackend animatedDrawableBackend,
AnimatedDrawableDiagnostics animatedDrawableDiagnostics,
MonotonicClock monotonicClock) {
mScheduledExecutorServiceForUiThread = scheduledExecutorServiceForUiThread;
mAnimatedDrawableBackend = animatedDrawableBackend;
mAnimatedDrawableDiagnostics = animatedDrawableDiagnostics;
mMonotonicClock = monotonicClock;
mDurationMs = mAnimatedDrawableBackend.getDurationMs();
mFrameCount = mAnimatedDrawableBackend.getFrameCount();
mAnimatedDrawableDiagnostics.setBackend(mAnimatedDrawableBackend);
mLoopCount = mAnimatedDrawableBackend.getLoopCount();
mTransparentPaint = new Paint();
mTransparentPaint.setColor(Color.TRANSPARENT);
mTransparentPaint.setStyle(Paint.Style.FILL);
// Show last frame when not animating.
resetToPreviewFrame();
}
private void resetToPreviewFrame() {
mScheduledFrameNumber = mAnimatedDrawableBackend.getFrameForPreview();
mScheduledFrameMonotonicNumber = mScheduledFrameNumber;
mPendingRenderedFrameNumber = NO_FRAME;
mPendingRenderedFrameMonotonicNumber = NO_FRAME;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (mLastDrawnFrame != null) {
mLastDrawnFrame.close();
mLastDrawnFrame = null;
}
}
/**
* Sets an id that will be logged with any of the logging calls. Useful for debugging.
*
* @param logId the id to log
*/
public void setLogId(String logId) {
mLogId = logId;
}
@Override
public int getIntrinsicWidth() {
return mAnimatedDrawableBackend.getWidth();
}
@Override
public int getIntrinsicHeight() {
return mAnimatedDrawableBackend.getHeight();
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
doInvalidateSelf();
}
@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
doInvalidateSelf();
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mApplyTransformation = true;
if (mLastDrawnFrame != null) {
mLastDrawnFrame.close();
mLastDrawnFrame = null;
}
mLastDrawnFrameNumber = -1;
mLastDrawnFrameMonotonicNumber = -1;
mAnimatedDrawableBackend.dropCaches();
}
private void onStart() {
if (!mIsRunning) {
return;
}
mAnimatedDrawableDiagnostics.onStartMethodBegin();
try {
mStartTimeMs = mMonotonicClock.now();
if (mIsPaused) {
mStartTimeMs -= mAnimatedDrawableBackend.getTimestampMsForFrame(mScheduledFrameNumber);
} else {
mScheduledFrameNumber = 0;
mScheduledFrameMonotonicNumber = 0;
}
long nextFrameMs = mStartTimeMs + mAnimatedDrawableBackend.getDurationMsForFrame(0);
scheduleSelf(mNextFrameTask, nextFrameMs);
mNextFrameTaskMs = nextFrameMs;
doInvalidateSelf();
} finally {
mAnimatedDrawableDiagnostics.onStartMethodEnd();
}
}
private void onNextFrame() {
mNextFrameTaskMs = -1;
if (!mIsRunning) {
return;
}
if (mDurationMs == 0) {
return;
}
mAnimatedDrawableDiagnostics.onNextFrameMethodBegin();
try {
computeAndScheduleNextFrame(true /* schedule next frame */);
} finally {
mAnimatedDrawableDiagnostics.onNextFrameMethodEnd();
}
}
private void computeAndScheduleNextFrame(boolean scheduleNextFrame) {
if (mDurationMs == 0) {
return;
}
long nowMs = mMonotonicClock.now();
int loops = (int) ((nowMs - mStartTimeMs) / mDurationMs);
if (mLoopCount != AnimatedImage.LOOP_COUNT_INFINITE && loops >= mLoopCount) {
//we stop the animation if we have exceeded the total loop count
return;
}
int timestampMs = (int) ((nowMs - mStartTimeMs) % mDurationMs);
int newCurrentFrameNumber = mAnimatedDrawableBackend.getFrameForTimestampMs(timestampMs);
boolean changed = mScheduledFrameNumber != newCurrentFrameNumber;
mScheduledFrameNumber = newCurrentFrameNumber;
mScheduledFrameMonotonicNumber = loops * mFrameCount + newCurrentFrameNumber;
if (!scheduleNextFrame) {
// We're about to draw. We don't need to schedule anything because we're going to draw
// that frame right now. the onDraw method just wants to make sure the current frame is set.
return;
}
if (changed) {
doInvalidateSelf();
} else {
int durationMs = mAnimatedDrawableBackend.getTimestampMsForFrame(mScheduledFrameNumber) +
mAnimatedDrawableBackend.getDurationMsForFrame(mScheduledFrameNumber) -
timestampMs;
int nextFrame = (mScheduledFrameNumber + 1) % mFrameCount;
long nextFrameMs = nowMs + durationMs;
if (mNextFrameTaskMs == -1 || mNextFrameTaskMs > nextFrameMs) {
FLog.v(TAG, "(%s) Next frame (%d) in %d ms", mLogId, nextFrame, durationMs);
unscheduleSelf(mNextFrameTask); // Cancel any existing task.
scheduleSelf(mNextFrameTask, nextFrameMs);
mNextFrameTaskMs = nextFrameMs;
}
}
}
@Override
public void draw(Canvas canvas) {
mAnimatedDrawableDiagnostics.onDrawMethodBegin();
try {
mWaitingForDraw = false;
if (mIsRunning && !mHaveWatchdogScheduled) {
mScheduledExecutorServiceForUiThread.schedule(
mWatchdogTask,
WATCH_DOG_TIMER_POLL_INTERVAL_MS,
TimeUnit.MILLISECONDS);
mHaveWatchdogScheduled = true;
}
if (mApplyTransformation) {
mDstRect.set(getBounds());
if (!mDstRect.isEmpty()) {
AnimatedDrawableCachingBackend newBackend =
mAnimatedDrawableBackend.forNewBounds(mDstRect);
if (newBackend != mAnimatedDrawableBackend) {
mAnimatedDrawableBackend.dropCaches();
mAnimatedDrawableBackend = newBackend;
mAnimatedDrawableDiagnostics.setBackend(newBackend);
}
mSx = (float) mDstRect.width() / mAnimatedDrawableBackend.getRenderedWidth();
mSy = (float) mDstRect.height() / mAnimatedDrawableBackend.getRenderedHeight();
mApplyTransformation = false;
}
}
if (mDstRect.isEmpty()) {
// Don't try to draw if the dest rect is empty.
return;
}
canvas.save();
canvas.scale(mSx, mSy);
// TODO(6169940) we overdraw if both pending frame is ready and current frame is ready.
boolean didDrawFrame = false;
if (mPendingRenderedFrameNumber != NO_FRAME) {
// We tried to render a frame and it wasn't yet ready. See if it's ready now.
boolean rendered =
renderFrame(canvas, mPendingRenderedFrameNumber, mPendingRenderedFrameMonotonicNumber);
didDrawFrame |= rendered;
if (rendered) {
FLog.v(TAG, "(%s) Rendered pending frame %d", mLogId, mPendingRenderedFrameNumber);
mPendingRenderedFrameNumber = NO_FRAME;
mPendingRenderedFrameMonotonicNumber = NO_FRAME;
} else {
// Try again later.
FLog.v(TAG, "(%s) Trying again later for pending %d", mLogId, mPendingRenderedFrameNumber);
scheduleInvalidatePoll();
}
}
if (mPendingRenderedFrameNumber == NO_FRAME) {
// We don't have a frame that's pending so render the current frame.
if (mIsRunning) {
computeAndScheduleNextFrame(false /* don't schedule yet */);
}
boolean rendered = renderFrame(
canvas,
mScheduledFrameNumber,
mScheduledFrameMonotonicNumber);
didDrawFrame |= rendered;
if (rendered) {
FLog.v(TAG, "(%s) Rendered current frame %d", mLogId, mScheduledFrameNumber);
if (mIsRunning) {
computeAndScheduleNextFrame(true /* schedule next frame */);
}
} else {
FLog.v(TAG, "(%s) Trying again later for current %d", mLogId, mScheduledFrameNumber);
mPendingRenderedFrameNumber = mScheduledFrameNumber;
mPendingRenderedFrameMonotonicNumber = mScheduledFrameMonotonicNumber;
scheduleInvalidatePoll();
}
}
if (!didDrawFrame) {
if (mLastDrawnFrame != null) {
canvas.drawBitmap(mLastDrawnFrame.get(), 0f, 0f, mPaint);
didDrawFrame = true;
FLog.v(TAG, "(%s) Rendered last known frame %d", mLogId, mLastDrawnFrameNumber);
}
}
if (!didDrawFrame) {
// Last ditch effort, use preview bitmap.
CloseableReference<Bitmap> previewBitmapReference =
mAnimatedDrawableBackend.getPreviewBitmap();
if (previewBitmapReference != null) {
canvas.drawBitmap(previewBitmapReference.get(), 0f, 0f, mPaint);
previewBitmapReference.close();
FLog.v(TAG, "(%s) Rendered preview frame", mLogId);
didDrawFrame = true;
}
}
if (!didDrawFrame) {
// TODO(6169940) this may not be necessary. Confirm with Rich.
canvas.drawRect(0, 0, mDstRect.width(), mDstRect.height(), mTransparentPaint);
FLog.v(TAG, "(%s) Failed to draw a frame", mLogId);
}
canvas.restore();
mAnimatedDrawableDiagnostics.drawDebugOverlay(canvas, mDstRect);
} finally {
mAnimatedDrawableDiagnostics.onDrawMethodEnd();
}
}
/**
* Schedule a task to invalidate the drawable. Used to poll for a rendered frame.
*/
private void scheduleInvalidatePoll() {
if (mInvalidateTaskScheduled) {
return;
}
mInvalidateTaskScheduled = true;
scheduleSelf(mInvalidateTask, POLL_FOR_RENDERED_FRAME_MS);
}
/**
* Returns whether a previous call to {@link #draw} would have rendered a frame.
*
* @return whether a previous call to {@link #draw} would have rendered a frame
*/
public boolean didLastDrawRender() {
return mLastDrawnFrame != null;
}
/**
* Renders the specified frame to the canvas.
*
* @param canvas the canvas to render to
* @param frameNumber the relative frame number (between 0 and frame count)
* @param frameMonotonicNumber the absolute frame number for stats purposes
* @return whether the frame was available and was rendered
*/
private boolean renderFrame(
Canvas canvas,
int frameNumber,
int frameMonotonicNumber) {
CloseableReference<Bitmap> bitmapReference =
mAnimatedDrawableBackend.getBitmapForFrame(frameNumber);
if (bitmapReference != null) {
canvas.drawBitmap(bitmapReference.get(), 0f, 0f, mPaint);
if (mLastDrawnFrame != null) {
mLastDrawnFrame.close();
}
if (mIsRunning && frameMonotonicNumber > mLastDrawnFrameMonotonicNumber) {
int droppedFrames = frameMonotonicNumber - mLastDrawnFrameMonotonicNumber - 1;
mAnimatedDrawableDiagnostics.incrementDrawnFrames(1);
mAnimatedDrawableDiagnostics.incrementDroppedFrames(droppedFrames);
if (droppedFrames > 0) {
FLog.v(TAG, "(%s) Dropped %d frames", mLogId, droppedFrames);
}
}
mLastDrawnFrame = bitmapReference;
mLastDrawnFrameNumber = frameNumber;
mLastDrawnFrameMonotonicNumber = frameMonotonicNumber;
FLog.v(TAG, "(%s) Drew frame %d", mLogId, frameNumber);
return true;
}
return false;
}
/**
* Checks to make sure we drop our caches if we haven't drawn in a while. There's no reliable
* way for a Drawable to determine if it's still actively part of a View, so we use a heuristic
* instead.
*/
private void doWatchdogCheck() {
mHaveWatchdogScheduled = false;
if (!mIsRunning) {
return;
}
long now = mMonotonicClock.now();
// Timeout if it's been more than 2 seconds with drawn since invalidation.
boolean hasNotDrawnWithinTimeout =
mWaitingForDraw && now - mLastInvalidateTimeMs > WATCH_DOG_TIMER_MIN_TIMEOUT_MS;
// Also timeout onNextFrame is more than 2 seconds late.
boolean hasNotAdvancedFrameWithinTimeout =
mNextFrameTaskMs != -1 && now - mNextFrameTaskMs > WATCH_DOG_TIMER_MIN_TIMEOUT_MS;
if (hasNotDrawnWithinTimeout || hasNotAdvancedFrameWithinTimeout) {
dropCaches();
doInvalidateSelf();
} else {
mScheduledExecutorServiceForUiThread.schedule(
mWatchdogTask,
WATCH_DOG_TIMER_POLL_INTERVAL_MS,
TimeUnit.MILLISECONDS);
mHaveWatchdogScheduled = true;
}
}
private void doInvalidateSelf() {
mWaitingForDraw = true;
mLastInvalidateTimeMs = mMonotonicClock.now();
invalidateSelf();
}
@VisibleForTesting
boolean isWaitingForDraw() {
return mWaitingForDraw;
}
@VisibleForTesting
boolean isWaitingForNextFrame() {
return mNextFrameTaskMs != -1;
}
@VisibleForTesting
int getScheduledFrameNumber() {
return mScheduledFrameNumber;
}
@Override
public void start() {
if (mDurationMs == 0 || mFrameCount <= 1) {
return;
}
mIsRunning = true;
scheduleSelf(mStartTask, mMonotonicClock.now());
}
@Override
public void stop() {
mIsPaused = false;
mIsRunning = false;
}
public void pause() {
mIsPaused = true;
mIsRunning = false;
}
@Override
public boolean isRunning() {
return mIsRunning;
}
@Override
protected boolean onLevelChange(int level) {
if (mIsRunning) {
// If the client called start on us, they expect us to run the animation. In that case,
// we ignore level changes.
return false;
}
int frame = mAnimatedDrawableBackend.getFrameForTimestampMs(level);
if (frame == mScheduledFrameNumber) {
return false;
}
try {
mScheduledFrameNumber = frame;
mScheduledFrameMonotonicNumber = frame;
doInvalidateSelf();
return true;
} catch (IllegalStateException e) {
// The underlying image was disposed.
return false;
}
}
@Override
public void dropCaches() {
FLog.v(TAG, "(%s) Dropping caches", mLogId);
if (mLastDrawnFrame != null) {
mLastDrawnFrame.close();
mLastDrawnFrame = null;
mLastDrawnFrameNumber = -1;
mLastDrawnFrameMonotonicNumber = -1;
}
mAnimatedDrawableBackend.dropCaches();
}
/**
* Get the animation duration of 1 loop.
* @return the animation duration in ms
*/
public int getDuration() {
return mDurationMs;
}
/**
* Get the number of frames for the animation.
* @return the number of frames of the animation
*/
public int getFrameCount() {
return mFrameCount;
}
/**
* Get the loop count of the animation.
* The returned value is either {@link AnimatedImage#LOOP_COUNT_INFINITE} if the animation
* is repeated infinitely or a positive integer that corresponds to the number of loops.
* @return the loop count of the animation or {@link AnimatedImage#LOOP_COUNT_INFINITE}
*/
public int getLoopCount() {
return mLoopCount;
}
protected AnimatedDrawableCachingBackend getAnimatedDrawableBackend() {
return mAnimatedDrawableBackend;
}
}