Android动画:两大必学动画详解之属性动画(Property Animation)

引言

    属性动画是Android 3.0(API 11)加入的,如果想在之前的版本使用属性动画,建议使用nineoldandroids,网址是:http://nineoldandroids.com

    属性动画的加入产生了一个新的问题,为什么要加入属性动画呢?

    View动画虽然简单,但也存在着一些问题:

    ①只能作用于View对象。

    ②不能改变真是属性值,而只是改变了视觉效果。

    ③只有四种动画效果,效果种类少。

    而属性动画(Property Animation)是一个强大的动画框架,允许作用在任意对象上。可以随着时间的变化改变任何对象的属性,不管这个对象是否绘制在屏幕上,然后通过对属性的应用而达到动画的效果。

1 ValueAnimator

    ValueAnimator是属性动画重要的类,ObjectAnimator也是继承自ValueAnimator,ValueAnimator有五个常用的方法。

图1-1 ValueAnimator方法

1.1 ofFloat、ofInt、OfArgb

    ofFloat构造并返回一个在float值之间动画的ValueAnimator。传入一个参数时这个参数就是目标值,但是只有一个参数的情况很少用,因为无法确定起始值是什么。所以通常情况下,需要传入两个或者以上的参数。先看看使用方法,如下代码所示。

    var anim: ValueAnimator = ValueAnimator.ofFloat(1f,0f,1f)

    anim.duration =3000

    anim.start()

    方法很简单,动画开始之后可以使用addUpdateListener方法添加AnimatorUpdateListener对动画进行监听。addUpdateListener方法是很重要的方法,因为ValueAnimator主要是对值进行操作的动画,因此需要通过监听去获取相应的值来进行操作以完成动画效果。

anim.addUpdateListener{

    var value =it.animatedValue as Float

    Log.d("MainActivity", value.toString())

}

    日志比较长我就不做截图或者复制了。从日志上可以看出来,值得变化是从1.0f开始然过渡到0.0f再过渡到1.0f。因为没有设置插值器,因此使用的是默认插值器AccelerateDecelerateInterpolator,这个插值器的效果是开始和结束慢中间加速。将value应用在Alpha上可以呈现如下图1-2效果。

private fun myOfFloat() {

    var anim: ValueAnimator = ValueAnimator.ofFloat(1.0f,0.0f,1.0f)

    anim.duration = 3000

    tv.setOnClickListener{

        anim.start()

    }

    anim.addUpdateListener{

        var value =it.animatedValue as Float

        Log.d("MainActivity", value.toString())

        tv.alpha = value

    }

}

图1-2 ofFloat应用效果图

    在某些情况下也许你并不需要float类型,而只是需要使用Int类型,就可以使用ofInt方法。先看看使用方法,代码如下所示:

var anim:ValueAnimator = ValueAnimator.ofInt(0,1)

anim.duration =1000

anim.start()

    ofArgb构造并返回一个在颜色值之间变化的ValueAnimator。ofArgb其实和ofInt本质上是一样的,可以从下图1-3、1-4源码可看出来,ofArgb只是设置了估值器(Evaluator), ArgbEvaluator是一个估值器,它决定了ofArgb传入的值是如何变化的。

图1-3 ofInt源码
图1-4 ofArgb源码

    使用ofArgb的方式也很简单,例如下面方法,传入两个32位颜色值即可达到图1-5效果。

private fun myOfArgb() {

    var startColor = resources.getColor(R.color.colorAccent)

    var enColor = resources.getColor(R.color.colorPrimary)

    var anim: ValueAnimator = ValueAnimator.ofArgb(startColor,enColor)

    anim.duration =3000

    tv.setOnClickListener{

        anim.start()

    }

    anim.addUpdateListener{

        var color: Int =it.animatedValue as Int

        tv.setBackgroundColor(color)

    }

}

图1-5 ofArgb效果

1.2 ofObject

    ofObject构造并返回一个在Object对象之间变化的ValueAnimator。通过下面图1-6源码可以看出来,ofObject至少需要传两个参数。

图1-6 ofObject源码

    evaluator 传入的是 null,Object只能传入两种类型,一种是float,另一种是int类型,传入其他的类型会把报错。可以在动画开始时初始化中看到相关的操作,源码如下:

图1-6 动画初始化操作源码

    再往下查看源码,可以看出来sIntEvaluator是一个IntEvaluator,sFloatEvaluator是一个FloatEvaluator。

图1-7 sIntEvaluator、sFloatEvaluator  实例化

    如果evaluator 传入的是 null,Object传入的是float类型,则可以把ofObject当成ofFloat使用,如下所示:

var anim: ValueAnimator = ValueAnimator.ofObject(null,0.0f,1.0f)

    如果TypeEvaluator传入的是ArgbEvaluator,Object传入的是32位颜色值,则可以把ofObject当成ofArgb使用,如下所示:

var startColor =resources.getColor(R.color.colorAccent)

var enColor =resources.getColor(R.color.colorPrimary)

var anim: ValueAnimator = ValueAnimator.ofObject(ArgbEvaluator(), startColor, enColor)

    ofObject的用处不止于此,它强大的地方在于可以自定义TypeEvaluator,然后传入任意对象。本文将下一小节进行讲解。

1.3 TypeEvaluator

    看了这么多关于TypeEvaluator的应用,肯定会很好奇TypeEvaluator到底是个什么东西。TypeEvaluator其实是一个接口,只有evaluate一个方法。

图1-8 TypeEvaluator

    以IntEvaluator为例子,可以看出来IntEvaluator实现了TypeEvaluator。

图1-9 IntEvaluator源码

    从上面IntEvaluator的源码可以看出,evaluate方法对值做了处理,其中fraction是变化的百分比,然后从startValue开始按照线性速率向endValue变化。因此,如果想要自定义TypeEvaluator ,只需要在evaluate方法上下功夫就可以了。

1.4 ofPropertyValuesHolder

    ofPropertyValuesHolder方法构造并返回一个指定在PropertyValuesHolder内的值进行动画的ValueAnimator。通过下面的源码可以看出来,ofPropertyValuesHolder需要传入至少一个PropertyValuesHolder。

图1-10 ofPropertyValuesHolder源码

    那PropertyValuesHolder又是什么呢?PropertyValuesHolder类保存有关属性的信息以及该属性在动画中应该呈现的值。PropertyValuesHolder对象可用于使用ValueAnimator或ObjectAnimator创建并行操作多个不同属性的动画。

    PropertyValuesHolder的方法有很多,图1-11的方法是最重要的。

图1-11 PropertyValuesHolder方法

    首先看ofFloat的使用方法,下面的代码先实例化了两个PropertyValuesHolder对象,然后再把这两个对象传入ofPropertyValuesHolder生成一个ValueAnimator。

private fun startOfPropertyValuesHolder() {

   val valuesHolderX = PropertyValuesHolder.ofFloat("getX",0.0f,10.0f)

    val valuesHolderY = PropertyValuesHolder.ofFloat("getY",0.0f,100.0f)

    val anim = ValueAnimator.ofPropertyValuesHolder(valuesHolderX,valuesHolderY)

    anim.duration =3000

    anim.addUpdateListener{

        var getX = it.getAnimatedValue("getX") as Float

    }

    anim.start()

}

    上面的代码中,查看源码发现通过getAnimatedValue(String propertyName)方法能获取指定的propertyName相对应的PropertyValuesHolder的值。还可以通过查看源码知道,使用getAnimatedValue()只能获取第一个PropertyValuesHolder的值。

图1-12 getAnimatedValue(String propertyName) 源码
图1-13 getAnimatedValue()源码

    此时可能会想到,之前使用的ofFloat不是也可以传递多值吗?它又是怎么操作的?

    其实查看ofFloat源码,如下图1-14就可以发现,ofFloat传递多值也是实例化了一个PropertyValuesHolder对象。因为没有产生多个PropertyValuesHolder对象,所以通过getAnimatedValue()方法获取到的只是一个PropertyValuesHolder对象的值。

1-14 ofFloat源码

    再来看看ofKeyframe方法,个人认为ofKeyframe方法是PropertyValuesHolder最重要的方法。ofKeyframe能更简单的实现动画,而不需要使用自定义TypeEvaluator。

    Keyframe这个类包含一个动画的 time/value对,ValueAnimator使用Keyframe定义动画目标在整个动画过程中所要用的值,ValueAnimator在当前Keyframe的值和下一个Keyframe的值之间动画。

    先看使用ofKeyframe方法的效果图,如1-15所示。

图1-15 Keyframe使用效果图

    再看看具体代码是怎么写的,代码如下所示:

private fun startKeyFrame() {

    var progressBar2 = findViewById(R.id.progressBar3)

    var key1 = Keyframe.ofInt(0.0f, 0)

    var key2 = Keyframe.ofInt(0.5f, 100)

    var key3 = Keyframe.ofInt(1f, 80)

    var propertyValuesHolder = PropertyValuesHolder.ofKeyframe("progress", key1, key2, key3)

    var anim = ValueAnimator.ofPropertyValuesHolder(propertyValuesHolder)

    anim.duration =3000

    var textView = findViewById(R.id.textView)

    textView.setOnClickListener{

        anim.start()

    }

    anim.addUpdateListener{

        var value =it.animatedValue as Int

        progressBar2.progress = value

        textView.text = value.toString().plus("%")

    }

}

    可以看见,首先使用Keyframe.ofInt获取Keyframe对象,传入两个参数,第一个参数是动画的百分比,第二个参数是相对应的数据值,接着就是创建PropertyValuesHolder传入OfPropertyValuesHolder方法中,最后可以通过addUpdateListener方法获取动画数值对progressBar进行操作完成动画效果。

    其它方法我就不做过多的说明了,因为我想不到有什么实例,哈哈哈哈~。但有几个参数还是要了解:

    TypeConverter<T, V>:该类是一个抽象类,可将类型T转换成类型V,当动画中的属性类型和值类型不相同时这是必要的,它的核心方法是convert。

    Property:该类是一个抽象类,用于表示宿主对象的可变值。

    Path:传入Path表示沿着path动画。

2 ObjectAnimator

    ObjectAnimator继承自ValueAnimator,支持目标对象上的属性进行动画。该类的构造函数使用参数定义将要动画的目标对象以及动画的属性名称,然后在内部选取合适的set/get方法,然后根据需要调用动画的属性。

    在代码中实现ObjectAnimator,代码如下所示,效果图如图2-1:

var anim = ObjectAnimator.ofFloat(textView, "Alpha", 1.0f, 0.0f, 1.0f)

anim.duration =1500

textView.setOnClickListener{

    anim.start()

}

图2-1 alpha效果图

    上面代码中,使用ofFloat构造ObjectAnimator,第一个参数就是目标对象,第二个参数是属性名,第三、四、五参数是具体动画数值。动画是如何实现的呢?是找到textView中的alpha属性进行赋值使用吗?但其实textview没有alpha属性、view也没有alpha属性。

   实际上alpha这个属性名用于反射,通过属性名获取setAlpha方法来使用,查看源码发现TextView中没有setAlpha方法,但是View中有setAlpha方法,因此实际上是调用了View的setAlpha方法

    同理,得到下面的例子:

var anim = ObjectAnimator.ofFloat(textView, "TranslationX", 0f,200f) //对象x轴从0移动到200

var anim = ObjectAnimator.ofFloat(textView, "ScaleX", 1.5f)//对象x轴方法1.5倍

var anim = ObjectAnimator.ofFloat(textView, "Rotation", 0f,360f)//对象旋转360度

    我们也可以在自定义View中使用这种方法,但是需要注意set方法后的第一个字符必须是大写的(驼峰命名法)。下面是自定义view的源代码,在onDraw中画了一条线,并提供了setPosition方法改变线的X轴。

class LineView : View {

    private var mPosition =0f

    constructor(context: Context?) :this(context, null)

    constructor(context: Context?, attrs: AttributeSet?) :this(context, attrs, 0)

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :super(context, attrs, defStyleAttr)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

    }

    override fun onDraw(canvas: Canvas) {

        var paint= Paint()

        paint.strokeWidth =20f

        paint.color =resources.getColor(R.color.colorAccent,null)

        canvas.drawLine(0f, 0f, mPosition, 0f, paint)

    }

    fun setPosition(position: Float) {

        mPosition = position

        invalidate()

    }

}

    接着实例化ObjectAnimator对象,可以看见ofFloat使用了“Position”作为属性名,并且从X轴从0f过渡到1000f。效果图如图2-2所示:

var lineView = findViewById(R.id.lineView)

var anim = ObjectAnimator.ofFloat(lineView, "Position", 0f, 1000f)

anim.duration =1000

anim.start()

图2-2 LineView效果图

    再来看看get方法,在LineView中加入get方法,如下所示:

fun getPosition(): Float {

return 500f

}

    get方法是用来获取初始值用的,当传入的参数是一个参数的时候就需要使用到get方法获取初始值。

var anim = ObjectAnimator.ofFloat(lineView, "Position", 1000f)

    动画首先从500开始过渡到1000,并不是从0开始过渡到1000。看看效果图,如下图2-3所示:

图2-3 LineView使用get方法效果图

    如果不提供get方法程序会报错吗?程序该从哪开始?答案是如果不提供get方法,程序会出现如下提示,但是程序仍然能正常运行,并且从初始值0开始过渡到1000,如图2-2所示。

2018-10-26 11:37:12.429 11681-11681/com.zhj.ktdemo W/PropertyValuesHolder: Method getPosition() with type null not found on target class class com.zhj.ktdemo.LineView

3 AnimatorSet

    AnimatorSet是一个按照顺序执行Animator对象的类。可以一起播放动画,也可以按顺序播放,还可以延时播放。

    有两种不同的方法可以添加动画到AnimatorSet,可以使用playTogether或者playSequentially方法一次添加一组动画,也可以将play方法与Builder类中的方法结合,逐个添加动画。

    使用Animation.play(Aniamtor)返回一个Builder的实例,也可以直接使用Animation.Builder(Animator),他们是一样的。接着就可以使用Builder下的方法配合传入的Animator使用。

    Builder类有四个重要的方法,如下图3-1所示:

图3-1 Builder类

after(Animator):将当前的动画插入到传入的动画之后执行

    例如:animatorSet.play(anim).after(anim2).after(anim3),anima2在anim3之后执行,anim在anim2之后执行

after(long):将动画延迟执行

    例如:animatorSet.play(anim).after(3000),anim延时3000毫秒之后执行

before(Animator):将之前的动画插入到传入的动画之前执行

    例如:animatorSet.play(anim).before(anim2).before(anim3),anim在anim2之前执行,anim2在anim3之前执行

with(Animator):将当前动画和传入的动画同时执行

    例如:animatorSet.with(anim).with(anim2).with(anim3),三个动画同时执行 

    可以看具体的使用,代码如下所示:

var textView = findViewById(R.id.textView)

var anim = ObjectAnimator.ofFloat(textView, "TranslationX", -500f, 0f)

anim.duration =1000

var anim2 = ObjectAnimator.ofFloat(textView, "Alpha", 0f, 1f)

anim2.duration =1000

var anim3 = ObjectAnimator.ofFloat(textView, "ScaleX", 1f, 2f)

anim3.duration =2000

var anim4 = ObjectAnimator.ofFloat(textView, "ScaleY", 1f, 2f)

anim3.duration =2000

var animatorSet = AnimatorSet()

animatorSet.play(anim).with(anim2).after(anim3).after(anim4)

textView.setOnClickListener{

    animatorSet.start()

}

    从效果图3-2可以看出来,after(anim4)执行在play(anim).with(anim2).after(anim3)之前,after(anim3)执行在play(anim).with(anim2)之前,play(anim).with(anim2)同时执行。

图3-2 AnimatorSet效果图

    如果有需要,还可以继续使用addListener添加动画监听,代码如下所示:

animatorSet.addListener(object :Animator.AnimatorListener{

    override fun onAnimationRepeat(animation: Animator?) {

    }

    override fun onAnimationEnd(animation: Animator?) {

    }

    override fun onAnimationCancel(animation: Animator?) { 

    }

    override fun onAnimationStart(animation: Animator?) {

    }

}

4 总结

    ValueAnimator继承自Animator,主要是对属性值进行操作。配合自定义估值器TypeEvaluator,可以自定义属性值进行动画。

    ObjectAnimator继承自ValueAnimator,可以直接操作对象,只要对象中有合适的set方法,可以利用其关键字符当属性名。例如:var anim = ObjectAnimator.ofFloat(textView, "Alpha", 0f, 1f),因为View中setAlpha方法,因此可使用Alpha当属性名。如果View中提供get方法,当只传入一个数值时,可使用get方法获取初始值。例如:var anim = ObjectAnimator.ofFloat(lineView, "Position", 1000f),因为lineView中有getPosition方法,从该方法获取的值可当初始值使用。

    AnimatorSet继承自Animator,可以将ValueAnimator和ObjectAnimator任意组合播放。可使用Play方法获取Builder类,配合Builder类的四个方法实现动画的多种效果。

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

推荐阅读更多精彩内容