带你走一波Android自定义Animator属性动画相关事项(一)

一、简介

image.png

如上图所示:android动画分类大致有两种一种是View动画一种是转场动画

帧动画:将图片一张一张按顺序播放,展现出动画效果。

补间动画:实现动画alpha(淡入淡出),translate(位移),scale(缩放大小),rotate(旋转)等效果,一般采用xml文件形式。

属性动画:(重点)它是对于对象属性的动画。补间动画的内容,都可以通过属性动画实现。

这里我们就不讲帧动画补间动画,这两个大家可以自己百度一下用法。(另外这篇文章中的动画都是在代码中实现的,如果要看xml的使用方法,可以看看Android 动画使用 scale、alpha、translate、rotate、set
这篇其他人写的这篇文章。

二、属性动画

基本使用 (ViewPropertyAnimator)

imag_view.animate().run {
translationX(400f) //设置左移
duration = 1000//设置动画运行的时间
setInterpolator(LinearInterpolator()) //设置线性插值器
}

gifeditor_20191202_151011.gif

上面是最基本的使用,api提供的有移动旋转缩放透明,看下面的api:
image.png

上面的api中可以看到 都存在绝对相对(方法后面-by)的方法,其中绝对的方法以上面的代码为例子,区别是:
translationX(400f)代表将translationX变成400
translationXBy(400f)代表将translationX增加400

这么多api就不做逐一展示了。

ObjectAnimator

1. 基本的使用方式:
  1. 用 ObjectAnimator.ofXXX() 创建 ObjectAnimator 对象;
  2. 添加时长、差值器等各种参数
  3. 用 start() 方法执行动画。
ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f).run {
    duration = 1000
    interpolator = LinearInterpolator()
    start()
}

gifeditor_20191202_164532.gif

上面是对一个系统提供的View进行动画展示,主要方法是ObjectAnimator.ofFloat(imag_view,View.ROTATION,0f,180f)
第一个参数:传入要进行属性动画的view
第二个参数:要变化的属性值,这里是传入View.ROTATION,也就是"rotation"
第三个参数:属性起始值
第四个参数:属性结束值
后面还可以加入多个参数值,从第三个参数开始到最后第n个参数,表示属性开始 ->中间值->中间值.... ->结束值
注意一点:并非所有的属性都是可以有set get方法,可以进行属性动画。

2. 自定义View动画

自定义View属性动画以及使用的步骤:

  1. 为要改变的属性添加setter/getter方法
  2. setter方法中调用invalidate()使其重新绘画
  3. 在onDraw()中根据改变的属性绘画出你要的效果
  4. 使用的时候跟ObjectAnimator基本使用方式一致

大致的模板代码


class CircleView : View {
  //提供给外面改变的属性值 这里使用kotlin语法 实际上它默认已经实现了setter/getter方法
// 但是这里个set方法我们要改写一下 记得调用 invalidate()
    var progress: Float = 0f
        set(value) {
            field = value
            invalidate()
        }

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        //省略......
        canvas!!.drawArc(rectF, 135f, progress * 2.7f, false, paint)
        //省略......
        canvas.drawText("${progress.toInt()}%", centerX, centerY+40, paint)
    }

}

真正的实现与调用

class CircleView : View {

    val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    val rectF = RectF()

    init {
        paint.run {
            textSize = dpToPixel(50f)
            textAlign = Paint.Align.CENTER
        }
    }

    var radius = dpToPixel(120f)

    var progress: Float = 0f
        set(value) {
            field = value
            invalidate()
        }

    constructor(context: Context) : super(context)
    constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet)


    @RequiresApi(Build.VERSION_CODES.M)
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        var centerX = width / 2f
        var centerY = height / 2f
        //画弧形进度条
        paint.run {
            color = context.getColor(R.color.colorAccent)
            style = Paint.Style.STROKE
            strokeCap = Paint.Cap.ROUND
            strokeWidth = dpToPixel(20f)
        }
        rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius)
        canvas!!.drawArc(rectF, 135f, progress * 2.7f, false, paint)

        //画百分比的数值
        paint.run {
            color = Color.BLACK
            style = Paint.Style.FILL
        }
        canvas.drawText("${progress.toInt()}%", centerX, centerY+40, paint)
    }

}
//调用代码
ObjectAnimator.ofFloat(circle_view,"progress",0f,80f).run {
  duration = 2000
  interpolator = OvershootInterpolator() //插值器 超过结束值后再回弹
  repeatCount = INFINITE  // 重复次数-无限循环
  repeatMode = RESTART //重复模式-重新开始
  start()
}
gifeditor_20191202_172002.gif

这里设置了OvershootInterpolator插值器,使得它可以超过后回弹回来。这里可以使用PropertyValuesHolders.ofKeyframe()做到类似效果,可以翻到下面对应内容看一下

ValueAnimator

这个是ObjectAnimator的父类,这个我并没怎么接触,后期再补充吧

2. 设置监听器

  • 设置ObjectAnimator的监听器
监听器类型.png
//添加停止的监听器
addPauseListener( object : Animator.AnimatorPauseListener{
  override fun onAnimationPause(animation: Animator?) {
    Log.i("MainActivity","addPauseListener -- onAnimationPause ------------------------------------")
  }
  override fun onAnimationResume(animation: Animator?) {
    Log.i("MainActivity","addPauseListener -- onAnimationResume ------------------------------------")
  }
})
//添加更新的监听器
addUpdateListener(object :ValueAnimator.AnimatorUpdateListener{
  override fun onAnimationUpdate(animation: ValueAnimator?) {
//    Log.i("MainActivity","addUpdateListener -- onAnimationUpdate ------------------------------------")
    }
  })
//添加监听器
addListener(object : Animator.AnimatorListener{
  override fun onAnimationRepeat(animation: Animator?) {
    Log.i("MainActivity","AnimatorListener -- onAnimationRepeat------------------------------------")
    }
   override fun onAnimationEnd(animation: Animator?) {
    Log.i("MainActivity","AnimatorListener -- onAnimationEnd------------------------------------")
    }
  override fun onAnimationCancel(animation: Animator?) {
  Log.i("MainActivity","AnimatorListener -- onAnimationCancel------------------------------------")
  }
  override fun onAnimationStart(animation: Animator?) {
      Log.i("MainActivity","AnimatorListener -- onAnimationStart------------------------------------")
     }
  })
}

设置点击事件监听

R.id.start_object_animator ->{
                省略...
                mObjectAnim.start()
                Log.i("MainActivity","点击开始状态 --${!mObjectAnim.isStarted}")
            }

            R.id.end_object_animator ->{
                mObjectAnim.end()
                Log.i("MainActivity","点击结束状态 --${!mObjectAnim.isRunning}")

            }
            R.id.cancel_object_animator ->{
                mObjectAnim.cancel()
                Log.i("MainActivity","点击取消状态 -- ")
            }
            R.id.pause_object_animator->{
                if (mObjectAnim.isRunning) {
                    mObjectAnim.pause()
                    Log.i("MainActivity","点击暂停状态 --${mObjectAnim.isPaused}")
                }
            }
            R.id.reverse_object_animator->{
                Log.i("MainActivity","点击反向状态 --")
                mObjectAnim.reverse()
            }
            R.id.resume_object_animator->{
                mObjectAnim.resume()
                Log.i("MainActivity","点击继续执行 --")
            }

点击顺序 (这里点击开始按钮是重新初始化动画,请大家不要误解)
开始start -> 暂停pause ->继续执行resume->结束end
开始start->取消cancel
开始start->结束end

效果图.gif
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 点击开始状态 --false
MainActivity: addPauseListener -- onAnimationPause ------------------------------------
MainActivity: 点击暂停状态 --true
MainActivity: addPauseListener -- onAnimationResume ------------------------------------
MainActivity: 点击继续执行 --
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 点击结束状态 --true
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 点击开始状态 --false
MainActivity: AnimatorListener -- onAnimationCancel------------------------------------
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 点击取消状态 -- 
MainActivity: AnimatorListener -- onAnimationStart------------------------------------
MainActivity: 点击开始状态 --false
MainActivity: AnimatorListener -- onAnimationEnd------------------------------------
MainActivity: 点击结束状态 --true

上面是点击产生的log日志,其中有个addUpdateListener监听我没有打印信息,因为动画运行中就会不停打印出来,所以就没有打印出来了。
可以看到所有的状态都是可以有回调方法监听的。

这里有个方法要注意一下 cancel()end()这个两个方法。

如果动画是已经结束了end()的时候,就不会有回调onAnimationCancelonAnimationEnd两个监听方法了,看一下ValueAnimator源码中有体现了

    @Override
    public void cancel() {
关键代码1
        if (mAnimationEndRequested) {
            return;
        }
        if ((mStarted || mRunning) && mListeners != null) {
            if (!mRunning) {
                notifyStartListeners();
            }
            ArrayList<AnimatorListener> tmpListeners =
                    (ArrayList<AnimatorListener>) mListeners.clone();
            for (AnimatorListener listener : tmpListeners) {
关键代码2
                  listener.onAnimationCancel(this);
            }
        }
关键代码3
        endAnimation();
    }
    private void endAnimation() {
关键代码4
        mAnimationEndRequested = true;
  省略......
            for (int i = 0; i < numListeners; ++i) {
关键代码5
                tmpListeners.get(i).onAnimationEnd(this, mReversing);
            }
        }
省略......
    }

关键代码1:mAnimationEndRequested == true则不走下面的逻辑,设置true是在关键代码4中设置的,也就是说当动画结束调用了endAnimation()就不会调用到两个回调onAnimationCancelonAnimationEnd
关键代码2跟3跟5:就是动画未结束,所以调用了回调onAnimationCancel()再调用回调onAnimationEnd()

小结:当调用cancel()的时候,动画未结束时则回调onAnimationCancel()onAnimationEnd(),当动画结束时,则不会调用任何监听回调方法

end方法有时候会回调两个回调 分别是onAnimationStartonAnimationEnd,看一下ValueAnimator源码中的逻辑

    public void end() {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        if (!mRunning) {
            // Special case if the animation has not yet started; get it ready for ending
            startAnimation();
            mStarted = true;
        } else if (!mInitialized) {
            initAnimation();
        }
        animateValue(shouldPlayBackward(mRepeatCount, mReversing) ? 0f : 1f);
        endAnimation();
    }

从上面的逻辑可以看到,当调用到!mRunning == true的时候,会调用startAnimation()导致回调多一个onAnimationStart方法。

小结: 当调用end()的时候,如果动画处于运行中,则回调onAnimationEnd,如果不处于运行中,则回调onAnimationStartonAnimationEnd

  • 设置ViewPropertyAnimator的监听器

image.png

相比ObjectAnimator的监听器,这里的ViewPropertyAnimator多了两个回调方法

withStartActionwithEndAction分别在开始跟结束调用,但是只会被调用一次。
setListener中的onAnimationRepeat回调不会被调用,因为ViewPropertyAnimator不支持重复
其它的回调跟ObjectAnimator一致

三. 属性动画组合

image.png
  • ViewPropertyAnimator
    改变尺寸同时改变透明度,下面这种写法是几种动画一块运行的
R.id.btn_viewProperty_mulity->{
  if (isSelect) {
    imag_view.animate().scaleX(1.5f).scaleY(1.5f).alpha(0f).duration =2000
  }else{
    imag_view.animate().scaleX(1.0f).scaleY(1.0f).alpha(1f).duration =2000
  }
   isSelect = !isSelect
}
gifeditor_20191203_163831.gif
  • PropertyValuesHolder
    ObjectAnimator使用时则是要通过PropertyValuesHolder来实现
    上面的代码可以用
R.id.btn_viewProperty_mulity->{
  if (isSelect) {
    var propertyValueHolder1 = PropertyValuesHolder.ofFloat("scaleX",1f,1.5f)
    var propertyValueHolder2 = PropertyValuesHolder.ofFloat("scaleY",1f,1.5f)
    var propertyValueHolder3 = PropertyValuesHolder.ofFloat("alpha",1f,0f)
    ObjectAnimator.ofPropertyValuesHolder(imag_view,propertyValueHolder1,propertyValueHolder2,propertyValueHolder3)
    .setDuration(2000).start()
  }else{
    var propertyValueHolder1 = PropertyValuesHolder.ofFloat("scaleX",1.5f,1f)
    var propertyValueHolder2 = PropertyValuesHolder.ofFloat("scaleY",1.5f,1f)
    var propertyValueHolder3 = PropertyValuesHolder.ofFloat("alpha",0f,1f)
    ObjectAnimator.ofPropertyValuesHolder(imag_view,propertyValueHolder1,propertyValueHolder2,propertyValueHolder3)
    .setDuration(2000).start()
    }
    isSelect = !isSelect
}
  • PropertyValuesHolders.ofKeyframe()同一个属性拆分
    ofKeyframe (关键帧),可以把同一个动画属性拆分成多个阶段
var keyframe = Keyframe.ofFloat(0f,0f)//关键帧 0刚才开时的时候
var keyframe1 = Keyframe.ofFloat(0.5f,95f)//关键帧 0.5进行到一半的时候
var keyFrame2 = Keyframe.ofFloat(1f,80f)//关键帧1最后的时候
var holder = PropertyValuesHolder.ofKeyframe("progress", keyframe, keyframe1, keyFrame2)
ObjectAnimator.ofPropertyValuesHolder(circle_view,holder).setDuration(3000).start()

gifeditor_20191203_175839.gif

实现类似interpolator = OvershootInterpolator()插值器超过结束值后再回弹的效果

  • AnimatorSet
    也是组合动画的,可以让多个动画配合执行,不过它可以让动画先后有序执行~~
    playTogether(同时执行)、playSequentially(顺序执行)
    精确配置顺序with(),before(),after()
示例代码:
var animator1 = ObjectAnimator.ofFloat(imag_view,"alpha",0f,1f)
animator1.interpolator = AccelerateDecelerateInterpolator()
var animator3 = ObjectAnimator.ofFloat(imag_view,"scaleX",0f,1f)
var animator2 = ObjectAnimator.ofFloat(imag_view,"scaleY",0f,1f)
var animator4 = ObjectAnimator.ofFloat(imag_view,"translationX",0f,200f)
animator4.interpolator = LinearInterpolator()
AnimatorSet().apply {
  playTogether(animator2,animator3)
  playSequentially(animator1,animator4)
  duration = 2000
  start()
}

gifeditor_20191203_172107.gif

解释一下上面的代码
上面使用 AnimatorSet将多个ObjectAnimator放在一块运行,并且playTogether(animator2,animator3) 代表一起运行
playSequentially(animator1,animator4) 代表先后顺序运行
所以就有了下面的效果图 (x轴、y轴方向放大跟透明度由0-1)是一起的,然后在x轴方向移动。

上面的播放逻辑也可以使用精确配置顺序with(),before(),after()来实现

AnimatorSet().apply {
  play(animator1).with(animator2).with(animator3).before(animator4)
  duration = 2000
  start()
 }

注意:每个传入给AnimatorSetanimator可以自己定义自己的动画运行时间、差值器等,但是如果在AnimatorSet设置了运行时间的话则以在AnimatorSet设置的为准。

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

推荐阅读更多精彩内容