项目中想使用一个框架实现常见的所有图片格式的加载,并且对代码的影响降到最低,Glide框架提供了很好的扩展,这里使用Glide+androidsvg+fresco实现加载GIF、SVG、WebP等多种格式的图片和动画文件。
代码:https://github.com/LiPengfei0106/GlideSupport
实现原理
Glide项目中给出了加载SVG的示例,通过自定义Module在Registry中注册新的处理对象,使用androidsvg提供的方法来将InputStream转为SVG对象,然后把SVG对象转为PictureDrawable加载到目标target中。
仿照该示例,仿照Glide实现加载GIF图片的方式,使用 Fresco的animated-webp来实现加载Webp图片,参考GlideWebpSupport
具体步骤
1、添加依赖
dependencies {
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
implementation 'com.caverock:androidsvg-aar:1.3'
implementation "com.facebook.fresco:animated-webp:1.9.0"
}
2、SVG
1、自定义ResourceDecoder,将InputStream转为SVG对象:
import android.support.annotation.NonNull;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.IOException;
import java.io.InputStream;
/**
* Decodes an SVG internal representation from an {@link InputStream}.
*/
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
// TODO: Can we tell?
return true;
}
public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
@NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}
2、自定义ResourceTranscoder,将SVG转为Drawable对象
import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.caverock.androidsvg.SVG;
/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one
* ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
@NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}
3、SVG格式不能硬解码,自定义RequestListener,在onResourceReady时设置ImageView为软解码
import android.graphics.drawable.PictureDrawable;
import android.widget.ImageView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.Target;
/**
* Listener which updates the {@link ImageView} to be software rendered, because
* {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on
* a hardware backed {@link android.graphics.Canvas Canvas}.
*/
public class SvgSoftwareLayerSetter implements RequestListener<PictureDrawable> {
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<PictureDrawable> target,
boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_NONE, null);
return false;
}
@Override
public boolean onResourceReady(PictureDrawable resource, Object model,
Target<PictureDrawable> target, DataSource dataSource, boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return false;
}
}
3、WebP
实现Webp相关类仿照Glide中的Gif,参考GlideWebpSupport
1、WebpFrameLoader,加载每帧图片
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Util;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class WebpFrameLoader {
final RequestManager requestManager;
private final GifDecoder gifDecoder;
private final Handler handler;
private final List<FrameCallback> callbacks;
private final BitmapPool bitmapPool;
private boolean isRunning;
private boolean isLoadPending;
private boolean startFromFirstFrame;
private RequestBuilder<Bitmap> requestBuilder;
private DelayTarget current;
private boolean isCleared;
private DelayTarget next;
private Bitmap firstFrame;
private Transformation<Bitmap> transformation;
public WebpFrameLoader(Glide glide, GifDecoder gifDecoder, int width, int height, Transformation<Bitmap> transformation, Bitmap firstFrame) {
this(glide.getBitmapPool(), Glide.with(glide.getContext()), gifDecoder, null, getRequestBuilder(Glide.with(glide.getContext()), width, height), transformation, firstFrame);
}
WebpFrameLoader(BitmapPool bitmapPool, RequestManager requestManager, GifDecoder gifDecoder, Handler handler, RequestBuilder<Bitmap> requestBuilder, Transformation<Bitmap> transformation, Bitmap firstFrame) {
this.callbacks = new ArrayList();
this.isRunning = false;
this.isLoadPending = false;
this.startFromFirstFrame = false;
this.requestManager = requestManager;
if (handler == null) {
handler = new Handler(Looper.getMainLooper(), new FrameLoaderCallback());
}
this.bitmapPool = bitmapPool;
this.handler = handler;
this.requestBuilder = requestBuilder;
this.gifDecoder = gifDecoder;
this.setFrameTransformation(transformation, firstFrame);
}
private static RequestBuilder<Bitmap> getRequestBuilder(RequestManager requestManager, int width, int height) {
return requestManager.asBitmap().apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.NONE).skipMemoryCache(true).override(width, height));
}
void setFrameTransformation(Transformation<Bitmap> transformation, Bitmap firstFrame) {
this.transformation = Preconditions.checkNotNull(transformation);
this.firstFrame = Preconditions.checkNotNull(firstFrame);
this.requestBuilder = this.requestBuilder.apply((new RequestOptions()).transform(transformation));
}
Transformation<Bitmap> getFrameTransformation() {
return this.transformation;
}
Bitmap getFirstFrame() {
return this.firstFrame;
}
void subscribe(FrameCallback frameCallback) {
if (this.isCleared) {
throw new IllegalStateException("Cannot subscribe to a cleared frame loader");
} else {
boolean start = this.callbacks.isEmpty();
if (this.callbacks.contains(frameCallback)) {
throw new IllegalStateException("Cannot subscribe twice in a row");
} else {
this.callbacks.add(frameCallback);
if (start) {
this.start();
}
}
}
}
void unsubscribe(FrameCallback frameCallback) {
this.callbacks.remove(frameCallback);
if (this.callbacks.isEmpty()) {
this.stop();
}
}
int getWidth() {
return this.getCurrentFrame().getWidth();
}
int getHeight() {
return this.getCurrentFrame().getHeight();
}
int getSize() {
return this.gifDecoder.getByteSize() + this.getFrameSize();
}
int getCurrentIndex() {
return this.current != null ? this.current.index : -1;
}
private int getFrameSize() {
return Util.getBitmapByteSize(this.getCurrentFrame().getWidth(), this.getCurrentFrame().getHeight(), this.getCurrentFrame().getConfig());
}
ByteBuffer getBuffer() {
return this.gifDecoder.getData().asReadOnlyBuffer();
}
int getFrameCount() {
return this.gifDecoder.getFrameCount();
}
int getLoopCount() {
return this.gifDecoder.getTotalIterationCount();
}
private void start() {
if (!this.isRunning) {
this.isRunning = true;
this.isCleared = false;
this.loadNextFrame();
}
}
private void stop() {
this.isRunning = false;
}
void clear() {
this.callbacks.clear();
this.recycleFirstFrame();
this.stop();
if (this.current != null) {
this.requestManager.clear(this.current);
this.current = null;
}
if (this.next != null) {
this.requestManager.clear(this.next);
this.next = null;
}
this.gifDecoder.clear();
this.isCleared = true;
}
Bitmap getCurrentFrame() {
return this.current != null ? this.current.getResource() : this.firstFrame;
}
private void loadNextFrame() {
if (this.isRunning && !this.isLoadPending) {
if (this.startFromFirstFrame) {
this.gifDecoder.resetFrameIndex();
this.startFromFirstFrame = false;
}
this.isLoadPending = true;
int delay = this.gifDecoder.getNextDelay();
long targetTime = SystemClock.uptimeMillis() + (long) delay;
this.gifDecoder.advance();
this.next = new DelayTarget(this.handler, this.gifDecoder.getCurrentFrameIndex(), targetTime);
this.requestBuilder.clone().apply(RequestOptions.signatureOf(new FrameSignature())).load(this.gifDecoder).into(this.next);
}
}
private void recycleFirstFrame() {
if (this.firstFrame != null) {
this.bitmapPool.put(this.firstFrame);
this.firstFrame = null;
}
}
void setNextStartFromFirstFrame() {
Preconditions.checkArgument(!this.isRunning, "Can\'t restart a running animation");
this.startFromFirstFrame = true;
}
void onFrameReady(DelayTarget delayTarget) {
if (this.isCleared) {
this.handler.obtainMessage(2, delayTarget).sendToTarget();
} else {
if (delayTarget.getResource() != null) {
this.recycleFirstFrame();
DelayTarget previous = this.current;
this.current = delayTarget;
for (int i = this.callbacks.size() - 1; i >= 0; --i) {
FrameCallback cb = this.callbacks.get(i);
cb.onFrameReady();
}
if (previous != null) {
this.handler.obtainMessage(2, previous).sendToTarget();
}
}
this.isLoadPending = false;
this.loadNextFrame();
}
}
public interface FrameCallback {
void onFrameReady();
}
static class FrameSignature implements Key {
private final UUID uuid;
public FrameSignature() {
this(UUID.randomUUID());
}
FrameSignature(UUID uuid) {
this.uuid = uuid;
}
public boolean equals(Object o) {
if (o instanceof FrameSignature) {
FrameSignature other = (FrameSignature) o;
return other.uuid.equals(this.uuid);
} else {
return false;
}
}
public int hashCode() {
return this.uuid.hashCode();
}
public void updateDiskCacheKey(MessageDigest messageDigest) {
throw new UnsupportedOperationException("Not implemented");
}
}
static class DelayTarget extends SimpleTarget<Bitmap> {
final int index;
private final Handler handler;
private final long targetTime;
private Bitmap resource;
DelayTarget(Handler handler, int index, long targetTime) {
this.handler = handler;
this.index = index;
this.targetTime = targetTime;
}
Bitmap getResource() {
return this.resource;
}
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
this.resource = resource;
Message msg = this.handler.obtainMessage(1, this);
this.handler.sendMessageAtTime(msg, this.targetTime);
}
}
private class FrameLoaderCallback implements Handler.Callback {
public static final int MSG_DELAY = 1;
public static final int MSG_CLEAR = 2;
FrameLoaderCallback() {
}
public boolean handleMessage(Message msg) {
DelayTarget target;
if (msg.what == 1) {
target = (DelayTarget) msg.obj;
WebpFrameLoader.this.onFrameReady(target);
return true;
} else {
if (msg.what == 2) {
target = (DelayTarget) msg.obj;
WebpFrameLoader.this.requestManager.clear(target);
}
return false;
}
}
}
}
2、WebpDrawable,用于播放每帧图片
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
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 android.support.annotation.VisibleForTesting;
import android.view.Gravity;
import com.bumptech.glide.Glide;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.util.Preconditions;
import java.nio.ByteBuffer;
/**
* An animated {@link android.graphics.drawable.Drawable} that plays the frames of an animated Webp.
*/
public class WebpDrawable extends Drawable implements WebpFrameLoader.FrameCallback, Animatable {
public static final int LOOP_FOREVER = -1;
public static final int LOOP_INTRINSIC = 0;
private final GifState state;
private boolean isRunning;
private boolean isStarted;
private boolean isRecycled;
private boolean isVisible;
private int loopCount;
private int maxLoopCount;
private boolean applyGravity;
private Paint paint;
private Rect destRect;
public WebpDrawable(Context context, GifDecoder gifDecoder, BitmapPool bitmapPool, Transformation<Bitmap> frameTransformation, int targetFrameWidth, int targetFrameHeight, Bitmap firstFrame) {
this(new GifState(bitmapPool, new WebpFrameLoader(Glide.get(context), gifDecoder, targetFrameWidth, targetFrameHeight, frameTransformation, firstFrame)));
}
WebpDrawable(GifState state) {
this.isVisible = true;
this.maxLoopCount = -1;
this.state = Preconditions.checkNotNull(state);
}
@VisibleForTesting
WebpDrawable(WebpFrameLoader frameLoader, BitmapPool bitmapPool, Paint paint) {
this(new GifState(bitmapPool, frameLoader));
this.paint = paint;
}
public int getSize() {
return this.state.frameLoader.getSize();
}
public Bitmap getFirstFrame() {
return this.state.frameLoader.getFirstFrame();
}
public void setFrameTransformation(Transformation<Bitmap> frameTransformation, Bitmap firstFrame) {
this.state.frameLoader.setFrameTransformation(frameTransformation, firstFrame);
}
public Transformation<Bitmap> getFrameTransformation() {
return this.state.frameLoader.getFrameTransformation();
}
public ByteBuffer getBuffer() {
return this.state.frameLoader.getBuffer();
}
public int getFrameCount() {
return this.state.frameLoader.getFrameCount();
}
public int getFrameIndex() {
return this.state.frameLoader.getCurrentIndex();
}
private void resetLoopCount() {
this.loopCount = 0;
}
public void startFromFirstFrame() {
Preconditions.checkArgument(!this.isRunning, "You cannot restart a currently running animation.");
this.state.frameLoader.setNextStartFromFirstFrame();
this.start();
}
public void start() {
this.isStarted = true;
this.resetLoopCount();
if (this.isVisible) {
this.startRunning();
}
}
public void stop() {
this.isStarted = false;
this.stopRunning();
}
private void startRunning() {
Preconditions.checkArgument(!this.isRecycled, "You cannot start a recycled Drawable. Ensure thatyou clear any references to the Drawable when clearing the corresponding request.");
if (this.state.frameLoader.getFrameCount() == 1) {
this.invalidateSelf();
} else if (!this.isRunning) {
this.isRunning = true;
this.state.frameLoader.subscribe(this);
this.invalidateSelf();
}
}
private void stopRunning() {
this.isRunning = false;
this.state.frameLoader.unsubscribe(this);
}
public boolean setVisible(boolean visible, boolean restart) {
Preconditions.checkArgument(!this.isRecycled, "Cannot change the visibility of a recycled resource. Ensure that you unset the Drawable from your View before changing the View\'s visibility.");
this.isVisible = visible;
if (!visible) {
this.stopRunning();
} else if (this.isStarted) {
this.startRunning();
}
return super.setVisible(visible, restart);
}
public int getIntrinsicWidth() {
return this.state.frameLoader.getWidth();
}
public int getIntrinsicHeight() {
return this.state.frameLoader.getHeight();
}
public boolean isRunning() {
return this.isRunning;
}
void setIsRunning(boolean isRunning) {
this.isRunning = isRunning;
}
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
this.applyGravity = true;
}
public void draw(Canvas canvas) {
if (!this.isRecycled) {
if (this.applyGravity) {
Gravity.apply(GifState.GRAVITY, this.getIntrinsicWidth(), this.getIntrinsicHeight(), this.getBounds(), this.getDestRect());
this.applyGravity = false;
}
Bitmap currentFrame = this.state.frameLoader.getCurrentFrame();
canvas.drawBitmap(currentFrame, null, this.getDestRect(), this.getPaint());
}
}
public void setAlpha(int i) {
this.getPaint().setAlpha(i);
}
public void setColorFilter(ColorFilter colorFilter) {
this.getPaint().setColorFilter(colorFilter);
}
private Rect getDestRect() {
if (this.destRect == null) {
this.destRect = new Rect();
}
return this.destRect;
}
private Paint getPaint() {
if (this.paint == null) {
this.paint = new Paint(2);
}
return this.paint;
}
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
public void onFrameReady() {
if (this.getCallback() == null) {
this.stop();
this.invalidateSelf();
} else {
this.invalidateSelf();
if (this.getFrameIndex() == this.getFrameCount() - 1) {
++this.loopCount;
}
if (this.maxLoopCount != LOOP_FOREVER && this.loopCount >= this.maxLoopCount) {
this.stop();
}
}
}
public ConstantState getConstantState() {
return this.state;
}
public void recycle() {
this.isRecycled = true;
this.state.frameLoader.clear();
}
boolean isRecycled() {
return this.isRecycled;
}
public void setLoopCount(int loopCount) {
if (loopCount <= 0 && loopCount != LOOP_FOREVER && loopCount != LOOP_INTRINSIC) {
throw new IllegalArgumentException("Loop count must be greater than 0, or equal to LOOP_FOREVER, or equal to LOOP_INTRINSIC");
} else {
if (loopCount == LOOP_INTRINSIC) {
int intrinsicCount = this.state.frameLoader.getLoopCount();
this.maxLoopCount = intrinsicCount == LOOP_INTRINSIC ? LOOP_FOREVER : intrinsicCount;
} else {
this.maxLoopCount = loopCount;
}
}
}
static class GifState extends ConstantState {
static final int GRAVITY = Gravity.FILL;
final BitmapPool bitmapPool;
final WebpFrameLoader frameLoader;
public GifState(BitmapPool bitmapPool, WebpFrameLoader frameLoader) {
this.bitmapPool = bitmapPool;
this.frameLoader = frameLoader;
}
public Drawable newDrawable(Resources res) {
return this.newDrawable();
}
public Drawable newDrawable() {
return new WebpDrawable(this);
}
public int getChangingConfigurations() {
return 0;
}
}
}
3、WebpDecoder 继承 GifDecoder,从InputStream源和byte []中读取图像数据。
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.support.annotation.NonNull;
import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.gifdecoder.GifHeader;
import com.facebook.animated.webp.WebPFrame;
import com.facebook.animated.webp.WebPImage;
import com.facebook.imagepipeline.animated.base.AnimatedDrawableFrameInfo;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* 可以从InputStream源和byte []中读取图像数据。
*/
public class WebpDecoder implements GifDecoder {
private WebPImage mWebPImage;
private BitmapProvider mProvider;
private int mFramePointer;
private int[] mFrameDurations;
private int downsampledWidth;
private int downsampledHeight;
private boolean[] mKeyFrame;
private int mSampleSize;
// 缓存上一帧,用于非关键帧
private Bitmap mCacheBmp;
public WebpDecoder(BitmapProvider provider, WebPImage webPImage, int sampleSize) {
mProvider = provider;
mWebPImage = webPImage;
mFrameDurations = webPImage.getFrameDurations();
mKeyFrame = new boolean[mFrameDurations.length];
downsampledWidth = webPImage.getWidth() / sampleSize;
downsampledHeight = webPImage.getHeight() / sampleSize;
mSampleSize = sampleSize;
}
@Override
public int getWidth() {
return mWebPImage.getWidth();
}
@Override
public int getHeight() {
return mWebPImage.getHeight();
}
@Override
public ByteBuffer getData() {
return null;
}
@Override
public int getStatus() {
return 0;
}
@Override
public void advance() {
mFramePointer = (mFramePointer + 1) % mWebPImage.getFrameCount();
}
@Override
public int getDelay(int n) {
int delay = -1;
if ((n >= 0) && (n < mFrameDurations.length)) {
delay = mFrameDurations[n];
}
return delay;
}
@Override
public int getNextDelay() {
if (mFrameDurations.length == 0 || mFramePointer < 0) {
return 0;
}
return getDelay(mFramePointer);
}
@Override
public int getFrameCount() {
return mWebPImage.getFrameCount();
}
@Override
public int getCurrentFrameIndex() {
return mFramePointer;
}
@Override
public void resetFrameIndex() {
mFramePointer = -1;
}
@Override
public int getLoopCount() {
return mWebPImage.getLoopCount();
}
@Override
public int getNetscapeLoopCount() {
return mWebPImage.getLoopCount();
}
@Override
public int getTotalIterationCount() {
if (mWebPImage.getLoopCount() == 0) {
return TOTAL_ITERATION_COUNT_FOREVER;
}
return mWebPImage.getFrameCount() + 1;
}
@Override
public int getByteSize() {
return mWebPImage.getSizeInBytes();
}
@Override
public Bitmap getNextFrame() {
Bitmap result = mProvider.obtain(downsampledWidth, downsampledHeight, Bitmap.Config.ARGB_8888);
int currentIndex = getCurrentFrameIndex();
WebPFrame currentFrame = mWebPImage.getFrame(currentIndex);
// render key frame
if (isKeyFrame(currentIndex)) {
mKeyFrame[currentIndex] = true;
currentFrame.renderFrame(downsampledWidth, downsampledHeight, result);
mCacheBmp = result;
} else {
int frameW = currentFrame.getWidth() / mSampleSize;
int frameH = currentFrame.getHeight() / mSampleSize;
int offX = currentFrame.getXOffset() / mSampleSize;
int offY = currentFrame.getYOffset() / mSampleSize;
Canvas canvas = new Canvas(result);
canvas.drawBitmap(mCacheBmp, 0, 0, null);
Bitmap frameBmp = mProvider.obtain(frameW, frameH, Bitmap.Config.ARGB_8888);
currentFrame.renderFrame(frameW, frameH, frameBmp);
canvas.drawBitmap(frameBmp, offX, offY, null);
mProvider.release(frameBmp);
mCacheBmp = result;
}
currentFrame.dispose();
return result;
}
private boolean isKeyFrame(int index) {
if (index == 0) {
return true;
}
AnimatedDrawableFrameInfo curFrameInfo = mWebPImage.getFrameInfo(index);
AnimatedDrawableFrameInfo prevFrameInfo = mWebPImage.getFrameInfo(index - 1);
if (curFrameInfo.blendOperation == AnimatedDrawableFrameInfo.BlendOperation.NO_BLEND
&& isFullFrame(curFrameInfo)) {
return true;
} else {
return prevFrameInfo.disposalMethod == AnimatedDrawableFrameInfo.DisposalMethod.DISPOSE_TO_BACKGROUND
&& isFullFrame(prevFrameInfo);
}
}
private boolean isFullFrame(AnimatedDrawableFrameInfo info) {
return info.yOffset == 0 && info.xOffset == 0
&& mWebPImage.getHeight() == info.width
&& mWebPImage.getWidth() == info.height;
}
@Override
public int read(InputStream inputStream, int i) {
return 0;
}
@Override
public void clear() {
mWebPImage.dispose();
mWebPImage = null;
}
@Override
public void setData(GifHeader gifHeader, byte[] bytes) {
}
@Override
public void setData(GifHeader gifHeader, ByteBuffer byteBuffer) {
}
@Override
public void setData(GifHeader gifHeader, ByteBuffer byteBuffer, int i) {
}
@Override
public int read(byte[] bytes) {
return 0;
}
@Override
public void setDefaultBitmapConfig(@NonNull Bitmap.Config format) {
}
}
4、WebpBytebufferDecoder和WebpResourceDecoder,将数据转为WebpDrawable对象
WebpBytebufferDecoder
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParserUtils;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.Initializable;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.UnitTransformation;
import com.bumptech.glide.load.resource.drawable.DrawableResource;
import com.bumptech.glide.load.resource.gif.GifBitmapProvider;
import com.facebook.animated.webp.WebPImage;
import com.facebook.soloader.SoLoader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
/**
* An {@link com.bumptech.glide.load.ResourceDecoder} that decodes {@link
* WebpDrawable} from {@link java.io.InputStream} data.
*/
public class WebpBytebufferDecoder implements ResourceDecoder<ByteBuffer, WebpDrawable> {
public final String TAG = "WebpBytebufferDecoder";
private final List<ImageHeaderParser> mParsers;
private final Context mContext;
private final BitmapPool mBitmapPool;
private final GifBitmapProvider mProvider;
private final ArrayPool mByteArrayPool;
public WebpBytebufferDecoder(Context context, Glide glide) {
this(context, glide.getRegistry().getImageHeaderParsers(), glide.getArrayPool(),
glide.getBitmapPool());
// if not init Soloader, will get error when decode
try {
SoLoader.init(context, 0);
} catch (IOException e) {
Log.v(TAG, "Failed to init SoLoader", e);
}
}
public WebpBytebufferDecoder(Context context, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool, BitmapPool bitmapPool) {
mContext = context.getApplicationContext();
mParsers = parsers;
mBitmapPool = bitmapPool;
mProvider = new GifBitmapProvider(bitmapPool, byteArrayPool);
mByteArrayPool = byteArrayPool;
}
private static int getSampleSize(int srcWidth, int srcHeight, int targetWidth, int targetHeight) {
int exactSampleSize = Math.min(srcHeight / targetHeight,
srcWidth / targetWidth);
int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize);
// Although functionally equivalent to 0 for BitmapFactory, 1 is a safer default for our code
// than 0.
int sampleSize = Math.max(1, powerOfTwoSampleSize);
return sampleSize;
}
@Override
public boolean handles(@NonNull ByteBuffer buffer, @NonNull Options options) throws IOException {
ImageHeaderParser.ImageType type = ImageHeaderParserUtils.getType(mParsers, buffer);
return type == ImageHeaderParser.ImageType.WEBP || type == ImageHeaderParser.ImageType.WEBP_A;
}
@Nullable
@Override
public Resource<WebpDrawable> decode(@NonNull ByteBuffer buffer, int width, int height, @NonNull Options options) {
byte[] arr;
if (buffer.hasArray())
arr = buffer.array();
else {
arr = new byte[buffer.capacity()];
buffer.get(arr);
}
WebPImage webp = WebPImage.create(arr);
int sampleSize = getSampleSize(webp.getWidth(), webp.getHeight(), width, height);
WebpDecoder webpDecoder = new WebpDecoder(mProvider, webp, sampleSize);
Bitmap firstFrame = webpDecoder.getNextFrame();
if (firstFrame == null) {
return null;
}
Transformation<Bitmap> unitTransformation = UnitTransformation.get();
return new WebpDrawableResource(new WebpDrawable(mContext, webpDecoder, mBitmapPool, unitTransformation, width, height,
firstFrame));
}
public class WebpDrawableResource extends DrawableResource<WebpDrawable> implements Initializable {
public WebpDrawableResource(WebpDrawable drawable) {
super(drawable);
}
public Class<WebpDrawable> getResourceClass() {
return WebpDrawable.class;
}
public int getSize() {
return drawable.getSize();
}
public void recycle() {
}
public void initialize() {
}
}
}
public class WebpBytebufferDecoder implements ResourceDecoder<ByteBuffer, WebpDrawable> {
public final String TAG = "WebpBytebufferDecoder";
private final List<ImageHeaderParser> mParsers;
private final Context mContext;
private final BitmapPool mBitmapPool;
private final GifBitmapProvider mProvider;
private final ArrayPool mByteArrayPool;
public WebpBytebufferDecoder(Context context, Glide glide) {
this(context, glide.getRegistry().getImageHeaderParsers(), glide.getArrayPool(),
glide.getBitmapPool());
// if not init Soloader, will get error when decode
try {
SoLoader.init(context, 0);
} catch (IOException e) {
Log.v(TAG, "Failed to init SoLoader", e);
}
}
public WebpBytebufferDecoder(Context context, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool, BitmapPool bitmapPool) {
mContext = context.getApplicationContext();
mParsers = parsers;
mBitmapPool = bitmapPool;
mProvider = new GifBitmapProvider(bitmapPool, byteArrayPool);
mByteArrayPool = byteArrayPool;
}
private static int getSampleSize(int srcWidth, int srcHeight, int targetWidth, int targetHeight) {
int exactSampleSize = Math.min(srcHeight / targetHeight,
srcWidth / targetWidth);
int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize);
// Although functionally equivalent to 0 for BitmapFactory, 1 is a safer default for our code
// than 0.
int sampleSize = Math.max(1, powerOfTwoSampleSize);
return sampleSize;
}
@Override
public boolean handles(@NonNull ByteBuffer buffer, @NonNull Options options) throws IOException {
ImageHeaderParser.ImageType type = ImageHeaderParserUtils.getType(mParsers, buffer);
return type == ImageHeaderParser.ImageType.WEBP || type == ImageHeaderParser.ImageType.WEBP_A;
}
@Nullable
@Override
public Resource<WebpDrawable> decode(@NonNull ByteBuffer buffer, int width, int height, @NonNull Options options) {
byte[] arr;
if (buffer.hasArray())
arr = buffer.array();
else {
arr = new byte[buffer.capacity()];
buffer.get(arr);
}
WebPImage webp = WebPImage.create(arr);
int sampleSize = getSampleSize(webp.getWidth(), webp.getHeight(), width, height);
WebpDecoder webpDecoder = new WebpDecoder(mProvider, webp, sampleSize);
Bitmap firstFrame = webpDecoder.getNextFrame();
if (firstFrame == null) {
return null;
}
Transformation<Bitmap> unitTransformation = UnitTransformation.get();
return new WebpDrawableResource(new WebpDrawable(mContext, webpDecoder, mBitmapPool, unitTransformation, width, height,
firstFrame));
}
public class WebpDrawableResource extends DrawableResource<WebpDrawable> implements Initializable {
public WebpDrawableResource(WebpDrawable drawable) {
super(drawable);
}
public Class<WebpDrawable> getResourceClass() {
return WebpDrawable.class;
}
public int getSize() {
return drawable.getSize();
}
public void recycle() {
}
public void initialize() {
}
}
}
WebpResourceDecoder
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.Nullable;
import android.util.Log;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParserUtils;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.Initializable;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.UnitTransformation;
import com.bumptech.glide.load.resource.drawable.DrawableResource;
import com.bumptech.glide.load.resource.gif.GifBitmapProvider;
import com.facebook.animated.webp.WebPImage;
import com.facebook.soloader.SoLoader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* Decodes {@link Bitmap}s from {@link WebpDecoder}s representing a particular frame of a particular
* WEBP image.
*/
public class WebpResourceDecoder implements ResourceDecoder<InputStream, WebpDrawable> {
public final String TAG = "WebpResourceDecoder";
private final List<ImageHeaderParser> mParsers;
private final ArrayPool mByteArrayPool;
private final Context mContext;
private final BitmapPool mBitmapPool;
private final GifBitmapProvider mProvider;
public WebpResourceDecoder(Context context, Glide glide) {
this(context, glide.getRegistry().getImageHeaderParsers(), glide.getArrayPool(),
glide.getBitmapPool());
// if not init Soloader, will get error when decode
try {
SoLoader.init(context, 0);
} catch (IOException e) {
Log.v(TAG, "Failed to init SoLoader", e);
}
}
public WebpResourceDecoder(Context context) {
this(context, Glide.get(context).getRegistry().getImageHeaderParsers(), Glide.get(context).getArrayPool(),
Glide.get(context).getBitmapPool());
// if not init Soloader, will get error when decode
try {
SoLoader.init(context, 0);
} catch (IOException e) {
Log.v(TAG, "Failed to init SoLoader", e);
}
}
public WebpResourceDecoder(Context context, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool, BitmapPool bitmapPool) {
mContext = context.getApplicationContext();
mParsers = parsers;
mByteArrayPool = byteArrayPool;
mBitmapPool = bitmapPool;
mProvider = new GifBitmapProvider(bitmapPool, byteArrayPool);
}
private static int getSampleSize(int srcWidth, int srcHeight, int targetWidth, int targetHeight) {
int exactSampleSize = Math.min(srcHeight / targetHeight,
srcWidth / targetWidth);
int powerOfTwoSampleSize = exactSampleSize == 0 ? 0 : Integer.highestOneBit(exactSampleSize);
// Although functionally equivalent to 0 for BitmapFactory, 1 is a safer default for our code
// than 0.
int sampleSize = Math.max(1, powerOfTwoSampleSize);
return sampleSize;
}
@Override
public boolean handles(InputStream inputStream, Options options) throws IOException {
ImageHeaderParser.ImageType type = ImageHeaderParserUtils.getType(mParsers, inputStream, mByteArrayPool);
return type == ImageHeaderParser.ImageType.WEBP || type == ImageHeaderParser.ImageType.WEBP_A;
}
@Nullable
@Override
public Resource<WebpDrawable> decode(InputStream inputStream, int width, int height, Options options) throws IOException {
ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
byte[] buff = new byte[100]; //buff用于存放循环读取的临时数据
int rc = 0;
while ((rc = inputStream.read(buff, 0, 100)) > 0) {
swapStream.write(buff, 0, rc);
}
byte[] in_b = swapStream.toByteArray();
WebPImage webp = WebPImage.create(in_b);
int sampleSize = getSampleSize(webp.getWidth(), webp.getHeight(), width, height);
WebpDecoder webpDecoder = new WebpDecoder(mProvider, webp, sampleSize);
Bitmap firstFrame = webpDecoder.getNextFrame();
if (firstFrame == null) {
return null;
}
Transformation<Bitmap> unitTransformation = UnitTransformation.get();
return new WebpDrawableResource(new WebpDrawable(mContext, webpDecoder, mBitmapPool, unitTransformation, width, height,
firstFrame));
}
public class WebpDrawableResource extends DrawableResource<WebpDrawable> implements Initializable {
public WebpDrawableResource(WebpDrawable drawable) {
super(drawable);
}
public Class<WebpDrawable> getResourceClass() {
return WebpDrawable.class;
}
public int getSize() {
return drawable.getSize();
}
public void recycle() {
}
public void initialize() {
}
}
}
5、WebpDrawableEncoder,将WebpDrawable转为OutPutStream
import android.support.annotation.NonNull;
import android.util.Log;
import com.bumptech.glide.load.EncodeStrategy;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.ResourceEncoder;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.util.ByteBufferUtil;
import java.io.File;
import java.io.IOException;
/**
* Writes the original bytes of a {@link WebpDrawable} to an
* {@link java.io.OutputStream}.
* @author Lee
* @date 2019/4/18
*/
public class WebpDrawableEncoder implements ResourceEncoder<WebpDrawable> {
private static final String TAG = WebpDrawableEncoder.class.getSimpleName();
@NonNull
@Override
public EncodeStrategy getEncodeStrategy(@NonNull Options options) {
return EncodeStrategy.SOURCE;
}
@Override
public boolean encode(@NonNull Resource<WebpDrawable> data, @NonNull File file,
@NonNull Options options) {
WebpDrawable drawable = data.get();
boolean success = false;
try {
ByteBufferUtil.toFile(drawable.getBuffer(), file);
success = true;
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to encode Webp drawable data", e);
}
}
return success;
}
}
3、创建Module
自定义Module,注册SVG和WebpDrawble
Glide4.0在包的根路径下创建CustomModule,makeProject后会自动加载Module
@GlideModule
public class CustomModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
@NonNull Registry registry) {
registry.register(WebpDrawable.class, new WebpDrawableEncoder())
.append(InputStream.class, WebpDrawable.class, new WebpResourceDecoder(context, glide))
.append(ByteBuffer.class, WebpDrawable.class, new WebpBytebufferDecoder(context, glide))
.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder());
}
// Disable manifest parsing to avoid adding similar modules twice.
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}
4、使用
// 加载本地图片
Glide.with(context).load(R.drawable.gif).into(view);
Glide.with(context).load("file:///android_asset/Spinner.svg").into(view);
Glide.with(context).load(R.drawable.webp).into(view);
// 加载网络图片
Glide.with(context).asGif().load("http://xxx.xxx.xxx.gif").into(view);
Glide.with(context).as(PictureDrawable.class)
.listener(new SvgSoftwareLayerSetter())
.load("http://xxx.xxx.xxx.svg").into(view);
Glide.with(context).load("http://xxx.xxx.xxx.webp").into(view);
参考:
https://github.com/bumptech/glide/tree/master/samples/svg
https://github.com/roths/GlideWebpSupport