/
AnimatedImageCompositor.java
277 lines (249 loc) · 10.7 KB
/
AnimatedImageCompositor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
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;
import com.facebook.imagepipeline.animated.base.AnimatedImageResult;
import com.facebook.imagepipeline.transformation.BitmapTransformation;
import com.facebook.infer.annotation.Nullsafe;
import javax.annotation.Nullable;
/**
* 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.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
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.
*/
@Nullable
CloseableReference<Bitmap> getCachedBitmap(int frameNumber);
}
private final AnimatedDrawableBackend mAnimatedDrawableBackend;
private final Callback mCallback;
private final Paint mTransparentFillPaint;
private final boolean mIsNewRenderImplementation;
public AnimatedImageCompositor(
AnimatedDrawableBackend animatedDrawableBackend,
boolean isNewRenderImplementation,
Callback callback) {
mAnimatedDrawableBackend = animatedDrawableBackend;
mCallback = callback;
mIsNewRenderImplementation = isNewRenderImplementation;
mTransparentFillPaint = new Paint();
mTransparentFillPaint.setColor(Color.TRANSPARENT);
mTransparentFillPaint.setStyle(Paint.Style.FILL);
mTransparentFillPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
}
public void renderDeltas(int frameNumber, Bitmap baseBitmap) {
Canvas canvas = new Canvas(baseBitmap);
mAnimatedDrawableBackend.renderDeltas(frameNumber, canvas);
}
/**
* 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) {
if (mIsNewRenderImplementation) {
renderDeltas(frameNumber, bitmap);
return;
}
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);
maybeApplyTransformation(bitmap);
}
/** 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 {
if (!mIsNewRenderImplementation) {
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();
}
private void maybeApplyTransformation(Bitmap bitmap) {
AnimatedImageResult animatedImageResult = mAnimatedDrawableBackend.getAnimatedImageResult();
if (animatedImageResult == null) {
return;
}
BitmapTransformation tr = animatedImageResult.getBitmapTransformation();
if (tr == null) {
return;
}
tr.transform(bitmap);
}
}