Android属性动画——没有什么动画是一个AnimSet不能解决的

没有什么动画是一个AnimSet不能解决的,如果有那就再来一个AnimSet。项目是Kotlin写的也不复杂,不懂Kotlin刚好可以学学。

系统动画那些坑

现在应该没人使用View动画了吧,还再使用怕是学的假Android了,所以这边讲的是属性动画。

先说说ValueAnimator

  • 不提供动画方向判断方法,这点一直很困惑,查看源码发现有一个很明显的字段mReversing,跟踪下方法发现shouldPlayBackward()方法,兴高采烈的去调用时才发现是私有方法😓。无奈之下只能用反射调用,结果在5.0系统突然崩了,去查5.0源码发现居然是不同字段mPlayingBackwards,再去查5.1的源码发现居然两个都存在,就不能专一点吗,8.0更新又不能获取了解决方法待定,感觉不能再玩反射太不靠谱了。
  • revese() 方向是个问题,字面上理解是反转的意思,不就是到着播放嘛,但是当你倒着播放时再掉reverse()又给正向播放了,然后还不告诉你方向不带这么玩的啊😢。
  • 播放时间,还是不给方向判断的坑😢。

再说说AnimatorSet

该怎么说远看像越野车近看才发现是拖拉机,能存在并被使用简直是个奇迹。

  • reverse()是隐藏方法也就是说不能用了,忍了看在能播放那么多动画的面子上。
  • 播放存在问题,当一个动画没结束再次start()会发现前面播放过的动画居然不播放了,这还怎么玩啊。
  • 看似播放方式多样但并没有什么卵用,with,before,after包含了多种播放方式,但是实际使用时基本都是一个动画没结束就开始下一个动画,这中理想的动画播放方式根本用不到。

实现效果

动画要求:总动画时间3s,红块直接开始时间3s,绿块1s后开始时间2s,蓝块2s后开始时间1s,动画执行过程中可以随时来回切换,可以暂停、继续、结束和取消,可以想象下使用系统提供的方式要怎么实现。

Kapture 2017-07-16 at 13.35.24.gif

ValueAnim

看看怎么填ValueAnimator的坑,获取播放方向问题,通过反射获取播放方向,利用Kotlin扩展方法的特性,对ValueAnimator进行扩展,但是mReversing的值只有再动画播放时才有效果,动画结束就被初始化为false了,结果还得在结束前把方向保存下来。Kotlin并不能真正给添加个参数到某个类,只能继承ValueAnimator进行扩展了。其次播放控制问题,为了保留原来的方法和避免reverse()存在的问题,添加了几个方法animStart()正向播放,animReverse()反向播放,animTrigger()切换方向(类似reverse()作用)。代码很简单并注释了以后就用它来替代ValueAnimator了,本来想也改下ObjectAnimator发现是final无法继承,看在没什么大问题的份上就放过它了。

package cn.wittyneko.anim

import android.animation.*

/**
 * Created by wittyneko on 2017/7/7.
 */

open class ValueAnim : ValueAnimator(), AnimListener {


    companion object {
        internal val argbEvaluator = ArgbEvaluator()

        fun ofInt(vararg values: Int): ValueAnim {
            val anim = ValueAnim()
            anim.setIntValues(*values)
            return anim
        }

        fun ofArgb(values: IntArray): ValueAnim {
            val anim = ValueAnim()
            anim.setIntValues(*values)
            anim.setEvaluator(argbEvaluator)
            return anim
        }

        fun ofFloat(vararg values: Float): ValueAnim {
            val anim = ValueAnim()
            anim.setFloatValues(*values)
            return anim
        }

        fun ofPropertyValuesHolder(vararg values: PropertyValuesHolder): ValueAnim {
            val anim = ValueAnim()
            anim.setValues(*values)
            return anim
        }

        fun ofObject(evaluator: TypeEvaluator<*>, vararg values: Any): ValueAnim {
            val anim = ValueAnim()
            anim.setObjectValues(*values)
            anim.setEvaluator(evaluator)
            return anim
        }

    }


    private var _isAnimReverse: Boolean = true

    var listener: AnimListener? = null

    var isAnimEnd: Boolean = false
        protected set

    var isAnimCancel: Boolean = false
        protected set

    //是否反向
    var isAnimReverse: Boolean
        get() {
            if (isRunning) {
                return isReversing
            } else {
                return _isAnimReverse
            }
        }
        internal set(value) {
            _isAnimReverse = value
        }

    //动画播放时间
    val animCurrentPlayTime: Long
        get() {
            if (isRunning && isAnimReverse) {
                return duration - currentPlayTime
            } else {
                return currentPlayTime
            }
        }

    init {
        addListener(this)
        addUpdateListener(this)
    }


    /**
     * 正向播放
     */
    open fun animStart() {
        when {
            isRunning && isAnimReverse -> {
                reverse()
            }
            !isRunning -> {
                start()
            }
        }
    }

    /**
     * 反向播放
     */
    open fun animReverse() {
        when {
            isRunning && !isAnimReverse -> {
                reverse()
            }
            !isRunning -> {
                reverse()
            }
        }
    }

    /**
     * 切换播放方向
     */
    open fun animTrigger() {
        if (isAnimReverse) {
            animStart()
        } else {
            animReverse()
        }
    }

    override fun start() {
        isAnimCancel = false
        isAnimEnd = false
        super.start()
    }

    override fun reverse() {
        isAnimCancel = false
        isAnimEnd = false
        super.reverse()
    }

    override fun end() {
        isAnimCancel = false
        isAnimEnd = true
        super.end()
    }

    override fun cancel() {
        isAnimCancel = true
        isAnimEnd = false
        super.cancel()
    }

    override fun onAnimationUpdate(animation: ValueAnimator?) {
        listener?.onAnimationUpdate(animation)
    }

    override fun onAnimationStart(animation: Animator?) {
        listener?.onAnimationStart(animation)
    }

    override fun onAnimationEnd(animation: Animator?) {
        if ((isStarted || isRunning) && animation is ValueAnimator) {
            _isAnimReverse = animation.isReversing
        }
        listener?.onAnimationEnd(animation)
    }

    override fun onAnimationCancel(animation: Animator?) {
        listener?.onAnimationCancel(animation)
    }

    override fun onAnimationRepeat(animation: Animator?) {
        listener?.onAnimationRepeat(animation)
    }
}

interface AnimListener : ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener

// 动画播放时方向 api22+
val ValueAnimator.isReversing: Boolean
    get() {
        try {
            var rfield = ValueAnimator::class.java.getDeclaredField("mReversing")
            rfield.isAccessible = true
            return rfield.get(this) as? Boolean ?: false
        } catch (e: Throwable) {
            return isPlayingBackwards
        }
    }

// 动画播放时方向 api21-
val ValueAnimator.isPlayingBackwards: Boolean
    get() {
        try {
            var rfield = ValueAnimator::class.java.getDeclaredField("mPlayingBackwards")
            rfield.isAccessible = true
            return rfield.get(this) as? Boolean ?: false
        } catch (e: Throwable) {
            return false
        }
    }

AnimSet

这才是本篇的重点,首先跟AnimatorSet没有半毛关系,AnimatorSet是个final类其次再它基础上修改,还不如重造一个容易。所以AnimSet当然是再拥有优良血统的ValueAnim上扩展出来的啦。为了避免AnimatorSet的坑AnimSet设计得很简单,如果想要AnimatorSet的的beforeafter的效果也可以很方便的扩展,为了偷懒不对是为了简单易懂,就不实现了毕竟没什么用。子动画播放时间只跟动画集合有关,通俗的讲假设动画集合播放1秒后开始播放第一个动画2秒后开始第二个动画,这样只要一个子动画相对集合的延迟时间就足够实现复杂动画了。任何复杂动画都能简单的实现,剩下的就是其它的优化了,比如子动画的播放方向,动画集合嵌套问题的处理了。代码重点在于addChildAnim()添加子动画,animChildPlayTime()计算子动画播放时间,onAnimationUpdate刷新子动画。

package cn.wittyneko.anim

import android.animation.*
import android.view.animation.LinearInterpolator

/**
 * Created by wittyneko on 2017/7/6.
 */

open class AnimSet : ValueAnim() {

    companion object {

        fun ofDef(): AnimSet {
            return ofFloat(0f, 1f)
        }

        fun ofInt(vararg values: Int): AnimSet {
            val anim = AnimSet()
            anim.setIntValues(*values)
            return anim
        }

        fun ofArgb(values: IntArray): AnimSet {
            val anim = AnimSet()
            anim.setIntValues(*values)
            anim.setEvaluator(argbEvaluator)
            return anim
        }

        fun ofFloat(vararg values: Float): AnimSet {
            val anim = AnimSet()
            anim.setFloatValues(*values)
            return anim
        }

        fun ofPropertyValuesHolder(vararg values: PropertyValuesHolder): AnimSet {
            val anim = AnimSet()
            anim.setValues(*values)
            return anim
        }

        fun ofObject(evaluator: TypeEvaluator<*>, vararg values: Any): AnimSet {
            val anim = AnimSet()
            anim.setObjectValues(*values)
            anim.setEvaluator(evaluator)
            return anim
        }

    }

    var childAnimSet: HashSet<AnimWrapper> = hashSetOf()

    init {
        interpolator = LinearInterpolator()
    }

    /**
     * 计算子动画播放时间
     * @param delayed 子动画延迟时间
     * @param duration 子动画时长
     *
     * @return 子动画当前播放时间
     */
    fun animChildPlayTime(delayed: Long, duration: Long): Long {
        var childPlayTime = animCurrentPlayTime - delayed
        when {
            childPlayTime < 0 -> {
                childPlayTime = 0
            }
            childPlayTime > duration -> {
                childPlayTime = duration
            }
        }
        return childPlayTime
    }

    /**
     * 添加子动画
     * @param childAnim 子动画
     * @param delayed 子动画延迟时间
     * @param tag 子动画tag标签
     */
    fun addChildAnim(childAnim: ValueAnimator, delayed: Long = 0, tag: String = AnimWrapper.EMPTY_TAG): AnimSet {
        addChildAnim(AnimWrapper(childAnim, delayed, tag))
        return this
    }

    /**
     * 添加子动画
     * @param child 子动画包装类
     *
     * @throws e duration grate than parent
     */
    fun addChildAnim(child: AnimWrapper): AnimSet {
        if (child.delayed + child.anim.duration > this.duration)
            throw Exception("duration greater than parent")
        childAnimSet.add(child)
        return this
    }

    override fun onAnimationUpdate(animation: ValueAnimator?) {
        super.onAnimationUpdate(animation)

        childAnimSet.forEach {
            //刷新子动画
            val anim = it.anim
            anim.currentPlayTime = animChildPlayTime(it.delayed, anim.duration)

            if(anim is ValueAnim) {
                anim.isAnimReverse = isAnimReverse
            }
        }
    }

    override fun onAnimationStart(animation: Animator?) {
        super.onAnimationStart(animation)

        childAnimSet.forEach {
            val anim = it.anim
            anim.listeners?.forEach {
                it.onAnimationStart(anim)
            }
        }
    }

    override fun onAnimationEnd(animation: Animator?) {
        super.onAnimationEnd(animation)

        childAnimSet.forEach {
            val anim = it.anim
            if (isAnimEnd) {
                if (isAnimReverse)
                    anim.currentPlayTime = 0
                else
                    anim.currentPlayTime = anim.duration
            }
            anim.listeners?.forEach {
                it.onAnimationEnd(anim)
            }
        }
    }

    override fun onAnimationCancel(animation: Animator?) {
        super.onAnimationCancel(animation)

        childAnimSet.forEach {
            val anim = it.anim
            anim.listeners?.forEach {
                it.onAnimationCancel(anim)
            }
        }
    }

    override fun onAnimationRepeat(animation: Animator?) {
        super.onAnimationRepeat(animation)

        childAnimSet.forEach {
            val anim = it.anim
            anim.listeners?.forEach {
                it.onAnimationRepeat(anim)
            }
        }
    }

    /**
     * 子动画包装类
     */
    class AnimWrapper(
            var anim: ValueAnimator,
            var delayed: Long = 0,
            var tag: String = AnimWrapper.EMPTY_TAG) {
        companion object {
            val EMPTY_TAG = ""
        }
    }
}

使用方法

见证奇迹的时刻,神兽保佑🙏代码无Bug。看看如何实现上面的动画要求。应该没什么需要解释的方案A只用一个AnimSet,方案B采用AnimSet嵌套AnimSet。

        val msec = 1000L
        val animTime = ValueAnim.ofFloat(0f, 1f)
        animTime.interpolator = LinearInterpolator()
        animTime.duration = msec * 3
        animTime.addUpdateListener {
            time.text = "time: ${animTime.animCurrentPlayTime}"
        }

        val objAnimRed = ObjectAnimator.ofFloat(red, "translationX", 0f, 300f)
        objAnimRed.interpolator = LinearInterpolator()
        objAnimRed.duration = msec * 3

        val objAnimGreen = ObjectAnimator.ofFloat(green, "translationX", 0f, 300f)
        objAnimGreen.interpolator = LinearInterpolator()
        objAnimGreen.duration = msec * 2

        val objAnimBlue = ObjectAnimator.ofFloat(blue, "translationX", 0f, 300f)
        objAnimBlue.interpolator = LinearInterpolator()
        objAnimBlue.duration = msec * 1

        animSet = AnimSet.ofDef()
        animSet.duration = msec * 3;
        //Plan A
//        animSet.addChildAnim(animTime)
//                .addChildAnim(objAnimRed)
//                .addChildAnim(objAnimGreen, msec * 1)
//                .addChildAnim(objAnimBlue, msec * 2)

        //Plan B
        val childSet = AnimSet.ofDef()
        childSet.duration = msec * 2
        childSet.addChildAnim(objAnimGreen)
                .addChildAnim(objAnimBlue, msec * 1)

        animSet.addChildAnim(animTime)
                .addChildAnim(objAnimRed)
                .addChildAnim(childSet, msec * 1)

        trigger.onClick {
            animSet.animTrigger()
        }
        start.onClick {
            animSet.animStart()
        }
        reverse.onClick {
            animSet.animReverse()
        }
        pause.onClick {
            animSet.pause()
        }
        resume.onClick {
            animSet.resume()
        }
        end.onClick {
            animSet.end()
        }
        cancel.onClick {
            animSet.cancel()
        }

还是贴下链接吧 https://github.com/wittyneko/libs-android/tree/master/base/src/main/kotlin/cn/wittyneko/anim

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。转载请保留作者及原文链接

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

推荐阅读更多精彩内容