/*
* 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.fresco.animation.bitmap;
import javax.annotation.Nullable;
import java.lang.annotation.Retention;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.IntDef;
import android.support.annotation.IntRange;
import com.facebook.common.references.CloseableReference;
import com.facebook.fresco.animation.backend.AnimationBackend;
import com.facebook.fresco.animation.backend.AnimationBackendDelegateWithInactivityCheck;
import com.facebook.fresco.animation.backend.AnimationInformation;
import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparationStrategy;
import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparer;
import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory;
import static java.lang.annotation.RetentionPolicy.SOURCE;
/**
* Bitmap animation backend that renders bitmap frames.
*
* The given {@link BitmapFrameCache} is used to cache frames and create new bitmaps. {@link
* AnimationInformation} defines the main animation parameters, like frame and loop count. {@link
* BitmapFrameRenderer} is used to render frames to the bitmaps aquired from the {@link
* BitmapFrameCache}.
*/
public class BitmapAnimationBackend implements AnimationBackend,
AnimationBackendDelegateWithInactivityCheck.InactivityListener {
public interface FrameListener {
/**
* Called when the backend started drawing the given frame.
*
* @param backend the backend
* @param frameNumber the frame number to be drawn
*/
void onDrawFrameStart(BitmapAnimationBackend backend, int frameNumber);
/**
* Called when the given frame has been drawn.
*
* @param backend the backend
* @param frameNumber the frame number that has been drawn
* @param frameType the {@link FrameType} that has been drawn
*/
void onFrameDrawn(BitmapAnimationBackend backend, int frameNumber, @FrameType int frameType);
/**
* Called when no bitmap could be drawn by the backend for the given frame number.
*
* @param backend the backend
* @param frameNumber the frame number that could not be drawn
*/
void onFrameDropped(BitmapAnimationBackend backend, int frameNumber);
}
/**
* Frame type that has been drawn. Can be used for logging.
*/
@Retention(SOURCE)
@IntDef({
FRAME_TYPE_UNKNOWN,
FRAME_TYPE_CACHED,
FRAME_TYPE_REUSED,
FRAME_TYPE_CREATED,
FRAME_TYPE_FALLBACK,
})
public @interface FrameType {
}
public static final int FRAME_TYPE_UNKNOWN = -1;
public static final int FRAME_TYPE_CACHED = 0;
public static final int FRAME_TYPE_REUSED = 1;
public static final int FRAME_TYPE_CREATED = 2;
public static final int FRAME_TYPE_FALLBACK = 3;
private final PlatformBitmapFactory mPlatformBitmapFactory;
private final BitmapFrameCache mBitmapFrameCache;
private final AnimationInformation mAnimationInformation;
private final BitmapFrameRenderer mBitmapFrameRenderer;
@Nullable
private final BitmapFramePreparationStrategy mBitmapFramePreparationStrategy;
@Nullable
private final BitmapFramePreparer mBitmapFramePreparer;
private final Paint mPaint;
@Nullable
private Rect mBounds;
private int mBitmapWidth;
private int mBitmapHeight;
private Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888;
@Nullable
private FrameListener mFrameListener;
public BitmapAnimationBackend(
PlatformBitmapFactory platformBitmapFactory,
BitmapFrameCache bitmapFrameCache,
AnimationInformation animationInformation,
BitmapFrameRenderer bitmapFrameRenderer,
@Nullable BitmapFramePreparationStrategy bitmapFramePreparationStrategy,
@Nullable BitmapFramePreparer bitmapFramePreparer) {
mPlatformBitmapFactory = platformBitmapFactory;
mBitmapFrameCache = bitmapFrameCache;
mAnimationInformation = animationInformation;
mBitmapFrameRenderer = bitmapFrameRenderer;
mBitmapFramePreparationStrategy = bitmapFramePreparationStrategy;
mBitmapFramePreparer = bitmapFramePreparer;
mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
updateBitmapDimensions();
}
/**
* Set the bitmap config to be used to create new bitmaps.
*
* @param bitmapConfig the bitmap config to be used
*/
public void setBitmapConfig(Bitmap.Config bitmapConfig) {
mBitmapConfig = bitmapConfig;
}
public void setFrameListener(@Nullable FrameListener frameListener) {
mFrameListener = frameListener;
}
@Override
public int getFrameCount() {
return mAnimationInformation.getFrameCount();
}
@Override
public int getFrameDurationMs(int frameNumber) {
return mAnimationInformation.getFrameDurationMs(frameNumber);
}
@Override
public int getLoopCount() {
return mAnimationInformation.getLoopCount();
}
@Override
public boolean drawFrame(
Drawable parent,
Canvas canvas,
int frameNumber) {
if (mFrameListener != null) {
mFrameListener.onDrawFrameStart(this, frameNumber);
}
boolean drawn = drawFrameOrFallback(canvas, frameNumber, FRAME_TYPE_CACHED);
// We could not draw anything
if (!drawn && mFrameListener != null) {
mFrameListener.onFrameDropped(this, frameNumber);
}
// Prepare next frames
if (mBitmapFramePreparationStrategy != null && mBitmapFramePreparer != null) {
mBitmapFramePreparationStrategy.prepareFrames(
mBitmapFramePreparer,
mBitmapFrameCache,
this,
frameNumber);
}
return drawn;
}
private boolean drawFrameOrFallback(Canvas canvas, int frameNumber, @FrameType int frameType) {
CloseableReference<Bitmap> bitmapReference = null;
boolean drawn = false;
int nextFrameType = FRAME_TYPE_UNKNOWN;
try {
switch (frameType) {
case FRAME_TYPE_CACHED:
bitmapReference = mBitmapFrameCache.getCachedFrame(frameNumber);
drawn = drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_CACHED);
nextFrameType = FRAME_TYPE_REUSED;
break;
case FRAME_TYPE_REUSED:
bitmapReference =
mBitmapFrameCache.getBitmapToReuseForFrame(frameNumber, mBitmapWidth, mBitmapHeight);
// Try to render the frame and draw on the canvas immediately after
drawn = renderFrameInBitmap(frameNumber, bitmapReference) &&
drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_REUSED);
nextFrameType = FRAME_TYPE_CREATED;
break;
case FRAME_TYPE_CREATED:
bitmapReference =
mPlatformBitmapFactory.createBitmap(mBitmapWidth, mBitmapHeight, mBitmapConfig);
// Try to render the frame and draw on the canvas immediately after
drawn = renderFrameInBitmap(frameNumber, bitmapReference) &&
drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_CREATED);
nextFrameType = FRAME_TYPE_FALLBACK;
break;
case FRAME_TYPE_FALLBACK:
bitmapReference = mBitmapFrameCache.getFallbackFrame(frameNumber);
drawn = drawBitmapAndCache(frameNumber, bitmapReference, canvas, FRAME_TYPE_FALLBACK);
break;
default:
return false;
}
} finally {
CloseableReference.closeSafely(bitmapReference);
}
if (drawn || nextFrameType == FRAME_TYPE_UNKNOWN) {
return drawn;
} else {
return drawFrameOrFallback(canvas, frameNumber, nextFrameType);
}
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public void setBounds(@Nullable Rect bounds) {
mBounds = bounds;
mBitmapFrameRenderer.setBounds(bounds);
updateBitmapDimensions();
}
@Override
public int getIntrinsicWidth() {
return mBitmapWidth;
}
@Override
public int getIntrinsicHeight() {
return mBitmapHeight;
}
@Override
public int getSizeInBytes() {
return mBitmapFrameCache.getSizeInBytes();
}
@Override
public void clear() {
mBitmapFrameCache.clear();
}
@Override
public void onInactive() {
clear();
}
private void updateBitmapDimensions() {
// Calculate the correct bitmap dimensions
mBitmapWidth = mBitmapFrameRenderer.getIntrinsicWidth();
if (mBitmapWidth == INTRINSIC_DIMENSION_UNSET) {
mBitmapWidth = mBounds == null ? INTRINSIC_DIMENSION_UNSET : mBounds.width();
}
mBitmapHeight = mBitmapFrameRenderer.getIntrinsicHeight();
if (mBitmapHeight == INTRINSIC_DIMENSION_UNSET) {
mBitmapHeight = mBounds == null ? INTRINSIC_DIMENSION_UNSET : mBounds.height();
}
}
/**
* Try to render the frame to the given target bitmap. If the rendering fails, the target bitmap
* reference will be closed and false is returned. If rendering succeeds, the target bitmap
* reference can be drawn and has to be manually closed after drawing has been completed.
*
* @param frameNumber the frame number to render
* @param targetBitmap the target bitmap
* @return true if rendering successful
*/
private boolean renderFrameInBitmap(
int frameNumber,
@Nullable CloseableReference<Bitmap> targetBitmap) {
if (!CloseableReference.isValid(targetBitmap)) {
return false;
}
// Render the image
boolean frameRendered =
mBitmapFrameRenderer.renderFrame(frameNumber, targetBitmap.get());
if (!frameRendered) {
CloseableReference.closeSafely(targetBitmap);
}
return frameRendered;
}
/**
* Helper method that draws the given bitmap on the canvas respecting the bounds (if set).
*
* If rendering was successful, it notifies the cache that the frame has been rendered with the
* given bitmap. In addition, it will notify the {@link FrameListener} if set.
*
* @param frameNumber the current frame number passed to the cache
* @param bitmapReference the bitmap to draw
* @param canvas the canvas to draw an
* @param frameType the {@link FrameType} to be rendered
* @return true if the bitmap has been drawn
*/
private boolean drawBitmapAndCache(
int frameNumber,
@Nullable CloseableReference<Bitmap> bitmapReference,
Canvas canvas,
@FrameType int frameType) {
if (!CloseableReference.isValid(bitmapReference)) {
return false;
}
if (mBounds == null) {
canvas.drawBitmap(bitmapReference.get(), 0f, 0f, mPaint);
} else {
canvas.drawBitmap(bitmapReference.get(), null, mBounds, mPaint);
}
// Notify the cache that a frame has been rendered.
// We should not cache fallback frames since they do not represent the actual frame.
if (frameType != FRAME_TYPE_FALLBACK) {
mBitmapFrameCache.onFrameRendered(
frameNumber,
bitmapReference,
frameType);
}
if (mFrameListener != null) {
mFrameListener.onFrameDrawn(this, frameNumber, frameType);
}
return true;
}
}