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)
    }
三角形.png
绘制圆弧
   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)
圆弧.png
区域 region

抽取绘制区域方法

  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)
区域.png
region的几种方法 描述
setEmpty 将原来的区域变量变成空变量在用set函数重新构造区域
set(Region) 利用新的区域替换原来的区域
set(Recf) 利用矩形所代表的的区域替换原来的区域
set(left , top , right , bottom) 根据矩形的两个角点构造出的矩形区域替换原来的区域
setPath(path , Region) 根据路径的区域与某区域的交集构造出新的区域
其他方法都比较简单 这里以setpath 做示例

取椭圆的上半部分

   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)
截取椭圆上半部分.png
区域相交 union函数
 // 该函数用于与指定矩形取并集 即将rect所指定的矩形加入当前区域中
        var region = Region(10,10,200,100)
        region.union(Rect(10,10,50,300))
        drawRegion(canvas,region,paint)
区域相交.png

/**
* 区域操作
* op(Rect , op)
* op(left,top,right,bottom,op)
* op(Region , op)
*/

public enum Op

枚举 描述
DIFFERENCE(0) r1与r2不同的区域
INTERSECT(1) r1与r2交集
UNION(2) r1与r2组合在一起的区域
XOR(3) r1与r2交集之外的区域
REVERSE_DIFFERENCE(4) r1与r2不同的区域
REPLACE(5) r2 的区域
canvas
        // 画布平移translate
        var rect = Rect(0,0,200,200)
        canvas.drawRect(rect,paint)
        canvas.translate(100f,100f)
        canvas.drawRect(rect,paint)

        //画布裁剪 clip
        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%"/>

</set>

代码中调用

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)
        })
代码实现动画

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

标签
scale ScaleAnimation
alpha AlphaAnimation
rotate RotateAnimation
translate TranslateAnimation
set AnimationSet

// 以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"/>

</set>

布局文件定义 四个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">
    <item
        android:duration="1000"
        android:drawable="@drawable/scan_circle"/>

    <item
        android:duration="1000"
        android:drawable="@drawable/scan_circle2"/>

    <item
        android:duration="1000"
        android:drawable="@drawable/scan_circle3"/>
    <item
        android:duration="1000"
        android:drawable="@drawable/scan_circle4"/>
    <item
        android:duration="1000"
        android:drawable="@drawable/scan_circle5"/>

</animation-list>

代码调用

 var animList = ima_frame.drawable
        (animList as AnimationDrawable).start()

AnimationDrawable 的常用函数

函数名 描述
void start () 开始播放逐帧动画
void stop () 停止播放逐帧动画
int getDuration (int index) 得到指定帧的持续时间
Drawable getFrame (int index) 得到执行帧的drawable对象
int getNumberOfFrames () 得到所有帧的数量
boolean isRunning () 判断当前动画是否在播放
void setOneShot (boolean oneShot) 设置动画是否播放一次 true播放一次 false循环播放
boolean isOneShot () 判断当前动画是否播放一次
void addFrame (Drawable frame, int duration) 给动画添加一帧 以及设置该帧持续的时间
donghua.gif
属性动画 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 函数汇总

函数 描述
ValueAnimator setDuration(long duration) 设置动画时长 单位-毫秒
Object getAnimationValue() 获取当前动画进度的值
void start() 开始动画
void setRepeatCount(int value) 设置循环次数
void setRepeatMode(int value) 设置循环模式
void cancel() 取消动画
    //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()



    }

}

图片跳动.gif
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)

    }

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


getlength.png

// 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)
        do {
            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)

getSegment.png

实现一条圆形路径从长度0慢慢增加到整个原 如此往复

 var animValue : Float
    var dst : Path
    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)

    }
5.1.gif
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)

圆形路径加箭头.gif

支付宝支付成功动画

class View5Alipay : View {

    var circle :Path
    var dst :Path
    var curx = 300f
    var cury = 300f
    var radius = 200f
    var pathMeasure : PathMeasure
    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)
    }


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

推荐阅读更多精彩内容