feat(android): support AnimatedImageDrawable draw gif

This commit is contained in:
siguangli
2023-01-03 14:03:14 +00:00
committed by hippy-actions[bot]
parent a0117449ea
commit 3ce06f2c84
4 changed files with 139 additions and 87 deletions
@@ -55,6 +55,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import com.tencent.renderer.component.image.ImageDataSupplier;
public class ContentDrawable extends Drawable {
private int mTintColor;
@@ -67,9 +69,7 @@ public class ContentDrawable extends Drawable {
@Nullable
private Paint mPaint;
@Nullable
private Bitmap mContentBitmap;
@Nullable
private Movie mGifMovie;
private ImageDataSupplier mImageHolder;
@Nullable
private GifMovieState mGifMovieState;
@Nullable
@@ -88,6 +88,8 @@ public class ContentDrawable extends Drawable {
mBackgroundHolder = holder;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
@@ -119,17 +121,11 @@ public class ContentDrawable extends Drawable {
}
public void clear() {
mContentBitmap = null;
mGifMovie = null;
mGifMovieState = null;
}
public void setContentBitmap(@Nullable Bitmap bitmap) {
mContentBitmap = bitmap;
}
public void setGifMovie(@Nullable Movie gifMovie) {
mGifMovie = gifMovie;
public void setImageData(@NonNull ImageDataSupplier imageHolder) {
mImageHolder = imageHolder;
}
private void updateContentRegionIfNeeded() {
@@ -144,7 +140,7 @@ public class ContentDrawable extends Drawable {
@Override
public void draw(@NonNull Canvas canvas) {
if (getBounds().width() == 0 || getBounds().height() == 0) {
if (getBounds().width() == 0 || getBounds().height() == 0 || mImageHolder == null) {
return;
}
updateContentRegionIfNeeded();
@@ -156,7 +152,11 @@ public class ContentDrawable extends Drawable {
} else {
canvas.clipRect(mContentRegion);
}
if (mContentBitmap != null && !mContentBitmap.isRecycled()) {
if (mImageHolder.getDrawable() != null) {
mImageHolder.getDrawable().draw(canvas);
} else if (mImageHolder.isAnimated()) {
drawGif(canvas, mImageHolder.getGifMovie());
} else {
if (mPaint == null) {
mPaint = new Paint();
} else {
@@ -166,18 +166,18 @@ public class ContentDrawable extends Drawable {
if (mTintColor != Color.TRANSPARENT) {
mPaint.setColorFilter(new PorterDuffColorFilter(mTintColor, mTintColorBlendMode));
}
drawBitmap(canvas);
} else if (mGifMovie != null) {
drawGif(canvas);
Bitmap bitmap = mImageHolder.getBitmap();
if (bitmap != null) {
drawBitmap(canvas, bitmap);
}
}
canvas.restore();
}
private void updateBitmapMatrix() {
assert mContentBitmap != null;
private void updateBitmapMatrix(@NonNull Bitmap bitmap) {
final RectF dst = new RectF(mContentRegion);
final float bitmapWidth = mContentBitmap.getWidth();
final float bitmapHeight = mContentBitmap.getHeight();
final float bitmapWidth = bitmap.getWidth();
final float bitmapHeight = bitmap.getHeight();
final float width = dst.width();
final float height = dst.height();
final float xScale = width / bitmapWidth;
@@ -219,30 +219,31 @@ public class ContentDrawable extends Drawable {
dst.left += mImagePositionX;
dst.right += mImagePositionX;
mBitmapMatrix.setRectToRect(
new RectF(0, 0, mContentBitmap.getWidth(), mContentBitmap.getHeight()),
new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()),
dst, Matrix.ScaleToFit.FILL);
}
private void drawBitmap(@NonNull Canvas canvas) {
assert mContentBitmap != null;
private void drawBitmap(@NonNull Canvas canvas, @NonNull Bitmap bitmap) {
assert mPaint != null;
updateBitmapMatrix();
updateBitmapMatrix(bitmap);
if (mScaleType == ScaleType.REPEAT) {
BitmapShader bitmapShader = new BitmapShader(mContentBitmap, Shader.TileMode.REPEAT,
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT);
mPaint.setShader(bitmapShader);
}
mPaint.setFilterBitmap(true);
canvas.drawBitmap(mContentBitmap, mBitmapMatrix, mPaint);
canvas.drawBitmap(bitmap, mBitmapMatrix, mPaint);
}
private void drawGif(@NonNull Canvas canvas) {
private void drawGif(@NonNull Canvas canvas, @Nullable Movie movie) {
if (movie == null) {
return;
}
if (mGifMovieState == null) {
mGifMovieState = new GifMovieState();
}
mGifMovieState.update(mContentRegion);
assert mGifMovie != null;
int duration = mGifMovie.duration();
mGifMovieState.update(mContentRegion, movie);
int duration = movie.duration();
if (duration == 0) {
duration = 1000;
}
@@ -256,10 +257,10 @@ public class ContentDrawable extends Drawable {
mGifMovieState.lastPlayTime = now;
int progress =
mGifMovieState.progress > Integer.MAX_VALUE ? 0 : (int) mGifMovieState.progress;
mGifMovie.setTime(progress);
movie.setTime(progress);
canvas.save();
canvas.scale(mGifMovieState.scaleX, mGifMovieState.scaleY);
mGifMovie.draw(canvas, mGifMovieState.startX, mGifMovieState.startY + 1.0f);
movie.draw(canvas, mGifMovieState.startX, mGifMovieState.startY + 1.0f);
canvas.restore();
scheduleSelf(new Runnable() {
@Override
@@ -342,7 +343,7 @@ public class ContentDrawable extends Drawable {
public long lastPlayTime = -1;
private boolean updateRequired = true;
public void update(@NonNull RectF dst) {
public void update(@NonNull RectF dst, @NonNull Movie movie) {
if (!updateRequired) {
return;
}
@@ -350,12 +351,11 @@ public class ContentDrawable extends Drawable {
startY = 0;
scaleX = 1;
scaleY = 1;
assert mGifMovie != null;
if (mGifMovie.width() <= 0 || mGifMovie.height() <= 0) {
if (movie.width() <= 0 || movie.height() <= 0) {
return;
}
scaleX = dst.width() / mGifMovie.width();
scaleY = dst.height() / mGifMovie.height();
scaleX = dst.width() / movie.width();
scaleY = dst.height() / movie.height();
switch (mScaleType) {
case CENTER:
// fall through
@@ -381,8 +381,8 @@ public class ContentDrawable extends Drawable {
break;
}
if (mScaleType != ScaleType.ORIGIN) {
startX = (dst.width() / scaleX - mGifMovie.width()) / 2.0f;
startY = (dst.height() / scaleY - mGifMovie.height()) / 2.0f;
startX = (dst.width() / scaleX - movie.width()) / 2.0f;
startY = (dst.height() / scaleY - movie.height()) / 2.0f;
}
updateRequired = false;
}
@@ -22,6 +22,8 @@ import static com.tencent.renderer.utils.EventUtils.EVENT_IMAGE_LOAD_PROGRESS;
import static com.tencent.renderer.utils.EventUtils.EVENT_IMAGE_LOAD_START;
import static com.tencent.renderer.utils.EventUtils.EVENT_IMAGE_ON_LOAD;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import androidx.annotation.ColorInt;
@@ -51,9 +53,9 @@ public class ImageComponent extends Component {
@Nullable
private String mBundlePath;
@Nullable
private ImageDataSupplier mImageData;
private ImageDataSupplier mImageHolder;
@Nullable
private ImageDataSupplier mDefaultImageData;
private ImageDataSupplier mDefaultImageHolder;
private ImageFetchState mImageFetchState = ImageFetchState.UNLOAD;
private ImageFetchState mDefaultImageFetchState = ImageFetchState.UNLOAD;
@@ -90,11 +92,11 @@ public class ImageComponent extends Component {
}
private void fetchImageIfNeeded() {
if ((mDefaultImageData == null || !mDefaultImageData.checkImageData())
if ((mDefaultImageHolder == null || !mDefaultImageHolder.checkImageData())
&& mDefaultImageFetchState == ImageFetchState.UNLOAD) {
fetchImageWithUrl(mDefaultUri, ImageSourceType.DEFAULT);
}
if ((mImageData == null || !mImageData.checkImageData())
if ((mImageHolder == null || !mImageHolder.checkImageData())
&& mImageFetchState == ImageFetchState.UNLOAD) {
fetchImageWithUrl(mUri, ImageSourceType.SRC);
}
@@ -116,15 +118,15 @@ public class ImageComponent extends Component {
@Override
public void clear() {
super.clear();
if (mImageData != null) {
mImageData.detached();
mImageData.clear();
mImageData = null;
if (mImageHolder != null) {
mImageHolder.detached();
mImageHolder.clear();
mImageHolder = null;
}
if (mDefaultImageData != null) {
mDefaultImageData.detached();
mDefaultImageData.clear();
mDefaultImageData = null;
if (mDefaultImageHolder != null) {
mDefaultImageHolder.detached();
mDefaultImageHolder.clear();
mDefaultImageHolder = null;
}
}
@@ -174,16 +176,28 @@ public class ImageComponent extends Component {
}
}
private void setImageData(@NonNull ImageDataSupplier imageHolder) {
mImageHolder = imageHolder;
Drawable drawable = imageHolder.getDrawable();
if (drawable != null) {
drawable.setCallback(this);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
if (drawable instanceof AnimatedImageDrawable) {
((AnimatedImageDrawable) drawable).start();
}
}
}
ensureContentDrawable().setImageData(imageHolder);
}
private void onFetchImageSuccess(@NonNull String uri, ImageSourceType sourceType,
@NonNull ImageDataSupplier imageData, boolean loadFromCache) {
if (sourceType == ImageSourceType.SRC) {
if (!uri.equals(mUri)) {
return;
}
mImageData = imageData;
mImageFetchState = ImageFetchState.LOADED;
ensureContentDrawable().setContentBitmap(imageData.getBitmap());
ensureContentDrawable().setGifMovie(imageData.getGifMovie());
setImageData(imageData);
if (mHostRef.get() != null && !loadFromCache) {
// send onLoad event
EventUtils.sendComponentEvent(mHostRef.get(), EVENT_IMAGE_ON_LOAD, null);
@@ -200,10 +214,10 @@ public class ImageComponent extends Component {
if (!uri.equals(mDefaultUri)) {
return;
}
mDefaultImageData = imageData;
mDefaultImageHolder = imageData;
mDefaultImageFetchState = ImageFetchState.LOADED;
if (mImageData == null || !mImageData.checkImageData()) {
ensureContentDrawable().setContentBitmap(imageData.getBitmap());
if (mImageHolder == null || !mImageHolder.checkImageData()) {
setImageData(imageData);
}
}
imageData.attached();
@@ -19,10 +19,16 @@ package com.tencent.renderer.component.image;
import static com.tencent.renderer.NativeRenderException.ExceptionCode.IMAGE_DATA_DECODE_ERR;
import android.graphics.ImageDecoder;
import android.graphics.ImageDecoder.ImageInfo;
import android.graphics.ImageDecoder.OnHeaderDecodedListener;
import android.graphics.ImageDecoder.Source;
import android.os.Build.VERSION_CODES;
import androidx.annotation.RequiresApi;
import com.tencent.renderer.NativeRenderException;
import com.tencent.renderer.utils.ImageDataUtils;
import java.io.File;
import java.io.IOException;
@@ -35,16 +41,23 @@ import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
public class ImageDataHolder implements ImageDataSupplier {
private static final int MAX_SOURCE_KEY_LEN = 32;
/** Mark that the image data has been cached. */
/**
* Mark that the image data has been cached.
*/
private static final int FLAG_CACHED = 0x00000001;
/** Mark that the image data has been attached to the view. */
/**
* Mark that the image data has been attached to the view.
*/
private static final int FLAG_ATTACHED = 0x00000002;
/** Mark that image data is decoded internally and needs to recycle. */
/**
* Mark that image data is decoded internally and needs to recycle.
*/
private static final int FLAG_RECYCLABLE = 0x00000004;
private int mStateFlags = 0;
private int mWidth;
@@ -141,7 +154,7 @@ public class ImageDataHolder implements ImageDataSupplier {
@Override
@Nullable
public Bitmap getBitmap() {
return mBitmap;
return mBitmap != null && !mBitmap.isRecycled() ? mBitmap : null;
}
@Override
@@ -164,7 +177,17 @@ public class ImageDataHolder implements ImageDataSupplier {
@Override
public boolean checkImageData() {
return (mBitmap != null && !mBitmap.isRecycled()) || mGifMovie != null;
if (mOptions == null) {
return false;
}
if (ImageDataUtils.isJpeg(mOptions) || ImageDataUtils.isPng(mOptions)) {
return mBitmap != null && !mBitmap.isRecycled();
}
if (ImageDataUtils.isGif(mOptions) && (android.os.Build.VERSION.SDK_INT
< android.os.Build.VERSION_CODES.P)) {
return mGifMovie != null;
}
return mDrawable != null;
}
@Override
@@ -174,24 +197,17 @@ public class ImageDataHolder implements ImageDataSupplier {
@Override
public int getImageWidth() {
if (mBitmap != null) {
return mBitmap.getWidth();
}
if (mGifMovie != null) {
return mGifMovie.width();
}
return 0;
return (mOptions != null) ? mOptions.outWidth : 0;
}
@Override
public int getImageHeight() {
if (mBitmap != null) {
return mBitmap.getHeight();
}
if (mGifMovie != null) {
return mGifMovie.height();
}
return 0;
return (mOptions != null) ? mOptions.outHeight : 0;
}
@Override
public boolean isAnimated() {
return mOptions != null && ImageDataUtils.isGif(mOptions);
}
public String getImageType() {
@@ -210,14 +226,21 @@ public class ImageDataHolder implements ImageDataSupplier {
try {
mOptions = ImageDataUtils.generateBitmapOptions(data);
if (ImageDataUtils.isGif(mOptions)) {
mGifMovie = Movie.decodeByteArray(data, 0, data.length);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
mDrawable = decodeGifForTarget28(data);
} else {
mGifMovie = Movie.decodeByteArray(data, 0, data.length);
}
mBitmap = null;
} else {
} else if (ImageDataUtils.isJpeg(mOptions) || ImageDataUtils.isPng(mOptions)) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
decodeLocalFileForTarget28(ByteBuffer.wrap(data));
} else {
decodeImageData(data);
decodeBitmap(data);
}
mGifMovie = null;
} else {
throw new RuntimeException("Unsupported picture type!");
}
} catch (OutOfMemoryError | Exception e) {
throw new NativeRenderException(IMAGE_DATA_DECODE_ERR, e.getMessage());
@@ -227,15 +250,28 @@ public class ImageDataHolder implements ImageDataSupplier {
public void setData(Bitmap bitmap) {
mBitmap = bitmap;
mGifMovie = null;
mDrawable = null;
resetStateFlag(FLAG_RECYCLABLE);
}
public boolean isAnimated() {
return mGifMovie != null;
@RequiresApi(api = VERSION_CODES.P)
@NonNull
private Drawable decodeGifForTarget28(@NonNull byte[] data) throws IOException {
ImageDecoder.Source source = ImageDecoder.createSource(ByteBuffer.wrap(data));
return ImageDecoder.decodeDrawable(source,
new OnHeaderDecodedListener() {
@Override
public void onHeaderDecoded(@NonNull ImageDecoder decoder,
@NonNull ImageInfo info, @NonNull Source source1) {
// This is a synchronous callback, although the callback parameter is not
// used at present, it is still reserved, image width and height can also be
// obtained through mOptions.
}
});
}
@RequiresApi(api = VERSION_CODES.P)
private void decodeImageSource(@Nullable ImageDecoder.Source source)
private void decodeBitmapForTarget28(@Nullable ImageDecoder.Source source)
throws IOException {
if (source != null) {
mBitmap = ImageDecoder.decodeBitmap(source);
@@ -247,25 +283,25 @@ public class ImageDataHolder implements ImageDataSupplier {
@RequiresApi(api = VERSION_CODES.P)
private void decodeLocalFileForTarget28(@NonNull File file) throws IOException {
ImageDecoder.Source source = ImageDecoder.createSource(file);
decodeImageSource(source);
decodeBitmapForTarget28(source);
}
@RequiresApi(api = VERSION_CODES.P)
private void decodeLocalFileForTarget28(@NonNull String fileName) throws IOException {
ImageDecoder.Source source = ImageDecoder.createSource(
ContextHolder.getAppContext().getAssets(), fileName);
decodeImageSource(source);
decodeBitmapForTarget28(source);
}
@RequiresApi(api = VERSION_CODES.P)
private void decodeLocalFileForTarget28(@NonNull ByteBuffer buffer) throws IOException {
ImageDecoder.Source source = ImageDecoder.createSource(buffer);
decodeImageSource(source);
decodeBitmapForTarget28(source);
}
private int getSampleSize(int outWidth, int outHeight) {
int sampleSize = 1;
if (mWidth <=0 || mHeight <= 0) {
if (mWidth <= 0 || mHeight <= 0) {
return sampleSize;
}
if (outWidth >= outHeight) {
@@ -281,7 +317,7 @@ public class ImageDataHolder implements ImageDataSupplier {
return sampleSize;
}
private void decodeImageData(@Nullable byte[] data) {
private void decodeBitmap(@Nullable byte[] data) {
if (data == null || data.length <= 0 || mOptions == null) {
return;
}
@@ -38,6 +38,8 @@ public interface ImageDataSupplier {
boolean isRecyclable();
boolean isAnimated();
@NonNull
String getSource();