android自定义view 【动画篇】

本文代码中使用的是kotlin语法 还没了解kotlin的请先看下语法篇

kotlin语法总结

绘制一个三角形
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        var paint = Paint()
        paint.setColor(Color.RED)        // 设置画笔颜色
        paint.style = Paint.Style.STROKE   // 设置画笔样式
        paint.strokeWidth = 5f           // 设置画笔宽度
        // 绘制三角形
        var path = Path()
        path.moveTo(10f, 10f)       // 移动起点到 10,10
        path.lineTo(10f, 100f)      // 画第一条直线
        path.lineTo(300f, 100f)     // 画第二条直线
        path.close()                     // 首尾连接 形成第三条直线
        canvas.drawPath(path, paint)
   var path = Path()
        path.moveTo(10f,10f)                // 定义起点
        var recf = RectF(10f,10f,200f,200f)
        path.arcTo(recf,0f,90f)  // 根据矩形 起始角度 绘制的角度 绘制圆弧    但是起点和圆弧起点会出现一条直线
        canvas.drawPath(path, paint)
        //        canvas.drawRect(recf,paint)  //查看矩形范围
        var path2 = Path()
        path2.moveTo(210f,10f)
        var recf2 = RectF(210f,10f ,400f,200f)
        path2.arcTo(recf2,0f,90f,true)  //相比上面的函数这个不会生成path起点到圆弧起点的直线
        canvas.drawPath(path2, paint)

抽取绘制区域方法

  fun drawRegion(canvas:Canvas , rgn:Region , paint:Paint){
        var iter = RegionIterator(rgn)
        var r = Rect()
        while (iter.next(r)){
            canvas.drawRect(r,paint)

区域和rect 相似 只不过区域可以是不规则的形状 rect则必须是矩形

     var region = Region(Rect(50,150,200,200))
        drawRegion(canvas,region,paint)
        //区域 rect
        canvas.drawRect(Rect(50,50,200,100),paint)

取椭圆的上半部分

   paint.setColor(Color.RED)        // 设置画笔颜色
        paint.style = Paint.Style.FILL   // 设置画笔样式
        paint.strokeWidth = 5f           // 设置画笔宽度
        var ovalPath = Path()
        var rect = RectF(50f,50f,200f,500f)
        ovalPath.addOval(rect,Path.Direction.CCW)
//        canvas.drawPath(ovalPath,paint)             // 先绘制一个椭圆
        var clipRect = RectF(50f,50f,200f,200f)
//        canvas.drawRect(clipRect,paint)             //在绘制一个矩形
        var rgn = Region()
        rgn.setPath(ovalPath , Region(50,50,200,200))
        drawRegion(canvas,rgn, paint)
 // 该函数用于与指定矩形取并集 即将rect所指定的矩形加入当前区域中
        var region = Region(10,10,200,100)
        region.union(Rect(10,10,50,300))
        drawRegion(canvas,region,paint)
        canvas.drawColor(Color.RED)
        canvas.clipRect(Rect(100,100,200,200))
        canvas.drawColor(Color.GREEN)
        //画布的保存与恢复
        // 先使用save 保存当前的状态  在用restore恢复到该状态
        canvas.drawColor(Color.RED)
        canvas.save()
        canvas.clipRect(Rect(100,100,200,200))
        canvas.drawColor(Color.GREEN)
        canvas.restore()
        canvas.drawColor(Color.BLUE)
*   视图动画由五种类型组成
* alpha 渐变透明度动画
* scale 渐变尺寸伸缩动画
* translate 画面变化位置移动动画
* rotate 画面移动旋转动画
* set 定义动画集合
* Animation 属性
* Animation 是所有动画的基类
* duration 动画的持续时间,以毫秒为单位
* fillAfter 如果设置为true 则控件动画结束时,将保持动画结束时的状态
* fillBefore 如果设置为true 则空话结束时,将还原到初始状态
* fillEnabled 与fillBefore 效果相同
* repeatCount 用于指定动画的重复次数 当值为infinite时 表示无限循环
* repeatMode 用于设定重复的类型 reverse表示倒序 restart表示重放
* interpolator 用于设定插值器 其实就是指定的动画效果 ,比如弹跳 匀加速 匀减速 效果等

动画的定义一般以xml的形式 在res目录下创建anim文件夹
xml示例

<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="1.0"
    android:toAlpha="0.1"
    android:duration="3000"
    android:fillAfter="true"/>
<!--分别为-->
<!--透明度开始的大小-->
<!--透明度结束的大小-->
<!--动画持续时间-->
<!--动画结束后保持状态-->
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="0"
    android:toDegrees="-600"
    android:duration="3000"
<!--分别为-->
<!--旋转开始的角度-->
<!--旋转结束的角度-->
<!--动画持续时间-->
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXScale="0.0"
    android:toXScale="1.4"
    android:fromYScale="0.0"
    android:toYScale="1.4"
    android:duration="700"
    android:pivotX="50%p"
    android:pivotY="50%p"/>
<!-- 分别为  -->
<!--    x开始的大小-->
<!--    x结束的大小-->
<!--    y开始的大小-->
<!--    y结束的大小-->
<!--    动画持续的时间-->
<!--    动画开始的x位置-->
<!--    动画开始的y位置-->
<!--    pivotX 的三种写法-->
<!--    50   原点的位置加50-->
<!--    50%  原点位置加控件的50%-->
<!--    50%p 原点位置加上父控件的50%-->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromXDelta="0"
    android:toXDelta="-80"
    android:fromYDelta="0"
    android:toYDelta="-80"
    android:duration="2000"/>
<!--分别为-->
<!--x轴开始的位置-->
<!--x轴结束的位置-->
<!--y轴开始的位置-->
<!--y轴结束的位置-->
<!--动画持续的时间-->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000">
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"/>
    <scale
        android:fromYScale="0"
        android:toYScale="2"
        android:fromXScale="0"
        android:toXScale="2"/>
    <rotate
        android:fromDegrees="0"
        android:toDegrees="600"
        android:pivotY="50%"
        android:pivotX="50%"/>

代码中调用

btn是一个button   tv是一个textview   
        btn?.setOnClickListener({
//            var animation = AnimationUtils.loadAnimation(this , R.anim.scaleanim)
//            var animation = AnimationUtils.loadAnimation(this , R.anim.alphaanim)
//            var animation = AnimationUtils.loadAnimation(this , R.anim.translateanim)
            var animation = AnimationUtils.loadAnimation(this , R.anim.setanim)
            tv.startAnimation(animation)
代码实现动画

显示场景用也会出现只会使用一次的动画这时我们可以使用代码来创建动画并调用
先来了解一下 各种动画对应的类

// 以ScaleAnimation 举例
这里以public ScaleAnimation(float fromX, float toX, float fromY, float toY,
int pivotXType, float pivotXValue, int pivotYType, float pivotYValue) 构造方法做示例 其他构造方法可自行尝试

       btn?.setOnClickListener({
            var scalAnim = ScaleAnimation(0f,1.4f,0f,1.4f,Animation.RELATIVE_TO_SELF ,0.5f ,
               Animation.RELATIVE_TO_SELF , 0.5f )
            scalAnim.duration = 700
            tv.startAnimation(scalAnim)
//            scalAnim.cancel()  // 动画取消
//            scalAnim.reset()   // 动画重置
            // 动画监听
            scalAnim.setAnimationListener(object : Animation.AnimationListener{
                override fun onAnimationRepeat(animation: Animation?) {
                    Log.e("animation","onAnimationRepeat")
                override fun onAnimationEnd(animation: Animation?) {
                    Log.e("animation","onAnimationEnd")
                override fun onAnimationStart(animation: Animation?) {
                    Log.e("animation","onAnimationStart")
镜头由远及近 BounceInterpolator 示例
var scaleAnimation = ScaleAnimation(
            1f, 2.4f, 1f, 2.4f,
            Animation.RELATIVE_TO_SELF, 0.5f,
            Animation.RELATIVE_TO_SELF, 0.5f
        scaleAnimation.repeatCount = Animation.INFINITE
        scaleAnimation.fillAfter = false
        scaleAnimation.duration = 4000
        scaleAnimation.interpolator = BounceInterpolator()
        img.startAnimation(scaleAnimation)
loading框无限旋转
var rotateAnimation = RotateAnimation(0f,360f,Animation.RELATIVE_TO_SELF , 0.5f,
            Animation.RELATIVE_TO_SELF,0.5f)
        rotateAnimation.repeatCount = Animation.INFINITE
        rotateAnimation.duration = 500
        rotateAnimation.interpolator = LinearInterpolator()
        loading.startAnimation(rotateAnimation)
水波纹动画

xml定义scale 和alpha 动画

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="3000">
    <scale
        android:repeatCount ="infinite"
        android:fromXScale="1"
        android:fromYScale="1"
        android:toXScale="3"
        android:toYScale="3"
        android:pivotX="50%"
        android:pivotY="50%"/>
    <alpha
        android:repeatCount ="infinite"
        android:fromAlpha=".4"
        android:toAlpha="0"/>

布局文件定义 四个imageview 和一个textview

<FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <ImageView
            android:id="@+id/circle1"
            android:layout_width="140dp"
            android:layout_height="140dp"
            android:layout_gravity="center"
            android:layout_marginTop="30dp"
            android:src="@drawable/scan_circle"/>
        <ImageView
            android:id="@+id/circle2"
            android:layout_width="140dp"
            android:layout_height="140dp"
            android:layout_gravity="center"
            android:layout_marginTop="30dp"
            android:src="@drawable/scan_circle"/>
        <ImageView
            android:id="@+id/circle3"
            android:layout_width="140dp"
            android:layout_height="140dp"
            android:layout_gravity="center"
            android:layout_marginTop="30dp"
            android:src="@drawable/scan_circle"/>
        <ImageView
            android:id="@+id/circle4"
            android:layout_width="140dp"
            android:layout_height="140dp"
            android:layout_gravity="center"
            android:layout_marginTop="30dp"
            android:src="@drawable/scan_circle"/>
        <TextView
            android:id="@+id/btn_circle"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_gravity="center"
            android:layout_marginTop="30dp"
            android:background="@mipmap/button"/>
    </FrameLayout>

代码设置点击事件 点击图标开始 依次延迟播放动画

   btn_circle.setOnClickListener({
            var animation1 = AnimationUtils.loadAnimation(this, R.anim.button_set)
            var animation2 = AnimationUtils.loadAnimation(this, R.anim.button_set)
            var animation3 = AnimationUtils.loadAnimation(this, R.anim.button_set)
            var animation4 = AnimationUtils.loadAnimation(this, R.anim.button_set)
            circle1.startAnimation(animation1)
            animation2.startOffset = 600
            circle2.startAnimation(animation2)
            animation3.startOffset = 1200
            circle3.startAnimation(animation3)
            animation4.startOffset = 1800
            circle4.startAnimation(animation4)
AnimationDrawable 逐帧动画

xml 定义逐帧动画

// 布局文件定义imageview 并把xml动画设为drawable 代码中调用getDrawable 获取
 <ImageView
        android:id="@+id/ima_frame"
        android:layout_marginTop="10dp"
        android:layout_gravity="center_horizontal"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/animation_list"/>
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
        android:duration="1000"
        android:drawable="@drawable/scan_circle"/>
        android:duration="1000"
        android:drawable="@drawable/scan_circle2"/>
        android:duration="1000"
        android:drawable="@drawable/scan_circle3"/>
        android:duration="1000"
        android:drawable="@drawable/scan_circle4"/>
        android:duration="1000"
        android:drawable="@drawable/scan_circle5"/>
</animation-list>
 var animList = ima_frame.drawable
        (animList as AnimationDrawable).start()

AnimationDrawable 的常用函数

属性动画 ValueAnimation

// 属性动画可以在动画完成之后 保留动画的属性 比如可以在最终的位置保持点击事件 视图动画只能在原位置保持点击事件

 textview.setOnClickListener({
            Toast.makeText(this,"hhhh" , Toast.LENGTH_SHORT).show()
        btn_start.setOnClickListener({
//            var valueAnimation = ValueAnimator.ofInt(0,400)
            //这里可以传入不同的参数 参数越多 动画的变化越复杂
            var valueAnimation = ValueAnimator.ofFloat(0f,400f,50f,300f)
            valueAnimation.duration = 3000
            //实时监听属性动画的进度 同时改变textview 的位置
            valueAnimation.addUpdateListener({
                var curValue : Int = (it.animatedValue as Float).toInt()
                textview.layout(curValue , curValue , curValue+textview.width , curValue + textview.height )

// 属性动画还有一个方法 传入 evaluator 和 可变传参object
示例 按钮的text从 A 到 Z

       // evaluator  用于根据传入的起始值和终点值 以及动画当前的进度 计算当时应该得到的值
        // 示例 textview 从 a 变化到 Z
        var objAnimator = ValueAnimator.ofObject(CharEvaluator() , 'A','Z')
        objAnimator.addUpdateListener({
            var text = it.animatedValue
            btn_start.text = text.toString()
        objAnimator.interpolator = AccelerateDecelerateInterpolator()
        objAnimator.duration = 5000
        objAnimator.start()
class CharEvaluator : TypeEvaluator<Char> {
        override fun evaluate(fraction: Float, startValue: Char?, endValue: Char?): Char {
            var startInt = startValue?.toInt()?:0
            var endInt = endValue?.toInt()?:0
            var cur = (startInt + fraction*(endInt - startInt)).toInt()
            var result = cur.toChar()
            return result

ValueAnimation 函数汇总

// AnimatorUpdateListener 监听动画时时改变的值
// AnimationListener 监听动画的开始 结束 取消 重复 四个状态

// 示例: 弹跳加载中效果
class View3 : ImageView{
    var mTop :Int = 0  // 当前控件的高度
    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        initView()
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        mTop = top   //m
    private fun initView() {
        var animation = ValueAnimator.ofInt(0 , 150 , 0)
        animation.repeatCount = ValueAnimator.INFINITE
        animation.repeatMode = ValueAnimator.RESTART
        animation.duration = 1000
        animation.interpolator = AccelerateDecelerateInterpolator()
        animation.addUpdateListener({
            var dx = it.animatedValue as Int
            top = mTop - dx   //监听动画的进度并改变控件的高度
        // 一共四张图片  每次重复动画次数累加  改变现实的图片
        var mCuttentIndex = 0
        val mCount = 4
        animation.addListener(object :Animator.AnimatorListener{
            override fun onAnimationRepeat(animation: Animator?) {
                mCuttentIndex++
                when(mCuttentIndex % mCount){
                    0 -> setImageDrawable(resources.getDrawable(R.mipmap.animal1))
                    1 -> setImageDrawable(resources.getDrawable(R.mipmap.animal2))
                    2 -> setImageDrawable(resources.getDrawable(R.mipmap.animal3))
                    3 -> setImageDrawable(resources.getDrawable(R.mipmap.animal4))
            override fun onAnimationEnd(animation: Animator?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            override fun onAnimationCancel(animation: Animator?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            override fun onAnimationStart(animation: Animator?) {
                setImageDrawable(resources.getDrawable(R.mipmap.animal1))
        animation.start()
ObjectAnimator
 //使用ValueAnimator有一个缺点 那就是如果想要对哪个控件执行操作就需要监听ValueAnimator的动画过程
    // 为了能让动画直接与对应控件想关联我们可以使用 ObjectAnimator 派生自ValueAnimator

// 示例 第一个参数为控件 第二个参数为属性名 之后是变化的值
var objanim = ObjectAnimator.ofFloat(btn_start , "alpha" , 1f,0f,1f)
objanim.duration = 2000
objanim.start()

// 第二个参数怎么来
// 其实所有的view都继承自view在view中有一下和动画相关的属性 第二个参数取以下的属性名即可
btn_start.alpha = 1f
btn_start.rotation = 30f
btn_start.rotationX = 10f
btn_start.rotationY = 10f
btn_start.translationX = 10f
btn_start.translationY = 10f
btn_start.translationZ = 10f
btn_start.scaleX = 2f
btn_start.scaleY = 2f

    // 如果第一个参数控件是自定义的并且具有单独且可以通过set赋值的属性  那么该属性可以被设置在第二个参数
AnimatorSet - 组合动画

// 主要有两个方法
// var animatorSet = AnimatorSet()
// animatorSet.playTogether() // 所有动画一起播放
// animatorSet.playSequentially() // 按顺序播放动画

// AnimatorSet.Builder
// 如果我们想要先播放 动画a 然后在同时播放动画b 和动画c 我们就需要使用AnimatorSet.Builder
// 主要有一下几个函数
// var animatorsetBuilder = animatorSet.play(Animator) // 播放动画
// animatorsetBuilder.with(Animator) //和前面的动画一起执行
// animatorsetBuilder.before(Animator) // 先执行这个动画在执行前面的动画
// animatorsetBuilder.after(Animator) // 先执行前面的动画在执行该动画
// animatorsetBuilder.after(long) // 延迟n毫秒之后在执行动画

// 路劲动画
// ValueAnimation 和 ObjectAnimation 都拥有ofPropertyValuesHolder 函数 可以同时执行多个动画

 var rotationHolder = PropertyValuesHolder.ofFloat("Rotation" , 60f,-60f,40f)
        var alphaHolder = PropertyValuesHolder.ofFloat("alpha" , 0.1f,1f,0.5f)
        var animator = ObjectAnimator.ofPropertyValuesHolder(tv , rotationHolder,alphaHolder)
        animator.duration = 2000
        animator.start()
KeyFrame - 关键帧 可以使用该类来生成动画
  // 第一个参数表示动画的进度  第二个参数表示该进度动画的值
        var keyframe0 = Keyframe.ofFloat(0f,0f)
        var keyframe1 = Keyframe.ofFloat(0.5f,10f)
        var keyframe2 = Keyframe.ofFloat(1f,100f)
        // 通过制定关键帧的方式生成动画 类似flash制作动画
        var frameHolder = PropertyValuesHolder.ofKeyframe("rotation" ,keyframe0,
            keyframe1,keyframe2)
        var frameAnimator = ObjectAnimator.ofPropertyValuesHolder(tv , frameHolder)
        frameAnimator.duration = 2000
        frameAnimator.start()
ViewPropertyAnimator
        // view 可以通过animate函数得到ViewPropertyAnimator对象  调用ViewPropertyAnimator的函数也可生成动画
//        ViewPropertyAnimator有很多的函数这里举例其中几个
        tv.animate().x(10f).y(100f).alpha(0.5f)
//        也可以添加监听
        tv.animate().setListener(object :Animator.AnimatorListener{
            override fun onAnimationRepeat(animation: Animator?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            override fun onAnimationEnd(animation: Animator?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            override fun onAnimationCancel(animation: Animator?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            override fun onAnimationStart(animation: Animator?) {
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
animateLayoutChanges 为viewgroup的组件添加动画
        // 在布局xml中定义该属性为true 即可  动画不可自定义
        var index = 0
        btn_add.setOnClickListener({
            index++
            var button = Button(this)
            button.text = "button $index"
            var params = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT )
            button.layoutParams = params
            container.addView(button,0)
        btn_remove.setOnClickListener({
            container.removeViewAt(0)
LayoutTransition 为viewgroup的组件自定义动画
      var layoutTransition = LayoutTransition()
        var animOut = ObjectAnimator.ofFloat(null , "rotationY",0f,90f,0f)
        layoutTransition.setAnimator(LayoutTransition.DISAPPEARING , animOut)
        var animIn = ObjectAnimator.ofFloat(null , "scaleY",0f,3f,1f)
        layoutTransition.setAnimator(LayoutTransition.APPEARING , animIn)
        container.layoutTransition = layoutTransition
//        LayoutTransition.DISAPPEARING            //元素在容器内消失时的动画
//        LayoutTransition.APPEARING              //元素在容器内出现时的动画
//        LayoutTransition.CHANGE_APPEARING       //元素在容器内出现时其他需要变化的元素的动画
//        LayoutTransition.CHANGE_DISAPPEARING     //元素在容器内消失时其他需要变化的元素的动画
//        CHANGE_APPEARING  和 CHANGE_DISAPPEARING 必须使用 PropertyValusHolder 所构造的动画才有效果
//        也就是说 ObjectAnimator构造的动画在这里没有效果 且动画开始的值和结束的值必须一致 不一致也不会有效果
//        同时 在构造PropertyValusHolder动画时 left 和 top 属性的变动是必须的 如果不需要变动则直接写为
        //CHANGE_APPEARING
          var pvLeft = PropertyValuesHolder.ofInt("left" , 0 , 0)
          var pvTop = PropertyValuesHolder.ofInt("top" , 0 , 0)
        var pvScalX = PropertyValuesHolder.ofFloat("scalX" , 1f , 0f, 1f)
        var changeAppearAnim = ObjectAnimator.ofPropertyValuesHolder(container , pvLeft,pvTop,pvScalX)
        //CHANGE_DISAPPEARING
        var disAppearFrame0 = Keyframe.ofFloat(0f ,0f)
        var disAppearFrame1 = Keyframe.ofFloat(0.2f ,0.2f)
        var disAppearFrame2 = Keyframe.ofFloat(0.4f ,0.4f)
        var disAppearFrame3 = Keyframe.ofFloat(0.6f ,0.6f)
        var disAppearFrame4 = Keyframe.ofFloat(1f ,0f)
        var disAppearHolder = PropertyValuesHolder.ofKeyframe("rotation" ,
            disAppearFrame0 , disAppearFrame1,disAppearFrame2,disAppearFrame3,disAppearFrame4)
        var changeDisAppearAnim = ObjectAnimator.ofPropertyValuesHolder(container,
            pvLeft,pvTop , disAppearHolder)
        var changeTransition = LayoutTransition()
        changeTransition.setAnimator(LayoutTransition.CHANGE_APPEARING , changeAppearAnim)
        changeTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING , changeDisAppearAnim)
        container.layoutTransition = changeTransition
        // LayoutTransition函数
//        void setDuration(long duration)     给所有的动画设置时长
//        void setDuration(int type,duration) 针对单个类型的动画设置时长
//        void setInterpolator(int type , TimeInterpolator interpolator)给单个类型的动画设置差值器
//        void setStartDelay(int type , long delay) 给单个类型的动画设置延迟
//        void setStagger(int type , long duration) 针对单个动画设置每个item动画的时间间隔
        //设置监听
        layoutTransition.addTransitionListener(object :LayoutTransition.TransitionListener{
            override fun startTransition(
                transition: LayoutTransition?,
                container: ViewGroup?,
                view: View?,
                transitionType: Int
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            override fun endTransition(
                transition: LayoutTransition?,
                container: ViewGroup?,
                view: View?,
                transitionType: Int
                TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
开源动画库 NineOldAndroids http://nineoldandroids.com/

// 这是一个兼容安卓低版本的动画库 支持3.0以下的版本使用 Animation API 唯一不支持的是 LayoutTransition

//PathMeasure实现路径动画

PathMeasure(Path path, boolean forceClosed)
// 第一个参数为path
// 第二个参数为是否闭合 对path本身的绘制没有影响 但对测量的结果有影响 会包含最后一段闭合的路径与原来的path不同

 override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.translate(200f,200f)
        var paint = Paint()
        paint.strokeWidth = 10f
        paint.style = Paint.Style.STROKE
        paint.color = Color.RED
        var path = Path()
        path.moveTo(0f,0f)
        path.lineTo(0f,100f)
        path.lineTo(100f,100f)
        path.lineTo(100f,0f)
        // 第一个参数为path
        // 第二个参数为是否闭合  对path本身的绘制没有影响 但对测量的结果有影响 会包含最后一段闭合的路径与原来的path不同
        var measure1 = PathMeasure(path , true)
        var measure2 = PathMeasure(path , false)
        // 获取路径的长度
        Log.e("view5","forceClosed=true ---->"+measure1.length)  // 400
        Log.e("view5","forceClosed=false ---->"+measure2.length) // 300
      canvas?.drawPath(path, paint)

可以看见最后一个参数不一样 对路径的长度计算结果会造成影响

// measure1.isClosed 用于获取path测量的路径是否闭合
// measure1.nextContour() path可以由多条曲线构成 但是getlength或其他函数都只会针对第一条曲线进行计算
// 该函数就是用于跳转到下一条曲线的函数 成功返回true 失败则返回false

      var path = Path()
        //  如果都是Path.Direction.CW 发现只会绘制最大的一个矩形
        path.addRect(-50f,-50f,50f,50f,Path.Direction.CCW)
        path.addRect(-100f,-100f,100f,100f,Path.Direction.CW)
        path.addRect(-120f,-120f,120f,120f,Path.Direction.CW)
        var pathMeasure = PathMeasure(path  , false)
            Log.e("view5","length=== ${pathMeasure.length}")
        }while (pathMeasure.nextContour())
getSegment 用于截取整个path中的某个片段
 // 通过startD 和 stopD 来控制截取的长度,并将截取后的path保存到参数dst中
    // startWithMoveTo 表示起始点是否使用moveto 将路径的新起始点移动到结果path 的起始点 通常为true以保证每次截取的path都是完整的

// boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

   var path = Path()
        path.addRect(-50f,-50f,50f,50f,Path.Direction.CW)
        var dst = Path()
//        dst.lineTo(0f,200f)  //如果dst本身就有路径 将会把截取的路径添加到原路径中
        var pathmeasure = PathMeasure(path ,false)
        pathmeasure.getSegment(0f,150f,dst,true)
        canvas?.drawPath(dst,paint)
    var circle :Path
    var pathMeasure :PathMeasure
    constructor(context: Context?, attrs: AttributeSet?):super(context,attrs){
        // 需要关闭硬件加速功能 否则绘图会出现问题
        setLayerType(LAYER_TYPE_SOFTWARE,null)
         animValue = 0f
         dst = Path()
         circle = Path()
        circle.addCircle(100f,100f,50f , Path.Direction.CW)
        pathMeasure = PathMeasure(circle , true)
        var animator = ValueAnimator.ofFloat(0f,1f)
        animator.repeatCount = ValueAnimator.INFINITE
        animator.addUpdateListener({
            animValue = it.animatedValue as Float
            invalidate()
        animator.duration = 2000
        animator.start()
  override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.translate(200f,200f)
        var paint = Paint()
        paint.strokeWidth = 10f
        paint.style = Paint.Style.STROKE
        paint.color = Color.RED
      canvas?.drawColor(Color.WHITE)
        var stop  = pathMeasure.length * animValue
        dst.reset()
        var start = 0f
        start = ( stop - (0.5 - Math.abs(animValue-0.5)) * pathMeasure.length).toFloat()
        pathMeasure.getSegment(start,stop , dst,true)
        canvas?.drawPath(dst , paint)
getPosTan 函数 用于得到路径上某一长度的位置以及该位置的正切值

// boolean getPosTan(float distance, float pos[], float tan[])
// pos[0]为x坐标 pos[1]为y坐标 tan表示该点的正切值 tan为直角三角形的三角函数 对边/邻边
// Math 有两个可以求反切值的函数
// double atan(double a) // 参数为弧度值
// double atan2(double y, double x) // 参数为坐标值

getMatrix 函数 用于得到路径上某一长度的位置以及该位置的正切值的矩形

// boolean getMatrix(float distance, Matrix matrix, int flags)
// distance 距离path起始点的长度
// matrix 根据flags封装好的matrix会根据flags 的设置存入不同的内容
// flags 有两个值 PathMeasure.POSITION_MATRIX_FLAG获取位置信息 PathMeasure.TANGENT_MATRIX_FLAG获取切边信息

在上面圆形路径的基础上增加箭头

        // 方法一
        pathMeasure.getPosTan(stop , pos,tan)
        var degress = (Math.atan2(tan[1].toDouble(), tan[0].toDouble())*180.0/Math.PI).toFloat()
        var matrix = Matrix()
        matrix.postRotate(degress  , (bmp.width/2.0).toFloat(), (bmp.height/2.0).toFloat())
        matrix.postTranslate(pos[0]- bmp.width/2, pos[1]-bmp.height/2)
        canvas?.drawBitmap(bmp,matrix,paint)
        // 方法二
//        var matrix = Matrix()
//        pathMeasure.getMatrix(stop , matrix , PathMeasure.POSITION_MATRIX_FLAG or PathMeasure.TANGENT_MATRIX_FLAG)
//        matrix.preTranslate((-bmp.width/2).toFloat(), (-bmp.height/2).toFloat())
//        canvas?.drawBitmap(bmp,matrix,paint)
    var animValue = 0f
    var paint:Paint
    constructor(context: Context?, attrs: AttributeSet?):super(context, attrs){
        setLayerType(LAYER_TYPE_SOFTWARE,null)
        paint = Paint()
        paint.isAntiAlias = true
        paint.strokeWidth = 10f
        paint.color = Color.BLACK
        paint.style = Paint.Style.STROKE
        circle = Path()
        dst = Path()
        circle.addCircle(curx,cury ,radius ,Path.Direction.CW)
        circle.moveTo(curx - radius/2 , cury)
        circle.lineTo(curx , cury + radius/2)
        circle.lineTo(curx + radius/2 , cury - radius/3)
        pathMeasure = PathMeasure(circle , false)
        var anim  = ValueAnimator.ofFloat(0f,2f)
        anim.addUpdateListener({
            animValue = it.animatedValue as Float
            invalidate()
            Log.e("alipay","value= $animValue")
        anim.duration = 2000
        anim.repeatCount = ValueAnimator.INFINITE
        anim.addListener(object : Animator.AnimatorListener {
            override fun onAnimationRepeat(animation: Animator?) {
                Log.e("alipay","onAnimationRepeat")
            override fun onAnimationEnd(animation: Animator?) {
                Log.e("alipay","onAnimationEnd")
            override fun onAnimationCancel(animation: Animator?) {
                Log.e("alipay","onAnimationCancel")
            override fun onAnimationStart(animation: Animator?) {
                Log.e("alipay","onAnimationStart")
        anim.start()
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawColor(Color.WHITE)
        if(animValue < 1){
            dst.reset()
            pathMeasure = PathMeasure(circle , false)
            var stop  = pathMeasure.length * animValue
            pathMeasure.getSegment(0f,stop ,dst , true)
        }else if(animValue == 1f){
            Log.e("alipay","nextContour")
            pathMeasure.getSegment(0f,pathMeasure.length , dst ,true)
            pathMeasure.nextContour()
        }else{
            var stop = pathMeasure.length *(animValue -1)
            pathMeasure.getSegment(0f,stop,dst ,true)
        canvas?.drawPath(dst,paint)