欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2022-06-22 23:33:15
使用自定义 View 绘制一个小球,进入应用时小球从屏幕中间的最高点落下,动画模拟重力作用下的落地效果,手指按住小球可以拖动小球进行移动,松开手指时小球从该位置落下,最终效果如下:一、实现简单的动画在开始实现这个小球之前先来实现一个最简单的动画:一个数字从 0 递增到 20000,增长速度逐渐变慢,代码如下所示(布局文件中只有一个用于展示数字的 TextView):// 设置动画内容是一个数字从 0 变到 20000val anim = ValueAnimator.ofInt(0, 20000)...


使用自定义 View 绘制一个小球,进入应用时小球从屏幕中间的最高点落下,动画模拟重力作用下的落地效果,手指按住小球可以拖动小球进行移动,松开手指时小球从该位置落下,最终效果如下:
Android 属性动画和自定义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()

实际运行效果如下:
Android 属性动画和自定义View的使用

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

二、通过自定义 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 文件中添加小球控件,运行程序就可以看到屏幕上的小球了,效果如下:
Android 属性动画和自定义View的使用

三、添加*落体的动画

我们在步骤二中完成了小球的绘制,现在开始对照步骤一中的简单动画为小球加入*落体的动画:
动画的开始我们通过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
    }
}

本文地址:https://blog.csdn.net/qingyunhuohuo1/article/details/109622092