/*
* 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.impl;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import com.facebook.common.references.CloseableReference;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableBackend;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.BlendOperation;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo.DisposalMethod;
import com.facebook.imagepipeline.animated.base.AnimatedImage;
/**
* Contains the logic for compositing the frames of an {@link AnimatedImage}. Animated image
* formats like GIF and WebP support inter-frame compression where a subsequent frame may require
* being blended on a previous frame in order to render the full frame. This class encapsulates
* the behavior to be able to render any frame of the image. Designed to work with a cache
* via a Callback.
*/
public class AnimatedImageCompositor {
/**
* Callback for caching.
*/
public interface Callback {
/**
* Called from within {@link #renderFrame} to let the caller know that while trying generate
* the requested frame, an earlier frame was generated. This allows the caller to optionally
* cache the intermediate result. The caller must copy the Bitmap if it wishes to cache it
* as {@link #renderFrame} will continue using it generate the requested frame.
*
* @param frameNumber the frame number of the intermediate result
* @param bitmap the bitmap which must not be modified or directly cached
*/
void onIntermediateResult(int frameNumber, Bitmap bitmap);
/**
* Called from within {@link #renderFrame} to ask the caller for a cached bitmap for the
* specified frame number. If the caller has the bitmap cached, it can greatly reduce the
* work required to render the requested frame.
*
* @param frameNumber the frame number to get
* @return a reference to the bitmap. The ownership of the reference is passed to the caller
* who must close it.
*/
CloseableReference<Bitmap> getCachedBitmap(int frameNumber);
}
private final AnimatedDrawableBackend mAnimatedDrawableBackend;
private final Callback mCallback;
private final Paint mTransparentFillPaint;
public AnimatedImageCompositor(
AnimatedDrawableBackend animatedDrawableBackend,
Callback callback) {
mAnimatedDrawableBackend = animatedDrawableBackend;
mCallback = callback;
mTransparentFillPaint = new Paint();
mTransparentFillPaint.setColor(Color.TRANSPARENT);
mTransparentFillPaint.setStyle(Paint.Style.FILL);
mTransparentFillPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
}
/**
* Renders the specified frame. Only should be called on the rendering thread.
*
* @param frameNumber the frame to render
* @param bitmap the bitmap to render into
*/
public void renderFrame(int frameNumber, Bitmap bitmap) {
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SRC);
// If blending is required, prepare the canvas with the nearest cached frame.
int nextIndex;
if (!isKeyFrame(frameNumber)) {
// Blending is required. nextIndex points to the next index to render onto the canvas.
nextIndex = prepareCanvasWithClosestCachedFrame(frameNumber - 1, canvas);
} else {
// Blending isn't required. Start at the frame we're trying to render.
nextIndex = frameNumber;
}
// Iterate from nextIndex to the frame number just preceding the one we're trying to render
// and composite them in order according to the Disposal Method.
for (int index = nextIndex; index < frameNumber; index++) {
AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
DisposalMethod disposalMethod = frameInfo.disposalMethod;
if (disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS) {
continue;
}
if (frameInfo.blendOperation == BlendOperation.NO_BLEND) {
disposeToBackground(canvas, frameInfo);
}
mAnimatedDrawableBackend.renderFrame(index, canvas);
mCallback.onIntermediateResult(index, bitmap);
if (disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) {
disposeToBackground(canvas, frameInfo);
}
}
AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(frameNumber);
if (frameInfo.blendOperation == BlendOperation.NO_BLEND) {
disposeToBackground(canvas, frameInfo);
}
// Finally, we render the current frame. We don't dispose it.
mAnimatedDrawableBackend.renderFrame(frameNumber, canvas);
}
/**
* Return value for {@link #isFrameNeededForRendering} used in the compositing logic.
*/
private enum FrameNeededResult {
/** The frame is required to render the next frame */
REQUIRED,
/** The frame is not required to render the next frame. */
NOT_REQUIRED,
/** Skip this frame and keep going. Used for GIF's DISPOSE_TO_PREVIOUS */
SKIP,
/** Stop processing at this frame. This means the image didn't specify the disposal method */
ABORT
}
/**
* Given a frame number, prepares the canvas to render based on the nearest cached frame
* at or before the frame. On return the canvas will be prepared as if the nearest cached
* frame had been rendered and disposed. The returned index is the next frame that needs to be
* composited onto the canvas.
*
* @param previousFrameNumber the frame number that is ones less than the one we're rendering
* @param canvas the canvas to prepare
* @return the index of the the next frame to process
*/
private int prepareCanvasWithClosestCachedFrame(int previousFrameNumber, Canvas canvas) {
for (int index = previousFrameNumber; index >= 0; index--) {
FrameNeededResult neededResult = isFrameNeededForRendering(index);
switch (neededResult) {
case REQUIRED:
AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
CloseableReference<Bitmap> startBitmap = mCallback.getCachedBitmap(index);
if (startBitmap != null) {
try {
canvas.drawBitmap(startBitmap.get(), 0, 0, null);
if (frameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) {
disposeToBackground(canvas, frameInfo);
}
return index + 1;
} finally {
startBitmap.close();
}
} else {
if (isKeyFrame(index)) {
return index;
} else {
// Keep going.
break;
}
}
case NOT_REQUIRED:
return index + 1;
case ABORT:
return index;
case SKIP:
default:
// Keep going.
}
}
return 0;
}
private void disposeToBackground(Canvas canvas, AnimatedDrawableFrameInfo frameInfo) {
canvas.drawRect(
frameInfo.xOffset,
frameInfo.yOffset,
frameInfo.xOffset + frameInfo.width,
frameInfo.yOffset + frameInfo.height,
mTransparentFillPaint);
}
/**
* Returns whether the specified frame is needed for rendering the next frame. This is part of
* the compositing logic. See {@link FrameNeededResult} for more info about the results.
*
* @param index the frame to check
* @return whether the frame is required taking into account special conditions
*/
private FrameNeededResult isFrameNeededForRendering(int index) {
AnimatedDrawableFrameInfo frameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
DisposalMethod disposalMethod = frameInfo.disposalMethod;
if (disposalMethod == DisposalMethod.DISPOSE_DO_NOT) {
// Need this frame so keep going.
return FrameNeededResult.REQUIRED;
} else if (disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND) {
if (isFullFrame(frameInfo)) {
// The frame covered the whole image and we're disposing to background,
// so we don't even need to draw this frame.
return FrameNeededResult.NOT_REQUIRED;
} else {
// We need to draw the image. Then erase the part the previous frame covered.
// So keep going.
return FrameNeededResult.REQUIRED;
}
} else if (disposalMethod == DisposalMethod.DISPOSE_TO_PREVIOUS) {
return FrameNeededResult.SKIP;
} else {
return FrameNeededResult.ABORT;
}
}
private boolean isKeyFrame(int index) {
if (index == 0) {
return true;
}
AnimatedDrawableFrameInfo currFrameInfo = mAnimatedDrawableBackend.getFrameInfo(index);
AnimatedDrawableFrameInfo prevFrameInfo = mAnimatedDrawableBackend.getFrameInfo(index - 1);
if (currFrameInfo.blendOperation == BlendOperation.NO_BLEND && isFullFrame(currFrameInfo)) {
return true;
} else
return prevFrameInfo.disposalMethod == DisposalMethod.DISPOSE_TO_BACKGROUND
&& isFullFrame(prevFrameInfo);
}
private boolean isFullFrame(AnimatedDrawableFrameInfo frameInfo) {
return frameInfo.xOffset == 0 &&
frameInfo.yOffset == 0 &&
frameInfo.width == mAnimatedDrawableBackend.getRenderedWidth() &&
frameInfo.height == mAnimatedDrawableBackend.getRenderedHeight();
}
}