如何完美监听帧动画?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动画就动不起来了

尾声

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

猜你喜欢

转载自blog.csdn.net/qq_34231329/article/details/131052484