Android View 動畫和屬性動畫學習筆記
以下內容來自 Android 開發藝術探索第七章 Android 動畫深入分析學習筆記。
Android 的動畫可以分為三種 : View 動畫、幀動畫和屬性動畫。
View 動畫:通過對場景里的對象不斷做圖像變化(平移、縮放、旋轉、透明度)從而產生動畫效果,它是一種漸近式動畫,并且 View 動畫支持自定義。
幀動畫: 通過順序播放一系列圖像從而產生動畫效果,可以簡單理解為圖片切換動畫,如果圖片過多過大就會導致 OOM ,其實幀動畫也屬于 View 動畫的一種,只不過它和平移、旋轉等常見的 View 動畫在表現形式上略有不同而已。
屬性動畫:通過動態地改變對象的屬性從而達到動畫效果,屬性動畫為 API 11 的新特性,在低版本中無法直接使用屬性動畫,但是可以使用動畫兼容庫 nineoldandroids 來使用它。
二、View 動畫
2.1 View 動畫的種類
View 動畫四種變換效果對應著 Animation 的四個子類:TranslateAnimation 、ScaleAnimation 、RotateAnimation 和 AlphaAnimation ,這四種動畫既可以通過 XML 來定義,也可以通過代碼來動態創建,建議采用 XML 來定義 View 動畫,這樣可讀性更好。
要使用 View 動畫,首先要創建動畫的 XML 文件,這個文件的路徑為: res/anim/filename.xml 。 View 的 XML 動畫描述文件是有固定語法的,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<setxmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
//動畫插值器,影響動畫的播放速度
android:interpolator="@android:anim/accelerate_interpolator"
//表示集合中的動畫是否和集合共享一個插值器
android:shareInterpolator="true" >
//透明度動畫,對應 AlphaAnimation 類,可以改變 View 的透明度
<alpha
android:duration="3000"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
//旋轉動畫,對應著 RotateAnimation ,它可以使 View 具有旋轉的動畫效果
<rotate
android:duration="2000"
android:fromDegrees="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:startOffset="3000"
android:toDegrees="180" />
<!--通過設置第一個alpha動畫播放3s后啟動rotate動畫實現組合動畫,如果不設置startOffset則同時播放
pivotX:表示旋轉時候的相對軸的坐標點,即圍繞哪一點進行旋轉,默認情況下軸點是 View 中心
-->
//平移動畫,對應 TranslateAnimation 類,可以使 View 完成垂直或者水平方向的移動效果。
<translate
android:fromXDelta="500"
android:toXDelta="0" />
//縮放動畫,對應 ScaleAnimation 類,可以使 View 具有放大和縮小的動畫效果。
<scale
android:duration="1000"
android:fromXScale="0.0"
android:fromYScale="0.0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:pivotX="50"
android:pivotY="50"
android:toXScale="2"
android:toYScale="2" />
</set>
從上面代碼可以看出 View 動畫既可以是單個動畫,也可以是一些列動畫組成。 <set> 標簽表示動畫集合,對應 Animationset 類,它可以表示若干個動畫,并且它的內部也可以嵌套其他動畫集合,它的兩個屬性含義如下:
android :interpopator
表示動畫集合所采用的插值器,插值器影響動畫的播放速度,比如實現非勻速動畫效果,這個屬性可以不指定,默認為 android:interpolator="@android:anim/accelerate_decelerate_interpolator" ,即加速減速插值器。
android:shareInterpolator
表示集合中的動畫是否和集合共享一個插值器,如果集合不指定插值器,那么子動畫就需要單獨指定所需要的插值器或者使用默認值。
android:fillAfter
表示動畫結束以后, View 是否停留在結束動畫的位置,如果為 false , View 會回到動畫開始的位置。
fillAfter是指動畫結束時畫面停留在最后一幀,這個參數不能在 </alpha>,</scale>,</translate>,</rotate> 中設置,這是沒有作用的,必須通過以下方式設置:在動畫 XML 文件的 </set> 節點中設置:在程序 Java 代碼中進行設置: setFillAfter(true) 。
以上為 View 動畫的 XML 格式,下面介紹在 Java 代碼中去如何使用:
//透明漸變動畫
Animation animation = AnimationUtils.loadAnimation(this, R.anim.alpha);
view.startAnimation(animation);
除了通過 XML 文件來實現動畫效果外,可以直接在 Java 代碼中去實現:
//創建一個透明度漸變動畫,在 2000ms 內,將 VIew 的透明度由 0.1f 變為 1.0f
AlphaAnimation alphaAnimation=new AlphaAnimation(0.1f,1.0f);
alphaAnimation.setDuration(2000);
alphaAnimation.setRepeatCount(4);
alphaAnimation.setRepeatMode(Animation.REVERSE);
view.startAnimation(alphaAnimation);
另外可以通過 setAnimationListener 給 View 動畫添加過程監聽,接口如下:
public static interfaceAnimationListener{
//監聽動畫開始
voidonAnimationStart(Animation animation);
/**
* <p>Notifies the end of the animation. This callback is not invoked
* for animations with repeat count set to INFINITE.</p>
*
* @param animation The animation which reached its end.
*/
voidonAnimationEnd(Animation animation);
/**
* <p>Notifies the repetition of the animation.</p>
*
* @param animation The animation which was repeated.
*/
voidonAnimationRepeat(Animation animation);
}
2.2 自定義 View 動畫
除了系統提供的四種動畫外,我們可以根據需求自定義動畫,自定義一個新的動畫只需要繼承 Animation 這個抽象類,然后重寫它的 inatialize 和 applyTransformation 這兩個方法,在 initialize 方法中做一些初始化工作,在 Transformation 方法中進行矩陣變換即可,很多時候才有 Camera 來簡化矩陣的變換過程,其實自定義動畫的主要過程就是矩陣變換的過程,矩陣變換是數學上的概念,需要掌握該方面知識方能輕松實現自定義動畫,例子可以參考 Android 的 APIDemos 中的一個自定義動畫 Rotate3dAnimation ,這是一個可以圍繞 Y 軸旋轉并同時沿著 Z 軸平移從而實現類似一種 3D 效果的動畫。
2.3 幀動畫
幀動畫是順序播放一組預先定義好的圖片,類似于電影播放。不同于 View 動畫,系統提供了另一個類 AnimationDrawble 來使用幀動畫,使用的時候,需要通過 XML 定義一個 AnimationDrawble ,如下:
//\res\drawable\frame_animation_list.xml
<?xml version="1.0" encoding="utf-8"?>
<!--
根標簽為 animation-list,其中 oneshot 代表著是否只展示一遍,設置為 false 會不停的循環播放動畫
根標簽下,通過 item 標簽對動畫中的每一個圖片進行聲明
android:duration 表示展示所用的該圖片的時間長度
-->
<animation-listxmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/one"
android:duration="2000"/>
<item
android:drawable="@drawable/two"
android:duration="2000"/>
<item
android:drawable="@drawable/three"
android:duration="2000"/>
<!--<item-->
<!--android:drawable="@drawable/four"-->
<!--android:duration="500"/>-->
<!--<item-->
<!--android:drawable="@drawable/five"-->
<!--android:duration="500"/>-->
<!--<item-->
<!--android:drawable="@drawable/six"-->
<!--android:duration="500"/>-->
</animation-list
然后將上述 Drawble 作為 View 的背景并通過 Drawble 來進行播放動畫:
//將圖片設置成背景圖片
view.setBackgroundResource(R.drawable.frame_animation_list);
AnimationDrawable frameAnimation = (AnimationDrawable) image.getBackground();
frameAnimation.start();
幀動畫使用比較簡單,但是容易引起 OOM ,所以盡量避免使用過多過大的圖片。
三、 View 動畫的特殊使用場景
View 動畫除了可以實現的四種基本的動畫效果外,還可以在一些特殊的場景下使用,比如在 ViewGroup 中可以控制子元素的出場效果,在 Activity 中可以實現不同 Activity 之間的切換效果。
3.1 LayoutAnimation
LayoutAnimation 作用于 ViewGroup ,為 ViewGroup 指定一個動畫,這樣當它的子元素出場時候就有動畫效果了。這種效果常常被用在 ListView 上,這樣 ListView 每個 Item 都會以一種動畫效果形式出現,LayoutAnimation 也是一個 View 動畫,給 ViewGroup 子元素添加動畫效果遵守以下幾個步驟:
(1) 定義 LayoutAnimation ,如下所示:
//res/anim/layout_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimationxmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.5"
android:animationOrder="normal"
android:animation="@anim/zoom_in">
</layoutAnimation>
其中幾個屬性解釋如下:
android:delay 表示子元素開始動畫的延時時間,取值為子元素入場動畫時間 duration 的倍數,比如子元素入場動畫時間周期為 300ms ,那么 0.5 表示每個子元素都需要延遲 150ms 才能播放入場動畫,即第一個子元素延遲 150ms 開始播放入場動畫,第二個子元素延遲 300ms 開始播放入場動畫,依次類推進行。
android:animationOrder表示子元素動畫的開場順序,normal(正序)、reverse(倒序)、random(隨機)。
android:animation表示為子元素指定具體的動畫效果,例如上面代碼中 “@anim/zoom_in” 是我們自己定義的一個 XML 動畫。
(2)為子元素指定具體的動畫效果:
//\res\anim\zoom_in.xml
<setxmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="1000"
android:fromAlpha="0.1"
android:toAlpha="1.0"/>
<scale
android:pivotY="50%"
android:pivotX="50%"
android:fromYScale="0.1"
android:toYScale="1.0"
android:duration="1000"
android:fromXScale="0.1"
android:toXScale="1.0"/>
</set>
(3) 為 ViewGroup 指定 android:layoutAnimation 屬性 : android:layoutAnimation="@anim/layout_animation" 對于 ListView 來說,這樣它的所有 Item 就具有入場動畫效果了,這種方式適合于所有的 ViewGroup 。
<ListView
android:layoutAnimation="@anim/layout_animation"
android:id="@+id/lv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ListView>
除了在 XML 文件中指定 layout Ainmation 屬性外,還可以通過 LayoutAnimationController 來實現,具體代碼如下:
//用于控制子 view 動畫效果
LayoutAnimationController layoutAnimationController= new LayoutAnimationController(AnimationUtils.loadAnimation(this,R.anim.zoom_in));
layoutAnimationController.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(layoutAnimationController);
listView.startLayoutAnimation();
3.2 Activity 的切換效果
Activity 有默認的切換效果,但是這個效果也可以我們自己定義,主要用到的方法是 overridePenddingTransation(int enterAnim,int exitAnim) ,該方法必須要在 startActivity(intent) 和 finish() 方法之后調用才會有效,參數含義:
enterAnim: Activity 打開時所需動畫的資源 id 。
exitAnim: Acitivty 退出時所需動畫的資源 id 。
當啟動一個 Activity 的時候,可以按照如下方式添加自定義的切換效果:
//啟動Activity帶動畫
Intent intent=new Intent(MainActivity.this,Main2Activity.class);
startActivity(intent);
overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
當 Activity 退出時,指定切換效果:
@Override
publicvoidfinish(){
super.finish();
overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
}
需要注意的是 overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out) 這個方法必須位于 startActivity(intent) 或者 finish() 方法后面,否則將不起作用。
Fragment 也可以添加切換動畫,通過 FragmentTransation 中的 setCustomAnimations() 方法來實現切換動畫,這個動畫需要的是 View 動畫,不能使用屬性動畫,因為屬性動畫也是 API11 才引入的,不兼容。
四、屬性動畫
屬性動畫是 API 11 引入的新特性,屬性動畫可以對任何對象做動畫,甚至還可以沒有對象。
4.1 使用屬性動畫
屬性動畫可以對任意對象的屬性做動畫而不僅僅是 View 對象,動畫默認時間間隔是 300ms ,默認幀率是 10ms/幀。其可以達到的效果是:在一個時間間隔內完成對象從一個屬性值到另一個屬性值的改變,因此屬性動畫幾乎無所不能,只要對象有這個屬性,它都能實現動畫效果,但是屬性動畫是從 API 11 才開始有,可以使用動畫兼容庫 nineoldandroids 動畫庫來兼容以前版本,在 API 11 以前其內部是通過代理 View 動畫來實現的,因此在低版本上,它本質上還是 View 動畫。nineoldandroids 動畫庫和原生屬性動畫 API 使用功能完全一樣。比較常見的幾個動畫類:ValueAnimator、ObjectAnimator 和 AnimatorSet ,其中 ObjectAnimator 繼承自 ValueAnimator ,AnimatorSet 是動畫集合,可以定義一組動畫,使用示例如下:
(1) 改變一個對象 TranslationY 屬性,讓其沿著 Y 軸平移一段距離
/**
* 將 View 沿著垂直方向移動 View 高度的距離
* @param targetView 被移動的 View
*/
privatevoidtranslateViewByObjectAnimator(View targetView){
//TranslationY 目標 View 要改變的屬性
//ivShow.getHeight() 要移動的距離
ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(targetView,"TranslationY",ivShow.getHeight());
objectAnimator.start();
}
(2)改變一個對象的背景色屬性,以下代碼實現改變一個 View 的背景色:
/**
* 改變 View 對象的背景色由紅色變為藍色
* @param targetView
*/
privatevoidchangeViewBackGroundColor(View targetView){
ValueAnimator valueAnimator=ObjectAnimator.ofInt(targetView,"backgroundColor", Color.RED,Color.BLUE);
valueAnimator.setDuration(3000);
//設置估值器,該處插入顏色估值器
valueAnimator.setEvaluator(new ArgbEvaluator());
//無限循環
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
//翻轉模式
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.start();
}
(3)動畫集合,5 秒內對 View 旋轉、平移、縮放和透明度進行了改變
/**
* 啟動一個動畫集合
* @param targetView
*/
privatevoidstartAnimationSet(View targetView){
AnimatorSet animatorSet=new AnimatorSet();
animatorSet.playTogether(ObjectAnimator.ofFloat(targetView,"rotationX",0,360),
//旋轉
ObjectAnimator.ofFloat(targetView,"rotationY",0,360),
ObjectAnimator.ofFloat(targetView,"rotation",0,-90),
//平移
ObjectAnimator.ofFloat(targetView,"translationX",0,90),
ObjectAnimator.ofFloat(targetView,"translationY",0,90),
//縮放
ObjectAnimator.ofFloat(targetView,"scaleX",1,1.5f),
ObjectAnimator.ofFloat(targetView,"scaleY",1,1.5f),
//透明度
ObjectAnimator.ofFloat(targetView,"alpha",1,0.25f,1));
animatorSet.setDuration(3000).start();
}
屬性動畫除了通過代碼實現以外,還可以通過 XML 來實現,屬性動畫需要定義在 res/animator/目錄下,示例如下:
\res\animator\value_animator.xml
<?xml version="1.0" encoding="utf-8"?><!--set 標簽對應著 AnimatorSet-->
<setxmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<!--對應著 ObjectAnimator-->
<objectAnimator
android:propertyName="x"
android:repeatCount="infinite"
android:repeatMode="reverse"
android:startOffset="10"
android:valueTo="300"
android:valueType="floatType" />
<!--其中propertyName 屬性設置為translationX ,valueType 設置為floatType 可以正常啟動
如果 valueType 設置為 intType 將報錯,即屬性類型必須為 floatType 類型,并且android:propertyName="translationX" 表示移動 300 ,而 android:propertyName="x"表示移動到300 ,是兩個不同屬性-->
<!--startOffset 指定延遲多久開始播放動畫-->
<!--valueType 表示指定的 android:propertyName 所指定屬性的類型,intType 表示指定屬性是整型的,
如果指定屬性為顏色,那么不需要指定 valueType 屬性,系統會自動處理
repeatCount 表示動畫循環次數,默認值為0,-1 表示無限循環-->
<objectAnimator
android:propertyName="y"
android:repeatCount="infinite"
android:repeatMode="reverse"
android:startOffset="10"
android:valueTo="300"
android:valueType="floatType" />
<!--對應著 ValueAnimator-->
<!--<animator-->
<!--android:propertyName="rotation"-->
<!--android:duration="300"-->
<!--android:valueFrom="0"-->
<!--android:valueTo="360"-->
<!--android:startOffset="10"-->
<!--android:repeatCount="infinite"-->
<!--android:repeatMode="reverse"-->
<!--android:valueType="intType"/>-->
</set>
實際開發中建議使用代碼實現屬性動畫。
4.2 理解插值器和估值器
Timeinterpolator:時間插值器,是一個接口類,它的作用是根據時間流逝的百分比來計算當前屬性改變的百分比,系統自帶有 LinearInterpolator (線性時間插值器,勻速動畫)、AccelerateDecelerateInterpolater(加速減速插值器,動畫兩頭慢,中間快)和DecelerateInterpolater (減速插值器,動畫越來越慢)等,均實現了該接口。
TypeEvaluator: 類型估值算法,也叫估值器,是一個接口類,它的作用是根據當前屬性改變的百分比來計算改變后的屬性值,系統自帶有 IntEvaluator(針對整型屬性)、FloatAvaluator(浮點型屬性)和 ArgbEvaluator(針對 Color 屬性)等,均實現了該接口。
如圖所示,表示一個勻速動畫,采用了線性插值器和整型估值算法,在 40ms 內,View 的 X 屬性實現了從 0 到 40 的變化。
由于動畫的默認刷新率為 10ms/幀,所以該動畫將分為 5 幀執行,對于第三幀(x=20,t=20ms),當時間 t=20ms 的時候,時間流逝的百分比是 0.5(20/40=0.5),表示時間流逝一半,對應的 x 值就是由插值器和估值算法來確定。對于線性插值器來說,當時間流逝一半時,那么 x 的變化值也應該是一半,即 x 的改變是 0.5,因為他是線性插值器,是實現勻速動畫的。下面看一下源碼:
/**
* An interpolator where the rate of change is constant
*/
@HasNativeInterpolator
public classLinearInterpolatorextendsBaseInterpolatorimplementsNativeInterpolatorFactory{
publicLinearInterpolator(){
}
publicLinearInterpolator(Context context, AttributeSet attrs){
}
//返回值
publicfloatgetInterpolation(floatinput){
return input;
}
/**@hide*/
@Override
publiclongcreateNativeInterpolator(){
return NativeInterpolatorFactoryHelper.createLinearInterpolator();
}
}
從上面代碼可以看出,線性插值器的返回值和輸入值一樣,因此插值器返回值是 0.5,這意味著 x 的改變值是 0.5,這個時候插值器的工作就完成了。具體 x 變成了什么值,這個需要估值算法來確定,看一下整型估值算法的源碼:
public classIntEvaluatorimplementsTypeEvaluator<Integer>{
/**
* This function returns the result of linearly interpolating the start and end values
* @param fraction 變化的比例系數
* @param startValue 起始值
* @param endValue 結束值
*/
publicIntegerevaluate(floatfraction, Integer startValue, Integer endValue){
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
上面源碼算法中的 evaluate 三個參數分別表示估值系數、起始值和結束值,對應于上面例子中的 0.5,0,40。根據上述算法整型估值器返回的結果是 0+40*0.5=20,這就是(x=20,t=20ms)的由來。
屬性動畫要求對象的該屬性有 set 和 get(可選) 方法,插值器和估值算法除了系統提供的外,我們還可以自己定義,插值器或者估值算法都是一個接口,且內部只有一個方法,我們只需要派生一個類實現該接口即可,然后就可以做出千變萬化的動畫效果了。具體而言是:自定義插值器需要實現 Interpolator 或者 TimeInterpolator ,自定義估值算法需要實現 TypeEvaluator 。
4.3 屬性動畫的監聽器
屬性動畫提供了監聽器用于監聽動畫的播放過程,主要有如下兩個接口:AnimatorUpdateListener 和 AnimatorListener 。AnimatorListener 源碼如下:
public static interfaceAnimatorListener{
//動畫開始
voidonAnimationStart(Animator animation);
//動畫結束
voidonAnimationEnd(Animator animation);
//動畫取消
voidonAnimationCancel(Animator animation);
//動畫重播
voidonAnimationRepeat(Animator animation);
}
AnimatorListenerAdapter 實現了 AnimatorListener 接口,這樣我們可以有選擇的實現上面四個方法,不需要每次都去重寫。AnimatorUpdateListener 接口源碼:
public static interfaceAnimatorUpdateListener{
//監聽整個動畫過程,動畫是由許多幀組成,動畫每播放一幀,該方法就會調用一次
voidonAnimationUpdate(ValueAnimator animation);
}
4.4 對任意屬性做動畫
當我們想給一個 Button 加一個動畫,讓這個 Button 寬度從當前寬度增加至 500px,直接用 View 動畫是無法達到效果的,因為 View 動畫只支持四種效果:平移、縮放、旋轉、透明度,當我們強行用 X 方向進縮放可以讓 Button 寬度放大,實際上只是 Button 寬度被放大了而已,由于只是 X 方向被放大,這個時候 Button 的背景及上面的文字都會被拉伸變形,效果很差,所以不是真正的對 Button 寬度做動畫,所以我們用屬性動畫方法去實現,代碼如下:
privatevoidperformAnimation(View button){
ObjectAnimator.ofInt(button,"width",500)
.setDuration(200)
.start();
}
上面代碼運行后發現無效,其實無效就對了,因為這么隨便的給定一個屬性值去執行屬性動畫,輕則無效果,重則程序直接炸掉,可怕。
下面分析屬性動畫原理:屬性動畫要求動畫作用的對象提供 get 方法和 set 方法,屬性動畫根據外界傳遞該屬性的初始值和最終值以動畫的效果去多次調用 set 方法,每次傳遞給 set 方法的值都不一樣,確切的來說是隨著時間的推移,所傳遞的值越來越接近最終值。總結一下,我們對 object 對象屬性 abc 做動畫,如果想要動畫生效,要同時滿足兩個條件:
(1)object 必須要提供 setAbc() 方法,如果動畫的時候沒有傳遞初始值,那么還要提供 getAbc() 方法,因為系統要去取 abc 屬性的初始值(如果這條不滿足,程序直接炸)。
(2)object 的 setAbc() 對屬性 abc 所做的改變必須能夠通過某種方法反應出來(即最終體現了 UI 的變化),比如會帶來 UI 的改變之類(如果這條不滿足,動畫無效果,但是程序不會炸)。
以上條件缺一不可,我們給 Button 的 width 屬性做動畫無效果但是沒有炸的原因就是 Button 內部提供了 setWidth 和 getWidth 方法,但是這個 setWidth 方法并不是改變 UI 大小的,而是用來設置最大寬度和最小寬度的。對于上面屬性動畫的兩個條件來說,這個例子只滿足了條件 1 而未滿足條件 2。
針對上面問題,官方文檔給出了 3 種解決方法:
(1)給你想要執行屬性動畫的對象加上 get 和 set 方法,如果你有權限的話。
(2)用一個類來包裝你的原始對象,間接的為其提供 get 和 set 方法。
(3)采用 ValueAnimator 監聽動畫整個過程,自己實現屬性的改變。
針對以上三種解決方案具體介紹如下:
(1)給你想要執行屬性動畫的對象加上 get 和 set 方法,如果你有權限的話。
這種方案就是如果你有權限的情況下很簡單,只需要給想要執行屬性動畫的對象直接加上 get 和 set 方法就行了,但是很多情況下我們是沒有權限這么做的,比如上面例子中給 Button 添加屬性動畫,我們就沒有辦法給 Button 添加一個合乎要求的 setWidth 方法,因為這是 Android 內部 SDK 的實現,我們是沒有權限的,所以這種方法實用性不是很強。
(2)用一個類來包裝你的原始對象,間接的為其提供 get 和 set 方法。
這是一個很有用的解決方案,使用起來也很方便,下面通過一個具體的例子來進行介紹:
/**
* 將 Button 沿著 X 軸方向放大
* @param button
*/
privatevoidperformAnimationByWrapper(View button){
ViewWrapper viewWrapper=new ViewWrapper(button);
ObjectAnimator.ofInt(viewWrapper,"width",800)
.setDuration(5000)
.start();
}
private classViewWrapper{
private View targetView;
publicViewWrapper(View targetView){
this.targetView = targetView;
}
publicintgetWidth(){
//注意調用此函數能得到 View 的寬度的前提是, View 的寬度是精準測量模式,即不可以是 wrap_content
//否則得不到正確的測量值
return targetView.getLayoutParams().width;
}
publicvoidsetWidth(intwidth){
//重寫設置目標 view 的布局參數,使其改變大小
targetView.getLayoutParams().width = width;
//view 大小改變需要調用重新布局
targetView.requestLayout();
}
}
以上代碼讓 Button 在 5s 內寬度變為 500px,為了達到這個效果我們專門提供了 ViewWrapper 類來包裝 View
,然后對 ViewWrapper 的屬性 width 做屬性動畫。
(3)采用 ValueAnimator 監聽動畫整個過程,自己實現屬性的改變。
ValueAnimator 本身不作用于任何對象,也就是說直接使用它沒有任何動畫效果(所以系統提供了它的子類 ObjectAnimator 供我們直接使用,作用于對象直接執行動畫效果,而 ValueAnimator 只是提供改變一個值的過程,并能監聽到整個值的改變過程,我們基于這個過程可以自己去實現動畫效果,在這個過程中做想要達到的效果,自己去實現)。它可以對一個值做動畫,然后我們可以監聽其動畫過程,在動畫過程中修改我們對象的屬性,這樣我們自己就實現了對對象做了動畫,示例說明如下:
//new 一個整型估值器,用于下面比例值計算使用(可以自己去計算,這里直接使用系統的)
private IntEvaluator intEvaluator = new IntEvaluator();
privatevoidperformAnimatorByValue(finalView targetView,finalintstart,finalintend){
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
publicvoidonAnimationUpdate(ValueAnimator animation){
//獲取當前動畫進度值
int currentValue = (int) animation.getAnimatedValue();
//獲取當前進度占整個動畫比例
int fraction = (int) animation.getAnimatedFraction();
//直接通過估值器根據當前比例計算當前 View 的寬度,然后設置給 View
targetView.getLayoutParams().width = intEvaluator.evaluate(fraction, start, end);
targetView.requestLayout();
}
});
valueAnimator.setDuration(5000)
.start();
}
上面代碼在 5s 內將一個數從 1 變到 100,然后動畫的每一幀會調用 onAnimationUpdate 方法,在這個方法里面我們手動去設置 View 的屬性值,去執行動畫效果。比如時間過了一半,那么當前值應該是 50 ,比例值是 0.5,假設 Button 起始寬度為 100 ,最終寬度變為 500,那么 Button 當前值應該為 100+(500-100)*0.5=300。
4.5 屬性動畫的工作原理
屬性動畫要求作用的對象提供該屬性的 set 方法,屬性動畫根據你傳遞的該屬性的初始值和最終值以動畫的效果去多次調用 set 方法。每次傳遞給 set 方法的值都不一樣,確切的說是隨著時間的推移,所傳遞的值越來越接近最終值。如果動畫的時候沒有傳遞初始值,那么還要提供 get 方法,因為系統要去獲取該屬性的初始值。對于屬性動畫來說,其動畫過程中所做的就這么多,下面分析一下源碼,首先找一個合適的入口,就從我們使用動畫的過程作為切入點, ObjectAnimator.ofInt(viewWrapper, "width", 800).setDuration(5000).start();
publicvoidstart(){
// See if any of the current active/pending animators need to be canceled
AnimationHandler handler = sAnimationHandler.get();
if (handler != null) {
int numAnims = handler.mAnimations.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
numAnims = handler.mPendingAnimations.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
numAnims = handler.mDelayedAnims.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
}
//以上代碼用于判斷如果當前動畫、等待動畫(Pendding)、延遲動畫(Delay)中有和當前動畫相同的動畫,就給取消掉
........
//調用父類 ValueAnimator 方法
super.start();
}
下面繼續查詢 ValueAnimator 的 start 方法源碼:
privatevoidstart(booleanplayBackwards){
//必須運行在 Looper 線程
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mPlayingBackwards = playBackwards;
if (playBackwards && mSeekFraction != -1) {
if (mSeekFraction == 0 && mCurrentIteration == 0) {
// special case: reversing from seek-to-0 should act as if not seeked at all
mSeekFraction = 0;
} else if (mRepeatCount == INFINITE) {
mSeekFraction = 1 - (mSeekFraction % 1);
} else {
mSeekFraction = 1 + mRepeatCount - (mCurrentIteration + mSeekFraction);
}
mCurrentIteration = (int) mSeekFraction;
mSeekFraction = mSeekFraction % 1;
}
if (mCurrentIteration > 0 && mRepeatMode == REVERSE &&
(mCurrentIteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
// if we were seeked to some other iteration in a reversing animator,
// figure out the correct direction to start playing based on the iteration
if (playBackwards) {
mPlayingBackwards = (mCurrentIteration % 2) == 0;
} else {
mPlayingBackwards = (mCurrentIteration % 2) != 0;
}
}
int prevPlayingState = mPlayingState;
mPlayingState = STOPPED;
mStarted = true;
mStartedDelay = false;
mPaused = false;
updateScaledDuration(); // in case the scale factor has changed since creation time
AnimationHandler animationHandler = getOrCreateAnimationHandler();
animationHandler.mPendingAnimations.add(this);
if (mStartDelay == 0) {
// This sets the initial value of the animation, prior to actually starting it running
if (prevPlayingState != SEEKED) {
setCurrentPlayTime(0);
}
mPlayingState = STOPPED;
mRunning = true;
notifyStartListeners();
}
//關鍵調用處
animationHandler.start();
}
上面代碼我們不要在意細節,抓主要與我們相關的看,也就是代碼最終調用的 animationHandler.start() 方法,這個 AnimationHandler 并不是 Handler 而是一個 Runnable ,看一下它的源碼會發現很快調用了 JNI 層代碼,這部分代碼我們通過 AS 是看不見的,不過 JNI 層最終還是會調用回來,它的 Run 方法最終會被調用,這部分我們忽略,直接看 ValueAnimator 的 doAnimationFrame 方法:
finalbooleandoAnimationFrame(longframeTime){
if (mPlayingState == STOPPED) {
mPlayingState = RUNNING;
if (mSeekFraction < 0) {
mStartTime = frameTime;
} else {
long seekTime = (long) (mDuration * mSeekFraction);
mStartTime = frameTime - seekTime;
mSeekFraction = -1;
}
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
if (mPaused) {
if (mPauseTime < 0) {
mPauseTime = frameTime;
}
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mStartTime += (frameTime - mPauseTime);
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
}
// The frame time might be before the start time during the first frame of
// an animation. The "current time" must always be on or after the start
// time to avoid animating frames at negative time intervals. In practice, this
// is very rare and only happens when seeking backwards.
final long currentTime = Math.max(frameTime, mStartTime);
//關鍵代碼處
return animationFrame(currentTime);
}
上面代碼末尾處調用了 animationFrame 方法,animationFrame 方法內部調用了 animateValue 方法,接著看 animateValue 源碼:
voidanimateValue(floatfraction){
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
//計算每幀動畫對應的屬性值,這里找到我們在執行動畫過程中
//給對象屬性設定值的數據來源是在這里計算獲取的
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
上面代碼中 calculateValue 計算出每幀動畫對應的屬性值,那么哪里調用屬性的 set 和 get 方法去設定和讀取屬性值呢,這個是我們要尋找的關鍵。
屬性動畫在初始化的時候,如果初始值沒有提供,那么會調用 get 方法去讀取初始值,一起看 PropertyValuesHolder 的 setupValue 方法:
privatevoidsetupValue(Object target, Keyframe kf){
if (mProperty != null) {
Object value = convertBack(mProperty.get(target));
kf.setValue(value);
}
try {
if (mGetter == null) {
//反射方法調用 get 方法
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());
}
}
通過上代碼會發現 get 方法是通過反射機制調用, 這里我們找到了 get 方法調用處。
當動畫下一幀到來時候,PropertyValuesHolder 的 setAnimatedValue 方法會將新的屬性值設置給對象,調用其 set 方法,其中 set 方法也是通過反射來調用:
voidsetAnimatedValue(Object target){
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
//反射調用 set 方法
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
4.6 使用屬性動畫的注意事項
主要分為以下幾類:
(1)OOM 問題
在幀動畫中如果圖片使用過多或者過大,容易產生該問題,應該盡量避免使用幀動畫。
(2)內存泄露問題
屬性動畫有一個類是無限播放模式,因此在退出 Activity 時候,應該停止動畫播放,否則導致 Activity 無法釋放,導致內存泄露,通過驗證發現 View 動畫并不存在此問題。
(3)兼容問題
動畫在 3.0 系統以下有兼容性問題,有些場合可能導致無法工作,因此要做好適配。
(4)View 動畫問題
View 動畫是對 View 的影像做動畫,并不是正在改變 View 狀態,因此有的時候會出現動畫完成以后 View 無法隱藏的現象,即 View.setVisibility(GONE) 失效,這個時候只需要調用 View.clearAnimation 清除動畫即可解決該問題。
(5)不要使用 px
動畫過程中不要使用 px ,應盡量使用 dp,避免不同設備適配問題。
(6)動畫元素的交互
在 3.0 以前的系統上,不管是 View 動畫還是屬性動畫,新位置無法觸發點擊事件,而老位置點擊事件仍然有效,盡管 View 視覺上已經不在老位置上。
(7)硬件加速
在動畫過程中建議開啟硬件加速,這樣增加動畫的流暢性。
以上為本次筆記內容,完結!!
來自:http://yongyu.itscoder.com/2016/12/25/animation_learning_note/