一、前言
移动客户端的一个特点就是炫酷的界面,而Android中的界面都是View,所以要想实现性能高且好看的界面,就必须需要学会自定义VIew。动画机制操作的对象就是View,动画是自定义View的一个技术点。简而言之,Android中的动画分为两大类。
第一类:称之为视图动画。
- Tween Animation:补间动画,通过平移(TranslateAnimation)、缩放(ScaleAnimation)、旋转(RotateAnimation)、透明度(AlphaAnimation)四个子类实现的动画,或是其相对应的XML方式实现的动画称之为补间动画。补间动画只需设定初始状态和结束状态,中间的状态有系统计算而得出的。
- Frame Animation:逐帧动画,即按序播放一组预先定义好的图片,实现起来比较简单,但大量的图片和快速的切换会导致oom内存溢出。
第二类:称之为属性动画。
顾名思义,属性动画即更改控件的View属性来完成,而补间动画是更改外在内容位置。
- ValueAnimator:ValueAnimator更像一个Map,利用时间估值器获得当前的数值。根据这个数据再更新当前View的属性值。
- ObjectAnimation:ObjectAnimator是ValueAnimator的子类,ObjectAnimation中就可以直接控制View的属性。
二、视图动画
1)、补间动画
补件动画共有四种类型,分别是:
- TranslateAnimation 位移动画
- ScaleAnimation 缩放动画
- RotateAnimation 旋转动画
- AlphaAnimation 透明度改变动画
- AnimationSet 补间动画四种动画组合
下面以TranslateAnimation 动画为例举例:
①、XML方式实现
1、首先在res/anim中构建一个XML文件,采用R.anim.XXXX ID引用方式访问
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="200"
android:toYDelta="200"
android:duration="2000"
android:fillBefore="true">
</translate>
2、第二步,自然是加载动画,让View控件和Animation绑定
系统在AnimationUtils类中提供了public static Animation loadAnimation(Context context, @AnimRes int id)方法,用于加载XML动画。
loadAnimation中首先调用XML解析器将XML布局解析成Animation类。然后在控件中startAnimation,即可开启动画。
Animation animation = AnimationUtils.loadAnimation(this, R.anim.xxx);
// 开启动画
view.startAnimation(animation);
②、Java代码方式实现
代码方式和XML方式没有本质区别。唯一的不同点在于,Animation的来源不一样,一个来自XML布局,另一个在代码中写死。直接看下代码:
LinearLayout linearLayout = (LinearLayout)findViewById(R.id.linear);
final TranslateAnimation translateAnim =
new TranslateAnimation(Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 400
,Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 400);
translateAnim.setDuration(1000);
translateAnim.setFillAfter(true);
linearLayout.startAnimation(translateAnim);
通过构造函数,构造一个TranslateAnimation,也可以通过TranslateAnimation方法设置一些执行属性。
Animation- setDuration : 设置动画执行时间
- setInterpolator : 设置插值器,配合时间估值器,更改值
- setRepeatMode : 设置重复模式,是否立即停止
- setRepeatCount : 重复次数
- setFillBefore : 是否结束后,停在开始界面
- setFillAfter : 是否结束后,停在最后界面
- setAnimationListener : 设置动画监听器
问题:补间动画只是更改控件的内容
直接通过代码分析,通过Animation让TextView移动(400,400),再点击View现在的位置不会触发View的点击事件,而点击View原位置,会触发点击事件,弹出Toast。
补间动画并没有改变控件内部的属性值,因为动画是ParentView不断调整 ChildView 的画布坐标系来实现的。他没有改变layout摆放的位置,只是在原始的位置上,加上相应的偏移量实现的。故此点击最后的位置,不会被响应。
TextView textView = (TextView) findViewById(R.id.text);
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(v.getContext(),"Toast",Toast.LENGTH_SHORT).show();
}
});
final TranslateAnimation translateAnim =
new TranslateAnimation(Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 400
,Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 400);
translateAnim.setDuration(1000);
translateAnim.setFillAfter(true);
textView.startAnimation(translateAnim);
2)、逐帧动画
逐帧动画和补间动画的XML方式有点类似,但不同点是补间动画只需要设置一头一尾的状态,中间的状态是有系统计算的,而逐帧动画则每一帧都是一张图片。
用例:
1、先在XML设置好每一张图片的顺序和播放时间
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false" >
<!-- 定义一个动画帧,Drawable为img0,持续时间50毫秒 -->
<item android:drawable="@drawable/img0" android:duration="50" />
</animation-list>
2、将XML转换成AnimationDrawable对象,并设置给View即可(setBackgroundDrawable)
// 通过逐帧动画的资源文件获得AnimationDrawable示例
frameAnim=(AnimationDrawable) getResources().getDrawable(R.drawable.bullet_anim);
// 把AnimationDrawable设置为ImageView的背景
view.setBackgroundDrawable(frameAnim);
二、属性动画
1)、ValueAnimator
老规矩,直接看Demo
public class MainActivity extends AppCompatActivity {
private final static String TAG = MainActivity.class.getSimpleName();
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text);
//构造一个ValueAnimator对象
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 100);
// 动画持续时间
valueAnimator.setDuration(3000);
// 动画持续次数 -1代表一致重复
valueAnimator.setRepeatCount(-1);
// 动画重复模式 分为两种 从头开始 从尾开始
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
// 动画状态的监听
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.e(TAG, "onAnimationStart: " );
}
@Override
public void onAnimationEnd(Animator animation) {
Log.e(TAG, "onAnimationEnd: " );
}
@Override
public void onAnimationCancel(Animator animation) {
Log.e(TAG, "onAnimationCancel: " );
}
@Override
public void onAnimationRepeat(Animator animation) {
Log.e(TAG, "onAnimationRepeat: " );
}
});
// 动画数值更新回调接口,这个数值从0开始到100
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
textView.layout(value, value, value + textView.getWidth()
, value + textView.getHeight());
}
});
}
}
@Override
public void onAttachedToWindow() {
// 开始执行
valueAnimator.start();
}
@Override
public void onDetachedFromWindow() {
// 取消执行
valueAnimator.cancel();
}
从上面代码可以看出,用法类似于Animation。而ValueAnimatior的用法是在一定的时间内,匀速的回调当前时间的数值。对于上例可以理解为,加速度a = 100/3000。初始值为0,结束值为100。在t时刻,值为多少。
当然这个是有点特别,没有插值器。简单理解插值器就是改变加速度a的值得东西,其他的求法和用法都一致。
最终在AnimatorUpdateListener回调中,可以得到当前的值,根据这个值,再对View进行布局或者其他的操作。
两个回调:
在ValueAnimatior中有两个回调监听,一个是回调动画状态的,另一个是返回当前时刻的值得。
// 动画状态的监听
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.e(TAG, "onAnimationStart: " );
}
@Override
public void onAnimationEnd(Animator animation) {
Log.e(TAG, "onAnimationEnd: " );
}
@Override
public void onAnimationCancel(Animator animation) {
Log.e(TAG, "onAnimationCancel: " );
}
@Override
public void onAnimationRepeat(Animator animation) {
Log.e(TAG, "onAnimationRepeat: " );
}
});
// 动画数值更新回调接口,这个数值从0开始到100
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
textView.layout(value, value, value + textView.getWidth()
, value + textView.getHeight());
}
});
}
}
- valueAnimator.removeAllUpdateListeners();可以移除所有的监听对象。
- valueAnimator.pause(); 暂停动画 API 19 以上
- valueAnimator.isPaused(); 判断是否暂停中 API 19 以上
- valueAnimator.isRunning(); 判断是否正在运行
- valueAnimator.isStarted(); 判断是否调用start()
生成ValueAnimator对象
-
构造方法
一般不会使用无参构造方法
public ValueAnimator() {
} -
静态方法生成
主要有以下几类:
1、ValueAnimator.ofInt(float... values);
2、ValueAnimator.ofFloat(float... values);
3、ValueAnimator.ofArgb(float... values); API21以上才有
4、ValueAnimator.ofObject(TypeEvaluator evaluator, Object... values);
使用的方法不同,最终返回值得类型也不一致,但使用方法都是一样的。但重点说下ofArgb和ofObject。其中ofArgb是ofObject的一种特例。
先来看下ValueAnimator.ofArgb(float... values);
public static ValueAnimator ofArgb(int... values) {
//1、生成对象
ValueAnimator anim = new ValueAnimator();
// 2、存放关键值
anim.setIntValues(values);
// 3、设置计算器Evaluator
anim.setEvaluator(ArgbEvaluator.getInstance());
return anim;
}
其中 anim.setEvaluator(ArgbEvaluator.getInstance());是最关键的一部,他需要自定义TypeEvaluator
下面就是TypeEvaluator的接口,它是通过一定的规则,返回当前时间的值,回调给用户使用。
public interface TypeEvaluator<T> {
public T evaluate(float fraction, T startValue, T endValue);
}
下面看下ArgbEvaluator.getInstance()代码:
颜色是ARGB,为32位,每一个通道8bit。fraction代表当前的百分值,通过fraction算出当前准确的颜色并返回。其中fraction是根据你传入的时间和当前的时间和插值器算出的一个值。
public Object evaluate(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
ValueAnimator.ofObject(TypeEvaluator evaluator, Object... values);方法含有两部分,一个是传入TypeEvaluator ,另一个传入关键的数值。
2)、ObjectAnimator
* This subclass of {@link ValueAnimator} provides support for animating properties on target objects.
* The constructors of this class take parameters to define the target object that will be animated
* as well as the name of the property that will be animated. Appropriate set/get functions
* are then determined internally and the animation will call these functions as necessary to
* animate the property.
解释:ObjectAnimator的作用就是对target控件提供属性动画。他需要传递一个target和更改的属性值。且target中提供的属性,需要有对应的setXXX和getXXX方法,这样才能通过反射执行方法,并将值传递过去。最终更改target的属性。
ObjectAnimator是继承自ValueAnimator,所有的ValueAnimator中的可用方法和变量,在ObjectAnimator都可以用。
在ValueAnimator介绍中,可以得出ValueAnimator并不会直接的和View控件绑定,它只会根据时间消逝的进度,得到一个有用的当前时刻对应的值,用户需要自己根据此值做出相应的动作。所有ObjectAnimator就对此进行了完善,在ObjectAnimator中就可以控制View控件的动画。
①、ObjectAnimator例子
TextView textView = (TextView) findViewById(R.id.text);
ObjectAnimator objectAnimator = ObjectAnimator
.ofFloat(textView,"textSize",10,30);
objectAnimator.setDuration(10000);
objectAnimator.start();
上述的例子中,在10S内,View的TextSize从10到30匀速变化。其中textView就是target,textSize为需要更改的属性方法后后半截方法名。10,30是大小变化的范围。
最终的执行会到达PropertyValuesHolder中的setAnimatedValue方法。
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
// 反射执行该方法
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
②、生成ObjectAnimator对象
1、构造方法:
private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}
private <T> ObjectAnimator(T target, Property<T, ?> property) {
setTarget(target);
setProperty(property);
}
这两个构造方法,一个传入的是String类型的propertyName,另一个是Property对象。
现在就看下用自定义Property的demo,通过动画给一个Activity的设置背景颜色。
Property的类中提供了两个必须重写的方法set/get方法。从文档中可看出,传递propertyName、Property都行。但是有些情况下,对待一个私有的属性,并不能提供相应的get/set方法,就需要使用Property了,来达到移花接木的作用。既然自己不能对外提供接口,那再给个方式(Property),你重新我的接口就行了。
// Drawable
private ColorDrawable mBgDrawable;
/**
* 给背景设置一个动画
*
* @param endProgress 动画的结束进度
* @param endCallback 动画结束时触发
*/
private void startAnim(float endProgress, final Runnable endCallback) {
// 获取一个最终的颜色
int finalColor = Resource.Color.WHITE; // UiCompat.getColor(getResources(), R.color.white);
// 运算当前进度的颜色
ArgbEvaluator evaluator = new ArgbEvaluator();
// 得到endProgress时刻的颜色
int endColor = (int) evaluator.evaluate(endProgress, mBgDrawable.getColor(), finalColor);
// 构建一个属性动画
ValueAnimator valueAnimator = ObjectAnimator.ofObject(this, property, evaluator, endColor);
valueAnimator.setDuration(1000); // 时间
valueAnimator.setIntValues(mBgDrawable.getColor(), endColor); // 开始结束值
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// 结束时触发
endCallback.run();
}
});
valueAnimator.start();
}
// 自定义Property
private final Property<MainActivity, Object> property = new Property<MainActivity, Object>(Object.class, "color") {
@Override
public void set(MainActivity object, Object value) {
object.mBgDrawable.setColor((Integer) value);
}
@Override
public Object get(MainActivity object) {
return object.mBgDrawable.getColor();
}
};
2、ofXXX方式
除了继承ValueAnimator的方法外,ObjectAnimator还有一些自己定义好的。先介绍下参数:
- target :被作用的对象
- TypeEvaluator : 计算器,计算当期时刻的值
- TypeConverter :转换器,动画中的值类型和属性类型不同时。如属性是int,值类型是float
- PropertyValuesHolder : 动画需要的一些参数的封装类,最终都会生成该对象
着重看下PropertyValuesHolder
- 例子
生成旋转的动画rotationHolder和改变背景颜色的colorHolder的组合动画。最后再ObjectAnimator.ofPropertyValuesHolder(mTextView, rotationHolder, colorHolder);生成对应的ObjectAnimator的对象。
PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofFloat("Rotation", -20f, 20f, 10f, -10f, 0f);
PropertyValuesHolder colorHolder = PropertyValuesHolder.ofInt("BackgroundColor", 0xffffffff, 0xffff00ff, 0xffffffff);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTextView, rotationHolder, colorHolder);
animator.setDuration(1000);
animator.start();
- PropertyValuesHolder代码分析
String mPropertyName; // 属性名
protected Property mProperty; // 传入的mProperty对象
Method mSetter = null; // 通过反射执行的set方法Method
private Method mGetter = null;// 通过反射执行的get方法Method
private TypeEvaluator mEvaluator; // 传入的数值计算器
private Object mAnimatedValue; // 动画的数值
private TypeConverter mConverter; // 类型转换器
在PropertyValuesHolder里面,存储了用户传入的参数,并提供了相应的方法,如生成textSize属性的set/get方法,和执行该属性的反射方法。
三、属性动画原理
1)、属性动画流程
Value和ObjectAnimator流程- 第一步,传入相应的参数,如ObjectAnimator.ofFloat(textView,"textSize",10,30);生成一个ValueAnimator对象
- 第二步,根据传入的setDuration执行时间,和插值器策略一同算出当前时刻的百分比
- 第三步,根据计算出的fraction值,算出此刻的value是start和end中间的哪一个。
Value和ObjectAnimator前三步大体相似,后面轻微的不同。
- ValueAnimator是将算出的值通过用户注册的AnimatorUpdateListener接口回调给使用者
- ObjectAnimator是根据反射执行target中的set方法,并将当前值传递。在setXXX中可以重新绘制
2)、插值器
常见的插值器:
插值器种类 | 描述 |
---|---|
LinearInterpolator | 线性插入器,默认的插值器,匀速变化 |
BounceInterpolator | 动画结束的时候弹起 |
AccelerateInterpolator | 动画开始的地方速率改变比较慢,然后开始加速 |
DecelerateInterpolator | 开始的地方快然后慢 |
Android的动画中,即使没有设置Interpolator,也会有个默认的插值器LinearInterpolator。插值器会将时间变化率转化成fraction的一个规则。
所有的插值器都会继承自TimeInterpolator,它会返回在设置的执行时间内,此刻时间过去的百分比。
public interface TimeInterpolator {
/**
* Maps a value representing the elapsed fraction of an animation to a value that represents
* the interpolated fraction. This interpolated value is then multiplied by the change in
* value of an animation to derive the animated value at the current elapsed animation time.
*
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
float getInterpolation(float input);
}
LinearInterpolator代码
代码中他会将时间百分比直接的返回。自己可以自定义差值器,主要的工作就是根据传入的input,映射成为另外的一个float值,即可。
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
// 将传入的input值,原路返回,就是默认的、匀速的插值器了
public float getInterpolation(float input) {
return input;
}
}
网友评论