Glide 加载webp动图实战(解决图片每帧间隔过长,动图单次播放,二次播放动图时首帧是动图最后一帧的问题)

零、前言

最近接了一个需求,要在某个地方加上动图的特效,最后方案确定下来有两种实现的方式

        一、lottie动画来展示

        二、类gif动图的方式来展示

考虑到时间的成本,优先使用类gif的方式来实现。

主要是原因第三方图片加载框架已经封装好了一套完整的图片展示的流程,包括图片下载,展示,回收,缓存等。我们直接进行调用即可,但是由于ui给出的动图是webp格式的,所以在使用过程中出现挺多棘手的问题,好在最后找到原因并解决。

一、具体的实现代码

        添加依赖

//Glide库
    //implementation 'com.github.bumptech.glide:glide:4.7.1'//support
    implementation 'com.github.bumptech.glide:glide:4.12.0'//androidx
    annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"//androidx
//Glide支持webp动图的库
    implementation "com.github.zjupure:webpdecoder:2.0.4.12.0"

        xml里面就是一个普通的ImageView,我这里就不贴出来了

        具体的实现java层代码

WebpDrawable mWebpDrawable = null;

private void startWebpGifAni(ImageView iv,String url,int defaultIcon){
//        if(mWebpDrawable!=null&&!mWebpDrawable.isRunning()){
//            mWebpDrawable.startFromFirstFrame();
//            mWebpDrawable.stop();
//        }

        //webp动图
        Transformation transformation = new CenterInside();
        Glide.with(this)
                .load(url)//不是本地资源就改为url即可
                .optionalTransform(transformation)
                .optionalTransform(WebpDrawable.class, new WebpDrawableTransformation(transformation))
                .addListener(new RequestListener() {
                    @Override
                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) {
                        iv.setImageResource(defaultIcon);
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) {
                        if (resource instanceof WebpDrawable) {
                            mWebpDrawable = (WebpDrawable) resource;

                            try {
                                //已知三方库的bug,webp的动图每一帧的时间间隔于实际的有所偏差,需要反射三方库去修改
                                //https://github.com/zjupure/GlideWebpDecoder/issues/33
                                Field gifStateField = mWebpDrawable.getClass().getDeclaredField("state");
                                gifStateField.setAccessible(true);//开放权限
                                Class gifStateClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpDrawable$WebpState");
                                Field gifFrameLoaderField = gifStateClass.getDeclaredField("frameLoader");
                                gifFrameLoaderField.setAccessible(true);

                                Class gifFrameLoaderClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpFrameLoader");
                                Field gifDecoderField = gifFrameLoaderClass.getDeclaredField("webpDecoder");
                                gifDecoderField.setAccessible(true);

                                WebpDecoder webpDecoder = (WebpDecoder) gifDecoderField.get(gifFrameLoaderField.get(gifStateField.get(resource)));
                                Field durations = webpDecoder.getClass().getDeclaredField("mFrameDurations");
                                durations.setAccessible(true);
                                int[] args = (int[]) durations.get(webpDecoder);
                                if (args.length > 0) {
                                    for (int i = 0; i < args.length; i++) {
                                        if (args[i] > 30) {
                                            //加载glide会比ios慢 这边把gif的间隔减少15s
                                            args[i] = args[i] - 15;
                                        }
                                    }
                                }
                                durations.set(webpDecoder, args);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

                            //需要设置为循环1次才会有onAnimationEnd回调
                            mWebpDrawable.setLoopCount(1);
                            mWebpDrawable.registerAnimationCallback(new Animatable2Compat.AnimationCallback() {
                                @Override
                                public void onAnimationStart(Drawable drawable) {
                                    super.onAnimationStart(drawable);
                                }

                                @Override
                                public void onAnimationEnd(Drawable drawable) {
                                    super.onAnimationEnd(drawable);
//第二次播放webp动图的时候 会显示改webp动图最后一帧的图片 然后才能正常显示
                                    if (mWebpDrawable != null && !mWebpDrawable.isRunning()) {
                                        mWebpDrawable.startFromFirstFrame();
                                        mWebpDrawable.stop();
                                    }
                                    mWebpDrawable.unregisterAnimationCallback(this);
                                }
                            });
                        }

                        return false;
                    }
                })
//                .skipMemoryCache(true)
                .into(iv);
    }

    private void cancelGifOnResume(){
        //解决使用webp动图播放一次的时候 页面重新显示之后 webp动图还会播放一次的问题 
        //在onresume调用即可
        if(mWebpDrawable==null){
            return;
        }
        try {
            Field isRunning = mWebpDrawable.getClass().getDeclaredField("isRunning");
            isRunning.setAccessible(true);
            isRunning.setBoolean(mWebpDrawable,true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

OK,这里以上就是所有的代码。

接下来是我怎么解决这些问题的思路,可能会有点啰嗦,感兴趣的话可以看下去,当然如果有更好的方法的话,也欢迎在评论区里面指出来。

二、碰到的问题&解决的思路

在一开始的方案确定的时候,我先试了工程中现有的代码,发现不能展示动图,baidu了一下之后发现  fresco 可以支持显示webp的动图,代码我这里也贴出来

 private void startAni(SimpleDraweeView iv, String webp1){
  
        ControllerListener controllerListener = new BaseControllerListener() {
            @Override
            public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
                if (animatable != null && AnimatedDrawable2.class.isInstance(animatable)) {
                    final AnimatedDrawable2 animatedDrawable2 = (AnimatedDrawable2) animatable;
                    animatedDrawable2.start();
                    final int totalCnt = animatedDrawable2.getFrameCount();
                    animatedDrawable2.setAnimationListener(new BaseAnimationListener() {
                        private int lastFrame; //防止无限循环 适时退出动画

                        @Override
                        public void onAnimationFrame(AnimatedDrawable2 drawable, int frameNumber) {
                            if (!(lastFrame == 0 && totalCnt <= 1) && lastFrame <= frameNumber) {
                                lastFrame = frameNumber;
                            } else {
                                animatedDrawable2.stop();
                            }
                        }

                        @Override
                        public void onAnimationStart(AnimatedDrawable2 drawable) {
                            lastFrame = -1;
                        }

                        @Override
                        public void onAnimationStop(AnimatedDrawable2 drawable) {

                        }
                    });
                }
            };
        };

        DraweeController controller = Fresco.newDraweeControllerBuilder()
                .setUri(Uri.parse(webp1))
                .setOldController(iv.getController())
                .setControllerListener(controllerListener)
                .build();
        iv.setController(controller);
    }

但是 fresco的三方包过大,并且原有的工程并未引入,所以这个方案作废。。。

后续找到了Glide的拓展库webpdecoder,这个库可以支持webp的动图。

不过在使用过程中,还是发现了几个问题。

1.动图播放的速度有点慢

将代码集成上去之后,发现动图会有点慢,深入代码找原因

//WebpDrawable.java
    public void startFromFirstFrame() {
        Preconditions.checkArgument(!isRunning, "You cannot restart a currently running animation.");
        state.frameLoader.setNextStartFromFirstFrame();
        start();
    }

    public void start() {
        isStarted = true;
        resetLoopCount();
        if(isVisible) {
            startRunning();
        }

    }

    private void startRunning() {
        Preconditions.checkArgument(!isRecycled, "You cannot start a recycled Drawable. Ensure thatyou clear any references to the Drawable when clearing the corresponding request.");
        if(state.frameLoader.getFrameCount() == 1) {
            invalidateSelf();
        } else if(!isRunning) {
            isRunning = true;
            state.frameLoader.subscribe(this);
            invalidateSelf();
        }

    }

可以看到最后通过   state.frameLoader.subscribe(this); 这行代码来加载图片,而frameLoader是webpDrawable创建的时候就被提供的

    WebpDrawable(WebpFrameLoader frameLoader, BitmapPool bitmapPool, Paint paint) {
        this(new WebpState(bitmapPool, frameLoader));
        this.paint = paint;
    }

我们接下去接着看

//WebpFrameLoader.java 
   void subscribe(FrameCallback frameCallback) {
        if (isCleared) {
            throw new IllegalStateException("Cannot subscribe to a cleared frame loader");
        }
        if (callbacks.contains(frameCallback)) {
            throw new IllegalStateException("Cannot subscribe twice in a row");
        }

        boolean start = callbacks.isEmpty();
        callbacks.add(frameCallback);
        if (start) {
            start();
        }
    }

    private void start() {
        if (isRunning) {
            return;
        }
        isRunning = true;
        isCleared = false;

        loadNextFrame();
    }


    private void loadNextFrame() {
        if (!isRunning || isLoadPending) {
            return;
        }

        if (startFromFirstFrame) {
            Preconditions.checkArgument(
                    pendingTarget == null, "Pending target must be null when starting from the first frame");
            webpDecoder.resetFrameIndex();
            startFromFirstFrame = false;
        }

        if (pendingTarget != null) {
            DelayTarget temp = pendingTarget;
            pendingTarget = null;
            onFrameReady(temp);
            return;
        }

        isLoadPending = true;

        // Get the delay before incrementing the pointer because the delay indicates the amount of time
        // we want to spend on the current frame.
        int delay = webpDecoder.getNextDelay();
        long targetTime = SystemClock.uptimeMillis() + delay;

        webpDecoder.advance();
        int frameIndex = webpDecoder.getCurrentFrameIndex();
        next = new DelayTarget(handler, frameIndex, targetTime);

        WebpFrameCacheStrategy cacheStrategy = webpDecoder.getCacheStrategy();
        RequestOptions options = RequestOptions.signatureOf(getFrameSignature(frameIndex))
                .skipMemoryCache(cacheStrategy.noCache());
        requestBuilder.apply(options).load(webpDecoder).into(next);
    }

可以看到这里是通过 loadNextFrame 这个方法读取每一帧的图片并展示的,具体展示的时间是由这两行代码控制

int delay = webpDecoder.getNextDelay();
long targetTime = SystemClock.uptimeMillis() + delay;

我们继续看webpDecoder的相关源码

//webpDecoder.java

    private final int[] mFrameDurations;
    private final WebpFrameInfo[] mFrameInfos;

   @Override
    public int getNextDelay() {
        if (mFrameDurations.length == 0 || mFramePointer < 0) {
            return 0;
        }

        return getDelay(mFramePointer);
    }

    @Override
    public int getDelay(int n) {
        int delay = -1;
        if ((n >= 0) && (n < mFrameDurations.length)) {
            delay = mFrameDurations[n];
        }
        return delay;
    }

可以看到是由 mFrameDurations 这个参数控制,mFrameDurations这个参数是在 WebpImage 类中由 native 方法返回。那么我们只要获取到这个类的这个属性就能控制每一帧之间的间隔了。

试了一下,没有办法直接去设置,那么我们就用反射找到这个对象,然后再去修改这个参数

try {
                                //已知三方库的bug,webp的动图每一帧的时间间隔于实际的有所偏差,需要反射三方库去修改
                                //https://github.com/zjupure/GlideWebpDecoder/issues/33
                                Field gifStateField = mWebpDrawable.getClass().getDeclaredField("state");
                                gifStateField.setAccessible(true);//开放权限
                                Class gifStateClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpDrawable$WebpState");
                                Field gifFrameLoaderField = gifStateClass.getDeclaredField("frameLoader");
                                gifFrameLoaderField.setAccessible(true);

                                Class gifFrameLoaderClass = Class.forName("com.bumptech.glide.integration.webp.decoder.WebpFrameLoader");
                                Field gifDecoderField = gifFrameLoaderClass.getDeclaredField("webpDecoder");
                                gifDecoderField.setAccessible(true);

                                WebpDecoder webpDecoder = (WebpDecoder) gifDecoderField.get(gifFrameLoaderField.get(gifStateField.get(resource)));
                                Field durations = webpDecoder.getClass().getDeclaredField("mFrameDurations");
                                durations.setAccessible(true);
                                int[] args = (int[]) durations.get(webpDecoder);
                                if (args.length > 0) {
                                    for (int i = 0; i < args.length; i++) {
                                        if (args[i] > 30) {
                                            //加载glide会比ios慢 这边把gif的间隔减少15s
                                            args[i] = args[i] - 15;
                                        }
                                    }
                                }
                                durations.set(webpDecoder, args);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }

我这里的话是将每一帧加快了15ms,这个值并不是固定,可以更具需求自己改,但要保证修改之后的数组长度不小于原数组长度。

2.单次播放的动图在页面切换(调用onResume)之后,会再播放一遍

这个问题是在切换界面的时候发现的,后面发现无论怎么设置都无法避免这个问题,后来才发现在页面onresume的时候,组件会调用一次 startFromFirstFrame 方法。估计可能是由于Glide监听了页面的生命周期,导致了每一次切换到了界面都会调用 startFromFirstFrame 这个方法。

如果不想要这个方法的话,我们就要将 isRunning 设为true 即可

//WebpDrawable.java
    public void startFromFirstFrame() {
        Preconditions.checkArgument(!isRunning, "You cannot restart a currently running animation.");
        state.frameLoader.setNextStartFromFirstFrame();
        start();
    }

    public void start() {
        isStarted = true;
        resetLoopCount();
        if(isVisible) {
            startRunning();
        }

    }

    private void startRunning() {
        Preconditions.checkArgument(!isRecycled, "You cannot start a recycled Drawable. Ensure thatyou clear any references to the Drawable when clearing the corresponding request.");
        if(state.frameLoader.getFrameCount() == 1) {
            invalidateSelf();
        } else if(!isRunning) {
            //只要将isRunning设置成true 动画将不会被触发
            isRunning = true;
            state.frameLoader.subscribe(this);
            invalidateSelf();
        }

    }

最后,我选择在页面的onresume中将isRunning设为true

    @Override
    protected void onResume() {
        super.onResume();
        cancelGifOnResume();
        ...
    }

    private void cancelGifOnResume(){
        if(mWebpDrawable==null){
            return;
        }
        try {
            Field isRunning = mWebpDrawable.getClass().getDeclaredField("isRunning");
            isRunning.setAccessible(true);
            isRunning.setBoolean(mWebpDrawable,true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3.二次播放动图时首帧是动图最后一帧

这个是在最后试效果的时候发现的,在网上之后得到的方法只有一个

    //禁止Glide缓存gif图片,否则会导致每次切换页面会先显示gif图片最后一帧,然后才开始播放动画
    RequestOptions options = new RequestOptions() .skipMemoryCache(true);

设置跳过缓存来避免这个问题,实测在这个过程中小的webp的动图效果还是可以的,但是只要动图稍微大一些,效果就不是特别的好了,会明显的先空白一下(处于加载过程中),然后才能正常显示,毕竟从内存中获取图片的速度是最快的。

然后去github,google搜索了一圈之后都没有结果。静下心来重新分析一下网上这个解决方法的思路。

skipMemoryCache(true) 的意思是不使用内存的缓存,并不是不使用disk缓存。那么为什么这么做呢?在Glide中缓存分为disk缓存,内存的缓存,在使用过程中优先匹配的内存缓存,然后是disk缓存,那么代表着在未使用 skipMemoryCache(true) 的过程中,优先匹配内存缓存,并且这一份内存缓存应该是同一份的。

所以我们第二次加载这个图片的时候,用的内存缓存的图片是上一次结束时最后一帧的图片。

那么怎么处理呢?我这边取了个巧,在单次动画结束的时候,有启动了一次,并马上停止,相当于重置内存缓存中的动图的到第一帧。

  mWebpDrawable.registerAnimationCallback(new Animatable2Compat.AnimationCallback() {
                                @Override
                                public void onAnimationStart(Drawable drawable) {
                                    super.onAnimationStart(drawable);
                                }

                                @Override
                                public void onAnimationEnd(Drawable drawable) {
                                    super.onAnimationEnd(drawable);
                                    if (mWebpDrawable != null && !mWebpDrawable.isRunning()) {
                                    //重置内存缓存动图为第一帧
                                        mWebpDrawable.startFromFirstFrame();
                                        mWebpDrawable.stop();
                                    }
                                    mWebpDrawable.unregisterAnimationCallback(this);
                                }
                            });

参考:

Glide加载webp动画及监听动画播放结束_Dway的博客-CSDN博客_glide加载webp

Android基于Glide(4.6.1)加载gif实践_mayundoyouknow的博客-CSDN博客_android glide加载gif

你可能感兴趣的