Android的动画分为三类:View动画,帧动画,属性动画。
View动画的作用对象是View,它支持四种效果,分别是平移,缩放,旋转和透明度动画。除了这四种典型的变换效果外,帧动画也属于View动画,但是帧动画的表现形式和上面的四种变换效果不太一样。
View动画的四种变换效果对应着Animation的四个子类:TranslateAnimation,ScaleAnimation,RotateAnimation和AlphaAnimation。这四种动画既可以通过XML来定义,也可以通过代码动态创建,对于View动画来说,建议采用XML来定义动画,这是因为XML格式的动画可读性更好。
名称 | 标签 | 子类 | 效果 |
---|---|---|---|
平移动画 | TranslateAnimation | 移动View | |
缩放动画 | ScaleAnimation | 放大或缩小View | |
旋转动画 | RotateAnimation | 旋转View | |
透明度动画 | AlphaAnimation | 改变View的透明度 |
要使用View动画,首先要创建动画的XML文件,这个文件的路径为:res/anim/filename.xml。View动画的描述文件是由固定的语法的,如下:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator=""
android:shareInterpolator="false">
<alpha
android:fromAlpha=""
android:toAlpha=""/>
<scale
android:fromXScale=""
android:fromYScale=""
android:pivotX=""
android:pivotY=""
android:toXScale=""
android:toYScale=""/>
<translate
android:fromXDelta=""
android:fromYDelta=""
android:toXDelta=""
android:toYDelta=""/>
<rotate
android:fromDegrees=""
android:toDegrees=""
android:pivotY=""
android:pivotX=""/>
set>
从上面的语法看,View动画既可以是单个动画,也可以由一系列动画组成。
表示动画集合,对应AnimationSet类,它可以包含若干个动画,并且它的内部也是可以嵌套其他动画集合的,它的两个属性的含义如下:
android:interpolator
表示动画集合所采用的插值器,插值器影响动画的速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。这个属性可以不指定,默认为@android:anim/accelerate_decelerate_interpolator,即加速减速插值器。
android :shareInterpolator
表示集合中的动画是否和集合共享一个插值器。如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或者使用默认值。
表示平移动画,对应TranslateAnimation类,它可以使一个View在水平和竖直方向完成平移的动画效果,它的一系列属性的含义如下:
表示缩放动画,对应ScaleAnimation,它可以使View具有放大或者缩小的动画效果,他的一系列属性的含义如下:
表示旋转动画,对应RotateAnimation,它可以使View具有旋转的动画效果,它的属性含义如下:
表示透明度动画,对应AlphaAnimation,它可以改变View的透明度,它的属性含义如下:
View动画还有一些常用的属性:
帧动画是顺序播放一组预先定义好的图片,类似于电影播放。不同于View动画,系统提供了另外一个类AnimationDrawable来使用帧动画。帧动画的使用比较简单,首先需要通过XML定义一个AnimationDrawable,如下:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/image1" android:duration="500"/>
<item android:drawable="@drawable/image2" android:duration="500"/>
<item android:drawable="@drawable/image3" android:duration="500"/>
animation-list>
然后将上述的Drawable作为View的背景并通过Drawable来播放动画即可:
Button button = findViewById(R.id.button);
button.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable drawable = (AnimationDrawable)button.getBackground();
drawable.start();
帧动画的使用比较简单,但是比较容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。
LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当他的子元素出场时都会有这样的动画效果。这种效果常常被用在ListView。LayoutAnimation也是一个View动画,为了给ViewGroup的子元素加上出场效果,步骤如下:
//res/anim/anim_layout.xml
<layoutAnimation
xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.5"
android:animationOrder="normal"
android:animation="@anim/anim_item"/>
它的属性含义如下:
android:delay
表示子元素开始动画的事件延迟,比如子元素入场动画的时间周期为300ms,那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。总体来说,第一个子元素延迟150ms开始播放入场动画,第2个子元素延迟300ms开始播放入场动画,以此类推。
android:animationOrder
表示子元素动画的顺序,有三种选项:normal,reverse和random,其中normal表示顺序显示,即排在前面的子元素先开始播放入场动画;reverse表示逆向显示,即排在后面的子元素先开始播放入场动画;random则是随机播放入场动画。
android:animation
为子元素指定具体的入场动画
//res/anim/anim_item.xml
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_interpolator"
android:shareIntrepolator="true">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"/>
<translate
android:fromXDelta="500"
android:toXDelta="0"/>
set>
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/anim_layout"
android:background="#fff4f7f9"
android:cacheColorHint="#00000000"
android:divider="#dddbdb"
android:dividerHeight="1.0px"
android:listSelector="@android:color/transparent"/>
除了在XML中指定LayoutAnimation外,还可以通过LayoutAnimationController来实现,具体代码如下;
ListView listView = findViewById(R.id.list);
Animation animation = AnimationUtils.loadAnimation(this,R.anim.anim_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5);
controller.setOrder(LayoutAnimationController.OREDER_NORMAL);
listView.setLayoutAnimation(controller);
Activity有默认的切换效果,但是这个效果我们是可以自定义的,主要用到overridePendingTransition(int enterAnim,int exitAnim)这个方法,这个方法必须在startActivity(Intent)或finish()之后调用才能生效,它的参数含义如下:
当启动一个Activity时,可以按照如下方式为其添加自定义的切换效果:
Intent intent = new Intent(this,TestActivity.class);
startActivity(intent);
overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);
当Activity退出时,也可以为其指定自己的切换效果,如下:
@Override
public void finish(){
super.finish();
overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);
}
属性动画中有ValueAnimator,ObjectAnimator和AnimatorSet等概念。
属性动画常用的几个动画类是:ValueAnimator,ObjectAnimator和AnimatorSet,其中ObjectAnimator继承自ValueAnimator,AnimatorSet是动画集合。
改变一个对象(myObject)的translationY属性,让其沿着Y轴向上平移一段距离:它的高度,该动画在默认时间内完成,动画的完成时间是可以自定义的。
ObjectAnimator.ofFloat(myObject,"translationY",0,-myObject.geteight()).start();
改变一个对象的背景色属性,典型的情形是改变View的背景色,下面的动画可以让背景色吧在三秒内实现从0xFFFF8080到0xFF8080FF的渐变,动画会无限循环而且会有反转的效果。
ValueAnimator animator = ObjectAnimator.ofInt(this,"backgroundColor",0xFFFF8080,OxFF8080FF);
animator.setDuration(3000);
animator.setEvaluator(new ArgbEvaluator());
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.start()
动画集合,5秒内对View的旋转,平移,缩放,透明度都进行了改变。
AnimatorSet set = new AnimatorSet();
set.platTogether{
ObjectAnimator.ofFloat(myView,"rotationX",0,360);
ObjectAnimator.ofFloat(myView,"rotationY",0,180);
ObjectAnimator.ofFloat(myView,"rotation",0,-90);
ObjectAnimator.ofFloat(myView,"translationX",0,90);
ObjectAnimator.ofFloat(myView,"translationY",0,90);
ObjectAnimator.ofFloat(myView,"scaleX",1,1.5f);
ObjectAnimator.ofFloat(myView,"scaleY",1,0.5f);
ObjectAnimator.ofFloat(myView,"alpha",1,0.25f,1);
};
set.setDuration(5*1000).start();
属性动画除了通过代码实现以外,还可以通过XML来定义。属性动画需要定义在res/animator/目录下,它的语法如下所示:
set>
属性动画的各种参数都比较好理解,在XML中可以定义ValueAnimator,ObjectAnimator以及AnimatorSet,其中
对于
对于一个动画来说,有两个属性需要特殊说明一下,一个是android:repeatCount,它表示动画循环的次数,默认值是0,其中-1表示无限循环;另一个是android:repeatMode,它表示动画的循环模式,有两个选项:“restart”和“reverse”,分别表示连续重复和逆向重复。连续重复比较好理解,就是动画每次都从头开始播放,而逆向重复是指第一次播放完以后,第二次会倒着播放动画,第三次再从头开始播放动画,第四次再倒着播放,如此反复。
例子:
//res/animtor/property_animator.xml
<set android:ordering="together">
<objectAnimator
android:propertyName="x"
android:duration="300"
android:valueTo="200"
android:valueType="intType"/>
<objectAnimator
android:propertyName="y"
android:duration="300"
android:valueTo="300"
android:valueType="intType"/>
set>
使用上面的动画:
AnimationSet set = (AnimatorSet)AnimatorInflater.loadAnimator(context,R.animator.property_animtor);
set.setTarget(mButton);
set.start();
TimeInterpolator中文翻译为时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器:匀速动画),AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢)等。TypeEvaluator的中文翻译为类型估值算法,也叫估值器,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有IntEvaluator(针对整型属性),FloatEvaluator(针对浮点型属性)和ArgbEvaluator(针对Color属性)。属性动画中的插值器和估值器很重要,它们是实现非匀速动画的重要手段。
如图所示,它表示一个匀速动画,采用了线性插值器和整型估值算法,在40ms内,veiw的x属性从0到40的变换。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3OsWArUc-1597305565435)(C:\Users\NewBeats\AppData\Roaming\Typora\typora-user-images\1597230889632.png)]
由于动画的刷新率是10ms/帧,所以该动画将分为5帧进行,我们来考虑第三帧(x = 20,t = 20ms),当时间t = 20ms时,时间流逝的百分比是0.5(20/40 = 0.5),意味着现在时间过了一半,那x应该改变多少呢?这个时候就由插值器和估值算法来确定。拿线性插值器来说,当时间流逝一半的时候,x的变换也应该是一半,即x = 0.5,为什么呢?因为它是线性插值器,匀速动画。下面是它的源码:
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return input;
}
}
很显然,线性插值器的返回值和输入值一样,因此插值器返回的值是0.5,这意味着x的改变是0.5,这个时候插值器的工作就完成了。具体x变成了什么值,这个需要估值算法来确定,我们来看看整型估值算法的源码:
public class IntEvaluator implements TypeEvaluator<Integer> {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
上述算法很简单,evaluator的三个参数分别表示估值小数,开始值和结束值,对应于上面的例子就分别是0.5,0,40.根据上述算法,整型估值返回给我们的结果是20,这就是(x=20,t=20ms)的由来。
属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorListener和AnimatorUpdateListener。
AnimatorListener的定义如下:
public static interface AnimatorListener {
default void onAnimationStart(Animator animation, boolean isReverse) {
onAnimationStart(animation);
}
default void onAnimationEnd(Animator animation, boolean isReverse) {
onAnimationEnd(animation);
}
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}
从AnimatorListener的定义可以看出,它可以监听动画的开始,结束,取消以及重复播放。
系统提供了AnimatorUpdateListener类,这个类是AnimatorListener的适配类,这样我们就可以有选择的实现上面的四个方法了。
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
AnimatorUpdateListener比较特殊,他会监听动画的整个过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate就会被调用一次。
这里先提出一个问题:给Button加一个动画,让这个Button的宽度从当前宽度增加到500px。但是View动画根本不支持对宽度进行动画。View动画只支持四种类型:平移(Translate),旋转(Rotate),缩放(Scale),透明度(Alpha)。当然用x方向上的缩放可以让Button在x方向放大,看起来好像是宽度增加了,实际上不是,只是Button被放大了而已,而且由于只x方向上被放大,这个时候Button的背景以及上面的文本都被拉伸了,甚至有可能Button会超出屏幕。
再来试试属性动画,如下:
ObjectAnimator.onInt(mButton,"width",500).setDuration(5000).start();
上述代码运行后发现没有效果。
下面分析属性动画的原理:属性动画要求动画作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对object的属性abc做动画,如果想要动画生效,要同时满足两个条件:
以上条件缺一不可。那么我们对Button的width属性做动画为什么会没有效果?这是因为Button内部虽然提供了getWidth和setWidth方法,但是这个setWidth方法并不是改变视图的大小,它是TextView新添加的方法,View是没有这个setWidth方法的,由于Button继承了TextView,所以Button也就有了setWidth方法。下面是setWidth和getWidth方法的源码:
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
public final int getWidth() {
return mRight - mLeft;
}
从上述源码可以看出,getWidth的确是获取View的宽度的,而setWidth是TextView及其子类的专属方法,它的作用不是设置View的宽度,而是设置TextView的最大宽度和最小宽度的,这个和TextView的宽度不是一个东西。具体来说,TextView的宽度对应XML中的android:layout_width
属性,而TextView还有一个属性android:width
,这个属性就对应了TextView的setWidth方法。总之,TextView和Button的setWidth和getWidth针对的不是同一属性,通过setWidth无法改变控件的宽度,所以对width做属性动画没有效果。
针对上述问题,官方文档告诉我们有3种解决方法:
给你的对象加上get和set方法,如果你有权限的话
这个意思很好理解,如果你有权限的话,加上get和set就搞定了。但是很多时候我们没有权限这么做。比如上面对于width的问题,你无法给Button加上一个合乎要求的setWidth方法,因为这是Android SDK内部实现的。这个方法最简单,但是往往是不可行的。
用一个类来包装原对象,间接为其提供get和set方法
这是一个很有用的解决方法,因为用起来方便,也很好理解,如下:
private void performAnimate(){
ViewWrapper wrapper = new ViewWrapper(mButton);
ObjectAnimator.ofInt(wrapper,"width",wrapper.getWidth(),500).setDuration(5000).start();
}
public void onClick(View v){
if(v == mButton){
performAnimate();
}
}
private static class ViewWrapper{
private View mTarget;
public ViewWrapper(View target){
mTarget = target;
}
public int getWidth(){
return mTarget.getLayoutParams().width;
}
public void setWidth(int width){
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}
上述代码在5s内让Button的宽度增加到了500px,为了达到这个效果,我们提供了ViewWrapper类专门用于包装View,具体本例是包装Button。然后我们对ViewWrapper的width属性做动画,并且在setWidth方法中修改其内部的target的宽度,而target实际上就是我们包装的Button。
采用ValueAnimator,监听动画过程,自己实现属性的改变
首先说说什么是ValueAnimator,ValueAnimator本身不作用于任何对象,也就是说直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们的对象的属性值,这样也就相当于我们的对象做了动画。如下:
private void performAnimate(final View target,final int start,final int end){
ValueAnimator valueAnimator = ValueAnimator.ofInt(1,100);
valueAnimator.addUpdateListener(new AnimatorUpdateListener(){
private IntEvaluator mEvaluator = new IntEvaluator();
public void onAnimationUpdate(ValueAnimator animator){
//获得当前动画的进度值,整型,1-100
int currentValue = (Integer)animator.getAnimatedValue();
//获得当前进度占整个动画的过程的比例,浮点型,0-1
float fraction = animator.getAnimatedFraction();
//直接调用整型估值器,通过比例计算出宽度,然后再设给Button
target.getLayoutParams().width = mEvaluator.evaluate(fraction,start,end);
target.requestLayout();
}
});
valueAnimator.setDuration(5000).start();
}
上述代码的效果图和采用ViewWrapper是一样的。