Android 的動(dòng)畫分類:
- View視圖動(dòng)畫(補(bǔ)間動(dòng)畫 / 逐幀動(dòng)畫)
-
屬性動(dòng)畫
Android 動(dòng)畫
0x01 View視圖動(dòng)畫
1.1 逐幀動(dòng)畫
frame-by-frame animation
幀動(dòng)畫是順序播放一組預(yù)先定義好的圖片,類似電影播放。
不同于補(bǔ)間動(dòng)畫,系統(tǒng)提供了另外一個(gè)類 AnimationDrawable 來使用幀動(dòng)畫。
實(shí)現(xiàn)方式 2 種:XML定義 和 代碼動(dòng)態(tài)創(chuàng)建
1.1.1 實(shí)現(xiàn)方式1: XML定義
創(chuàng)建動(dòng)畫的 XML 文件路徑:res/drawable/frame_animation.xml
根標(biāo)簽:animation-list
| 屬性名稱 | 說明 |
|---|---|
| oneshot | 是否僅播放一次 |
| duration | 每一幀的顯示時(shí)長(zhǎng) |
XML 示例代碼如下:
// res/drawable/frame_animation.xml
<?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/img_0"
android:duration="50" />
<item
android:drawable="@drawable/img_1"
android:duration="50" />
<item
android:drawable="@drawable/img_2"
android:duration="50" />
</animation-list >
動(dòng)畫加載 Java 示例代碼如下:
ImageView imageView = new ImageView(this);
imageView.setImageResource(R.drawable.frame_animation);
AnimationDrawable animationDrawable = (AnimationDrawable)imageView.getDrawable();
// 播放開始
animationDrawable.start();
// 播放結(jié)束
animationDrawable.stop();
1.1.2 實(shí)現(xiàn)方式2: 代碼動(dòng)態(tài)創(chuàng)建
Java 示例代碼如下:
AnimationDrawable frameAnimation = new AnimationDrawable();
// 參數(shù)1:圖片;參數(shù)2:顯示時(shí)長(zhǎng);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_0), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_1), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_2), 50);
// 是否重復(fù)
frameAnimation.setOneShot(false);
// 播放開始
frameAnimation.start();
// 播放結(jié)束
frameAnimation.stop();
1.1.3 優(yōu)缺點(diǎn)
缺點(diǎn):使用大量圖片,易導(dǎo)致OOM
建議:直接使用 gif,大小可以明顯改善
1.2 補(bǔ)間動(dòng)畫
View的作用對(duì)象是 View。
它支持4種動(dòng)畫效果,分別是 平移動(dòng)畫、縮放動(dòng)畫、旋轉(zhuǎn)動(dòng)畫 和 透明度動(dòng)畫。
而 幀動(dòng)畫 的表現(xiàn)形式和上面4種變換效果不太一樣,單獨(dú)介紹。
View 動(dòng)畫的 4 種變換效果對(duì)應(yīng)著 Animation 的 4 個(gè)子類:TranslateAnimation、ScaleAniamtion、RotateAnimation 和 AlphaAnimation。
| 名稱 | XML標(biāo)簽 | Animation子類 | 效果 |
|---|---|---|---|
| 平移動(dòng)畫 | <translate> | TranslateAnimation | 移動(dòng) View |
| 縮放動(dòng)畫 | <scale> | ScaleAniamtion | 放大/縮小 View |
| 旋轉(zhuǎn)動(dòng)畫 | <rotate> | RotateAnimation | 旋轉(zhuǎn) View |
| 透明度動(dòng)畫 | <alpha> | AlphaAnimation | 改變 View 的透明度 |
| 組合動(dòng)畫 | <set> | AnimationSet | 多個(gè)動(dòng)畫的結(jié)合 |
實(shí)現(xiàn)方式 2 種:XML定義 和 代碼動(dòng)態(tài)創(chuàng)建
對(duì)于 View 動(dòng)畫來說,建議采用 XML 定義動(dòng)畫,可讀性更好。
View 動(dòng)畫既可以是單個(gè)動(dòng)畫,也可以由一系列動(dòng)畫組成。
<set>標(biāo)簽表示動(dòng)畫集合,對(duì)應(yīng) AnimationSet 類,它可以包含若干個(gè)動(dòng)畫,并且它的內(nèi)部也是可以嵌套其他動(dòng)畫集合的。
1.2.1 實(shí)現(xiàn)方式1: XML定義
創(chuàng)建動(dòng)畫的 XML 文件路徑:res/anim/view_animation.xml。
XML 示例代碼如下:
// res/anim/view_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000" // 動(dòng)畫時(shí)長(zhǎng)
android:fillAfter="true" // 動(dòng)畫結(jié)束后,是否留在結(jié)束位置,優(yōu)先級(jí)高于fillBefore屬性,默認(rèn)是false
android:fillBefore="false" //動(dòng)畫結(jié)束后,是否留在開始位置,默認(rèn)是true。
android:fillEnabled="true" //是否應(yīng)用fillBefore的值,對(duì)fillAfter無影響,默認(rèn)是true
android:repeatCount="0" //infinite無限重復(fù)
android:repeatMode="restart" // restart正序重播/reverse反轉(zhuǎn)重播
android:interpolator="@android:anim/overshoot_interpolator"
android:shareInterpolator="true" // 組合動(dòng)畫屬性,組合動(dòng)畫是否和集合(<set></set>)共享一個(gè)插值器,去過不指定,子動(dòng)畫需要單獨(dú)設(shè)定
android:startOffset="100" //組合動(dòng)畫默認(rèn)是全部動(dòng)畫同時(shí)開始,如果不同動(dòng)畫不同開始需要使用該屬性延遲開始時(shí)間
/>
<translate
android:fromXDelta="0" // 水平x方向的起始值
android:fromYDelta="0" // 豎直y方向的起始值
android:toXDelta="500" // 水平x方向的結(jié)束值
android:toYDelta="500" // 豎直y方向的結(jié)束值
/>
<scale
android:fromXScale="0.0" // 起始縮放x的倍數(shù)
android:fromYScale="0.0" // 起始縮放y的倍數(shù)
android:toXScale="1.0" // 結(jié)束縮放x的倍數(shù)
android:toYScale="2.0" // 結(jié)束縮放y的倍數(shù)
android:pivotX="50%" // 縮放中心點(diǎn)的x坐標(biāo)
android:pivotY="50%" // 縮放中心點(diǎn)的y坐標(biāo)
/>
<rotate
android:fromDegrees="0" // 開始的角度
android:toDegrees="180" // 結(jié)束的角度
android:pivotX="20%" // 旋轉(zhuǎn)中心點(diǎn) x 坐標(biāo)
android:pivotY="20%" // 旋轉(zhuǎn)中心點(diǎn) y 坐標(biāo)
/>
<alpha
android:fromAlpha="1.0" // 開始透明度 0.0-1.0
android:toAlpha="0.0" //結(jié)束透明度 0.0-1.0
/>
</set>
動(dòng)畫加載 Java 示例代碼如下:
Animation translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
view.startAnimation(translateAnimation);
pivotX / pivotY 的取值有三個(gè)類型
- 數(shù)字, eg:android:pivotX="50", 該View左上角在x方向上平移50px的點(diǎn),對(duì)應(yīng)Java代碼中設(shè)置參數(shù) Animation.ABSOLUTE。
- 百分比, eg:android:pivotX="50%", 該View左上角在x方向上平移自身寬度50%的點(diǎn),對(duì)應(yīng)Java代碼中設(shè)置參數(shù) Animation.RELATIVE_TO_SELF。
- 百分比p (parent), eg:android:pivotX="50%p"該View左上角在x方向上平移父布局寬度50%的點(diǎn),對(duì)應(yīng)Java代碼中設(shè)置參數(shù) Animation.RELATIVE_TO_PARENT。
1.2.2 實(shí)現(xiàn)方式2: 代碼動(dòng)態(tài)創(chuàng)建
Java 示例代碼如下:
// 組合動(dòng)畫設(shè)置
// 步驟1:創(chuàng)建組合動(dòng)畫shareInterpolator對(duì)象(設(shè)置為true)
AnimationSet setAnimation = new AnimationSet(true);
// 步驟2:設(shè)置組合動(dòng)畫的屬性
// 特別說明以下情況
// 因?yàn)樵谙旅娴男D(zhuǎn)動(dòng)畫設(shè)置了無限循環(huán)(RepeatCount = INFINITE)
// 所以動(dòng)畫不會(huì)結(jié)束,而是無限循環(huán)
// 所以組合動(dòng)畫的下面兩行設(shè)置是無效的
setAnimation.setRepeatMode(Animation.RESTART);
setAnimation.setRepeatCount(1);// 設(shè)置了循環(huán)一次,但無效
// 步驟3:逐個(gè)創(chuàng)建子動(dòng)畫(方式同單個(gè)動(dòng)畫創(chuàng)建方式,此處不作過多描述)
// 子動(dòng)畫1:旋轉(zhuǎn)動(dòng)畫
Animation rotate = new RotateAnimation(0,360,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
rotate.setDuration(1000);
rotate.setRepeatMode(Animation.RESTART);
rotate.setRepeatCount(Animation.INFINITE);
// 子動(dòng)畫2:平移動(dòng)畫
Animation translate = new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT,-0.5f,
TranslateAnimation.RELATIVE_TO_PARENT,0.5f,
TranslateAnimation.RELATIVE_TO_SELF,0
,TranslateAnimation.RELATIVE_TO_SELF,0);
translate.setDuration(10000);
// 子動(dòng)畫3:透明度動(dòng)畫
Animation alpha = new AlphaAnimation(1,0);
alpha.setDuration(3000);
alpha.setStartOffset(7000); // 通過延遲播放達(dá)到順序效果
// 子動(dòng)畫4:縮放動(dòng)畫
Animation scale = new ScaleAnimation(1,0.5f,1,0.5f,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
scale1.setDuration(1000);
scale1.setStartOffset(4000); // 通過延遲播放達(dá)到順序效果
// 步驟4:將創(chuàng)建的子動(dòng)畫添加到組合動(dòng)畫里
setAnimation.addAnimation(alpha);
setAnimation.addAnimation(rotate);
setAnimation.addAnimation(translate);
setAnimation.addAnimation(scale);
// 步驟5:播放動(dòng)畫
mButton.startAnimation(setAnimation);
1.2.3 優(yōu)缺點(diǎn)
缺點(diǎn):
- 作用對(duì)象局限;根據(jù)包分類可知 android.view.animation,只針對(duì)View進(jìn)行動(dòng)畫操作。
- 只改變視覺效果,沒有改變屬性;例如點(diǎn)擊只在原始位置有效(經(jīng)API27測(cè)試,當(dāng)屬性動(dòng)畫View完全不可見時(shí),點(diǎn)擊位置和范圍為原始位置)。
- 動(dòng)畫效果單一,只有四種。
1.2.4 自定義 View 動(dòng)畫
自定義 View 動(dòng)畫是一件既簡(jiǎn)單又復(fù)雜的事情。
簡(jiǎn)單,是因?yàn)榕缮环N新動(dòng)畫只需要繼承Animation這個(gè)抽象類,然后重寫它的 initialize 和 applyTransformation方法,在 initialize 方法中做一些初始化工作,在 applyTransformation 中進(jìn)行相應(yīng)的矩陣變換即可,很多時(shí)候需要采用 Camera 來簡(jiǎn)化矩陣變換的過程。
復(fù)雜,是因?yàn)樽远x View 動(dòng)畫的過程主要是矩陣變換的過程,而矩陣變換是數(shù)學(xué)上的概念。
1.2.5 View 動(dòng)畫的特殊使用場(chǎng)景
比如在 ViewGroup 中可以控制子元素的出場(chǎng)效果,在 Activity 中可以實(shí)現(xiàn)不同 Activity 之間的切換效果。
-
LayoutAnimation
LayoutAnimation 作用于 ViewGroup,為 ViewGroup 指定一個(gè)動(dòng)畫,這樣當(dāng)它的子元素出場(chǎng)時(shí)都會(huì)具有這種動(dòng)畫效果。這種效果常常被用在ListView。
步驟如下:- 定義LayoutAnimation
- 定義子元素具體的入場(chǎng)動(dòng)畫
- 為 ViewGroup 指定
android:layoutAnimation屬性
// 1 /res/anim/layout_animation
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/layout_animation_item"
android:animationOrder="normal"
android:delay="0.5" />
// 2 /res/anim/layout_animation_item
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_interpolator"
android:shareInterpolator="true">
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0" />
<translate
android:fromXDelta="500"
android:toXDelta="0" />
</set>
// 3 在布局文件的ViewGroup中使用 layoutAnimation屬性
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/layout_animation" />
除了在 XML 中指定 LayoutAnimation 外,還可以通過 LayoutAnimationController 來實(shí)現(xiàn)。
Animation animation = AnimationUtils.loadAnimation(this, R.anim.layout_animation_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
- Activity 的切換效果
Activity 有默認(rèn)的切換效果,但是這個(gè)效果我們可以通過 overridePendingTransition(int enterAnim, int exitAnim) 這個(gè)方法進(jìn)行自定義,這個(gè)方法必須在 startActivity(intent) 或者 finish() 之后被調(diào)用才能生效,之前無效。
Fragment 也可以添加動(dòng)畫,由于 Fragment 是在 API 11 中新引入的類,因此為了兼容性我們需要使用support-v4
這個(gè)兼容包,在這個(gè)情況下我們可以通過 FragmentTransaction 中的 setCustomAnimation(int enter, int exit) 方法來添加切換動(dòng)畫,這個(gè)動(dòng)畫需要是 View 動(dòng)畫,之所以不能采用屬性動(dòng)畫是因?yàn)閷傩詣?dòng)畫也是API 11 新引入的。
0x02 屬性動(dòng)畫


屬性動(dòng)畫是 API 11 新加入的特性,和 View 動(dòng)畫不同,它對(duì)作用對(duì)象進(jìn)行了擴(kuò)展,屬性動(dòng)畫可以對(duì)任何對(duì)象做動(dòng)畫,甚至還可以沒有對(duì)象。
屬性動(dòng)畫中有 ViewPropertyAnimator、 ValueAnimator、ObjectAnimator 和 AnimatorSet 等概念,通過它們可以實(shí)現(xiàn)絢麗的動(dòng)畫。
ObjectAnimator extends ValueAnimator extends Animator 位于 android.animation。
ViewPropertyAnimator 位于 android.view,其本質(zhì)內(nèi)部使用 ValueAnimator。
ViewPropertyAnimator < ObjectAnimator < ValueAnimator,使用難度越來越高,但越來越靈活。
三種使用方式舉例:
// ViewPropertyAnimator
view.animate().alphaBy(0.8f)
// ObjectAnimator
Object.ofFloat(view, "alpha", 1.0f, 0.0f, 1.0f).start();
// ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.0f, 0.0f, 1.0f);
value.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
view.setAlpha(animatedValue);
}
});
valueAnimator.start();
ValueAnimator 本身不作用于任何對(duì)象,也就是說直接使用它沒有任何動(dòng)畫效果。它可以對(duì)一個(gè)值做動(dòng)畫,然后我們可以監(jiān)聽其動(dòng)畫過程,在動(dòng)畫過程中修改我們對(duì)象的屬性值,這樣就相當(dāng)于我們的對(duì)象做了動(dòng)畫。
2.1 ViewPropertyAnimator
view.animate().alphaBy(0.8f);
為了滿足面向?qū)ο缶幊趟枷耄?strong>View 中引入了ViewPropertyAnimator,可以鏈?zhǔn)秸{(diào)用。
注意 ViewPropertyAnimator 沒有 setRepeatCount() 和 setRepeatMode(),不能重復(fù)
| View 中的方法 | 功能 | 對(duì)應(yīng) ViewPropertyAnimator 的方法 |
|---|---|---|
| setTranslationX() | 設(shè)置x軸偏移 | translationX() / translationXBy() |
| setTranslationY() | 設(shè)置y軸偏移 | translationY() / translationYBy() |
| setTranslationZ() | 設(shè)置z軸偏移 | translationZ() / translationZBy() |
| setX() | 設(shè)置x軸絕對(duì)位置 | x() / xBy() |
| setY() | 設(shè)置y軸絕對(duì)位置 | y() / yBy() |
| setZ() | 設(shè)置z軸絕對(duì)位置 | z() / zBy() |
| setRotation() | 設(shè)置平面旋轉(zhuǎn) | rotation() / rotationBy() |
| setRotationX() | 設(shè)置沿x軸旋轉(zhuǎn) | rotationX() / rotationXBy() |
| setRotationY() | 設(shè)置沿y軸旋轉(zhuǎn) | rotationY() / rotationYBy() |
| setScaleX() | 設(shè)置橫向放縮 | scaleX() / scaleXBy() |
| setScaleY() | 設(shè)置縱向放縮 | scaleY() / scaleYBy() |
| setAlpha() | 設(shè)置透明度 | alpha() / alphaBy() |
注意
- View#setX()沒有動(dòng)畫漸變效果,直接將該View放到設(shè)置位置;而 ViewPropertyAnimator#x() 有平移過渡動(dòng)畫。
- ViewPropertyAnimator#x() 如果有動(dòng)畫執(zhí)行x()將取消。例如view.x(500).translationX(50),將只執(zhí)行translationX(50),不執(zhí)行x(500)
- ViewPropertyAnimator#By() 表示在當(dāng)前位置的基礎(chǔ)上進(jìn)行操作
2.2.1 實(shí)現(xiàn)方式1: XML定義
創(chuàng)建 XML 文件路徑:res/animator/view_animator.xml
XML示例代碼
如下:
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially">
<objectAnimator
android:duration="2000"
android:propertyName="translationX"
android:valueFrom="0"
android:valueTo="500"
android:valueType="floatType" />
<set android:ordering="together">
<objectAnimator
android:duration="3000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360"
android:valueType="floatType" />
<set android:ordering="sequentially">
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</set>
</set>
動(dòng)畫加載 Java 示例代碼如下:
Animator animator = AnimatorInflater.loadAnimator(context,R.animator.view_animator);
animator.setTarget(view);
animator.start();
2.2.2 實(shí)現(xiàn)方式2: 代碼動(dòng)態(tài)創(chuàng)建
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", 0, 500f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).before(moveIn); // with / before / after
animSet.setDuration(5000);
animSet.start();
2.3 屬性動(dòng)畫的原理
屬性動(dòng)畫要求動(dòng)畫作用的對(duì)象提供該屬性的 get 和 set 方法,屬性動(dòng)畫根據(jù)外界傳遞的該屬性的初始值和最終值,以動(dòng)畫的效果多次去調(diào)用 set 方法,每次傳遞給 set 方法的值都不一樣,確切來說是隨著時(shí)間的推移,所傳遞的值越來越接近最終的值。
自定義屬性示例:
public class SportsView extends View {
float progress = 0;
......
// 創(chuàng)建 getter 方法
public float getProgress() {
return progress;
}
// 創(chuàng)建 setter 方法
public void setProgress(float progress) {
this.progress = progress;
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);
......
}
}
......
// 創(chuàng)建 ObjectAnimator 對(duì)象
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "progress", 0, 65);
// 執(zhí)行動(dòng)畫
animator.start();
對(duì) object 的屬性 abc 做動(dòng)畫,如果想讓動(dòng)畫生效,需要滿足兩個(gè)條件:
- object 必須提供 setAbc 方法,如果動(dòng)畫的時(shí)候沒有傳遞初始值,那么還要提供 getAbc 方法,因此系統(tǒng)要去取 abc 屬性的初始值。(如果這條不滿足,程序直接Crash)
- object 的 setAbc 對(duì)屬性 abc 的改變必須反映出來(requestLayout/invalidate),才會(huì)有效果。(如果這條不滿足,動(dòng)畫無效果但不會(huì)Crash)
動(dòng)畫不生效,只滿足條件1而未滿足條件2,解決方法有 3 種:
給你的對(duì)象加上 get 和 set 方法,如果你有權(quán)限的話;
這個(gè)方法最簡(jiǎn)單,加上 get 和 set 就搞定了,但針對(duì)Android SDK內(nèi)部實(shí)現(xiàn)的View往往沒有權(quán)限不可行。用一個(gè)類來包裝原始對(duì)象,間接為其提供 get 和 set 方法;
private void performAnimation(View view) {
ViewWrapper wrapper = new ViewWrapper(view);
ObjectAnimator.ofInt(wrapper, "width", 0)
.setDuration(5000)
.start();
}
private static class ViewWrapper {
private View mTarget;
public ViewWrapper(View target) {
this.mTarget = target;
}
public int getWidth() {
return mTarget.getLayoutParams().width;
}
public void setWidth(int width) {
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}
- 采用 ValueAnimator,監(jiān)聽動(dòng)畫過程,自己實(shí)現(xiàn)屬性的改變;
private void valueAnimation(View targetView, int start, int end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
private IntEvaluator evaluator = new IntEvaluator();
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
Integer curValue = evaluator.evaluate(fraction, start, end);
targetView.getLayoutParams().width = curValue;
targetView.requestLayout();
}
});
valueAnimator.setDuration(5000).start();
}
2.4 時(shí)間插值器 TimeInterpolator / 類型估值器 TypeEvaluator
TimeInterpolator:時(shí)間插值器
根據(jù) 時(shí)間流逝的百分比 來計(jì)算 當(dāng)前屬性值改變的百分比。
TypeEvaluator:類型估值算法
根據(jù) 當(dāng)前屬性值改變的百分比 來計(jì)算改變后的 屬性值。
2.4.1 時(shí)間插值器 TimeInterpolator
android.animation.TimeInterpolator
android.view.animation.Interpolator
Interpolator extends TimeInterpolator
屬性動(dòng)畫內(nèi)部使用的是 TimeInterpolator,補(bǔ)間動(dòng)畫內(nèi)部使用的是 Interpolator。
TimeInterpolator 接口是屬性動(dòng)畫中新增的,用于兼容Interpolator 接口,這樣原先 Interpolator 的實(shí)現(xiàn)類就可以直接在屬性動(dòng)畫中使用
列舉官方插值器:
| Java類 | 描述 |
|---|---|
| LinearInterpolator | 勻速 |
| AccelerateInterpolator | 加速 |
| DecelerateInterpolator | 減速 |
| AccelerateDecelerateInterpolator | 先加速后減速(默認(rèn)) |
| AnticipateInterpolator | 先退后然后加速前進(jìn) |
| OvershootInterpolator | 完成動(dòng)畫,超出后回到結(jié)束位置 |
| AnticipateOvershootInterpolator | 先退后再加速前進(jìn),超出終點(diǎn)后再回終點(diǎn) |
| BounceInterpolator | 最后階段彈球效果 |
| CycleInterpolator | 周期運(yùn)動(dòng) |
| PathInterpolator | 自定義動(dòng)畫完成度/時(shí)間完成度曲線(0-1) |
| FastOutLinearInInterpolator | 加速 |
| LinearOutSlowInInterpolator | 持續(xù)減速 |
| FastOutSlowInInterpolator | 先加速再減速 |
最后三個(gè)是Android5.0(API21)新增,和之前的類似,但是軌跡稍有區(qū)別
代碼舉例說明:
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {
// 僅貼出關(guān)鍵代碼
...
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
// input的運(yùn)算邏輯如下:
// 使用了余弦函數(shù),因input的取值范圍是0到1,那么cos函數(shù)中的取值范圍就是π到2π。
// 而cos(π)的結(jié)果是-1,cos(2π)的結(jié)果是1
// 所以該值除以2加上0.5后,getInterpolation()方法最終返回的結(jié)果值還是在0到1之間。只不過經(jīng)過了余弦運(yùn)算之后,最終的結(jié)果不再是勻速增加的了,而是經(jīng)歷了一個(gè)先加速后減速的過程
// 所以最終,fraction值 = 運(yùn)算后的值 = 先加速后減速
// 所以該差值器是先加速再減速的
}
}
2.4.2 類型估值器 TypeEvaluate
根據(jù)當(dāng)前屬性值變化的百分比、初始值、結(jié)束值來計(jì)算當(dāng)前屬性的具體數(shù)值。當(dāng)前屬性值 = 初始值 + (結(jié)束值 - 初始值) * 百分比
實(shí)現(xiàn)
- 實(shí)現(xiàn)
TypeEvaluator<T>接口 - 重寫
public T evaluate(float fraction, T startValue, T endValue)方法
官方示例
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
完整示例
public class Point {
private float x;
private float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
}
public class PointEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
float startValueX = startValue.getX();
float startValueY = startValue.getY();
float currentX = startValueX + (endValue.getX() - startValueX) * fraction;
float currentY = startValueY + (endValue.getY() - startValueY) * fraction;
return new Point(currentX, currentY);
}
}
public class PointView extends View {
public static final float RADIUS = 70f;
private Point ccurrentPoint;
private Paint mPaint;
public PointView(Context context) {
this(context, null);
}
public PointView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (ccurrentPoint == null) {
ccurrentPoint = new Point(RADIUS, RADIUS);
}
canvas.drawCircle(ccurrentPoint.getX(), ccurrentPoint.getY(), RADIUS, mPaint);
}
public Point getCurrentPoint() {
return ccurrentPoint;
}
public void setCurrentPoint(Point currentPoint) {
this.ccurrentPoint = currentPoint;
invalidate();
}
}
Point pointStart = new Point(70, 70);
Point pointEnd = new Point(700, 1000);
// 使用 ObjectAnimator
ObjectAnimator objectAnimator = ObjectAnimator.ofObject(point_view, "currentPoint", new PointEvaluator(), pointStart, pointEnd);
objectAnimator.setDuration(1000);
objectAnimator.start();
// 使用 ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointEvaluator(), pointStart, pointEnd);
valueAnimator.setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point animatedValue = (Point) animation.getAnimatedValue();
point_view.setCurrentPoint(animatedValue);
}
});
valueAnimator.start();
2.5 屬性動(dòng)畫的監(jiān)聽器
屬性動(dòng)畫提供了監(jiān)聽器用于監(jiān)聽動(dòng)畫的播放過程,主要:AnimatorUpdateListener、AnimatorPauseListener和 AnimatorListener。
Animator
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);
}
public static interface AnimatorPauseListener {
void onAnimationPause(Animator animation);
void onAnimationResume(Animator animation);
}
為了便于開發(fā),系統(tǒng)還提供了 AnimatorListenerAdapter 這個(gè)類,它是AnimatorListener 和 AnimatorPauseListener 的適配器類,這樣我們就可以有選擇的實(shí)現(xiàn)上面的6個(gè)方法。
ValueAnimator extends Animator
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
AnimatorUpdateListener 比較特殊,它會(huì)監(jiān)聽整個(gè)動(dòng)畫過程,動(dòng)畫是由許多幀組成的,每播放一幀,onAnimationUpdate 就會(huì)被調(diào)用一次。
2.5.1 添加監(jiān)聽器
ViewPropertyAnimator # setListener() / setUpdateListener()
ObjectAnimator # addListener() / addUpdateListener / addPauseListener()
VauleAnimator # addListener() / addUpdateListener / addPauseListener()
2.5.2 移除監(jiān)聽器
ViewPropertyAnimator # setListener(null) / setUpdateListener(null) 填null來移除
ObjectAnimator # removeListener() / removeUpdateListener() / removePauseListener()
VauleAnimator # removeListener() / removeUpdateListener() / removePauseListener()
ObjectAnimator 支持pause()方法暫停
ViewPropertyAnimator 不支持setRepeatMode() / setRepeatCount() 方法
ViewPropertyAnimator 獨(dú)有withStartAction(Runnable runnable) 和 withEndAction(Runnable runnable) 方法,可設(shè)置一次動(dòng)畫開始或結(jié)束的監(jiān)聽。即使重新開始動(dòng)畫,也不會(huì)回調(diào),是一次性的。而AnimatorListener是持續(xù)有效的。
withEndAction() 只有在動(dòng)畫正常結(jié)束才會(huì)調(diào)用,而在動(dòng)畫被取消時(shí)是不會(huì)執(zhí)行的。而 AnimatorListener.onAnimationEnd() 在取消之后也會(huì)被調(diào)用,在調(diào)用 onAnimationCancel()之后調(diào)用
2.6 PropertyValuesHolder 同一動(dòng)畫中改變多個(gè)屬性
ObjectAnimator.ofPropertyValuesHolder
關(guān)鍵字:一邊一邊,一個(gè)動(dòng)畫屬性同時(shí)執(zhí)行,區(qū)別多個(gè)動(dòng)畫先后執(zhí)行。一個(gè)動(dòng)畫需要共享開始時(shí)間/結(jié)束時(shí)間/Interpolator等等設(shè)定,PropertyValuesHolder不能有先后次序執(zhí)行動(dòng)畫了。
很多時(shí)候,在同一個(gè)動(dòng)畫中需要改變多個(gè)屬性,例如改變透明度的同時(shí)改變尺寸。
使用 ViewPropertyAnimator如下:
view.animate()
.scaleX(0.0f)
.scaleY(0.0f)
.alpha(0.0f)
但是ObjectAnimator,是不能這么用的。需要使用PropertyValuesHolder來同時(shí)在一個(gè)動(dòng)畫里改變多個(gè)屬性
PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 0.0f);
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 0.0f);
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 0.0f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder1, holder2, holder3)
animator.start();
ViewPropertyAnimator 動(dòng)畫完成之后會(huì)停留在結(jié)束位置,再次點(diǎn)擊執(zhí)行動(dòng)作操作不會(huì)執(zhí)行動(dòng)畫
ObjectAnimator 動(dòng)畫完成之后會(huì)停留在結(jié)束位置,再次點(diǎn)擊執(zhí)行動(dòng)作操作會(huì)從最原始狀態(tài)重新執(zhí)行一次動(dòng)畫。且Animator沒有Animation的setFillAfter() 和setFillBefore()方法
關(guān)于點(diǎn)擊范圍的測(cè)試說明(API27),屬性動(dòng)畫執(zhí)行后屬性發(fā)生變化,即點(diǎn)擊范圍和位置會(huì)更新,但經(jīng)測(cè)試當(dāng)View完全不可見時(shí),點(diǎn)擊位置和范圍為原始位置
ObjectAnimator.ofInt()
ObjectAnimator.ofFloat()
ObjectAnimator.ofMultiFloat()
ObjectAnimator.ofPropertyValuesHolder
2.7 AnimatorSet 多個(gè)動(dòng)畫配合執(zhí)行
關(guān)鍵字:一邊一邊,一個(gè)動(dòng)畫屬性同時(shí)執(zhí)行,區(qū)別多個(gè)動(dòng)畫先后執(zhí)行。一個(gè)動(dòng)畫需要共享開始時(shí)間/結(jié)束時(shí)間/Interpolator等等設(shè)定,PropertyValuesHolder不能有先后次序執(zhí)行動(dòng)畫了。
區(qū)別 AnimationSet,只能通過設(shè)置單個(gè)動(dòng)畫的 setStartOffset 來延遲時(shí)間進(jìn)行先后執(zhí)行順序。
animatorSet.play(a1) / playTogether(a1,a2) / playSequentially(a1,a2)
.with(a3)
.before(a4)
.after(a5)
其中以 playXXX 開始得到 AnimatorSet.Builder 對(duì)象,然后調(diào)用with/before/after進(jìn)行管理。
2.8 PropertyValuesHolders.ofKeyframe() 把同一個(gè)屬性拆分
把一個(gè)屬性拆分成多段,執(zhí)行更加精細(xì)的屬性動(dòng)畫。
Keyframe keyframe1 = Keyframe.ofFloat(0.0f, 0);
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
Keyframe keyframe3 = Keyframe.ofFloat(1.0f, 80);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("translationX", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTextMessage, holder);
animator.start();
0x03 使用動(dòng)畫的注意
OOM問題
主要出現(xiàn)在幀動(dòng)畫中,但圖片數(shù)量過多且圖片較大時(shí),極易出現(xiàn)OOM,盡量避免使用幀動(dòng)畫內(nèi)存泄漏
在 屬性動(dòng)畫 中有一類無限循環(huán)的動(dòng)畫,這類動(dòng)畫需要在Activity退出時(shí)及時(shí)停止,否則將導(dǎo)致Activity無法釋放從而造成內(nèi)存泄漏。
View動(dòng)畫 不存在這個(gè)問題。兼容性問題
動(dòng)畫在 3.0 以下的系統(tǒng)上(API 11)有兼容性問題,在某些特殊場(chǎng)景可能無法正常工作,因此要做好適配View動(dòng)畫的問題
View動(dòng)畫是對(duì)View的影響做動(dòng)畫,并不是真正地改變View的狀態(tài),因此有時(shí)候會(huì)出現(xiàn)動(dòng)畫完成后View無法隱藏的現(xiàn)象,即setVisibility(View.GONE)失效,這時(shí)只需調(diào)用view.clearAnimation()清除View動(dòng)畫即可解決此問題。不要使用px
在進(jìn)行動(dòng)畫的過程中,要盡量使用dp。使用px會(huì)導(dǎo)致在不同的設(shè)備上有不同的效果。動(dòng)畫元素的交互
將 view 移動(dòng)(平移)后,在Android 3.0 以前的系統(tǒng)上,不管是 View動(dòng)畫 還是 屬性動(dòng)畫,新位置 均無法觸發(fā)單擊事件,同時(shí),老位置仍然可以觸發(fā)單擊事件。View只是在視覺上不存在了,移動(dòng)回原位置后,單擊事件繼續(xù)生效。
從3.0開始,屬性動(dòng)畫的單擊事件觸發(fā)位置為移動(dòng)后的位置,但View動(dòng)畫仍然在原位置。硬件加速
在使用動(dòng)畫的過程中,開啟硬件加速,會(huì)提高動(dòng)畫的流暢性。
但不可濫用!
1.硬件層會(huì)比普通view繪制多做很多的工作。首先將view繪制到GPU的一個(gè)層中,然后GPU再把這個(gè)層繪制到window上。
2.與所有的緩存一樣,GPU的硬件緩存也會(huì)有失敗幾率。如果動(dòng)畫進(jìn)行中調(diào)用 invalidate(),緩存的層會(huì)不得不重新渲染。如果不斷有無效的硬件層產(chǎn)生的話,還不如不使用硬件加速。因?yàn)樵黾佑布泳彺鏁?huì)增加額外的開銷。
簡(jiǎn)單的view繪制 使用硬件加速會(huì)增加不必要的開銷,不建議開啟。
使用示例:
// 設(shè)置硬件加速
myView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
// 設(shè)置動(dòng)畫
ObjectAnimator animator = ObjectAnimator.ofFloat(myView, View.TRANSLATION_X, 150);
// 設(shè)置一個(gè)回調(diào),在動(dòng)畫完成時(shí)取消硬件加速
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
myView.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
// 開始動(dòng)畫
animator.start();
在API 14以上使用屬性動(dòng)畫,可以更簡(jiǎn)潔:
myView.animate()
.translationX(150)
.withLayer()
.start();
0x04 參考資料
感謝以下文章作者
HenCoder Android 自定義 View 1-6:屬性動(dòng)畫 Property Animation(上手篇)
Android:這是一份全面 & 詳細(xì)的補(bǔ)間動(dòng)畫使用教程
Android開發(fā)藝術(shù)探索 第7章 Android動(dòng)畫深入分析
Android 屬性動(dòng)畫詳解與源碼分析
