一、Android 動畫分類
總的來說,Android動畫可以分為兩類,最初的傳統動畫和Android3.0 之后出現的屬性動畫;
傳統動畫又包括 幀動畫(Frame Animation)和補間動畫(Tweened Animation)。
二、傳統動畫
幀動畫
幀動畫是最容易實現的一種動畫,這種動畫更多的依賴於完善的UI資源,他的原理就是將一張張單獨的圖片連貫的進行播放,從而在視覺上產生一種動畫的效果;有點類似於某些軟件制作gif動畫的方式。
如上圖中的京東加載動畫,代碼要做的事情就是把一幅幅的圖片按順序顯示,造成動畫的視覺效果。
京東動畫實現
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
<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>
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(); }
可以說,圖片資源決定了這種方式可以實現怎樣的動畫
在有些代碼中,我們還會看到android:oneshot="false" ,這個oneshot 的含義就是動畫執行一次(true)還是循環執行多次。
這里其他幾個動畫實現方式都是一樣,無非就是圖片資源的差異。
補間動畫
補間動畫又可以分為四種形式,分別是 alpha(淡入淡出),translate(位移),scale(縮放大小),rotate(旋轉)。 補間動畫的實現,一般會采用xml 文件的形式;代碼會更容易書寫和閱讀,同時也更容易復用。
XML 實現
首先,在res/anim/ 文件夾下定義如下的動畫實現方式
alpha_anim.xml 動畫實現
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:fromAlpha="1.0" android:interpolator="@android:anim/accelerate_decelerate_interpolator" android:toAlpha="0.0" />
scale.xml 動畫實現
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:fromXScale="0.0" android:fromYScale="0.0" android:pivotX="50%" android:pivotY="50%" android:toXScale="1.0" android:toYScale="1.0"/>
然后,在Activity中
Animation animation = AnimationUtils.loadAnimation(mContext, R.anim.alpha_anim); img = (ImageView) findViewById(R.id.img); img.startAnimation(animation);
這樣就可以實現ImageView alpha 透明變化的動畫效果。
也可以使用set 標簽將多個動畫組合(代碼源自Android SDK API)
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@[package:]anim/interpolator_resource" android:shareInterpolator=["true" | "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> ... </set>
</set>
可以看到組合動畫是可以嵌套使用的。
各個動畫屬性的含義結合動畫自身的特點應該很好理解,就不一一闡述了;這里主要說一下interpolator 和 pivot。
Interpolator 主要作用是可以控制動畫的變化速率 ,就是動畫進行的快慢節奏。
Android 系統已經為我們提供了一些Interpolator ,比如 accelerate_decelerate_interpolator,accelerate_interpolator等。更多的interpolator 及其含義可以在Android SDK 中查看。同時這個Interpolator也是可以自定義的,這個后面還會提到。
pivot 決定了當前動畫執行的參考位置
pivot 這個屬性主要是在translate 和 scale 動畫中,這兩種動畫都牽扯到view 的“物理位置“發生變化,所以需要一個參考點。而pivotX和pivotY就共同決定了這個點;它的值可以是float或者是百分比數值。
我們以pivotX為例,
pivotX取值 | 含義 |
---|---|
10 | 距離動畫所在view自身左邊緣10像素 |
10% | 距離動畫所在view自身左邊緣 的距離是整個view寬度的10% |
10%p | 距離動畫所在view父控件左邊緣的距離是整個view寬度的10% |
pivotY 也是相同的原理,只不過變成的縱向的位置。如果還是不明白可以參考源碼,在Tweened Animation中結合seekbar的滑動觀察rotate的變化理解。
Java Code 實現
有時候,動畫的屬性值可能需要動態的調整,這個時候使用xml 就不合適了,需要使用java代碼實現
private void RotateAnimation() { animation = new RotateAnimation(-deValue, deValue, Animation.RELATIVE_TO_SELF, pxValue, Animation.RELATIVE_TO_SELF, pyValue); animation.setDuration(timeValue); if (keep.isChecked()) { animation.setFillAfter(true); } else { animation.setFillAfter(false); } if (loop.isChecked()) { animation.setRepeatCount(-1); } else { animation.setRepeatCount(0); } if (reverse.isChecked()) { animation.setRepeatMode(Animation.REVERSE); } else { animation.setRepeatMode(Animation.RESTART); } img.startAnimation(animation); }
這里animation.setFillAfter決定了動畫在播放結束時是否保持最終的狀態;animation.setRepeatCount和animation.setRepeatMode 決定了動畫的重復次數及重復方式,具體細節可查看源碼理解。
好了,傳統動畫的內容就說到這里了。
三、屬性動畫
屬性動畫,顧名思義它是對於對象屬性的動畫。因此,所有補間動畫的內容,都可以通過屬性動畫實現。
屬性動畫入門
首先我們來看看如何用屬性動畫實現上面補間動畫的效果
private void RotateAnimation() { ObjectAnimator anim = ObjectAnimator.ofFloat(myView, "rotation", 0f, 360f); anim.setDuration(1000); anim.start(); } private void AlpahAnimation() { ObjectAnimator anim = ObjectAnimator.ofFloat(myView, "alpha", 1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.0f); anim.setRepeatCount(-1); anim.setRepeatMode(ObjectAnimator.REVERSE); anim.setDuration(2000); anim.start(); }
這兩個方法用屬性動畫的方式分別實現了旋轉動畫和淡入淡出動畫,其中setDuration、setRepeatMode及setRepeatCount和補間動畫中的概念是一樣的。
可以看到,屬性動畫貌似強大了許多,實現很方便,同時動畫可變化的值也有了更多的選擇,動畫所能呈現的細節也更多。
當然屬性動畫也是可以組合實現的
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(myView, "alpha", 1.0f, 0.5f, 0.8f, 1.0f); ObjectAnimator scaleXAnim = ObjectAnimator.ofFloat(myView, "scaleX", 0.0f, 1.0f); ObjectAnimator scaleYAnim = ObjectAnimator.ofFloat(myView, "scaleY", 0.0f, 2.0f); ObjectAnimator rotateAnim = ObjectAnimator.ofFloat(myView, "rotation", 0, 360); ObjectAnimator transXAnim = ObjectAnimator.ofFloat(myView, "translationX", 100, 400); ObjectAnimator transYAnim = ObjectAnimator.ofFloat(myView, "tranlsationY", 100, 750); AnimatorSet set = new AnimatorSet(); set.playTogether(alphaAnim, scaleXAnim, scaleYAnim, rotateAnim, transXAnim, transYAnim); // set.playSequentially(alphaAnim, scaleXAnim, scaleYAnim, rotateAnim, transXAnim, transYAnim);
set.setDuration(3000); set.start();
可以看到這些動畫可以同時播放,或者是按序播放。
屬性動畫核心原理
在上面實現屬性動畫的時候,我們反復的使用到了ObjectAnimator 這個類,這個類繼承自ValueAnimator,使用這個類可以對任意對象的任意屬性進行動畫操作。而ValueAnimator是整個屬性動畫機制當中最核心的一個類;這點從下面的圖片也可以看出。
屬性動畫核心原理,此圖來自於Android SDK API 文檔。
屬性動畫的運行機制是通過不斷地對值進行操作來實現的,而初始值和結束值之間的動畫過渡就是由ValueAnimator這個類來負責計算的。它的內部使用一種時間循環的機制來計算值與值之間的動畫過渡,我們只需要將初始值和結束值提供給ValueAnimator,並且告訴它動畫所需運行的時長,那么ValueAnimator就會自動幫我們完成從初始值平滑地過渡到結束值這樣的效果。除此之外,ValueAnimator還負責管理動畫的播放次數、播放模式、以及對動畫設置監聽器等。
從上圖我們可以了解到,通過duration、startPropertyValue和endPropertyValue 等值,我們就可以定義動畫運行時長,初始值和結束值。然后通過start方法開始動畫。 那么ValueAnimator 到底是怎樣實現從初始值平滑過渡到結束值的呢?這個就是由TypeEvaluator 和TimeInterpolator 共同決定的。
具體來說,TypeEvaluator 決定了動畫如何從初始值過渡到結束值。個人覺得可以看作一個方向。
TimeInterpolator 決定了動畫從初始值過渡到結束值的節奏。個人覺得可以看作一個速度。
說的通俗一點,你每天早晨出門去公司上班,TypeEvaluator決定了你是坐公交、坐地鐵還是騎車;而當你決定騎車后,TimeInterpolator決定了你一路上騎行的方式,你可以勻速的一路騎到公司,你也可以前半程騎得飛快,后半程騎得慢悠悠。
如果,還是不理解,那么就看下面的代碼吧。首先看一下下面的這兩個gif動畫,一個小球在屏幕上以 y=sin(x) 的數學函數軌跡運行,同時小球的顏色和半徑也發生着變化,可以發現,兩幅圖動畫變化的節奏也是不一樣的。
如果不考慮屬性動畫,這樣的一個動畫純粹的使用Canvas+Handler的方式繪制也是有可能實現的。但是會復雜很多,而且加上各種線程,會帶來很多意想不到的問題。
這里就通過自定義屬性動畫的方式看看這個動畫是如何實現的。
屬性動畫自定義實現
這個動畫最關鍵的三點就是 運動軌跡、小球半徑及顏色的變化;我們就從這三個方面展開。最后我們在結合Interpolator說一下TimeInterpolator的意義。
用TypeEvaluator 確定運動軌跡
前面說了,TypeEvaluator決定了動畫如何從初始值過渡到結束值。這個TypeEvaluator是個接口,我們可以實現這個接口。
public class PointSinEvaluator implements TypeEvaluator { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { Point startPoint = (Point) startValue; Point endPoint = (Point) endValue; float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX()); float y = (float) (Math.sin(x * Math.PI / 180) * 100) + endPoint.getY() / 2; Point point = new Point(x, y); return point; } }
PointSinEvaluator 繼承了TypeEvaluator類,並實現了他唯一的方法evaluate;這個方法有三個參數,第一個參數fraction 代表當前動畫完成的百分比,這個值是如何變化的后面還會提到;第二個和第三個參數代表動畫的初始值和結束值。這里我們的邏輯很簡單,x的值隨着fraction 不斷變化,並最終達到結束值;y的值就是當前x值所對應的sin(x) 值,然后用x 和 y 產生一個新的點(Point對象)返回。
這樣我們就可以使用這個PointSinEvaluator 生成屬性動畫的實例了。
Point startP = new Point(RADIUS, RADIUS);//初始值(起點)
Point endP = new Point(getWidth() - RADIUS, getHeight() - RADIUS);//結束值(終點)
final ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointSinEvaluator(), startP, endP); valueAnimator.setRepeatCount(-1); valueAnimator.setRepeatMode(ValueAnimator.REVERSE); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); postInvalidate(); } });
這樣我們就完成了動畫軌跡的定義,現在只要調用valueAnimator.start() 方法,就會繪制出一個正弦曲線的軌跡。
顏色及半徑動畫實現
之前我們說過,使用ObjectAnimator 可以對任意對象的任意屬性進行動畫操作,這句話是不太嚴謹的,這個任意屬性還需要有get 和 set 方法。
public class PointAnimView extends View { /** * 實現關於color 的屬性動畫 */
private int color; private float radius = RADIUS; ..... }
這里在我們的自定義view中,定義了兩個屬性color 和 radius,並實現了他們各自的get set 方法,這樣我們就可以使用屬性動畫的特點實現小球顏色變化的動畫和半徑變化的動畫。
ObjectAnimator animColor = ObjectAnimator.ofObject(this, "color", new ArgbEvaluator(), Color.GREEN, Color.YELLOW, Color.BLUE, Color.WHITE, Color.RED); animColor.setRepeatCount(-1); animColor.setRepeatMode(ValueAnimator.REVERSE); ValueAnimator animScale = ValueAnimator.ofFloat(20f, 80f, 60f, 10f, 35f,55f,10f); animScale.setRepeatCount(-1); animScale.setRepeatMode(ValueAnimator.REVERSE); animScale.setDuration(5000); animScale.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { radius = (float) animation.getAnimatedValue(); } });
這里,我們使用ObjectAnimator 實現對color 屬性的值按照ArgbEvaluator 這個類的規律在給定的顏色值之間變化,這個ArgbEvaluator 和我們之前定義的PointSinEvaluator一樣,都是決定動畫如何從初始值過渡到結束值的,只不過這個類是系統自帶的,我們直接拿來用就可以,他可以實現各種顏色間的自由過渡。
對radius 這個屬性使用了ValueAnimator,使用了其ofFloat方法實現了一系列float值的變化;同時為其添加了動畫變化的監聽器,在屬性值更新的過程中,我們可以將變化的結果賦給radius,這樣就實現了半徑動態的變化。
這里radius 也可以使用和color相同的方式,只需要把ArgbEvaluator 替換為FloatEvaluator,同時修改動畫的變化值即可;使用添加監聽器的方式,只是為了介紹監聽器的使用方法而已。
好了,到這里我們已經定義出了所有需要的動畫,前面說過,屬性動畫也是可以組合使用的。因此,在動畫啟動的時候,同時播放這三個動畫,就可以實現圖中的效果了。
animSet = new AnimatorSet(); animSet.play(valueAnimator).with(animColor).with(animScale); animSet.setDuration(5000); animSet.setInterpolator(interpolatorType); animSet.start();
PointAnimView 源碼
public class PointAnimView extends View { public static final float RADIUS = 20f; private Point currentPoint; private Paint mPaint; private Paint linePaint; private AnimatorSet animSet; private TimeInterpolator interpolatorType = new LinearInterpolator(); /** * 實現關於color 的屬性動畫 */
private int color; private float radius = RADIUS; public PointAnimView(Context context) { super(context); init(); } public PointAnimView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public PointAnimView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public int getColor() { return color; } public void setColor(int color) { this.color = color; mPaint.setColor(this.color); } public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } private void init() { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.TRANSPARENT); linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); linePaint.setColor(Color.BLACK); linePaint.setStrokeWidth(5); } @Override protected void onDraw(Canvas canvas) { if (currentPoint == null) { currentPoint = new Point(RADIUS, RADIUS); drawCircle(canvas); // StartAnimation();
} else { drawCircle(canvas); } drawLine(canvas); } private void drawLine(Canvas canvas) { canvas.drawLine(10, getHeight() / 2, getWidth(), getHeight() / 2, linePaint); canvas.drawLine(10, getHeight() / 2 - 150, 10, getHeight() / 2 + 150, linePaint); canvas.drawPoint(currentPoint.getX(), currentPoint.getY(), linePaint); } public void StartAnimation() { Point startP = new Point(RADIUS, RADIUS); Point endP = new Point(getWidth() - RADIUS, getHeight() - RADIUS); final ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointSinEvaluator(), startP, endP); valueAnimator.setRepeatCount(-1); valueAnimator.setRepeatMode(ValueAnimator.REVERSE); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { currentPoint = (Point) animation.getAnimatedValue(); postInvalidate(); } }); // ObjectAnimator animColor = ObjectAnimator.ofObject(this, "color", new ArgbEvaluator(), Color.GREEN, Color.YELLOW, Color.BLUE, Color.WHITE, Color.RED); animColor.setRepeatCount(-1); animColor.setRepeatMode(ValueAnimator.REVERSE); ValueAnimator animScale = ValueAnimator.ofFloat(20f, 80f, 60f, 10f, 35f,55f,10f); animScale.setRepeatCount(-1); animScale.setRepeatMode(ValueAnimator.REVERSE); animScale.setDuration(5000); animScale.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { radius = (float) animation.getAnimatedValue(); } }); animSet = new AnimatorSet(); animSet.play(valueAnimator).with(animColor).with(animScale); animSet.setDuration(5000); animSet.setInterpolator(interpolatorType); animSet.start(); } private void drawCircle(Canvas canvas) { float x = currentPoint.getX(); float y = currentPoint.getY(); canvas.drawCircle(x, y, radius, mPaint); } public void setInterpolatorType(int type ) { switch (type) { case 1: interpolatorType = new BounceInterpolator(); break; case 2: interpolatorType = new AccelerateDecelerateInterpolator(); break; case 3: interpolatorType = new DecelerateInterpolator(); break; case 4: interpolatorType = new AnticipateInterpolator(); break; case 5: interpolatorType = new LinearInterpolator(); break; case 6: interpolatorType=new LinearOutSlowInInterpolator(); break; case 7: interpolatorType = new OvershootInterpolator(); default: interpolatorType = new LinearInterpolator(); break; } } @TargetApi(Build.VERSION_CODES.KITKAT) public void pauseAnimation() { if (animSet != null) { animSet.pause(); } } public void stopAnimation() { if (animSet != null) { animSet.cancel(); this.clearAnimation(); } } }
imeInterpolator 介紹
Interpolator的概念其實我們並不陌生,在補間動畫中我們就使用到了。他就是用來控制動畫快慢節奏的;而在屬性動畫中,TimeInterpolator 也是類似的作用;TimeInterpolator 繼承自Interpolator。我們可以繼承TimerInterpolator 以自己的方式控制動畫變化的節奏,也可以使用Android 系統提供的Interpolator。
下面都是系統幫我們定義好的一些Interpolator,我們可以通過setInterpolator 設置不同的Interpolator。
這里我們使用的Interpolator就決定了 前面我們提到的fraction。變化的節奏決定了動畫所執行的百分比。不得不說,這么ValueAnimator的設計的確是很巧妙。
XML 屬性動畫
這里提一下,屬性動畫當然也可以使用xml文件的方式實現,但是屬性動畫的屬性值一般會牽扯到對象具體的屬性,更多是通過代碼動態獲取,所以xml文件的實現會有些不方便。
<set android:ordering="sequentially">
<set>
<objectAnimator android:propertyName="x" android:duration="500" android:valueTo="400" android:valueType="intType"/>
<objectAnimator android:propertyName="y" android:duration="500" android:valueTo="300" android:valueType="intType"/>
</set>
<objectAnimator android:propertyName="alpha" android:duration="500" android:valueTo="1f"/>
</set>
使用方式:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext, R.anim.property_animator); set.setTarget(myObject); set.start();
xml 文件中的標簽也和屬性動畫的類相對應。
ValueAnimator --- <animator> ObjectAnimator --- <objectAnimator> AnimatorSet --- <set>
這些就是屬性動畫的核心內容。現在使用屬性動畫的特性自定義動畫應該不是難事了。其余便簽的含義,結合之前的內容應該不難理解了。
四、傳統動畫 VS 屬性動畫
相較於傳統動畫,屬性動畫有很多優勢。那是否意味着屬性動畫可以完全替代傳統動畫呢。其實不然,兩種動畫都有各自的優勢,屬性動畫如此強大,也不是沒有缺點。
- 從上面兩幅圖比較可以發現,補間動畫中,雖然使用translate將圖片移動了,但是點擊原來的位置,依舊可以發生點擊事件,而屬性動畫卻不是。因此我們可以確定,屬性動畫才是真正的實現了view的移動,補間動畫對view的移動更像是在不同地方繪制了一個影子,實際的對象還是處於原來的地方。
- 當我們把動畫的repeatCount設置為無限循環時,如果在Activity退出時沒有及時將動畫停止,屬性動畫會導致Activity無法釋放而導致內存泄漏,而補間動畫卻沒有問題。因此,使用屬性動畫時切記在Activity執行 onStop 方法時順便將動畫停止。(對這個懷疑的同學可以自己通過在動畫的Update 回調方法打印日志的方式進行驗證)。
- xml 文件實現的補間動畫,復用率極高。在Activity切換,窗口彈出時等情景中有着很好的效果。
- 使用幀動畫時需要注意,不要使用過多特別大的圖,容易導致內存不足。
五、原文地址
https://github.com/LRH1993/android_interview/blob/master/android/basis/animator.md