Android的動畫可以分為三種,View動畫、幀動畫、屬性動畫。View動畫通過對場景裡的物件不斷做影像變化(平移、縮放、旋轉、透明度)從而產生動畫效果,它是一種漸進式動畫,而且View動畫支援自定義。幀動畫可以理解為圖片切換動畫(如果圖片過多,會導致OOM)。屬性動畫為API11的新特性,屬性動畫通過通過動態地改變物件的屬性從而達到動畫效果。
- View動畫
View動畫的種類:View動畫的四種變換效果對應著Animation的四個子類。TranslateAnimation,ScaleAnimation,RotateAnimation,AlphaAnimation。這四種動畫可以通過XML來定義,也可以通過程式碼動態建立。對於View動畫來說,建議XML來定義,這是因為XML格式的動畫可讀性好。
名稱 | 標籤 | 子類 | 效果 |
平移動畫 | <translate> | TranslateAnimation | 移動View |
縮放動畫 | <scale> | ScaleAnimation | 放大或縮小View |
旋轉動畫 | <rotate> | RotateAnimation | 旋轉View |
透明度動畫 | <alpha> | AlphaAnimation | 改變View的透明度 |
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_decelerate_interpolator" android:shareInterpolator="true" android:duration="1000" android:fillAfter="false"> <alpha android:fromAlpha="float" android:toAlpha="float" /> <scale android:fromXScale="float" android:toXScale="float" android:fromYScale="float" android:toYScale="float" android:pivotX="float" android:pivotY="float" /> <translate android:fromXDelta="float" android:toXDelta="float" android:fromYDelta="float" android:toYDelta="float" /> <rotate android:fromDegrees="float" android:toDegrees="float" android:pivotX="float" android:pivotY="float" /> </set>
View動畫可以是單個動畫,也可以是一系列動畫組成。<set>表示動畫集合,對應AnimationSet類。
android:interpolator:表示動畫集合所使用的插值器,插值器影響動畫的速度,比如非勻速動畫就需要通過插值器來控制動畫的播放速度。
android:shareInterpolator:表示集合中的動畫是否共享同一個插值器。如果不指定插值器,那麼子動畫就需要單獨指定所需要的插值器或者使用預設值。
AlphaAnimation anim = (AlphaAnimation) AnimationUtils.loadAnimation(this, R.anim.anim_test); TextView tv = findViewById(R.id.tv); tv.startAnimation(anim);
除了XML定義動畫之外,還可以使用程式碼建立動畫。
AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1); alphaAnimation.setDuration(300); TextView tv = findViewById(R.id.tv); tv.startAnimation(alphaAnimation);
注意:通過Animation的setAnimationListener的方法可以給View動畫新增過程監聽。如適用於帶動畫的啟動頁,監聽動畫完成後進入MainActivity。
anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) {} @Override public void onAnimationEnd(Animation animation) {} @Override public void onAnimationRepeat(Animation animation) {} });
自定義View動畫:
自定義View動畫是一件簡單而複雜的事情。說簡單是:因為派生一種新動畫只需要整合Animation這個抽象類,然後重寫它的initialize和applyTransformation方法。在initialize中進行初始化工作,在applyTransformation中進行相應的矩陣變換即可。很多時候需要Camera來簡化矩陣變換過程。說複雜,是因為需要運用到矩陣變換的過程,使用到數學上的概念。之後再做相關的分析講解。
- 幀動畫
幀動畫順序播放一組預先定義好的圖片,類似電影播放。不同於View動畫,系統提供另外一個類AnimationDrawable來使用幀動畫。幀動畫的使用比較簡單,先使用XML來定義一個AnimationDrawable。在drawable目錄下建立:
<?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/img1" android:duration="500"/> <item android:drawable="@drawable/img2" android:duration="500"/> <item android:drawable="@drawable/img3" android:duration="500"/> </animation-list>
將上述的Drawable作為View的背景並通過Drawable來播放動畫。
TextView tv = findViewById(R.id.tv); tv.startAnimation(alphaAnimation); AnimationDrawable animationDrawable = (AnimationDrawable) tv.getBackground(); animationDrawable.start();
注意幀動畫比較容易引起OOM,所以避免使用尺寸較大的圖片。
- View動畫的特殊場景使用
在一些特殊場景下,如ViewGroup中控制子元素的出場效果,在Activity中實現不同Activity之間的切換效果。
LayoutAnimation作用於ViewGroup,作為ViewGroup指定一個動畫,這樣當它的子元素出場時都具有這樣的效果。常見的ListView的Item的出場以一定的動畫效果出現。
定義LayoutAnimation如下:
//res/anim/anim_layout.xml
<?xml version="1.0" encoding="utf-8"?> <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:delay="0.5" android:animationOrder="normal" android:animation="@anim/anim_item" />
delay表示子元素開始動畫的時間延遲。比如說元素入場動畫週期為300ms,那麼0.5表示每個子元素都需要延遲150ms才能播放入場動畫。第一個子元素延遲150ms開始播放入場動畫,第二個子元素延遲300m開始播放入場,以此類推。
animationOrder:子元素動畫順序,normal, reverse和random。
android:animation: 為元素指定具體的入場動畫。
為ViewGroup指定android:layoutAnimation屬性,對於ListView而言,指定之後就是Item的出場動畫了。除了XML中指定LayoutAnimation之外,還可以通過LayoutAnimationController來實現,具體如下:
ListView listView = findViewById(R.id.list_view); Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item); LayoutAnimationController controller = new LayoutAnimationController(animation); controller.setDelay(0.5f); controller.setOrder(LayoutAnimationController.ORDER_NORMAL); listView.setLayoutAnimation(controller);
Activity的切換效果:
Activity有預設的切換效果,但是這個效果是可以自定義的,主要用到overridePendingTransition(int enterAnim, int exitAnim)這個方法,這個必須在startActivity(intent)和finish()之後被呼叫才能生效,它的引數含義如下:
enterAnim - Activity被開啟時,所需要的動畫資源id
exitAnim - Activity被暫停時,所需要的動畫資源id
當啟動一個Activity時,可以按照如下方式為其新增自定義的切換效果:
Intent intent = new Intent(this, TestActivity.class); startActivity(intent); overridePendingTransition(R.anim.enter_anim, R.anim.exitAnim);
當Activityt退出時,也可以為其指定自定義的切換效果:
@Override public void finish() { super.finish(); overridePendingTransition(R.anim.enter_anim, R.anim.exitAnim); }
Fragment也可以新增切換效果,可以通過FragmentTransaction中的setCustomAnimations()方法來新增切換動畫,這個切換動畫需要時View動畫。還有其他的方法為Activity和Fragment新增切換動畫,但是需要考略相容為題。
- 屬性動畫
屬性動畫是API11新加入的特性,和View動畫不同,它對作用的物件進行擴充套件,屬性動畫可以對任何物件做動畫,甚至還可以沒有物件。屬性動畫可以對任意物件的屬性進行動畫而不僅僅是View。屬性動畫中有ValueAnimator, ObjectAnimator和AnimatorSet等概念。為相容之前的版本,採用nineoldandroids源動畫庫。
ObjectAnimator繼承自ValueAnimator,AnimatorSet是動畫集合。屬性動畫需要定義在res/animator/目錄下。
- 改變一個物件myObject的translationY屬性,讓其沿著Y軸向上移動一段距離,在預設的時間完成。
-
ObjectAnimator.ofFloat(tv, "translationY", -myObject.getHeight()) .start();
-
- 改變一個物件背景色屬性,典型的改變View背景,下面的動畫可以讓背景色在3s內實現從0xFFFF8080到0xFF8080FF的漸變,動畫會無限迴圈而且會有反轉的效果。
-
ValueAnimator colorAnim = ObjectAnimator.ofInt(myObject, "backgroundColor", 0xFFFF8080, 0xFF8080FF); colorAnim.setDuration(3000); colorAnim.setEvaluator(new ArgbEvaluator()); colorAnim.setRepeatCount(ValueAnimator.INFINITE); colorAnim.setRepeatMode(ValueAnimator.REVERSE); colorAnim.start();
-
- 動畫集合,5秒內對View的旋轉,平移,縮放和透明度都進行改變。
-
AnimatorSet set = new AnimatorSet(); set.playTogether( ObjectAnimator.ofFloat(view, "rotationX", 0, 360), ObjectAnimator.ofFloat(view, "rotationY", 0, 180), ObjectAnimator.ofFloat(view, "rotation", 0, -90), ObjectAnimator.ofFloat(view, "translationX", 0, 100), ObjectAnimator.ofFloat(view, "translationY", 0, 100), ObjectAnimator.ofFloat(view, "scaleX", 1, 1.5f), ObjectAnimator.ofFloat(view, "scaleY", 1, 0.5f), ObjectAnimator.ofFloat(view, "alpha", 1, 0.5f, 1) ); set.setDuration(5 * 1000).start();
-
屬性動畫除了程式碼實現之外,還可以通過XML來定義。
eg.
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <objectAnimator android:propertyName="x" android:duration="300" android:valueTo="300" android:valueType="intType" /> <objectAnimator android:propertyName="y" android:duration="300" android:valueTo="300" android:valueType="intType"/> </set>
載入上面的屬性動畫:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.property_animator); set.setTarget(view); set.start();
建議:屬性動畫採用程式碼來實現,這是因為程式碼的方式會比較簡單,更重要的是,在一個屬性起始值無法確定的情況下,將無法將屬性定義在xml檔案中。
- 插值器和估值器
TimeInterpolater:時間插值器,它的作用是根據時間流逝的百分比來計算出當前屬性改變的百分比,系統預置的有LinearInterpolator(線性插值器:勻速動畫)、AccelerateDecelerateInterpolator(加速減速插值器:動畫兩頭慢,中間快),DeceleratrInterpolator(減速插值器:動畫越來越慢)等。
TypeEvaluator:型別估值演算法,也叫做估值器。它的作用是根據當前屬性改變的百分比來計算改變後的屬性值,系統預置的有IntEvaluator(針對整型屬性),FloatEvaluator(針對浮點型屬性),ArgbEvaluator(針對Color屬性)。
線性差值器,屬於勻速動畫,其原始碼為:
/** * An interpolator where the rate of change is constant */ @HasNativeInterpolator public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory { public LinearInterpolator() { } public LinearInterpolator(Context context, AttributeSet attrs) { } public float getInterpolation(float input) { return input; /** 輸入值和返回值一樣 */ } /** @hide */ @Override public long createNativeInterpolator() { return NativeInterpolatorFactoryHelper.createLinearInterpolator(); } }
那到底輸入數值是什麼?這需要估值演算法來確定,如整型估值器的原始碼為:
public class IntEvaluator implements TypeEvaluator<Integer> { /** * This function returns the result of linearly interpolating the start and end values, with * <code>fraction</code> representing the proportion between the start and end values. The * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>, * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>, * and <code>t</code> is <code>fraction</code>. * * @param fraction The fraction from the starting to the ending values * @param startValue The start value; should be of type <code>int</code> or * <code>Integer</code> * @param endValue The end value; should be of type <code>int</code> or <code>Integer</code> * @return A linear interpolation between the start and end values, given the * <code>fraction</code> parameter. */ public Integer evaluate(float fraction, Integer startValue, Integer endValue) { int startInt = startValue; return (int)(startInt + fraction * (endValue - startInt)); } }
evaluate方法的三個引數分別是估值小數,起始值,結束值。例子:勻速動畫,物件從0位置到100,當估值小數位0.5的時候,根據演算法result = 0 + 0.5(100 - 0) = 50。
屬性動畫要求物件有該屬性的set方法和get方法(可選)。除了系統預置的插值器和估值演算法外,我們還可以自定義。自定義差值演算法需要實現Interpolator或TimeInterpolator;自定義估值演算法需要實現TypeEvaluator。
屬性動畫的監聽:
有兩個常用的屬性動畫監聽:AnimatorListener, AnimatorUpdateListener
ObjectAnimator animator = new ObjectAnimator(); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) {} @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} });
從AnimatorListener的定義來看,它可以監聽動畫的開始,結束,取消和重複動畫。
public static interface AnimatorUpdateListener { /** * <p>Notifies the occurrence of another frame of the animation.</p> * * @param animation The animation which was repeated. */ void onAnimationUpdate(ValueAnimator animation); }
AnimatorUpdateListener比較特殊,它會監聽整個動畫過程,動畫是由許多幀組成的,每播放一幀onAnimationUpdate()就會被呼叫一次,利用這個特徵,我們可以做一些特殊事情。
- 對任意屬性做動畫
例如TextView的寬度動畫變為800的動畫,當layout_width的屬性為wrap_content時,是有動畫的,但是如果設定為固定值之後,下面的屬性動畫就沒有效果了。
//高度從原來的預設值變為800 ObjectAnimator objectAnimator = ObjectAnimator.ofInt(textView, "width", 800); objectAnimator.setDuration(500); objectAnimator.start();
這是因為:
public void setWidth(int pixels) { mMaxWidth = mMinWidth = pixels; mMaxWidthMode = mMinWidthMode = PIXELS; requestLayout(); invalidate(); }
public final int getWidth() { return mRight - mLeft; }
setWidth不是所謂意義上的寬度設定,所以就不會產生動畫。對任意屬性做動畫,一般需要有set和get方法,設定流程如下:
如果沒有屬性設定獲取許可權,可以定義一個包裹類進行封裝,實現set和get方法;如:
public static class ViewWrapper{ private View mTarget; public ViewWrapper(View target){ mTarget = target; } public void setWidth(int width){ mTarget.getLayoutParams().width = width; mTarget.requestLayout(); } public int getWidth(){ return mTarget.getLayoutParams().width; } }
ViewWrapper viewWrapper = new ViewWrapper(view); ObjectAnimator widthAnimator = ObjectAnimator.ofInt(viewWrapper, "width", btn.getWidth(), 800) .setDuration(500); widthAnimator.start();
這樣,上面的TextView就可以實現寬度的動畫效果了。
自定義View設定相關屬性,進行動畫案例:
/** * 自定義圓環 * 對屬性progress設定set和get方法 * 就可以對CircleProgress的progress進行屬性動畫 */ public class CircleProgress extends View{ private Paint mPaint; private RectF rectF; private int progress = 270; int width, height; public CircleProgress(Context context) { super(context); init(); } public CircleProgress(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public CircleProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(20); mPaint.setColor(Color.RED); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.EXACTLY){ width = widthSize; }else{ width = 100; //設定為預設值 } if (heightMode == MeasureSpec.EXACTLY){ height = heightSize; }else{ height = 100; //設定為預設值 } rectF = new RectF(20, 20, width-20, height-20); setMeasuredDimension(Math.min(width, height), Math.min(width, height)); //矩形 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawArc(rectF, -90, progress, false, mPaint); invalidate(); } public void setProgress(int progress){ this.progress = progress; } public int getProgress(){ return progress; } }