当前位置: 代码网 > it编程>App开发>Android > 如何完美监听帧动画?AnimationDrawable深度解析

如何完美监听帧动画?AnimationDrawable深度解析

2024年08月01日 Android 我要评论
作为苦逼的程序员,产品和设计提出来的需求咱也没法拒绝,这不,前两天设计就给提了个需求,要求在帧动画结束后,把原位置的动画替换成一段文字,我们该怎么监听AnimationDrawable的结束事件呢

简介

作为苦逼的程序员,产品和设计提出来的需求咱也没法拒绝,这不,前两天设计就给提了个需求,要求在帧动画结束后,把原位置的动画替换成一段文字。我们知道,在android中,帧动画的实现类为animationdrawable,而这玩意儿又不像animator一样可以通过addlistener之类的方法监听动画的开始、结束等事件,那我们该怎么监听animationdrawable的结束事件呢?

目前网上大多数的做法都是获取帧动画的总时长,然后用handler做一个postdelayed执行结束后的事情。这种方法怎么说呢?能用,但是不够精准也不够优雅,本文我们将从源码层面解析animationdrawable是如何将一帧帧的图片组合起来展示成连续的动画的,再从中寻求动画监听的切入点。

注:只想看实现的朋友们可以直接跳到 包装drawable.callback 这一节看最终实现

imageview如何展示drawable

animationdrawable说到底它也就是个drawable,而我们一般都是使用imageview作为drawable展示的布局,那我们就以此作为入口开始分析drawableimageview中是如何被展示的。

回想一下,我们想要给一个imageview设置图片一般可以用下面几种方法:

  • setimagebitmap
  • setimageresource
  • setimageuri
  • setimagedrawable

setimagebitmap会将bitmap包装成一个bitmapdrawable,然后再调用setimagedrawable方法。

setimageresourcesetimageuri方法会通过resolveuri方法从resourceuri中解析出drawable,然后调用updatedrawable方法

setimagedrawable方法则会直接调用updatedrawable方法

最终殊途同归走到updatedrawable方法中

private void updatedrawable(drawable d) {
    ...
    if (mdrawable != null) {
        samedrawable = mdrawable == d;
        mdrawable.setcallback(null);
        unscheduledrawable(mdrawable);
        ...
    }

    mdrawable = d;

    if (d != null) {
        d.setcallback(this);
        ...
    } else {
        ...
    }
}

可以看到,这里将我们设置的图片资源赋值到mdrawable上。注意,这里有一个drawable动起来的关键点,同时也是我们动画监听的最终切入点:drawable.setcallback(this),我们后面分析帧切换的时候会详细去聊它。

我们知道,一个控件想要绘制内容得在ondraw方法中操作canvas,所以让我们再来看看ondraw方法

protected void ondraw(canvas canvas) {
    super.ondraw(canvas);

    if (mdrawable == null) {
        return; // couldn't resolve the uri
    }

    if (mdrawablewidth == 0 || mdrawableheight == 0) {
        return;     // nothing to draw (empty bounds)
    }

    ...
    mdrawable.draw(canvas);
    ...
}

可以看到,这里调用了drawable.draw方法将drawable自身绘制到imageviewcanvas

drawablecontainer

查看animationdrawable的继承关系我们可以得知它继承自drawablecontainer,从命名中我们就能看出来,它是drawable的容器,我们来看一下它所实现的draw方法:

public void draw(canvas canvas) {
    if (mcurrdrawable != null) {
        mcurrdrawable.draw(canvas);
    }
    if (mlastdrawable != null) {
        mlastdrawable.draw(canvas);
    }
}

mlastdrawable是为了完成动画的切换效果(出入场动画)所准备的,我们可以不用关心它。

我们可以发现,它的内部有一个名为mcurrdrawable的成员变量,我们可以合理猜测它是通过切换mcurrdrawable指向的目标drawable来完成展示不同图片的功能,那么事实是这样吗?

没错,drawablecontainer给我们提供了一个selectdrawable方法,用来切换不同的图片:

public boolean selectdrawable(int index) {
    if (index == mcurindex) {
        return false;
    }

    ...

    if (index >= 0 && index < mdrawablecontainerstate.mnumchildren) {
        final drawable d = mdrawablecontainerstate.getchild(index);
        mcurrdrawable = d;
        mcurindex = index;
        ...
    } else {
        mcurrdrawable = null;
        mcurindex = -1;
    }

    ...

    invalidateself();

    return true;
}

可以看到,和我们猜想的一样,在drawablecontainer的内部有一个子类drawablecontainerstate用于保存所有的drawable,它继承自drawable.constantstate,是用来储存drawable间的常量状态和数据的。在drawablecontainerstate中有一个mdrawables数组用于保存所有的drawable,通过addchild方法将drawable加入到这个数组中

而在selectdrawable方法中,它通过getchild方法去获取当前应该显示的drawable,并将其和index分别赋值给它的两个成员变量mcurrdrawablemcurindex,然后调用invalidateself方法执行重绘:

public void invalidateself() {
    final callback callback = getcallback();
    if (callback != null) {
        callback.invalidatedrawable(this);
    }
}

invalidateself被定义实现在drawable类中,还记得我之前让大家注意的callback吗?在设置图片这一步时,它就被赋值了,实际上这个接口被view所实现,所以在前面我们可以看到调用setcallback时,我们传入的参数为this

不过imageview在继承view的同时也重写了这个invalidatedrawable方法,最终调用了invalidate方法执行重绘,此时,一张新的图片就被展示到我们的屏幕上了

//imageview.invalidatedrawable
public void invalidatedrawable(@nonnull drawable dr) {
    if (dr == mdrawable) {
        if (dr != null) {
            // update cached drawable dimensions if they've changed
            final int w = dr.getintrinsicwidth();
            final int h = dr.getintrinsicheight();
            if (w != mdrawablewidth || h != mdrawableheight) {
                mdrawablewidth = w;
                mdrawableheight = h;
                // updates the matrix, which is dependent on the bounds
                configurebounds();
            }
        }
        /* we invalidate the whole view in this case because it's very
            * hard to know where the drawable actually is. this is made
            * complicated because of the offsets and transformations that
            * can be applied. in theory we could get the drawable's bounds
            * and run them through the transformation and offsets, but this
            * is probably not worth the effort.
            */
        invalidate();
    } else {
        super.invalidatedrawable(dr);
    }
}

animationdrawable

drawablecontainer分析完后,我们可以很自然的想到,animationdrawable就是通过drawablecontainer这种可以切换图片的机制,每隔一定时间执行一下selectdrawable便可以达成帧动画的效果了。

我们先回想一下,在代码中怎么构造出一个多帧的animationdrawable?没错,用默认构造方法实例化出来后,调用它的addframe方法往里一帧帧的添加图片:

public void addframe(@nonnull drawable frame, int duration) {
    manimationstate.addframe(frame, duration);
    if (!mrunning) {
        setframe(0, true, false);
    }
}

可以看到animationdrawable也有一个内部类animationstate,继承自drawablecontainerstate,它的addframe方法就是调用drawablecontainerstate.addchild方法添加图片,同时将这张图片的持续时间保存在mdurations数组中:

public void addframe(drawable dr, int dur) {
    int pos = super.addchild(dr);
    mdurations[pos] = dur;
}

想让animationdrawable动起来的话,我们得要调用它的start方法,那我们就从这个方法开始分析:

public void start() {
    manimating = true;

    if (!isrunning()) {
        // start from 0th frame.
        setframe(0, false, manimationstate.getchildcount() > 1
                || !manimationstate.moneshot);
    }
}

这里将manimating状态置为true,然后调用setframe方法从第0帧开始展示图片

private void setframe(int frame, boolean unschedule, boolean animate) {
    if (frame >= manimationstate.getchildcount()) {
        return;
    }
    manimating = animate;
    mcurframe = frame;
    selectdrawable(frame);
    if (unschedule || animate) {
        unscheduleself(this);
    }
    if (animate) {
        // unscheduling may have clobbered these values; restore them
        mcurframe = frame;
        mrunning = true;
        scheduleself(this, systemclock.uptimemillis() + manimationstate.mdurations[frame]);
    }
}

这里可以看到,和我们所想的一样,调用了drawablecontainer.selectdrawable切换当前展示图片,由于我们之前将manimating赋值为了true,所以会调用scheduleself方法调度展示下一张图片,时间为当前帧持续时间后

public void scheduleself(@nonnull runnable what, long when) {
    final callback callback = getcallback();
    if (callback != null) {
        callback.scheduledrawable(this, what, when);
    }
}

scheduleself方法调用了drawable.callback.scheduledrawable方法,我们去view里面看实现:

public void scheduledrawable(@nonnull drawable who, @nonnull runnable what, long when) {
    if (verifydrawable(who) && what != null) {
        final long delay = when - systemclock.uptimemillis();
        if (mattachinfo != null) {
            mattachinfo.mviewrootimpl.mchoreographer.postcallbackdelayed(
                    choreographer.callback_animation, what, who,
                    choreographer.subtractframedelay(delay));
        } else {
            // postpone the runnable until we know
            // on which thread it needs to run.
            getrunqueue().postdelayed(what, delay);
        }
    }
}

实际上两个分支最终都是通过handler实现延时调用,而调用的runnable对象就是之前scheduleself传入的this。没错,animationdrawable实现了runnable接口:

public void run() {
    nextframe(false);
}

private void nextframe(boolean unschedule) {
    int nextframe = mcurframe + 1;
    final int numframes = manimationstate.getchildcount();
    final boolean islastframe = manimationstate.moneshot && nextframe >= (numframes - 1);

    // loop if necessary. one-shot animations should never hit this case.
    if (!manimationstate.moneshot && nextframe >= numframes) {
        nextframe = 0;
    }

    setframe(nextframe, unschedule, !islastframe);
}

可以看到,在一帧持续时间结束后,便会调用nextframe方法,计算下一帧的index,然后调用setframe方法切换下一帧,形成一个循环,这样一帧帧的图片便动了起来,形成了帧动画

包装drawable.callback

我们从源码层面分析了帧动画是如何运作的,那么怎么监听动画事件相信各位应该都能得出结论了吧?没错,就是重设drawablecallback

drawable被设置到控件中后,控件会将自身作为drawable.callback设置给drawable,那么我们只需要重新给drawable设置一个drawable.callback,在其中调用view回调方法的同时,加入自己的监听逻辑即可

val animdrawable = imageview.drawable as animationdrawable
val callback = object : drawable.callback {
    override fun invalidatedrawable(who: drawable) {
        imageview.invalidatedrawable(who)
        if (animdrawable.getframe(animdrawable.numberofframes - 1) == current 
                && animdrawable.isoneshot 
                && animdrawable.isrunning 
                && animdrawable.isvisible
        ) {
            val lastframeduration = getduration(animdrawable.numberofframes - 1)
            postdelayed({ ...//结束后需要做的事 }, lastframeduration.tolong())
        }
    }

    override fun scheduledrawable(who: drawable, what: runnable, `when`: long) {
        imageview.scheduledrawable(who, what, `when`)
    }

    override fun unscheduledrawable(who: drawable, what: runnable) {
        imageview.unscheduledrawable(who, what)
    }
}
//注意一定需要用一个成员变量或其他方式持有这个callback
//因为drawable.callback是以弱引用的形式被保存在drawable内的,很容易被回收
mcallbackholder = callback
animdrawable.callback = callback
animdrawable.start()

以上的代码便是示例,当满足动画运行到最后一帧,且满足结束状态时,在最后一帧的持续时间后处理结束后需要做的事

animationdrawable切换visible状态为false时,动画会被暂停,如果在动画结束后触发setvisible(false)事件,也会触发invalidatedrawable回调,所以这里需要额外判断一下isvisible

自己包装的drawable.callback一定需要找个东西将它强引用起来,因为drawable.callback是以弱引用的形式被保存在drawable内的,很容易被回收,一旦被回收,整个animationdrawable动画就动不起来了

尾声

为了这么简单一个小功能,还得跑到源码里看怎么实现,对此我的感受是:一入安卓深似海,从此头发是路人

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com