Android使用Glide加载SVG、Webp格式的图片

项目中想使用一个框架实现常见的所有图片格式的加载,并且对代码的影响降到最低,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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容