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
}
}
本文地址:https://blog.csdn.net/qingyunhuohuo1/article/details/109622092
上一篇: JavaScript如何监测数组的变化
推荐阅读
-
Android开发使用自定义view实现ListView下拉的视差特效功能
-
Android自定义View播放Gif动画的示例
-
Android 自定义View的使用介绍
-
HTML5自定义data-* data(obj)属性和jquery的data()方法的使用
-
android自定义ListView实现底部View自动隐藏和消失的功能
-
Android开发使用自定义View将圆角矩形绘制在Canvas上的方法
-
Android自定义view Path 的高级用法之搜索按钮动画
-
android自定义ListView实现底部View自动隐藏和消失的功能
-
Android自定义view仿QQ的Tab按钮动画效果(示例代码)
-
Android自定义view之围棋动画效果的实现