Android 属性动画和自定义View的使用


使用自定义 View 绘制一个小球,进入应用时小球从屏幕中间的最高点落下,动画模拟重力作用下的落地效果,手指按住小球可以拖动小球进行移动,松开手指时小球从该位置落下,最终效果如下:
最终效果图

一、实现简单的动画

在开始实现这个小球之前先来实现一个最简单的动画:一个数字从 0 递增到 20000,增长速度逐渐变慢,代码如下所示(布局文件中只有一个用于展示数字的 TextView):

// 设置动画内容是一个数字从 0 变到 20000
val anim = ValueAnimator.ofInt(0, 20000)
// 设置动画持续时间
anim.setDuration(3000)
// 设置动画的变化速度会增长速度逐渐变慢
anim.interpolator = DecelerateInterpolator(1.5f)
// 添加监听器,在动画执行过程中会不断回调
anim.addUpdateListener {
    
    
    val value = it.animatedValue
    // valuetext 是页面中用于显示数字的 TextView
    valuetext.text = value.toString()
}
// 启动动画
anim.start()

实际运行效果如下:
数字动画

这样就实现了一个最简单的动画,现在根据这个动画的实现过程来实现最开始的目标。

二、通过自定义 View 绘制小球

首先定义一个 Point 类用于表示小球的坐标:

// 一个数据类,x,y分别表示小球的横纵坐标
data class Point(val x: Float, val y: Float)

然后继承 View 类自定义小球:

class MyBall: View {
    
    
	// 设置小球的半径
    private val radius = 100f
    // 创建画笔
    private var paint = Paint()
    // 用于记录小球的坐标
    private lateinit var point: Point

	// 在构造函数中将画笔设置为绿色
    constructor(context: Context): super(context){
    
    
        paint.color = Color.GREEN
    }
    constructor(context: Context, set: AttributeSet): super(context, set){
    
    
        paint.color = Color.GREEN
    }

	// 在布局的时候定义小球的初始位置
    override fun layout(l: Int, t: Int, r: Int, b: Int) {
    
    
        super.layout(l, t, r, b)
        //判断小球位置是否已经初始化
        if(!this::point.isInitialized){
    
    
            point = Point((width / 2).toFloat(), radius)
        }
    }

	// 绘制小球
    override fun onDraw(canvas: Canvas) {
    
    
        super.onDraw(canvas)
        canvas.drawCircle(point.x, point.y, radius, paint)
    }
}

接下来在 xml 文件中添加小球控件,运行程序就可以看到屏幕上的小球了,效果如下:
小球

三、添加自由落体的动画

我们在步骤二中完成了小球的绘制,现在开始对照步骤一中的简单动画为小球加入自由落体的动画:
动画的开始我们通过val anim = ValueAnimator.ofInt(0, 20000)实现了数字从 0 到 20000 的变化,这个递增的过程是系统通过以下这个类实现的:

class IntEvaluator: TypeEvaluator<Int> {
    
    
    override fun evaluate(fraction: Float, startValue: Int, endValue: Int): Int {
    
    
        val startInt = startValue
        return (int)(startInt + fraction * (endValue - startValue));
    }
}

fraction 是一个 0-1 之间的值,表示的是动画的进度,动画开始时 fraction 的值为 0,动画结束时 fraction 的值为 1;startValue 和 endValue 则表示动画的初始值和结束值,evaluate() 方法根据当前的动画进度返回动画的当前值,根据代码中的当前值计算方式可以看出数值的变化是匀速的,通过这个类系统就知道应该如何从 0 递增到 20000。

属性动画可以对任意对象添加动画,因此我们也可以对小球添加动画,小球的动画实际上就是小球位置的变化,故我们首先实现 TypeEvaluator 接口告知系统小球应该如何从起始位置移动到结束位置:

class PointEvaluator: TypeEvaluator<Point> {
    
    
    override fun evaluate(fraction: Float, startValue: Point, endValue: Point): Point {
    
    
        val x = startValue.x + fraction * (endValue.x - startValue.x)
        val y = startValue.y + fraction * (endValue.y - startValue.y)
        return Point(x, y)
    }
}

与上面的数值变化一样,这里也采用了匀速变化的方式,即小球的移动跟动画的进度是均匀进行的。
此时系统知道该如何移动小球了,我们就可以创建动画了

anim = ValueAnimator.ofObject(PointEvaluator(), 起始位置, 结束位置)

完成了小球的移动过程,动画持续时间和监听器添加没有什么变化,接下来就要处理小球的移动速度问题,在上面的简单动画中我们通过anim.interpolator = DecelerateInterpolator(1.5f)实现了递增速度的逐渐变慢,这个过程是通过补间器 DecelerateInterpolator 来实现的,所有的补间器都是通过实现 TimeInterpolator 接口实现的。TimeInterpolator 的代码如下:

interface TimeInterpolator {
    
    
    fun getInterpolator(input:Float):Float
}

TimeInterpolator 中只有一个 getInterpolator() 方法,有一个 input 参数,input 的取值范围是 0-1,可以将其理解为时间比例,例如动画的持续时间为 100s,input 为 0.5 则表示现在是第 50s。getInterpolator() 方法返回的值即是当前的动画进度,即上述的 fraction 参数,当 input = fraction 时即为匀速变化。DecelerateInterpolator 补间器的 getInterpolation() 方法如下:

fun getInterpolation(input:Float):Float {
    
    
    val result:Float
    // mFactor 默认值为 1.0f,其值受 DecelerateInterpolator 的参数的影响(即加速度)
    if (mFactor === 1.0f)
    {
    
    
        result = (1.0f - (1.0f - input) * (1.0f - input)).toFloat()
    }
    else
    {
    
    
        result = (1.0f - Math.pow((1.0f - input).toDouble(), 2 * mFactor)).toFloat()
    }
    return result
}

我们也可以通过实现 TimeInterpolator 接口定义自己的补间器,示例如下:

class MyInterpolator: TimeInterpolator {
    
    
    override fun getInterpolation(input: Float): Float{
    
    
        val result:Float
        // 计算方式一
        /*if (input <= 0.5)
        {
            result = (Math.sin(Math.PI * input)).toFloat() / 2
        }
        else
        {
            result = (2 - Math.sin(Math.PI * input)).toFloat() / 2
        }*/
        
        // 计算方式二
        /*result = (Math.sin(Math.PI * input * 0.5)).toFloat()*/
        
        // 计算方式三
        result = (Math.sin(Math.PI * (Math.sin(input * Math.PI * 0.5)) * 0.5)).toFloat()
        return result
    }
}

实现重力作用下的弹球效果可以使用系统自带的 BounceInterpolator,其代码如下:

fun getInterpolation(t:Float):Float {
    
    
    // _b(t) = t * t * 8
    // bs(t) = _b(t) for t < 0.3535
    // bs(t) = _b(t - 0.54719) + 0.7 for t < 0.7408
    // bs(t) = _b(t - 0.8526) + 0.9 for t < 0.9644
    // bs(t) = _b(t - 1.0435) + 0.95 for t <= 1.0
    // b(t) = bs(t * 1.1226)
    t *= 1.1226f
    if (t < 0.3535f)
    return bounce(t)
    else if (t < 0.7408f)
    return bounce(t - 0.54719f) + 0.7f
    else if (t < 0.9644f)
    return bounce(t - 0.8526f) + 0.9f
    else
    return bounce(t - 1.0435f) + 0.95f
}

更多系统自带的补间器可以查看这里
至此便完成了自由落体的动画。

四、添加触摸移动小球逻辑

通过重写 onTouchEvent() 方法可以实现小球的触摸移动事件,代码如下:

override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
    // 为了提高用户体验,增加小球的实际触碰范围
    val unrealRadius = radius + 20f
    //检测手指是否按到小球,没有按到小球时不对触碰事件做处理,isTouch 变量用于记录是否在拖动小球
    //是的会则不进行前面的位置检测,避免用户手指移动过快,超过系统反应时间
    if((!isTouch) && (event.x < point.x - unrealRadius || event.x > point.x + unrealRadius
    	 || event.y < point.y - unrealRadius || event.y > point.y + unrealRadius)){
    
    
        return true
    }

	// 当手指触碰屏幕的一瞬间
    if (event.action == MotionEvent.ACTION_DOWN){
    
    
        // 判断动画是否在进行,是的话停止
        if (anim.isRunning){
    
    
            anim.cancel()
        }
        // 将标志设为 true,说明用户正在拖动小球
        isTouch = true
    }

    var x = event.x
    var y = event.y

	// 避免小球滑出屏幕
    if(x < radius){
    
    
        x = radius
    }
    if(x > width - radius){
    
    
        x = width - radius
    }

    if(y < radius){
    
    
        y = radius
    }
    if(y > height - radius){
    
    
        y = height - radius
    }

    point = Point(x, y)
    // 当手指离开屏幕的一瞬间
    if(event.action == MotionEvent.ACTION_UP){
    
    
        // 将标志设为 false,说明用户结束拖动小球
        isTouch = false
        // 开启自由落体动画
        startMyAnimation()
    }
    // 重新绘制小球
    invalidate()
    // 返回 true,说明事件已经处理
    return true
}

至此便完成了我们的预期效果。

五、小球类的代码

以下是小球类的完整代码:

class MyBall: View {
    
    
    private val radius = 100f
    private var paint = Paint()
    private lateinit var point: Point
    private var isTouch = false
    private lateinit var anim: ValueAnimator

    constructor(context: Context): super(context){
    
    
        paint.color = Color.GREEN
    }
    constructor(context: Context, set: AttributeSet): super(context, set){
    
    
        paint.color = Color.GREEN
    }

    override fun layout(l: Int, t: Int, r: Int, b: Int) {
    
    
        super.layout(l, t, r, b)
        if(!this::point.isInitialized){
    
    
            point = Point((width / 2).toFloat(), radius)
            startMyAnimation()
        }
    }

    override fun onDraw(canvas: Canvas) {
    
    
        super.onDraw(canvas)
        canvas.drawCircle(point.x, point.y, radius, paint)
    }

    fun startMyAnimation(){
    
    
        val sPoint = Point(point.x, point.y)
        val ePoint = Point(point.x, height - radius)
        anim = ValueAnimator.ofObject(PointEvaluator(), sPoint, ePoint)
        anim.addUpdateListener {
    
    
            point = anim.animatedValue as Point
            invalidate()
        }
        anim.interpolator = BounceInterpolator()
        anim.duration = 3000
        anim.start()
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
        val unrealRadius = radius + 20f
        if((!isTouch) && (event.x < point.x - unrealRadius || event.x > point.x + unrealRadius
        	 || event.y < point.y - unrealRadius || event.y > point.y + unrealRadius)){
    
    
            return true
        }

        if (event.action == MotionEvent.ACTION_DOWN){
    
    
            if (anim.isRunning){
    
    
                anim.cancel()
            }
            isTouch = true
        }

        var x = event.x
        var y = event.y

        if(x < radius){
    
    
            x = radius
        }
        if(x > width - radius){
    
    
            x = width - radius
        }

        if(y < radius){
    
    
            y = radius
        }
        if(y > height - radius){
    
    
            y = height - radius
        }

        point = Point(x, y)
        if(event.action == MotionEvent.ACTION_UP){
    
    
            isTouch = false
            startMyAnimation()
        }
        invalidate()
        return true
    }
}

猜你喜欢

转载自blog.csdn.net/qingyunhuohuo1/article/details/109622092