(六)Android动画
(1)View动画(视图动画)
视图动画的作用对象是视图(View),分为补间动画和逐帧动画
1.1 帧动画(AnimationDrawable)
原理就是将一张张单独的图片连贯的进行播放,从而在视觉上产生一种动画的效果,图片资源决定了这种方式可以实现怎样的动画
(1)作用对象
视图控件(View)
1、如Android的TextView、Button等等 2、不可作用于View组件的属性,如:颜色、背景、长度等等
(2)原理 将动画拆分为 帧 的形式,且定义每一帧 = 每一张图片,并按序播放一组预先定义好的图片。 (3)特点 优点:使用简单、方便 缺点:容易引起OOM,因为会使用大量&尺寸较大的图片资源(应该避免使用尺寸较大的图片) (4)应用场景 适用于复杂、个性化的动画效果。 (5)具体使用 步骤1:将动画资源(每张图片资源)放到drawable文件夹里 步骤2:在XML中实现动画(文件路径:res/anim文件夹创建动画效果的.xml文件) res/anim/frame_anim1.xml
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">//设置是否只播放一次,默认为false
//item = 动画图片资源;duration = 设置一帧持续时间(ms)
<item android:drawable="@drawable/a_0" android:duration="100" />
<item android:drawable="@drawable/a_1" android:duration="100" />
<item android:drawable="@drawable/a_2" android:duration="100" />
</animation-list>
步骤3:在Java代码中载入&启动&停止动画
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_frame_animation);
ImageView animationImg1 = (ImageView) findViewById(R.id.animation1);
animationImg1.setImageResource(R.drawable.frame_anim1);//设置动画
AnimationDrawable animationDrawable1 = (AnimationDrawable) animationImg1.getDrawable();//获取动画对象
animationDrawable1.start();//启动动画
}
protected void onStop() {
super.onStop();
AnimationDrawable animationDrawable1 = (AnimationDrawable) animationImg1.getDrawable();//获取动画对象
animationDrawable1.stop();//停止动画
}
1.2补间动画(Animation)
(1)作用对象
视图对象(View)
(2)原理
通过确定开始的视图样式 & 结束的视图样式、中间动画变化过程由系统补全来确定一个动画。即只需要开发者设置动画的起始值和结束值,中间的动画由系统自动帮我们完成。
(3)分类
补间动画包含四种动画类型:透明度(AlphaAnimation),缩放(ScaleAnimation),旋转(RotateAnimation),位移(TranslateAnimation)继承自 Animation 类。不同的动画对应不同的子类。
(4)具体使用
既可以在Java代码中动态的指定这四种动画效果,也可在XML代码指定。
(4.1)XML代码中设置
xml文件中属性动画的目录是res/anim/file_name.xml(不推荐)xml 文件中视图动画代码如下,透明度动画对应标签 ,缩放动画对应标签 ,旋转动画对应标签 ,位移动画对应标签 ,根标签 就表示一个动画集合 AnimationSet;
<set xmlns:android="http://schemas.android.com/apk/res/android"
//组合动画独特的属性,表示祝贺动画是否 和集合共享一个插值器
//如果集合不指定插值器,那么自动化需要单独设置
android:shareInterpolator="true" >
<!--透明度-->
<alpha
android:fromAlpha="透明度起始值,0表示完全透明"
android:toAlpha="透明度最终值,1表示不透明"
android:duration="动画持续时间"
android:fillAfter="true表示保持动画结束时的状态,false表示不保持"/>
<!--缩放-->
<scale
android:fromXScale="水平方向缩放的起始值,比如0"
android:fromYScale="竖直方向缩放的起始值,比如0"
android:toXScale="水平方向缩放的结束值,比如2"
android:toYScale="竖直方向缩放的结束值,比如2"
android:pivotX="缩放支点的x坐标"
android:pivotY="缩放支点的y坐标(支点可以理解为缩放的中心点,缩放过程中这点的坐标是不变的;支点默认在中心位置)" />
<!--位移-->
<translate
android:fromXDelta="x起始值"
android:toXDelta="x结束值"
android:fromYDelta="y起始值"
android:toYDelta="y结束值" />
<!--旋转-->
<rotate
android:fromDegrees="旋转起始角度"
android:toDegrees="旋转结束角度"
android:pivotX="缩放支点的x坐标"
android:pivotY="缩放支点的y坐标" />
</set>
(1)单个动画实现
ImageView ivAni = (ImageView) findViewById(R.id.iv_ani);//创建设置动画的视图View
Animation ani = AnimationUtils.loadAnimation(this, R.anim.ani_view);//创建动画对象 并传入设置的动画效果xml文件
ivAni.startAnimation(ani);//播放动画
(4.2)Java代码实现 每种补间动画拥有自己的子类。
llGroup = (LinearLayout) findViewById(R.id.ll_group);
// 创建动画集合
AnimationSet aniSet = new AnimationSet(false);
// 透明度动画
AlphaAnimation alpha = new AlphaAnimation(0, 1);
alpha.setDuration(4000);
aniSet.addAnimation(alpha);
// 旋转动画
RotateAnimation rotate = new RotateAnimation(0, 360,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
rotate.setDuration(4000);
aniSet.addAnimation(rotate);
// 缩放动画
ScaleAnimation scale = new ScaleAnimation(1.5f, 0.5f, 1.5f, 0.5f);
scale.setDuration(4000);
aniSet.addAnimation(scale);
// 位移动画
TranslateAnimation translate = new TranslateAnimation(0, 160, 0, 240);
translate.setDuration(4000);
aniSet.addAnimation(translate);
// 把动画设置给llGroup
llGroup.startAnimation(aniSet);
(5)监听动画 Animation类通过监听动画开始 / 结束 / 重复时刻来进行一系列操作,如跳转页面等等。可采用动画适配器AnimatorListenerAdapter,解决实现接口繁琐 的问题。
anim.addListener(new AnimatorListenerAdapter() {
// 向addListener()方法中传入适配器对象AnimatorListenerAdapter()
// 由于AnimatorListenerAdapter中已经实现好每个接口
// 所以这里不实现全部方法也不会报错
@Override
public void onAnimationStart(Animator animation) {
// 如想只想监听动画开始时刻,就只需要单独重写该方法就可以
}
@Override
public void onAnimationEnd(Animation animation) {
// 动画结束时回调
}
@Override
public void onAnimationRepeat(Animation animation) {
//动画重复执行的时候回调
}
});
(6)自定义View动画 所有的自定义动画都需要继承 android.view.animation.Animation 抽象类,然后重写 initialize() 和 applyTransformation() 这两个方法 (1)initialize() 方法中对一些变量进行初始化 (2)applyTransformation() 方法中通过矩阵(Matrix)修改动画数值,从而控制动画的实现过程,这也是自定义动画的核心。 applyTransformation(float interpolatedTime, Transformation t) 方法在动画的执行过程中会不断地调用 float interpolatedTime 表示当前动画进行的时间与动画总时间(一般在 setDuration() 方法中设置)的比值,从0逐渐增大到1; Transformation t 传递当前动画对象,一般可以通过代码 android.graphics.Matrix matrix = t.getMatrix() 获得 Matrix 矩阵对象,再设置 Matrix 对象,一般要用到 interpolatedTime 参数,以此达到控制动画实现的结果(随时间变换) 具体案例:实现QQ抖动效果
public class QQTrembleAnimation extends Animation{
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
setDuration(1000);//设置默认时长1秒
setFillAfter(true);//保持动画结束状态
setInterpolator(new LinearInterpolator());//设置线性插值器
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
//自定义动画的核心,在动画的执行过程中会不断回调此方法,并且每次回调interpolatedTime值都在不断变化(0----1)
//Matrix.setTranslate(dx,dy)中dx、dy表示移动距离,是根据interpolatedTime计算出正弦值,实现了抖动
t.getMatrix().setTranslate(
(float)Math.sin(interpolatedTime * 50)*8,
(float)Math.sin(interpolatedTime * 50)*8
);
super.applyTransformation(interpolatedTime, t);
}
}
myAnimationView = (ImageView) findViewById(R.id.my_animation_view);
//实现抖动动画
private void TrembleAnimation() {
QQTrembleAnimation tremble = new QQTrembleAnimation();
tremble.setRepeatCount(2);//重复次数2次(不包含第一次)
myAnimationView.startAnimation(tremble);
}
(7)应用场景 Activity的切换效果 Activity启动/退出时的动画效果。自定义淡入淡出效果&左右滑动效果。淡入淡出采用透明度动画(Alpha)左右滑动采用平移动画(Translate) (7.1)进入动画xml文件 enter_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
>
//从右边滑动到中间
<translate
android:duration="500"
android:fromXDelta="100%p"
android:toXDelta="0%p"
/>
//淡入
<alpha
android:duration="1500"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
</set>
(7.2)退出动画xml文件 exit_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
>
//从中间滑动到左边
<translate
android:duration="500"
android:fromXDelta="0%p"
android:toXDelta="-100%p"
/>
//淡出
<alpha
android:duration="1500"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
</set>
(7.3)设置动画
Intent intent = new Intent(MainActivity.this, SecActivity.class);
startActivity(intent);
// 自定义的Activity切换动画效果
overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
}
注意: overridePendingTransition()必须要在startActivity(intent)和finish()后被调用才能生效。 Fragment的切换效果 (1)系统自带
FragmentTransaction fragmentTransaction = mFragmentManager
.beginTransaction();
fragmentTransaction.setTransition(int transit);
// 通过setTransition(int transit)进行设置
// transit参数说明
// 1. FragmentTransaction.TRANSIT_NONE:无动画
// 2. FragmentTransaction.TRANSIT_FRAGMENT_OPEN:标准的打开动画效果
// 3. FragmentTransaction.TRANSIT_FRAGMENT_CLOSE:标准的关闭动画效果
// 标准动画设置好后,在Fragment添加和移除的时候都会有。
(2)自定义动画
// 采用`FragmentTransavtion`的 `setCustomAnimations()`进行设置
FragmentTransaction fragmentTransaction = mFragmentManager
.beginTransaction();
fragmentTransaction.setCustomAnimations(
R.anim.in_from_right,
R.anim.out_to_left);
视图组中子元素出场动画 视图组(ViewGroup)中子元素可以具备出场时的补间动画效果,常用于为ListView的item设置出场动画 步骤1:设置子元素的出场动画 res/anim/view_animation.xml
<?xml version="1.0" encoding="utf-8"?>
// 此处采用了组合动画
<set xmlns:android="http://schemas.android.com/apk/res/android" >
android:duration="3000"
<alpha
android:duration="1500"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
<translate
android:fromXDelta="500"
android:toXDelta="0"
/>
</set>
步骤2:设置 视图组(ViewGroup)的动画文件 res/ anim /anim_layout.xml
<?xml version="1.0" encoding="utf-8"?>
// 采用LayoutAnimation标签
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.5"
// 子元素开始动画的时间延迟
// 如子元素入场动画的时间总长设置为300ms
// 那么 delay = "0.5" 表示每个子元素都会延迟150ms才会播放动画效果
// 第一个子元素延迟150ms播放入场效果;第二个延迟300ms,以此类推
android:animationOrder="normal"
// 表示子元素动画的顺序
// 可设置属性为:
// 1. normal :顺序显示,即排在前面的子元素先播放入场动画
// 2. reverse:倒序显示,即排在后面的子元素先播放入场动画
// 3. random:随机播放入场动画
android:animation="@anim/view_animation"
// 设置入场的具体动画效果
// 将步骤1的子元素出场动画设置到这里
/>
步骤3:为视图组(ViewGroup)指定andorid:layoutAnimation属性 指定的方式有两种: XML / Java代码设置 方式1:在 XML 中指定
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:orientation="vertical" >
<ListView
android:id="@+id/listView1"
android:layoutAnimation="@anim/anim_layout"
// 指定layoutAnimation属性用以指定子元素的入场动画
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
方式2:在Java代码中指定 这样就不用额外设置res/ anim /anim_layout.xml该xml文件了
ListView lv = (ListView) findViewById(R.id.listView1);
Animation animation = AnimationUtils.loadAnimation(this,R.anim.anim_item);
// 加载子元素的出场动画
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
// 设置LayoutAnimation的属性
lv.setLayoutAnimation(controller);
// 为ListView设置LayoutAnimation的属性
(2)属性动画(Animator)
1、简介
作用对象:任意Java对象(不再局限视图View对象) 实现的动画效果:可自定义各种动画效果(不再局限于4种基本变换) 作用领域:API11(Android3.0)后引入
2、核心原理
在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,从而实现该对象在该属性上的动画效果。这里的可以是任意对象的任意属性。
可理解为一种按照一定变化率对属性值进行操作的机制,变化率就是依赖Interpolator控制,而值操作则是TypeEvaluator控制。
从工作原理可以看出属性动画有2个重要的类——ValueAnimator,ObjectAnimator。属性动画的使用基本依靠这两个类:
3、具体使用
3.1 ValueAnimator类
(1)简介
属性动画机制中最核心的一个类。通过不断控制值的变化,再不断手动赋给对象的属性,从而实现动画效果。
从上面原理可以看出:ValueAnimator类中有3个重要方法:
1、ValueAnimator.ofInt(int values)
2、ValueAnimator.ofFloat(float values)
3、ValueAnimator.ofObject(int values)
(2)ValueAnimator.ofInt(int values)
作用:将初始值 以整型数值的形式 过渡到结束值,即估值器是整型估值器 - IntEvaluator。ValueAnimator本质是一种值的操作机制,值从一个int的初始值平滑过渡到一个int结束值,开发者通过手动将这些值赋给对象的属性值。从而实现动画。
// 步骤1:设置动画属性的初始值 & 结束值
ValueAnimator anim = ValueAnimator.ofInt(0, 3);
// ofInt()作用有两个
// 1. 创建动画实例
// 2. 将传入的多个Int参数进行平滑过渡:此处传入0和1,表示将值从0平滑过渡到1
// 如果传入了3个Int参数 a,b,c ,则是先从a平滑过渡到b,再从b平滑过渡到C,以此类推
// ValueAnimator.ofInt()内置了整型估值器,直接采用默认的.不需要设置,即默认设置了如何从初始值 过渡到 结束值
// 关于自定义插值器我将在下节进行讲解
// 下面看看ofInt()的源码分析 ->>关注1
// 步骤2:设置动画的播放各种属性
anim.setDuration(500);
// 设置动画运行的时长
anim.setStartDelay(500);
// 设置动画延迟播放时间
anim.setRepeatCount(0);
// 设置动画重复播放次数 = 重放次数+1
// 动画播放次数 = infinite时,动画无限重复
anim.setRepeatMode(ValueAnimator.RESTART);
// 设置重复播放动画模式
// ValueAnimator.RESTART(默认):正序重放
// ValueAnimator.REVERSE:倒序回放
// 步骤3:将改变的值手动赋值给对象的属性值:通过动画的更新监听器
// 设置 值的更新监听器
// 即:值每次改变、变化一次,该方法就会被调用一次
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int currentValue = (Integer) animation.getAnimatedValue();
// 获得改变后的值
System.out.println(currentValue);
// 输出改变后的值
// 步骤4:将改变后的值赋给对象的属性值,下面会详细说明
View.setproperty(currentValue);
// 步骤5:刷新视图,即重新绘制,从而实现动画效果
View.requestLayout();
}
});
anim.start();
// 启动动画
}
// 关注1:ofInt()源码分析
public static ValueAnimator ofInt(int... values) {
// 允许传入一个或多个Int参数
// 1. 输入一个的情况(如a):从0过渡到a;
// 2. 输入多个的情况(如a,b,c):先从a平滑过渡到b,再从b平滑过渡到C
ValueAnimator anim = new ValueAnimator();
// 创建动画对象
anim.setIntValues(values);
// 将传入的值赋值给动画对象
return anim;
}
值从初始值过渡到结束值的过程如下:
实例:通过动画的更新监听器,将改变的值手动赋值给对象的属性值。实现效果:将按钮的宽度从150px放大到500px
Button mButton = (Button) findViewById(R.id.Button);
// 创建动画作用对象:此处以Button为例
// 步骤1:设置属性数值的初始值 & 结束值
ValueAnimator valueAnimator = ValueAnimator.ofInt(mButton.getLayoutParams().width, 500);
// 初始值 = 当前按钮的宽度,此处在xml文件中设置为150
// 结束值 = 500
// ValueAnimator.ofInt()内置了整型估值器,直接采用默认的.不需要设置
// 即默认设置了如何从初始值150 过渡到 结束值500
// 步骤2:设置动画的播放各种属性
valueAnimator.setDuration(2000);
// 设置动画运行时长:1s
// 步骤3:将属性数值手动赋值给对象的属性:此处是将 值 赋给 按钮的宽度
// 设置更新监听器:即数值每次变化更新都会调用该方法
valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
int currentValue = (Integer) animator.getAnimatedValue();
// 获得每次变化后的属性值
System.out.println(currentValue);
// 输出每次变化后的属性值进行查看
mButton.getLayoutParams().width = currentValue;
// 每次值变化时,将值手动赋值给对象的属性
// 即将每次变化后的值 赋 给按钮的宽度,这样就实现了按钮宽度属性的动态变化
// 步骤4:刷新视图,即重新绘制,从而实现动画效果
mButton.requestLayout();
}
});
valueAnimator.start();
// 启动动画
}
效果:
(3)ValueAnimator.ofFloat(float values)
作用:将初始值 以整型数值的形式 过渡到结束值,即估值器是浮点型估值器 - FloatEvaluator。
ValueAnimator anim = ValueAnimator.ofFloat(0, 3);
效果图
从上面可以看出,ValueAnimator.ofInt()与ValueAnimator.oFloat()仅仅只是在估值器上的区别:(即如何从初始值 过渡 到结束值)
ValueAnimator.oFloat()采用默认的浮点型估值器 (FloatEvaluator)
ValueAnimator.ofInt()采用默认的整型估值器(IntEvaluator)
(4)ValueAnimator.ofObject(int values)
作用:
将初始值以对象的形式过渡到结束值,通过操作实现动画效果。
使用模板:
// 创建初始动画时的对象 & 结束动画时的对象
myObject object1 = new myObject();
myObject object2 = new myObject();
ValueAnimator anim = ValueAnimator.ofObject(new myObjectEvaluator(), object1, object2);
// 创建动画对象 & 设置参数
// 参数说明
// 参数1:自定义的估值器对象(TypeEvaluator 类型参数) - 下面会详细介绍
// 参数2:初始动画的对象
// 参数3:结束动画的对象
anim.setDuration(5000);
anim.start();
实例说明: 步骤1:定义对象类 因为ValueAnimator.ofObject()是面向对象操作的,所以需要自定义对象类。本例需要操作的对象是 圆的点坐标 Point.java
public class Point {
// 设置两个变量用于记录坐标的位置
private float x;
private float y;
// 构造方法用于设置坐标
public Point(float x, float y) {
this.x = x;
this.y = y;
}
// get方法用于获取坐标
public float getX() {
return x;
}
public float getY() {
return y;
}
}
步骤2:根据需求实现TypeEvaluator接口 估值器(TypeEvaluator)介绍 作用:设置动画 如何从初始值 过渡到 结束值 的逻辑
插值器(Interpolator)决定 值 的变化模式(匀速、加速blabla) 估值器(TypeEvaluator)决定 值的具体变化数值
ValueAnimator.ofFloat()实现了 **将初始值 以浮点型的形式 过渡到结束值 ** 的逻辑,那么这个过渡逻辑具体是怎么样的呢? 其实是系统内置了一个 FloatEvaluator估值器,内部实现了初始值与结束值 以浮点型的过渡逻辑,我们来看一下 FloatEvaluator的代码实现:
public class FloatEvaluator implements TypeEvaluator {
// FloatEvaluator实现了TypeEvaluator接口
// 重写evaluate()
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 参数说明
// fraction:表示动画完成度(根据它来计算当前动画的值)
// startValue、endValue:动画的初始值和结束值
float startFloat = ((Number) startValue).floatValue();
return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
// 初始值 过渡 到结束值 的算法是:
// 1. 用结束值减去初始值,算出它们之间的差值
// 2. 用上述差值乘以fraction系数
// 3. 再加上初始值,就得到当前动画的值
}
}
ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具备系统内置的估值器,即FloatEvaluator & IntEvaluator,即系统已经默认实现了 如何从初始值 过渡到 结束值 的逻辑;但对于ValueAnimator.ofObject(),从上面的工作原理可以看出并没有系统默认实现,因为对对象的动画操作复杂 & 多样,系统无法知道如何从初始对象过度到结束对象。因此,对于ValueAnimator.ofObject(),我们需自定义估值器(TypeEvaluator)来告知系统如何进行从 初始对象 过渡到 结束对象的逻辑。 自定义实现的逻辑如下
// 实现TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{
// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 参数说明
// fraction:表示动画完成度(根据它来计算当前动画的值)
// startValue、endValue:动画的初始值和结束值
... // 写入对象动画过渡的逻辑
return value;
// 返回对象动画过渡的逻辑计算后的值
}
实现TypeEvaluator接口的目的是自定义如何 从初始点坐标 过渡 到结束点坐标;本例实现的是一个从左上角到右下角的坐标过渡逻辑。 PointEvaluator.java
// 实现TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {
// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 将动画初始值startValue 和 动画结束值endValue 强制类型转换成Point对象
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
// 根据fraction来计算当前动画的x和y的值
float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
// 将计算后的坐标封装到一个新的Point对象中并返回
Point point = new Point(x, y);
return point;
}
}
步骤3:将属性动画作用到自定义View当中 MyView.java
public class MyView extends View {
// 设置需要用到的变量
public static final float RADIUS = 70f;// 圆的半径 = 70
private Point currentPoint;// 当前点坐标
private Paint mPaint;// 绘图画笔
// 构造方法(初始化画笔)
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}
// 复写onDraw()从而实现绘制逻辑
// 绘制逻辑:先在初始点画圆,通过监听当前坐标值(currentPoint)的变化,每次变化都调用onDraw()重新绘制圆,从而实现圆的平移动画效果
@Override
protected void onDraw(Canvas canvas) {
// 如果当前点坐标为空(即第一次)
if (currentPoint == null) {
currentPoint = new Point(RADIUS, RADIUS);
// 创建一个点对象(坐标是(70,70))
// 在该点画一个圆:圆心 = (70,70),半径 = 70
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x, y, RADIUS, mPaint);
// (重点关注)将属性动画作用到View中
// 步骤1:创建初始动画时的对象点 & 结束动画时的对象点
Point startPoint = new Point(RADIUS, RADIUS);// 初始点为圆心(70,70)
Point endPoint = new Point(700, 1000);// 结束点为(700,1000)
// 步骤2:创建动画对象 & 设置初始值 和 结束值
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
// 参数说明
// 参数1:TypeEvaluator 类型参数 - 使用自定义的PointEvaluator(实现了TypeEvaluator接口)
// 参数2:初始动画的对象点
// 参数3:结束动画的对象点
// 步骤3:设置动画参数
anim.setDuration(5000);
// 设置动画时长
// 步骤3:通过 值 的更新监听器,将改变的对象手动赋值给当前对象
// 此处是将 改变后的坐标值对象 赋给 当前的坐标值对象
// 设置 值的更新监听器
// 即每当坐标值(Point对象)更新一次,该方法就会被调用一次
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
// 将每次变化后的坐标值(估值器PointEvaluator中evaluate()返回的Piont对象值)到当前坐标值对象(currentPoint)
// 从而更新当前坐标值(currentPoint)
// 步骤4:每次赋值后就重新绘制,从而实现动画效果
invalidate();
// 调用invalidate()后,就会刷新View,即才能看到重新绘制的界面,即onDraw()会被重新调用一次
// 所以坐标值每改变一次,就会调用onDraw()一次
}
});
anim.start();
// 启动动画
} else {
// 如果坐标值不为0,则画圆
// 所以坐标值每改变一次,就会调用onDraw()一次,就会画一次圆,从而实现动画效果
// 在该点画一个圆:圆心 = (30,30),半径 = 30
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x, y, RADIUS, mPaint);
}
}
}
步骤4:在布局文件加入自定义View空间 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="scut.carson_ho.valueanimator_ofobject.MainActivity">
<scut.carson_ho.valueanimator_ofobject.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
步骤5:在主代码文件设置显示视图 MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
效果图:
从上面可以看出,其实ValueAnimator.ofObject()的本质还是操作 ** 值 **,只是是采用将 多个值 封装到一个对象里的方式 同时对多个值一起操作而已
就像上面的例子,本质还是操作坐标中的x,y两个值,只是将其封装到Point对象里,方便同时操作x,y两个值而已
3.2 ObjectAnimator类
(1)实现动画原理 直接对对象的属性值进行改变操作,从而实现动画效果
- 如直接改变 View的 alpha 属性 从而实现透明度的动画效果
- 继承自ValueAnimator类,即底层的动画实现机制是基于ValueAnimator类
本质原理: 通过不断控制 值 的变化,再不断 自动 赋给对象的属性,从而实现动画效果。如下图:
从上面的工作原理可以看出:ObjectAnimator与 ValueAnimator类的区别:
ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;
ObjectAnimator 类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;
(2)具体使用
使用格式
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);
// ofFloat()作用有两个
// 1. 创建动画实例
// 2. 参数设置:参数说明如下
// Object object:需要操作的对象
// String property:需要操作的对象的属性
// float ....values:动画初始值 & 结束值(不固定长度)
// 若是两个参数a,b,则动画效果则是从属性的a值到b值
// 若是三个参数a,b,c,则则动画效果则是从属性的a值到b值再到c值
// 以此类推
// 至于如何从初始值 过渡到 结束值,同样是由估值器决定,此处ObjectAnimator.ofFloat()是有系统内置的浮点型估值器FloatEvaluator,同ValueAnimator讲解
anim.setDuration(500);// 设置动画运行的时长
anim.setStartDelay(500);// 设置动画延迟播放时间
anim.setRepeatCount(0);// 设置动画重复播放次数 = 重放次数+1,动画播放次数 = infinite时,动画无限重复
anim.setRepeatMode(ValueAnimator.RESTART);// 设置重复播放动画模式:ValueAnimator.RESTART(默认):正序重放,ValueAnimator.REVERSE:倒序回放
animator.start(); // 启动动画
使用实例:四种基本变换:平移、旋转、缩放&透明度。通过在ObjectAnimator.ofFloat()的第二个参数String property传入alpha、rotation、translationX 和 scaleY设置改变的属性值。
//动画作用对象是mButton,属性是透明度alpha,效果是常规-全透明-常规
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f);
//动画作用对象是mButton,属性是旋转rotation,效果是0-360
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
//动画作用对象是mButton,属性是X轴平移,效果是从当前位置平移到x=300再平移到初始位置
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);
//动画作用对象是mButton,属性是X轴缩放,效果是放大到3倍再缩小到初始大小
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "scaleX", 1f, 3f, 1f);
(3)自定义对象属性实现动画效果 ofFloat()的第二个参数可以传入任意属性值。 ObjectAnimator 类 对 对象属性值 进行改变从而实现动画效果的本质是:通过不断控制 值 的变化,再不断 自动 赋给对象的属性,从而实现动画效果。 而 自动赋给对象的属性的本质是调用该对象属性的set() & get()方法进行赋值 所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名 去寻找 该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值。而 自动赋给对象的属性的本质是调用该对象属性的set() & get()方法进行赋值 所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名 去寻找 该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值。 源码分析
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
// 其实Button对象中并没有rotation这个属性值
// ObjectAnimator并不是直接对我们传入的属性名进行操作
// 而是根据传入的属性值"rotation" 去寻找对象对应属性名对应的get和set方法,从而通过set() & get()对属性进行赋值
// 因为Button对象中有rotation属性所对应的get & set方法
// 所以传入的rotation属性是有效的
// 所以才能对rotation这个属性进行操作赋值
public void setRotation(float value);
public float getRotation();
// 实际上,这两个方法是由View对象提供的,所以任何继承自View的对象都具备这个属性
自动赋值
// 使用方法
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);
anim.setDuration(500);
animator.start();
// 启动动画,源码分析就直接从start()开始
<-- start() -->
@Override
public void start() {
AnimationHandler handler = sAnimationHandler.get();
if (handler != null) {
// 判断等待动画(Pending)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉
numAnims = handler.mPendingAnimations.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
// 判断延迟动画(Delay)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉
numAnims = handler.mDelayedAnims.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
}
super.start();
// 调用父类的start()
// 因为ObjectAnimator类继承ValueAnimator类,所以调用的是ValueAnimator的star()
// 经过层层调用,最终会调用到 自动赋值给对象属性值的方法
// 下面就直接看该部分的方法
}
<-- 自动赋值给对象属性值的逻辑方法 ->>
// 步骤1:初始化动画值
private void setupValue(Object target, Keyframe kf) {
if (mProperty != null) {
kf.setValue(mProperty.get(target));
// 初始化时,如果属性的初始值没有提供,则调用属性的get()进行取值
}
kf.setValue(mGetter.invoke(target));
}
}
// 步骤2:更新动画值
// 当动画下一帧来时(即动画更新的时候),setAnimatedValue()都会被调用
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
// 内部调用对象该属性的set()方法,从而从而将新的属性值设置给对象属性
}
}
自动赋值的逻辑: 初始化时,如果属性的初始值没有提供,则调用属性的 get()进行取值; 当 值 变化时,用对象该属性的 set()方法,从而从而将新的属性值设置给对象属性。
- ObjectAnimator 类针对的是任意对象 & 任意属性值,并不是单单针对于View对象 如果需要采用ObjectAnimator类实现动画效果,那么需要操作的对象就必须有该属性的set() & get()
- 同理,针对上述另外的三种基本动画效果,View也存在着setRotation()、getRotation()、setTranslationX()、getTranslationX()、setScaleY()、getScaleY()等set()& get() 。 实例:一个圆颜色渐变 对于属性动画,其拓展性在于:不局限于系统限定的动画,可以自定义动画,即自定义对象的属性,并通过操作自定义的属性从而实现动画。 那么,该如何自定义属性呢?本质上,就是:
- 为对象设置需要操作属性的set() & get()方法
- 通过实现TypeEvaluator类从而定义属性变化的逻辑 步骤1:设置对象类属性的set() & get()方法 设置对象类属性的set() & get()有两种方法:
- 通过继承原始类,直接给类加上该属性的 get()& set(),从而实现给对象加上该属性的 get()& set()
- 通过包装原始动画对象,间接给对象加上该属性的 get()& set()。即 用一个类来包装原始对象 这里示范第一种方法: MyView2.java
public class MyView2 extends View {
// 设置需要用到的变量
public static final float RADIUS = 100f;// 圆的半径 = 100
private Paint mPaint;// 绘图画笔
private String color;
// 设置背景颜色属性
// 设置背景颜色的get() & set()方法
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
mPaint.setColor(Color.parseColor(color));
// 将画笔的颜色设置成方法参数传入的颜色
invalidate();
// 调用了invalidate()方法,即画笔颜色每次改变都会刷新视图,然后调用onDraw()方法重新绘制圆
// 而因为每次调用onDraw()方法时画笔的颜色都会改变,所以圆的颜色也会改变
}
// 构造方法(初始化画笔)
public MyView2(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}
// 复写onDraw()从而实现绘制逻辑
// 绘制逻辑:先在初始点画圆,通过监听当前坐标值(currentPoint)的变化,每次变化都调用onDraw()重新绘制圆,从而实现圆的平移动画效果
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(500, 500, RADIUS, mPaint);
}
}
步骤2:在布局文件加入自定义View控件 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="scut.carson_ho.valueanimator_ofobject.MainActivity">
<scut.carson_ho.valueanimator_ofobject.MyView2
android:id="@+id/MyView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
步骤3:根据需求实现TypeEvaluator接口 此处实现估值器的本质是:实现 颜色过渡的逻辑。 ColorEvaluator.java
public class ColorEvaluator implements TypeEvaluator {
// 实现TypeEvaluator接口
private int mCurrentRed;
private int mCurrentGreen ;
private int mCurrentBlue ;
// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑:此处是写颜色过渡的逻辑
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 获取到颜色的初始值和结束值
String startColor = (String) startValue;
String endColor = (String) endValue;
// 通过字符串截取的方式将初始化颜色分为RGB三个部分,并将RGB的值转换成十进制数字
// 那么每个颜色的取值范围就是0-255
int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);
int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);
// 将初始化颜色的值定义为当前需要操作的颜色值
mCurrentRed = startRed;
mCurrentGreen = startGreen;
mCurrentBlue = startBlue;
// 计算初始颜色和结束颜色之间的差值
// 该差值决定着颜色变化的快慢:初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢;否则,变化则很快
// 具体如何根据差值来决定颜色变化快慢的逻辑写在getCurrentColor()里.
int redDiff = Math.abs(startRed - endRed);
int greenDiff = Math.abs(startGreen - endGreen);
int blueDiff = Math.abs(startBlue - endBlue);
int colorDiff = redDiff + greenDiff + blueDiff;
if (mCurrentRed != endRed) {
mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
fraction);
// getCurrentColor()决定如何根据差值来决定颜色变化的快慢 ->>关注1
} else if (mCurrentGreen != endGreen) {
mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
redDiff, fraction);
} else if (mCurrentBlue != endBlue) {
mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
redDiff + greenDiff, fraction);
}
// 将计算出的当前颜色的值组装返回
String currentColor = "#" + getHexString(mCurrentRed)
+ getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
// 由于我们计算出的颜色是十进制数字,所以需要转换成十六进制字符串:调用getHexString()->>关注2
// 最终将RGB颜色拼装起来,并作为最终的结果返回
return currentColor;
}
// 关注1:getCurrentColor()
// 具体是根据fraction值来计算当前的颜色。
private int getCurrentColor(int startColor, int endColor, int colorDiff,
int offset, float fraction) {
int currentColor;
if (startColor > endColor) {
currentColor = (int) (startColor - (fraction * colorDiff - offset));
if (currentColor < endColor) {
currentColor = endColor;
}
} else {
currentColor = (int) (startColor + (fraction * colorDiff - offset));
if (currentColor > endColor) {
currentColor = endColor;
}
}
return currentColor;
}
// 关注2:将10进制颜色值转换成16进制。
private String getHexString(int value) {
String hexString = Integer.toHexString(value);
if (hexString.length() == 1) {
hexString = "0" + hexString;
}
return hexString;
}
}
步骤4:调用ObjectAnimator.ofObject()方法 MainActivity.java
public class MainActivity extends AppCompatActivity {
MyView2 myView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myView2 = (MyView2) findViewById(R.id.MyView2);
ObjectAnimator anim = ObjectAnimator.ofObject(myView2, "color", new ColorEvaluator(),
"#0000FF", "#FF0000");
// 设置自定义View对象、背景颜色属性值 & 颜色估值器
// 本质逻辑:
// 步骤1:根据颜色估值器不断 改变 值
// 步骤2:调用set()设置背景颜色的属性值(实际上是通过画笔进行颜色设置)
// 步骤3:调用invalidate()刷新视图,即调用onDraw()重新绘制,从而实现动画效果
anim.setDuration(8000);
anim.start();
}
}
效果图
注:如何手动设置对象类属性的set()&get()
ObjectAnimator 类 自动赋给对象的属性 的本质是调用该对象属性的set() & get()方法进行赋值。所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名 去寻找 该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值。
从上面的原理可知,如果想让对象的属性a的动画生效,属性a需要同时满足下面两个条件:
1、对象必须要提供属性a的set()方法
a. 如果没传递初始值,那么需要提供get()方法,因为系统要去拿属性a的初始值 b. 若该条件不满足,程序直接Crash
2、对象提供的 属性a的set()方法 对 属性a的改变 必须通过某种方法反映出来
a. 如带来ui上的变化 b. 若这条不满足,动画无效,但不会Crash)
比如说:由于View的setWidth()并不是设置View的宽度,而是设置View的最大宽度和最小宽度的;所以通过setWidth()无法改变控件的宽度;所以对View视图的width做属性动画没有效果。具体请看下面Button按钮的例子
Button mButton = (Button) findViewById(R.id.Button);
// 创建动画作用对象:此处以Button为例
// 此Button的宽高设置具体为具体宽度200px
ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
// 设置动画的对象
针对上述对象属性的set()不是设置属性 或 根本没有set() / get ()的情况应该如何处理?手动设置对象类属性的set() & get()。共有两种方法: (1)通过继承原始类,直接给类加上该属性的 get()& set(),从而实现给对象加上该属性的 get()& set() (2)通过包装原始动画对象,间接给对象加上该属性的 get()& set()。即 用一个类来包装原始对象 对于第一种方法,在上面的例子已经说明;下面主要讲解第二种方法:通过包装原始动画对象,间接给对象加上该属性的get()& set() 本质上是采用了设计模式中的装饰模式,即通过包装类从而扩展对象的功能 还是采用上述 Button 按钮的例子
public class MainActivity extends AppCompatActivity {
Button mButton;
ViewWrapper wrapper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.Button);
// 创建动画作用对象:此处以Button为例
wrapper = new ViewWrapper(mButton);
// 创建包装类,并传入动画作用的对象
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(3000).start();
// 设置动画的对象是包装类的对象
}
});
}
// 提供ViewWrapper类,用于包装View对象
// 本例:包装Button对象
private static class ViewWrapper {
private View mTarget;
// 构造方法:传入需要包装的对象
public ViewWrapper(View target) {
mTarget = target;
}
// 为宽度设置get() & set()
public int getWidth() {
return mTarget.getLayoutParams().width;
}
public void setWidth(int width) {
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}
}
效果图:
3.3 ValueAnimator & ObjectAnimator总结
对比ValueAnimator类 & ObjectAnimator 类,其实二者都属于属性动画,本质上都是一致的:先改变值,然后 赋值 给对象的属性从而实现动画效果。 但二者的区别在于:
- ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;本质上是一种 改变 值的操作机制
- ObjectAnimator类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;
可以理解为:ObjectAnimator更加智能、自动化程度更高
4、动画监听
动画适配器AnimatorListenerAdapter进行监听动画的部分时刻,解决实现接口繁琐问题
anim.addListener(new AnimatorListenerAdapter() {
// 向addListener()方法中传入适配器对象AnimatorListenerAdapter()
// 由于AnimatorListenerAdapter中已经实现好每个接口
// 所以这里不实现全部方法也不会报错
@Override
public void onAnimationStart(Animator animation) {
// 如想只想监听动画开始时刻,就只需要单独重写该方法就可以
}
});
5、应用实例
(1)实现影子特效(安卓自带属性)
常用的属性动画属性值:
translationX、translationY----控制view对象相对其左上角坐标在X、Y轴上偏移的距离 rotation、rotationX、rotationY----控制view对象绕支点进行2D和3D旋转 scaleX、scaleY----控制view对象绕支点进行2D缩放 pivotX、pivotY----控制view对象的支点位置,这个位置一般就是view对象的中心点。围绕这个支点可以进行旋转和缩放处理 x、y----描述view对象在容器中的最终位置,是最初的左上角坐标和translationX、translationY值的累计和 alpha----表示view对象的透明度。默认值是1(完全透明)、0(不透明)
myAnimationView1 = (ImageView) findViewById(R.id.my_animation_view1);
private void ShadowAnimator() {
//创建ObjectAnimator属性对象,参数为动画要设置的View对象、动画属性、属性值
ObjectAnimator animator1 = ObjectAnimator.ofFloat(myAnimationView1,"alpha",0,1);//渐变
ObjectAnimator animator2 = ObjectAnimator.ofFloat(myAnimationView2,"translationY",0,200F);//上移
ObjectAnimator animator3 = ObjectAnimator.ofFloat(myAnimationView3,"translationY",0,-200F);//下移
ObjectAnimator animator4 = ObjectAnimator.ofFloat(myAnimationView4,"translationX",0,200F);//右移
ObjectAnimator animator5 = ObjectAnimator.ofFloat(myAnimationView5,"translationX",0,-200F);//左移
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(3000);
animatorSet.setInterpolator(new BounceInterpolator());//弹跳效果插值器
animatorSet.playTogether(animator1,animator2,animator3,animator4,animator5);//组合动画共同播放
animatorSet.start();
}
(2)实现颜色渐变特效(自定义属性实现)
方法1:ValueAnimator和属性动画的监听
为ValueAnimator对象设置动画监听,代码如下所示:valueAnimator.addUpdateListener(),需要传入一个AnimatorUpdateListener对象,一般我们传入的是AnimatorUpdateListener的匿名对象,即:valueAnimator.addUpdateListener(new AnimatorUpdateListener(){...}),需要重写它的onAnimationUpdate()方法,那么上述值的计算逻辑就放在onAnimationUpdate()方法体内;
//6秒内把一个view控件的背景颜色从从红色渐变到蓝色
private void ColorChangeAnimator(final String start,final String end) {
final ValueAnimator animator = ValueAnimator.ofFloat(0,100f);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//获取当前动画的进度值,1~100
float currentValue = (float) valueAnimator.getAnimatedValue();
//获取当前动画的百分比,0~1
float fraction = valueAnimator.getAnimatedFraction();
//调用evaluateForColor,根据百分比计算出对应的颜色
String colorResult = evaluateForColor(fraction,start,end);
//通过Color.parseColor解析字符串颜色值,传给ColorDrawable
ColorDrawable colorDrawable = new ColorDrawable(Color.parseColor(colorResult));
myAnimationView1.setBackground(colorDrawable);
//Android视图机制中在主线程中调用它,用于触发视图的绘制刷新
myAnimationView1.invalidate();
}
});
animator.setDuration(6*1000);
animator.start();
}
方法2:重写TypeEvaluator估值器,用来计算属性动画某个时刻的属性值的具体值
(1)自定义Interpolator Interpolator直译过来就是插补器,也译作插值器,直接控制动画的变化速率,这涉及到变化率概念,形象点说就是加速度,可以简单理解为变化的快慢。从上面的继承关系可以清晰的看出来,Interpolator是一个接口,并未提供插值逻辑的具体实现,它的非直接子类有很多,比较常用的有下面四个:
加减速插值器AccelerateDecelerateInterpolator; 线性插值器LinearInterpolator; 加速插值器AccelerateInterpolator; 减速插值器DecelerateInterpolator;
当你没有为动画设置插值器时,系统默认会帮你设置加减速插值器AccelerateDecelerateInterpolator (2)自定义TypeEvaluator
public class PositionEvaluator implements TypeEvaluator {
// 创建PositionView对象,用来调用createPoint()方法创建当前PositionPoint对象
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 将startValue,endValue强转成PositionPoint对象
PositionPoint point_1 = (PositionPoint) startValue;
// 获取起始点Y坐标
float currentY = point_1.getY();
// 调用forCurrentX()方法计算X坐标
float x = forCurrentX(fraction);
// 调用forCurrentY()方法计算Y坐标
float y = forCurrentY(fraction, currentY);
return new PositionPoint(x,y);
}
/**
* 计算Y坐标
*/
private float forCurrentY(float fraction, float currentY) {
float resultY = currentY;
if (fraction != 0f) {
resultY = fraction * 400f + 20f;
}
return resultY;
}
/**
* 计算X坐标
*/
private float forCurrentX(float fraction) {
float range = 120f;// 振幅
float resultX = 160f + (float) Math.sin((6 * fraction) * Math.PI) * range;// 周期为3,故为6fraction
return resultX;
}
}
ValueAnimator animator1 = ValueAnimator.ofObject(
new PositionEvaluator(),
new PositionPoint(RADIUS, RADIUS),
new PositionPoint(getWidth() - RADIUS, getHeight() - RADIUS));
animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (PositionPoint) animation.getAnimatedValue();
invalidate();
}
});
6、属性动画主要使用类
(3)View动画与属性动画对比
视图动画缺点:
- 作用对象局限:View 即补间动画 只能够作用在视图View上,即只可以对一个Button、TextView、甚至是LinearLayout、或者其它继承自View的组件进行动画操作,但无法对非View的对象进行动画操作。 有些情况下的动画效果只是视图的某个属性 & 对象而不是整个视图;如,现需要实现视图的颜色动态变化,那么就需要操作视图的颜色属性从而实现动画效果,而不是针对整个视图进行动画操作
- 没有改变View的属性,只改变视觉效果 补间动画只是改变了View的视觉效果,而不会真正去改变View的属性。如,将屏幕左上角的按钮 通过补间动画 移动到屏幕的右下角,点击当前按钮位置(屏幕右下角)是没有效果的,因为实际上按钮还是停留在屏幕左上角,补间动画只是将这个按钮绘制到屏幕右下角,改变了视觉效果而已。
- 动画效果单一 补间动画只能实现平移、旋转、缩放 & 透明度这些简单的动画需求一旦遇到相对复杂的动画效果,即超出了上述4种动画效果,那么补间动画则无法实现。功能和可扩展性具有局限性。
属性动画优点: 属性动画是在Android3.0(API 11)后才提供的一种全新动画模式。
- 作用对象是任何一个Object对象 也就是说我们完全可以给任意Object对象设置属性动画,而这个对象可以不是一个View组件,也不管这个对象是否是可见的,而视图动画的作用对象只能是一个View对象,这是最大的不同;
- 实际改变了View对象的属性 视图动画的一个致命缺陷就是,通过视图动画将一个View对象(比如一个TextView,Button)位置改编后,该对象的触摸事件的焦点依然在原位置,属性动画就很好的解决了这一缺陷;
- 功能与可扩展性强
- 可以控制动画执行过程中的任意时刻的任意属性值 视图动画从本质上来说是一种补间动画,他只对动画的起始值和结束值进行赋值,属性动画就提供了很好地解决方案,就是自定义估值器控制动画执行过程中的属性值
视图动画优点:
- 当我们把动画的repeatCount设置为无限循环时,如果在Activity退出时没有及时将动画停止,属性动画会导致Activity无法释放而导致内存泄漏,而补间动画却没有问题。因此,使用属性动画时切记在Activity执行 onStop 方法时顺便将动画停止。
- xml 文件实现的补间动画,复用率极高。在Activity切换,窗口弹出时等情景中有着很好的效果。
(4)插值器与估值器
4.1插值器
(1)简介
定义:一个接口。 作用:设置 属性值 从初始值过渡到结束值 的变化规律,如匀速、加速 & 减速 等等,即确定了 动画效果变化的模式,如匀速变化、加速变化 等等。
(2)应用场景
实现非线性运动的动画效果。
(3)系统内置插值器类型
| 作用 | 资源ID | 对应Java类 |
|---|---|---|
| 动画加速进行 | @android:anim/accelerate_interpolator | AccelerateInterpolator |
| 先加速再减速 | @android:anim/accelerate_decelerate_interpolator | AccelerateDecelerateInterpolator |
| 弹球效果 | @android:anim/bounce_interpolator | BounceInterpolator |
| 周期运动 | @android:anim/cycle_interpolator | CycleInterpolator |
| 减速 | @android:anim/decelerate_interpolator | DecelerateInterpolator |
| 匀速 | @android:anim/linear_interpolator | LinearInterpolator |
| 系统默认的插值器是AccelerateDecelerateInterpolator,即先加速后减速 |
(4)自定义插值器
本质:根据动画的进度(0%-100%)计算出当前属性值改变的百分比 具体使用:自定义插值器需要实现 Interpolator / TimeInterpolator接口 & 复写getInterpolation() 补间动画 实现 Interpolator接口;属性动画实现TimeInterpolator接口;TimeInterpolator接口是属性动画中新增的,用于兼容Interpolator接口,这使得所有过去的Interpolator实现类都可以直接在属性动画使用。
// Interpolator接口
public interface Interpolator {
// 内部只有一个方法
float getInterpolation(float input) {
// 参数说明
// input值值变化范围是0-1,且随着动画进度(0% - 100% )均匀变化
// 即动画开始时,input值 = 0;动画结束时input = 1
// 而中间的值则是随着动画的进度(0% - 100%)在0到1之间均匀增加
...// 插值器的计算逻辑
return xxx;
// 返回的值就是用于估值器继续计算的fraction值,下面会详细说明
}
// TimeInterpolator接口
// 同上
public interface TimeInterpolator {
float getInterpolation(float input);
}
系统内置插值器源码:匀速插值器LinearInterpolator、先加速再减速插值器AccelerateDecelerateInterpolator
// 匀速差值器:LinearInterpolator
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
// 仅贴出关键代码
...
public float getInterpolation(float input) {
return input;
// 没有对input值进行任何逻辑处理,直接返回
// 即input值 = fraction值
// 因为input值是匀速增加的,因此fraction值也是匀速增加的,所以动画的运动情况也是匀速的,所以是匀速插值器
}
// 先加速再减速 差值器:AccelerateDecelerateInterpolator
@HasNativeInterpolator
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {
// 仅贴出关键代码
...
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
// input的运算逻辑如下:
// 使用了余弦函数,因input的取值范围是0到1,那么cos函数中的取值范围就是π到2π。
// 而cos(π)的结果是-1,cos(2π)的结果是1
// 所以该值除以2加上0.5后,getInterpolation()方法最终返回的结果值还是在0到1之间。只不过经过了余弦运算之后,最终的结果不再是匀速增加的了,而是经历了一个先加速后减速的过程
// 所以最终,fraction值 = 运算后的值 = 先加速后减速
// 所以该差值器是先加速再减速的
}
}
自定义插值器的关键在于:对input值 根据动画的进度(0%-100%)通过逻辑计算 计算出当前属性值改变的百分比。(input取值从0~1)
(5)实例
效果:写一个自定义Interpolator:先减速后加速 步骤1:根据需求实现Interpolator接口 DecelerateAccelerateInterpolator.java
public class DecelerateAccelerateInterpolator implements TimeInterpolator {
@Override
public float getInterpolation(float input) {
float result;
if (input <= 0.5) {
result = (float) (Math.sin(Math.PI * input)) / 2;
// 使用正弦函数来实现先减速后加速的功能,逻辑如下:
// 因为正弦函数初始弧度变化值非常大,刚好和余弦函数是相反的
// 随着弧度的增加,正弦函数的变化值也会逐渐变小,这样也就实现了减速的效果。
// 当弧度大于π/2之后,整个过程相反了过来,现在正弦函数的弧度变化值非常小,渐渐随着弧度继续增加,变化值越来越大,弧度到π时结束,这样从0过度到π,也就实现了先减速后加速的效果
} else {
result = (float) (2 - Math.sin(Math.PI * input)) / 2;
}
return result;
// 返回的result值 = 随着动画进度呈先减速后加速的变化趋势
}
}
步骤2:设置插值器 MainActivity.java
mButton = (Button) findViewById(R.id.Button);
// 创建动画作用对象:此处以Button为例
float curTranslationX = mButton.getTranslationX();
// 获得当前按钮的位置
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);
// 创建动画对象 & 设置动画效果
// 表示的是:
// 动画作用对象是mButton
// 动画作用的对象的属性是X轴平移
// 动画效果是:从当前位置平移到 x=1500 再平移到初始位置
animator.setDuration(5000);
animator.setInterpolator(new DecelerateAccelerateInterpolator());
// 设置插值器
animator.start();
// 启动动画
4.2估值器
(1)简介
设置属性值从初始值过渡到结束值的变化具体数值的一个接口,用于决定值的变化规律(如匀速、加速等),是属性动画特有的属性。
(2)应用场景
协助插值器实现非线性运动的动画效果。
(3)系统内置估值器类型
| 插值器 | 说明 |
|---|---|
| IntEvaluator | 以整型的形式从初始值 - 结束值 进行过渡 |
| FloatEvaluator | 以浮点型的形式从初始值 - 结束值 进行过渡 |
| ArgbEvaluator | 以Argb类型的形式从初始值 - 结束值 进行过渡 |
(4)自定义估值器
本质:根据 插值器计算出当前属性值改变的百分比 & 初始值 & 结束值 来计算 当前属性具体的数值
如:动画进行了50%(初始值=100,结束值=200),那么匀速插值器计算出了当前属性值改变的百分比是50%,那么估值器则负责计算当前属性值 = 100 + (200-100)x50% =150.
具体使用:自定义估值器需要实现 TypeEvaluator接口 & 复写evaluate()
public interface TypeEvaluator {
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 参数说明
// fraction:插值器getInterpolation()的返回值
// startValue:动画的初始值
// endValue:动画的结束值
....// 估值器的计算逻辑
return xxx;
// 赋给动画属性的具体数值
// 使用反射机制改变属性变化
// 特别注意
// 那么插值器的input值 和 估值器fraction有什么关系呢?
// 答:input的值决定了fraction的值:input值经过计算后传入到插值器的getInterpolation(),然后通过实现getInterpolation()中的逻辑算法,根据input值来计算出一个返回值,而这个返回值就是fraction了
}
}
在学习自定义插值器前,我们先来看一个已经实现好的系统内置差值器:浮点型插值器:FloatEvaluator
public class FloatEvaluator implements TypeEvaluator {
// FloatEvaluator实现了TypeEvaluator接口
// 重写evaluate()
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 参数说明
// fraction:表示动画完成度(根据它来计算当前动画的值)
// startValue、endValue:动画的初始值和结束值
float startFloat = ((Number) startValue).floatValue();
return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
// 初始值 过渡 到结束值 的算法是:
// 1. 用结束值减去初始值,算出它们之间的差值
// 2. 用上述差值乘以fraction系数
// 3. 再加上初始值,就得到当前动画的值
}
}
属性动画中的ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具备系统内置的估值器,即FloatEvaluator & IntEvaluator 即系统已经默认实现了 如何从初始值 过渡到 结束值 的逻辑 但对于ValueAnimator.ofObject(),从上面的工作原理可以看出并没有系统默认实现,因为对对象的动画操作复杂 & 多样,系统无法知道如何从初始对象过度到结束对象 因此,对于ValueAnimator.ofObject(),我们需自定义估值器(TypeEvaluator)来告知系统如何进行从 初始对象 过渡到 结束对象的逻辑 自定义实现的逻辑如下
// 实现TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{
// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 参数说明
// fraction:表示动画完成度(根据它来计算当前动画的值)
// startValue、endValue:动画的初始值和结束值
... // 写入对象动画过渡的逻辑
return value;
// 返回对象动画过渡的逻辑计算后的值
}
(5)实例
自定义TypeEvaluator接口并通过ValueAnimator.ofObject()实现动画效果。实现的动画效果:一个圆从一个点 移动到 另外一个点 实现TypeEvaluator接口的目的是自定义如何 从初始点坐标 过渡 到结束点坐标;本例实现的是一个从左上角到右下角的坐标过渡逻辑。
PointEvaluator.java
// 实现TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {
// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 将动画初始值startValue 和 动画结束值endValue 强制类型转换成Point对象
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
// 根据fraction来计算当前动画的x和y的值
float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
// 将计算后的坐标封装到一个新的Point对象中并返回
Point point = new Point(x, y);
return point;
}
}
(七)Android事件分发机制
1、什么叫事件分发机制
Android上面的View是树形结构,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢?为了解决这个问题,就有了事件分发机制。 事件分发是:当发生了一个事件时,在屏幕上找到一个合适的控件来处理这个事件的过程。 其实事件分发的本质将点击屏幕产生的MotionEvent对象传递到某个具体的View然后处理消耗这个事件的整个过程。
2、常见事件
当用户点击屏幕里View或者ViewGroup的时候,将会产生一个事件对象,这个事件对象就是MotionEvent对象,这个对象记录了事件的类型,触摸的位置,以及触摸的时间等。MotionEvent里面定义了事件的类型,其实很容易理解,因为用户可以在屏幕触摸,滑动,离开屏幕动作,分别对应MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_UP;
ACTION_DOWN:手指 初次接触到屏幕 时触发。 ACTION_MOVE:手指 在屏幕上滑动 时触发,会会多次触发。 ACTION_UP:手指 离开屏幕 时触发。 ACTION_CANCEL:事件 被上层拦截 时触发。
因此用户在触摸屏幕到离开屏幕会产生一系列事件,ACTION _ DOWN->ACTION _ MOVE(0个或者多个)->ACTION _ UP,那么ACTION _ CANCEL事件是怎么回事呢?请看下面的图你就懂的更彻底了:
cancel的理解:
当控件收到前驱事件(什么叫前驱事件?一个从DOWN一直到UP的所有事件组合称为完整的手势,中间的任意一次事件对于下一个事件而言就是它的前驱事件)之后,后面的事件如果被父控件拦截,那么当前控件就会收到一个CANCEL事件,并且把这个事件会传递给它的子事件。(注意:这里如果在控件的onInterceptTouchEvent中拦截掉CANCEL事件是无效的,它仍然会把这个事件传给它的子控件)之后这个手势所有的事件将全部拦截,也就是说这个事件对于当前控件和它的子控件而言已经结束了。
简单的理解产生Cancel事件的条件就是:
父View收到ACTION_DOWN,如果没有拦截事件,则ACTION_DOWN前驱事件被子视图接收,父视图后续事件会发送到子View。
此时如果在父View中拦截ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。
来个例子,我们知道ViewPager如何用户在A页滑动到B页,滑动到不及一半的位置,那么ViewPager就会给用户回退到A页,这是ViewPager的Cancel事件处理的。
ViewPager的onTouchEvent对ACTION_CANCEL的处理:
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
scrollToItem(mCurItem, true, 0, false);
mActivePointerId = INVALID_POINTER;
endDrag();
needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
}
break;
拿ViewPager来说,在ScrollView包含ViewPager的情况下,对ViewPager做左右滑动,滑到一页的一半时往上下滑,ViewPager收到MotionEvent.ACTION_CANCEL后就能够回到先前那一页,而不是停在中间。
3、Android事件分发方法
(1)dispatchTouchEvent 用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件。 (2)onInterceptTouchEvent 在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,返回结果表示是否拦截当前事件。如果当前View拦截了某个事件,则交给onTouchEvent继续处理。并且同一个事件序列当中,此方法不会被再次调用。 (3)onTouchEvent 同样也会在dispatchTouchEvent内部调用,用来处理Touch事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
Android事件分发由三个方法完成:
// 父View调用dispatchTouchEvent()开始分发事件
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
// 父View决定是否拦截事件
if(onInterceptTouchEvent(event)){
// 父View调用onTouchEvent(event)消费事件,如果该方法返回true,表示
// 该View消费了该事件,后续该事件序列的事件(Down、Move、Up)将不会在传递
// 该其他View。
consume = onTouchEvent(event);
}else{
// 调用子View的dispatchTouchEvent(event)方法继续分发事件
consume = child.dispatchTouchEvent(event);
}
return consume;
}
通过上述伪代码,我们可以得知点击事件的传递规则:对于一个根ViewGroup而言,点击事件产生后,首先会传递给它,这时它的dispatchTouch就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前的事件,接着事件就会交给这个ViewGroup处理,即它的onTouch方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么当前View的onTouchEvent方法不会被调用。由此可见,给View设置的onTouchListener的优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener,其优先级最低,即处于事件传递的尾端。
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View,即事件总数先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理, 即Activity的onTouchEvent方法会被调用。这个过程其实很好理解,我们可以换一种思路,假设点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那就只能交给水平更高的上级解决(上级的onTouchEvent被调用),如果上级再搞不定,那就只能交给上级的上级去解决,就这样难题一层层地向上抛,这是公司内部一种常见的处理问题的过程。
4、屏幕控件
(1)布局结构
<com.xyl.touchevent.test.RootView>
<com.xyl.touchevent.test.ViewGroupA>
<com.xyl.touchevent.test.View1/>
</com.xyl.touchevent.test.ViewGroupA>
<com.xyl.touchevent.test.View2/>
</com.xyl.touchevent.test.RootView>
(2)View树
5、事件分发流程
在如上图View的树形结构中,事件发生时,最先由Activity接收,然后再一层层的向下层传递,直到找到合适的处理控件。大致如下: ACTIVITY -> PHONEWINDOW -> DECORVIEW -> VIEWGROUP -> … -> VIEW 但是如果事件传递到最后的View还是没有找到合适的View消费事件,那么事件就会向相反的方向传递,最终传递给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃: ACTIVITY <- PHONEWINDOW <- DECORVIEW <- VIEWGROUP <- … <- VIEW
6、Android中事件具体分发流程
7、总结
(1)对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父View的onTouchEvent方法。 (2)ViewGroup 想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true 把事件拦截下来。ViewGroup 的拦截器onInterceptTouchEvent 默认是不拦截的,所以return super.onInterceptTouchEvent()=return false; (3)View 没有拦截器,为了让View可以把事件分发给自己的onTouchEvent,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent。 (4)View和ViewGroup相关事件分发回调方法:
- 【dispatchTouchEvent】(View&&ViewGroup) 事件分发,那么这个事件可能分发出去的四个目标(注:------> 后面代表事件目标需要怎么做。) 1、 自己消费,终结传递。------->return true ; 2、 给自己的onTouchEvent处理-------> 调用super.dispatchTouchEvent()系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。 3、 传给子View------>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子类。 4、 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent------->return false; 注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发,View的事件分发没有上面提到的4个目标的第3点。
- 【onTouchEvent】(View&&ViewGroup) 事件处理的,那么这个事件只能有两个处理方式: 1、自己消费掉,事件终结,不再传给谁----->return true; 2、继续从下往上传,不消费事件,让父View也能收到到这个事件----->return false;View的默认实现是不消费的。所以super==false。
- 【onInterceptTouchEvent】(ViewGroup) 对于事件有两种情况: 1、拦截下来,给自己的onTouchEvent处理--->return true; 2、不拦截,把事件往下传给子View---->return false,ViewGroup默认是不拦截的,所以super==false;
8、几点注意
(1)同一见事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以down开始,中间含有数量不定的move事件,最终以up事件结束。 (2)正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某个事件,那么同一个事件序列的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。 (3)某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否拦截了。 (4)某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一件序列中的其他事件都不会再交给它处理,并且事件 将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短时间内上级就不敢再把事件交给这个程序员做了,二者是类似的道理。 (5)如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。 (6)ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。 (7)View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用。 (8)View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。 (9)View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。 (10)onClick会发生的前提是当前View是可点击的,并且它接收到了down和up事件。 (11)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
9、事件分发中的onTouch 和onTouchEvent 有什么区别,又该如何使用?
(1)重写onTouchEvent重写onTouchEvent()处理ACTION_MOVE/DOWN/UP事件
public class TestButton extends Button {
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean value = super.onTouchEvent(event);
System.out.println("super.onTouchEvent: " + value+ " event: " + event.getAction());
return value;
}
(2)实现onTouchListener接口重写onTouch()处理ACTION_MOVE/DOWN/UP事件
class OnTouchListenerTest implements View.OnTouchListener{
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
}
TestButton b = (TestButton)findViewById(R.id.button);
OnTouchListenerTest listener = new OnTouchListenerTest();
b.setOnTouchListener(listener);
(3)onTouchEvent与onTouch监听区别 1、源码对于View中dispatchTouchEvent实现
public boolean dispatchTouchEvent(MotionEvent event){
... ...
if(onFilterTouchEventForSecurity(event)){
ListenerInfo li = mListenerInfo;
if(li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if(onTouchEvent(event)){
return true;
}
}
... ...
return false;
}
2、总结 (1)onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发。 (2)假如onTouch方法返回false会接着触发onTouchEvent,反之onTouchEvent方法不会被调用。 (3)控件内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。
(八)View的刷新机制
1、invalidate()流程图
invalidate主要给需要重绘的视图添加DIRTY标记,并通过不断回溯父视图做矩形运算求得真正需要绘制的区域,并最终保存在ViewRoot中的mDirty变量中,最后调用scheduleTraversals发起重绘请求,scheduleTraversals会发送一个异步消息,最终调用performTraversals()执行重绘(performTraversals()遍历所有相关联的 View ,触发它们的 onDraw 方法进行绘制)
2、源码分析
1、子View需要刷新时,调用invalidate,通知父View完成——首先找到自己父View(View的成员变量mParent记录自己的父View),然后将AttachInfo中保存的信息告诉父View刷新自己。
void invalidate(boolean invalidateCache) {
final AttachInfo ai = mAttachInfo;//AttachInfo是在View第一次attach到Window时,ViewRoot传给自己的子View的
final ViewParent p = mParent;//父View
//对于开启硬件加速的应用程序,则调用父视图的invalidateChild函数绘制整个区域,
// 否则只绘制dirty区域(r变量所指的区域),这是一个向上回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集。
if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
if (p != null && ai != null && ai.mHardwareAccelerated) {
p.invalidateChild(this, null);
return;
}
}
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop);
p.invalidateChild(this, r);
}
}
2、在invalidate中,调用父View的invalidateChild,这是一个从第向上回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集。
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
final int[] location = attachInfo.mInvalidateChildLocation;
// 需要刷新的子View的位置
location[CHILD_LEFT_INDEX] = child.mLeft;
location[CHILD_TOP_INDEX] = child.mTop;
do {
//不断的将子视图的dirty区域与父视图做运算来确定最终要重绘的dirty区域,
// 最终循环到ViewRoot(ViewRoot的parent为null)为止,并将dirty区域保存到ViewRoot的mDirty变量中
...
parent = parent.invalidateChildInParent(location, dirty);
} while (parent != null);
}
}
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
//运算,计算最终需要重绘的dirty区域
...
}
3、向上回溯的过程直到ViewRoot那里结束,由ViewRoot对这个最终的刷新区域重新绘制performTraversals。
public void invalidateChild(View child, Rect dirty) {
scheduleTraversals();
}
3、invalidate和postInvalidate
1、Invalidate()方法不能放在线程中,所以需要把Invalidate()方法放在Handler中。在MyThread中只需要在规定时间内发送一个Message给handler,当Handler接收到消息就调用Invalidate()方法。postInvalidate()方法就可以放在线程中做处理,就不需要Handler(postInvalidate 最终通过 Handler 切换到主线程,调用 invalidate) 2、Invalidate()方法和postInvalidate()都可以在主线程中调用而刷新视图。