Android中動畫分為:View動畫、幀動畫(也屬于View動畫)、屬性動畫。
View動畫是對View做圖形變換(平移、縮放、旋轉、透明度)從而產生動畫效果。
幀動畫就是順序播放一系列圖片來產生動畫效果。
屬性動畫可以動態改變對象的屬性來達到動畫效果。
一、View動畫
View動畫的平移、縮放、旋轉、透明度 分別對應 Animation的的4個子類:TranslateAnimation、ScaleAnimation、RotateAnimation、AlphaAnimation。View可以用xml定義、也可以用代碼創建。推薦使用xml,可讀性好。
1.1 xml方式
如下所示,R.anim.animation_test 是xml定義的動畫。
其中標簽 translate、scale、alpha、rotate,就是對應四種動畫。set標簽是動畫集合,對應AnimationSet類,有多個動畫構成。
其中android:duration是指動畫時間,fillAfter為true是動畫結束后保持,false會回到初始狀態。interpolator是指動畫的執行速度,默認是先加速后減速。其他標簽及屬性較簡單可自行研究驗證。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="5000"
android:fillAfter="true"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
<!--set里面的duration如果有值,會覆蓋子標簽的duration-->
<translate
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="400" />
<scale
android:duration="2000"
android:fromXScale="0.5"
android:fromYScale="0.5"
android:toXScale="1"
android:toYScale="1" />
<alpha
android:duration="3000"
android:fromAlpha="0.2"
android:toAlpha="1" />
<rotate
android:fromDegrees="0"
android:toDegrees="90" />
</set>
定義好動畫后,使用也很簡單,調用view的startAnimation方法即可。
//view動畫使用,方式一:xml,建議使用。
Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_test);
textView1.startAnimation(animation);
1.2 代碼動態創建
代碼創建舉例如下,也很簡單。
//view動畫使用,方式二:new 動畫對象
AnimationSet animationSet = new AnimationSet(false);
animationSet.setDuration(3000);
animationSet.addAnimation(new TranslateAnimation(0, 100, 0, 0));
animationSet.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f));
animationSet.setFillAfter(true);
textView2.startAnimation(animationSet);
//view動畫使用,方式二:new 動畫對象,使用setAnimation
AnimationSet animationSet2 = new AnimationSet(false);
animationSet2.setDuration(3000);
animationSet2.addAnimation(new TranslateAnimation(0, 100, 0, 0));
animationSet2.addAnimation(new ScaleAnimation(0.1f, 1f, 0.1f, 1f));
animationSet2.setFillAfter(true);
animationSet2.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
MyToast.showMsg(AnimationTestActivity.this, "View動畫:代碼 set:View動畫結束~");
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
textView3.setAnimation(animationSet2);
注意點:
- startAnimation方法是立刻播放動畫;setAnimation是設置要播放的下一個動畫。
- setAnimationListener可以監聽動畫的開始、結束、重復。
1.3 自定義View動畫
通常我們不需要自定義View動畫,上面4種基本夠用。
自定義View動畫,需要繼承Animation,重寫initialize和applyTransformation方法。在initialize中做初始化工作,在applyTransformation中做相應的矩陣變換(需要用到Camera),需要用到數學知識。這個給出一個例子Rotate3dAnimation,沿Y軸旋轉并沿Z軸平移,到達3d效果。
/**
* 沿Y軸旋轉并沿Z軸平移,到達3d效果
* An animation that rotates the view on the Y axis between two specified angles.
* This animation also adds a translation on the Z axis (depth) to improve the effect.
*/
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
/**
* Creates a new 3D rotation on the Y axis. The rotation is defined by its
* start angle and its end angle. Both angles are in degrees. The rotation
* is performed around a center point on the 2D space, definied by a pair
* of X and Y coordinates, called centerX and centerY. When the animation
* starts, a translation on the Z axis (depth) is performed. The length
* of the translation can be specified, as well as whether the translation
* should be reversed in time.
*
* @param fromDegrees the start angle of the 3D rotation 沿y軸旋轉開始角度
* @param toDegrees the end angle of the 3D rotation 沿y軸旋轉結束角度
* @param centerX the X center of the 3D rotation 沿y軸旋轉的軸點x(相對于自身)
* @param centerY the Y center of the 3D rotation 沿y軸旋轉的軸點y(相對于自身)
* @param depthZ z軸的平移。如果>0,越大就越遠離,視覺上變得越小。
* @param reverse true if the translation should be reversed, false otherwise
*/
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
}
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
1.4 幀動畫
幀動畫對應AnimationDrawable類,用來順序播放多張圖片。使用很簡單,先xml定義一個AnimationDrawable,然后作為背景或資源設置給view并開始動畫即可。
舉例如下:
R.drawable.frame_animation
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/home_icon_guide00"
android:duration="50"/>
<item
android:drawable="@drawable/home_icon_guide01"
android:duration="50"/>
<item
android:drawable="@drawable/home_icon_guide02"
android:duration="50"/>
<item
android:drawable="@drawable/home_icon_guide03"
android:duration="50"/>
......
</animation-list>
tvFrameAnimation.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable frameAnimationBackground = (AnimationDrawable) tvFrameAnimation.getBackground();
frameAnimationBackground.start();
1.5 View動畫的特殊使用場景
1.5.1 給ViewGroup指定child的出場動畫
使用LayoutAnimation給ViewGroup指定child的出場動畫,方法如下:
1.先用xml定義標簽LayoutAnimation:
- android:animation設置child的出場動畫
- android:animationOrder設置child的出場順序,normal就是順序
- delay是指:每個child延遲(在android:animation中指定的動畫時間)0.8倍后播放動畫。如果android:animation中的動畫時間是100ms,那么每個child都會延遲800ms后播放動畫。 如果不設置delay,那么所有child同時執行動畫。
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/enter_from_left_for_child_of_group"
android:animationOrder="normal"
android:delay="0.8">
</layoutAnimation>
R.anim.enter_from_left_for_child_of_group
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1000"
android:fromXDelta="-100%p"
android:toXDelta="0"/>
</set>
2.把LayoutAnimation設置給ViewGroup
<LinearLayout
android:id="@+id/ll_layout_animation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layoutAnimation="@anim/layout_animation">
<TextView
android:layout_width="50dp"
android:layout_height="wrap_content"
android:textColor="#ff0000"
android:text="呵呵呵"/>
<TextView
android:layout_width="60dp"
android:layout_height="wrap_content"
android:textColor="#ff0000"
android:text="qq"
android:background="@color/colorPrimary"/>
<TextView
android:layout_width="30dp"
android:layout_height="wrap_content"
android:textColor="#ff0000"
android:text="啊啊"/>
</LinearLayout>
除了xml,當然也可以使用LayoutAnimationController 指定:
//代碼設置LayoutAnimation,實現ViewGroup的child的出場動畫
Animation enterAnim = AnimationUtils.loadAnimation(this, R.anim.enter_from_left_for_child_of_group);
LayoutAnimationController controller = new LayoutAnimationController(enterAnim);
controller.setDelay(0.8f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
llLayoutAnimation.setLayoutAnimation(controller);
1.5.2 Activity的切換效果
Activity默認有切換動畫效果,我們也可以自定義:overridePendingTransition(int enterAnim, int exitAnim) 可以指定activity開大或暫停時的動畫效果。
- enterAnim,指要打開的activity進入的動畫
- exitAnim,要暫停的activity退出的動畫
注意 必須在startActivity或finish之后使用才能生效。如下所示:
public static void launch(Activity activity) {
Intent intent = new Intent(activity, AnimationTestActivity.class);
activity.startActivity(intent);
//打開的activity,從右側進入,暫停的activity退出到左側。
activity.overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left);
}
......
@Override
public void finish() {
super.finish();
//打開的activity,就是上一個activity從左側進入,要finish的activity退出到右側
overridePendingTransition(R.anim.enter_from_left, R.anim.exit_to_right);
}
R.anim.enter_from_right
<?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"/>
</set>
R.anim.exit_to_left
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="0"
android:toXDelta="-100%p"/>
</set>
R.anim.enter_from_left
<?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"/>
</set>
R.anim.exit_to_right
<?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"/>
</set>
二、屬性動畫
屬性動畫幾乎無所不能,只要對象有這個屬性,就可以對這個屬性做動畫。甚至還可以沒有對象。可用通過ObjectAnimator、ValueAnimator、AnimatorSet實現豐富的動畫。
2.1 使用方法
屬性動畫可對任意對象做動畫,不僅僅是View。默認動畫時間是300ms,10ms/幀。具體理解就是:可在給定的時間間隔內 實現 對象的某屬性值 從 value1 到 value2的改變。
使用很簡單,可以直接代碼實現(推薦),也可xml實現,舉例如下:
//屬性動畫使用,方式一:代碼,建議使用。 橫移
ObjectAnimator translationX = ObjectAnimator
.ofFloat(textView6, "translationX", 0, 200)
.setDuration(1000);
translationX.setInterpolator(new LinearInterpolator());
setAnimatorListener(translationX);
//屬性動畫使用,方式二:xml。 豎移
Animator animatorUpAndDown = AnimatorInflater.loadAnimator(this, R.animator.animator_test);
animatorUpAndDown.setTarget(textView6);
//文字顏色變化
ObjectAnimator textColor = ObjectAnimator
.ofInt(textView6, "textColor", 0xffff0000, 0xff00ffff)
.setDuration(1000);
textColor.setRepeatCount(ValueAnimator.INFINITE);
textColor.setRepeatMode(ValueAnimator.REVERSE);
//注意,這里如果不設置 那么顏色就是跳躍的,設置ArgbEvaluator 就是連續過度的顏色變化
textColor.setEvaluator(new ArgbEvaluator());
//animatorSet
mAnimatorSet = new AnimatorSet();
mAnimatorSet
.play(animatorUpAndDown)
.with(textColor)
.after(translationX);
mAnimatorSet.start();
/**
* 設置屬性動畫的監聽
* @param translationX
*/
private void setAnimatorListener(ObjectAnimator translationX) {
translationX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//每播放一幀,都會調用
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
translationX.addPauseListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationResume(Animator animation) {
super.onAnimationResume(animation);
}
});
}
translationX.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
});
}
R.animator.animator_test,是放在res/animator中。
<?xml version="1.0" encoding="utf-8"?>
<!--屬性動畫test,一般建議采用代碼實現,不用xml-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially">
<!--repeatCount:默認是0,-1是無限循環-->
<!--repeatMode:重復模式:restart-從頭來一遍、reverse-反向來一遍-->
<!--valueType:指定propertyName的類型可選intType、floatType-->
<!--android:pathData=""
android:propertyXName=""
android:propertyYName=""-->
<objectAnimator
android:propertyName="translationY"
android:duration="1000"
android:valueFrom="0"
android:valueTo="120"
android:startOffset="0"
android:repeatCount="0"
android:repeatMode="reverse"
android:valueType="floatType"
android:interpolator="@android:interpolator/accelerate_decelerate" />
<!--animator對用vueAnimator,比objectAnimator少了propertyName-->
<!--<animator-->
<!--android:duration="2000"-->
<!--android:valueFrom=""-->
<!--android:valueTo=""-->
<!--android:startOffset=""-->
<!--android:repeatCount=""-->
<!--android:repeatMode=""-->
<!--android:valueType=""-->
<!--android:interpolator=""-->
<!--android:pathData=""-->
<!--android:propertyXName=""-->
<!--android:propertyYName=""/>-->
</set>
translationX是實現橫移,animatorUpAndDown是實現豎移、textColor是實現文字顏色變化。其中animatorUpAndDown是使用xml定義,標簽含義也很好理解。 最后使用AnimatorSet的play、with、after 實現 先橫移,然后 豎移和顏色變化 同時的動畫集合效果。
注意點:
- 關于View動畫和屬性動畫的平移,屬性動畫改變屬性值setTranslationX 的視圖效果像view動畫的平移一樣,都是view實際的layout位置沒變,只改變了視圖位置;不同點是屬性動畫 給觸摸點生效區域增加了位移(而view動畫僅改變了視圖位置)。
- 插值器:Interpolator,根據 時間流逝的百分比,計算當前屬性值改變的百分比。 例如duration是1000,start后過了200,那么時間百分比是0.2,那么如果差值器是LinearInterpolator線性差值器,那么屬性值改變的百分比也是0.2
- 估值器:Evaluator,就是根據 差值器獲取的 屬性值百分比,計算改變后的屬性值。 ofInt、onFloat內部會自動設置IntEvaluator、FloatEvaluator。如果使用ofInt且是顏色相關的屬性,就要設置ArgbEvaluator。 上面例子中 文字顏色變化動畫 設置了ArgbEvaluator:textColor.setEvaluator(new ArgbEvaluator())。
- 動畫監聽:主要是兩個監聽接口,AnimatorUpdateListener、AnimatorListenerAdapter。AnimatorUpdateListener的回調方法在每幀更新時都會調用一次;AnimatorListenerAdapter可以監聽開始、結束、暫停、繼續、重復、取消,重寫你要關注的方法即可。
2.2對任意屬性做動畫
一個問題,針對下面的Button,如何實現 的寬度逐漸拉長的動畫,即文字不變,僅拉長背景寬度?
<Button
android:id="@+id/button_animator_test"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:text="任意屬性動畫-寬度拉長"/>
首先,View動畫的ScaleAnimation是無法實現的,因為view的scale是把view的視圖放大,這樣文字也會拉長變形。那么屬性動畫呢?試試~
ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000);
width1.setDuration(2000);
width1.start();
但是發現,沒有效果!這是為啥呢?解釋如下.
對object 的任意屬性做動畫 要求兩個條件:
- object有 對應屬性 的set方法,動畫中沒設置初始值 還要有get方法,系統要去取初始值(不滿足則會crash)。
- set方法要對object有所改變,如UI的變化。不滿足則會沒有動畫效果
上面Button沒有動畫效果,就是沒有滿足第二條。看下Button的setWidth方法:
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
實際就是TextView的setWidth方法,看到設置進去的值僅影響了寬度最大值和最小值。按照官方注釋和實測,發現只有當Button/TextView在xml中設置android:layout_width為"wrap_content"時,才會setWidth改變寬度;而當Button/TextView在xml中設置android:layout_width為固定dp值時,setWidth無效。 而我們上面給出的Button xml中確實是固定值180dp,所以是屬性"width"的setWidth是無效的,即不滿足第二條要求,就沒有動畫效果了。(當修改Button xml中設置android:layout_width為"wrap_content"時,上面執行的屬性動畫是生效的。)
那么,當不滿足條件時,如何解決此問題呢? 有如下處理方法:
- 給object添加set、get方法,如果有權限。(一般不行,如TextView是SDK里面的不能直接改)
- 給Object包裝一層,在包裝類中提供set、get方法。
- 使用ValueAnimator,監聽Value變化過程,自己實現屬性的改變。
private void testAnimatorAboutButtonWidth() {
//Button width 屬性動畫:如果xml中寬度是wrap_content,那么動畫有效。
// 如果設置button確切的dp值,那么無效,因為對應屬性"width"的setWidth()方法就是 在wrap_content是才有效。
ObjectAnimator width1 = ObjectAnimator.ofInt(button, "width", 1000);
width1.setDuration(2000);
// width1.start();
//那么,想要在button原本有確切dp值時,要能對width動畫,怎么做呢?
//方法一,包一層,然后用layoutParams
ViewWrapper wrapper = new ViewWrapper(button);
ObjectAnimator width2 = ObjectAnimator.ofInt(wrapper, "width", 1000);
width2.setDuration(2000);
// width2.start();
//方法二,使用ValueAnimator,每一幀自己顯示寬度的變化
ValueAnimator valueAnimator = ValueAnimator.ofInt(button.getLayoutParams().width, 1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int animatedValue = (Integer) animation.getAnimatedValue();
Log.i("hfy", "onAnimationUpdate: animatedValue=" + animatedValue);
// IntEvaluator intEvaluator = new IntEvaluator();
//// 獲取屬性值改變比例、計算屬性值
// float animatedFraction = animation.getAnimatedFraction();
// Integer evaluate = intEvaluator.evaluate(animatedFraction, 300, 600);
// Log.i("hfy", "onAnimationUpdate: evaluate="+evaluate);
if (button != null) {
button.getLayoutParams().width = animatedValue;
button.requestLayout();
}
}
});
valueAnimator.setDuration(4000).start();
}
/**
* 包一層,提供對應屬性的set、get方法
*/
private class ViewWrapper {
private final View mView;
public ViewWrapper(View view) {
mView = view;
}
public int getWidth() {
return mView.getLayoutParams().width;
}
public void setWidth(int width) {
ViewGroup.LayoutParams layoutParams = mView.getLayoutParams();
layoutParams.width = width;
mView.setLayoutParams(layoutParams);
mView.requestLayout();
}
}
2.3 屬性動畫的原理
屬性動畫,要求對象有這個屬性的set方法,執行時會根據傳入的 屬性初始值、最終值,在每幀更新時調用set方法設置當前時刻的 屬性值。隨著時間推移,set的屬性值會接近最終值,從而達到動畫效果。如果沒傳入初始值,那么對象還要有get方法,用于獲取初始值。
在獲取初始值、set屬性值時,都是使用 反射 的方式,進行 get、set方法的調用。
見PropertyValuesHolder的setupValue、setAnimatedValue方法:
private void setupValue(Object target, Keyframe kf) {
if (mProperty != null) {
Object value = convertBack(mProperty.get(target));
kf.setValue(value);
} else {
try {
if (mGetter == null) {
Class targetClass = target.getClass();
setupGetter(targetClass);
if (mGetter == null) {
// Already logged the error - just return to avoid NPE
return;
}
}
Object value = convertBack(mGetter.invoke(target));
kf.setValue(value);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
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());
}
}
}
以上效果圖:
三、使用動畫的注意事項
- 使用幀動畫,避免OOM。因為圖片多。
- 屬性動畫 如果有循環動畫,在頁面退出時要及時停止,避免內存泄漏。
- 使用View動畫后,調用setVisibility(View.GONE)失效時,使用view.clearAnimation()可解決。
附上之前記錄的一些動畫效果
自定義view:TextSwitcher使用
自定義view:信息飄窗/彈幕——AutoSwitchTextView
自定義view:ProgressBar 前景色、背景色、平滑顯示進度