知识点摘要:掌握 视图动画、属性动画、布局动画、手势。
在 Android 中动画分为属性动画(Property Animation)和视图动画(View Animation)。
View 动画
视图动画框架支持补间和逐帧动画,它们都可以用XML声明。
补间动画(Tween animation)
补间动画就是以 XML 格式定义的动画,用于执行图形上的旋转,淡入淡出,移动和拉伸等过渡。
在 Android 中的补间动画有四种 rotate、alpha、translate、scale,使用方法也很简单,我们只需要在 res/anim 创建一个对应的 xml 文件。如:scaleanim.xml
所有的补间动画都是继承自 Animation,想要用好动画,当然需要掌握 Animation 的重要的属性:
- duration: 动画的持续时间,单位 ms;
- fillAfter: 保持动画结束的状态;
- fillBefore: 还原初始化的状态;
- fillEnable: 与 fillBefore 一样;
- repeatCount: 动画循环次数,包括初始的那一次,如设置为 3,实际会播放四次动画;
- repeatMode: 动画的重复模式,只有在设置了 repeatCount 才有效果。提供了两个值,reverse 表示倒序播放动画;restart 表示顺序播放动画;
- interpolator: 插值器,指定 view 需要执行的操作,可以取的值有如下这些:
- AccelerateDecelerateInterpolator:在动画开始与结束的地方速率改变比较慢,在中间的时候加速;
- AccelerateInterpolator:在动画开始的地方速率改变比较慢,然后开始加速;
- AnticipateInterpolator: 开始的时候向后,然后向前甩;
- AnticipateOvershootInterpolator:开始的时候向后,然后向前甩一定值后返回最后的值;
- BounceInterpolator:动画结束的时候弹起;
- CycleInterpolator:动画循环播放特定的次数,速率改变沿着正弦曲线;
- DecelerateInterpolator: 在动画开始的地方快然后慢;
- LinearInterpolator: 以常量速率改变;
- OvershootInterpolator: 向前甩一定值后, 再回到原来位置。
学习了从 Animation 继承的属性之后,我当然要学习自身的属性,下面分别进行介绍。
scale 标签
scale 标签是缩放动画,可以实现动态调节控件尺寸的效果。主要有下面几个属性:
- fromXScale: 起始相对 x 方向的长度的缩放比例;
- fromYScale: 起始相对 y 方向的长度的缩放比例;
- toXScale: 结束相对 x 方向的长度的缩放比例;
- toYScale: 结束相对 y 方向的长度的缩放比例;
- pivotX: 起始点的 x 坐标,可以是 数值、百分数和百分数p。如 50,50%,50%p。其中 50%p 为当前控件的左上角坐标加上父控件的宽度的 50%,其值作为起始坐标。
- pivotY: 起始点的 y 坐标。
scale 标签的使用方法也很简单,在 res/anim 下创建 scaleanim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:fillEnabled="true"
android:fromXScale="0.0"
android:fromYScale="0.0"
android:interpolator="@android:anim/overshoot_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="0"
android:repeatMode="reverse"
android:toXScale="1.4"
android:toYScale="1.4" />
复制代码
创建方式相当简单,但是我们要怎么使用呢?先用 AnimationUtils.loadAnimatio 方法解析 xml 文件,然后在控件上设置 startAnimation,如:
mScaleAnimation = AnimationUtils.loadAnimation(this, R.anim.scaleanim)
text_view.startAnimation(mScaleAnimation)
复制代码
最后的效果应该是以中间点进行缩放,并且会放大到比 1.4 倍大,然后再回到 view 的本来大小。最好自己进行测试。
rotate 标签
rotate 标签是旋转动画,可以实现旋转控件的效果。主要有一下几个属性:
- fromDegrees: 开始时旋转的角度的位置,负值为逆时针旋转;
- toDegrees: 结束时旋转的角度的位置;
- pivotX: 缩放中心点的 x 坐标,值可以为数值、百分数、百分数p;
- pivotY: 缩放中心点的 y 坐标。
在 res/anim 下创建 rotateanim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fillAfter="true"
android:fromDegrees="0"
android:interpolator="@android:anim/bounce_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" />
复制代码
其效果是沿着中心点顺时针旋转 360 度,然后在动画结束的时候会回弹。
alpha 标签
alpha 标签是淡入淡出动画,可以实现控件淡入淡出的效果。属性如下:
- fromAlpha: 动画开始时的透明度,其中 0.0 为完全透明,1.0 为完全不透明
- toAlpha: 动画结束时的透明度
我们在 res/anim 下创建 alphaanim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000"
android:fillBefore="true"
android:fromAlpha="1.0"
android:interpolator="@android:anim/overshoot_interpolator"
android:repeatCount="0"
android:repeatMode="reverse"
android:toAlpha="0.1" />
复制代码
其效果是控件从完全不透明渐变成透明度为 0.1,然后由于插值器的作用会降到完全透明。
translate 标签
translate 标签是移动动画,能实现控件移动的效果。主要的属性有:
- fromXDelta: 起始点 x 坐标,相对于当前坐标的偏移量(如果是负值就代表向左向上)。其值可以是数值、百分数、百分数p。。
- fromYDelta: 起始点 y 坐标
- toXDelta: 结束点 x 坐标
- toYDelta: 结束点 y 坐标
在 res/anim 创建 translateanim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:fillAfter="true"
android:fromXDelta="0"
android:interpolator="@android:anim/overshoot_interpolator"
android:fromYDelta="0"
android:toXDelta="50%"
android:toYDelta="50%" />
复制代码
最后的效果是这样,首先控件从当前位置移动到左下偏移半个控件的距离,然后由于插值器的作用会多移动一点距离,最后返回到偏移半个控件的位置。
set 标签
最后还有一个 set 标签,这个标签其实就是将上面学习的四种标签组合起来,实现更复杂的效果。
创建 res/anim/setanim.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000"
android:fillAfter="true">
<alpha
android:fromAlpha="0.1"
android:toAlpha="1.0" />
<scale
android:fromXScale="0"
android:fromYScale="0.0"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.4"
android:toYScale="1.4" />
<rotate
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="720" />
</set>
复制代码
很简单吧,看到上面的代码你能说出来最终的效果吗?
代码生成补间动画
我们知道 xml 代码只是一种标记语言,而最后所有带都需要编译成机器代码,而 xml 代码最后都会通过 Java 代码解析成计算机能看懂的代码。那么四种补间动画肯定有对应的 Java 类。
- scale 标签对应的类是 ScaleAnimation;
- alpha 标签对应的是 AlphaAnimation;
- rotate 标签对应的是 RotateAnimation;
- translate 标签对应的是 TranslateAnimation;
ScaleAnimation 的构造函数如下:
- ScaleAnimation(Context context, AttributeSet attrs) 从XML文件加载动画,基本用不到
- ScaleAnimation(float fromX, float toX, float fromY, float toY)
- ScaleAnimation(float fromX, float toX, float fromY, float toY, float pivotX, float pivotY)
- ScaleAnimation(float fromX, float toX, float fromY, float toY, int pivotXType, float pivotXValue, int pivotYType, float pivotYValue)
其实我们可以从第一个构造函数看出端倪,我们前面写的 scaleanim.xml 在这个类中被解析,既然可以通过解析 xml 得到想要的效果,那我们也可以通过其他的构造函数在代码中设置动画效果。
我举例一些用法,剩下的靠读者自己去学习。
/**
* 使用代码生成动画
*/
mScaleAnimation = ScaleAnimation(
0.0f, 1.4f, 0f, 1.4f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
)
mScaleAnimation.duration = 1000
mScaleAnimation.interpolator = CycleInterpolator(5f)
mAlphaAnimation = AlphaAnimation(0.1f, 1.0f)
mAlphaAnimation.duration = 1000
mAlphaAnimation.interpolator = AnticipateInterpolator()
mRotateAnimation = RotateAnimation(0f,230f)
mRotateAnimation.duration = 1000
mRotateAnimation.interpolator = AnticipateInterpolator()
复制代码
逐帧动画(Frame Animation)
以XML格式定义的动画,按顺序显示一系列图像(如电影)。
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item
android:drawable="@drawable/a"
android:duration="500" />
<item
android:drawable="@drawable/b"
android:duration="500" />
<item
android:drawable="@drawable/c"
android:duration="500" />
<item
android:drawable="@drawable/d"
android:duration="500" />
</animation-list>
复制代码
逐帧动画用到的标签是 animation-list,主要属性有 oneshot 接收一个 bool 类型的变量,当值为 true 时,动画只播放一边,为 false 动画一直循环播放。子元素是 item 标签,表示单帧动画,主要属性有 drawable 表示当前单帧动画的图片资源,duration 则表示这一帧动画播放多久。
动画准备好了之后,使用方法也很简单。首先逐帧动画的载体是 ImageView,然后使用的方法如下:
// kotlin 代码
val rocketImage: ImageView = findViewById(R.id.rocket_image)
rocketImage.setBackgroundResource(R.drawable.rocket)
val rocketAnimation = rocketImage.background
if (rocketAnimation is Animatable) {
rocketAnimation.start()
}
复制代码
最后的效果就是这样
手势
普通手势
首先我们来学习一下监听 View 的 Touch 事件,其实很简单对于可以修改源码的直接重写 onTouchEvent() 方法即可,而对于我们不能修改源码的,其实也只要实现 setOnTouchListener() 方法。
// 重新实现 Activity 的 Touch 事件
override fun onTouchEvent(event: MotionEvent): Boolean {
// Touch 事件的简单检测,只处理 DOWN、MOVE、UP 这种
val action: Int = MotionEventCompat.getActionMasked(event)
return when (action) {
MotionEvent.ACTION_DOWN -> {
Log.d(TAG, "Action was DOWN")
true
}
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "Action was MOVE")
true
}
MotionEvent.ACTION_UP -> {
Log.d(TAG, "Action was UP")
true
}
MotionEvent.ACTION_CANCEL -> {
Log.d(TAG, "Action was CANCEL")
true
}
MotionEvent.ACTION_OUTSIDE -> {
Log.d(TAG, "Movement occurred outside bounds of currentscreen element")
true
}
else -> super.onTouchEvent(event)
}
}
复制代码
// 处理控件的 Touch 事件
text_view.setOnTouchListener { v, event ->
val action: Int = MotionEventCompat.getActionMasked(event
when (action) {
MotionEvent.ACTION_DOWN -> {
Log.d(TAG, "Action was DOWN")
true
}
MotionEvent.ACTION_MOVE -> {
Log.d(TAG, "Action was MOVE")
true
}
MotionEvent.ACTION_UP -> {
Log.d(TAG, "Action was UP")
true
}
MotionEvent.ACTION_CANCEL -> {
Log.d(TAG, "Action was CANCEL")
true
}
MotionEvent.ACTION_OUTSIDE -> {
Log.d(TAG, "Movement occurred outside bounds of currenscreen element")
true
}
else -> super.onTouchEvent(event)
}
}
复制代码
由代码可以看出来,我们是处理了 View 的 onTouchEvent 事件中 MotionEvent,而 MotionEvent 类封装了 DOWN(按下)、MOVE(移动)、UP(抬起)等,所以我们只要在对应的动作中处理逻辑即可。
按照上面那种方法处理还有一个不方便的地方,比如我想监听用户双击了 button,这个时候我们就得再加入一个变量来记录两次点击(DOWN + UP 动作),而这两次的间隔时间也不能太长,这个时间多长合适呢?这些都是我们需要考虑的。这还只是一个简单的双击事件,如果我还想三击、四击,或者长按呢?那就更复杂。
是不是感觉一个头两个大,不过不用担心,官方已经为我们考虑过这个问题了,所以就有个 GestureDetector(手势检测器) 这个类,这个类封装了各种日常会用到的手势操作,如:双击、长按、滚动等。
用法:
// 1. 实现一个监听回调
private class MyGestureListener :GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean {
Log.d(TAG, "onDown: $e")
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX:Float, velocityY: Float): Boolean {
Log.d(TAG, "onFling: $e1 $e2")
return true
}
}
// 2. 创建一个手势检测器,GestureDetectorCompat 是 GestureDetector 的一种实现
private lateinit var mDetector: GestureDetectorCompat
mDetector = GestureDetectorCompat(this, MyGestureListener())
// 3. 重写 onTouchEvent 方法
override fun onTouchEvent(event: MotionEvent): Boolean {
return if (mDetector.onTouchEvent(event)) {
true
} else {
super.onTouchEvent(event)
}
}
// 或者实现
view.setOnTouchListener { v, event ->
mDetector.onTouchEvent(event)
}
复制代码
上面代码实现了 SimpleOnGestureListener 的子类,它是 GestureDetector 对外提供的接口,能让我们处理各种手势操作。还提供了另外几种接口,如下表:
解释一下 SimpleOnGestureListener 常用的原因,OnGestureListener 等其他接口需要我们必须实现很多方法,这就导致了本来有些方法我不关心,但是还是要实现。而 SimpleOnGestureListener 将这些方法进行了空实现,如果我们对某个方法感兴趣再重写就好了,这样我们的代码就会比较简洁。
各种接口更为详细的介绍可以参考 安卓自定义View进阶-手势检测(GestureDetector)