本篇文章主要介紹屬性動畫,需要了解補間動畫和幀動畫相關知識的,建議閱讀Android動畫詳解(上)。屬性動畫非常強大,運用也非常靈活,爲了便於理解,本文首先從類的角度介紹了屬性動畫的繼承關係,然後針對一些重點類介紹了其內的主要方法,最後通過demo的方式對屬性的動畫的常見用法進行了演示。
類繼承關係
屬性動畫存放在android.animation包下,主要的類繼承關係如下:
主要方法
Animator是一個抽象類,其內提供了一些公共方法。需要注意的是,一些方法是需要子類去重寫的,比如getInterpolator()這個方法目前直接返回null,子類需要根據實際情況返回Interpolator。
ValueAnimator是屬性動畫中非常常用的一個類,除了實現或重寫了父類的上述方法外,其還增加了一些新的功能,如常用的估值器的設置。
AnimatorSet主要用來控制動畫組合的播放順序及方式,特別是其的Builder,功能強大,使用方便。
常見用法
屬性動畫的用法非常靈活,首先從簡單的平移動畫開始入手,效果如如下:
代碼比較簡單,老規矩,介紹xml和Java兩種實現方式:
xml實現方式:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="translationX"
android:duration="2000"
android:valueFrom="0"
android:valueTo="500"
android:repeatCount="1"
android:repeatMode= "reverse"
/>
Java中調用代碼:
ObjectAnimator objectAnimator= (ObjectAnimator)AnimatorInflater.loadAnimator(this,R.animator.propertyanimation);
objectAnimator.setTarget(mAnimationBtn0);
objectAnimator.start();
需要注意的是xml代碼是放在res的animator目錄下,另外注意repeatCount的值和repeatMode選項。
Java實現方式:
ObjectAnimator translationXAnimation = ObjectAnimator.ofFloat(mAnimationBtn0, "translationX", 0, 500);
translationXAnimation.setDuration(2000);
translationXAnimation.setRepeatCount(1);
translationXAnimation.setRepeatMode(ValueAnimator.REVERSE);
translationXAnimation.start();
上述Java代碼之所以能夠實現平移效果是因爲mAnimationBtn0這個Target屬於View包含translationX對應的get/set方法。至於爲啥是ofFloat而不是ofInt,是因爲其get/set方法對應的方法參數爲float類型,它們是相互關聯的。
旋轉、縮放等動畫與平移動畫類似,就不再介紹了。
假如我們需要改變一個View的寬度,但是View裏面卻沒有對應的get/set該如何實現呢?方案很多,爲了對比學習,挑選了三種典型的方案,效果圖如下:
從圖中可以看到紅、藍、綠三個View的變化效果一致,但是正如其顯示的內容一樣,使用了不同的方式,分別爲ObjectAnimator.ofInt、ValueAnimator.ofInt 、ValueAnimator.ofObject。
ObjectAnimator.ofInt 方式
該方式是處理這種沒有對應set/get方法卻想使用屬性動畫情況的常用的方式,主要思路是利用裝飾者模式,對現有target進行裝飾,在裝飾類裏提供set/get操作,完成相關屬性的設置。實現步驟如下:
步驟一:創建裝飾類
public class WidthWrapper {
private View mTargetView;
WidthWrapper(View view) {
mTargetView = view;
}
public int getWidth(){
return mTargetView.getLayoutParams().width;
}
public void setWidth(int width) {
mTargetView.getLayoutParams().width = width;
mTargetView.requestLayout();
}
}
步驟二:配置屬性動畫
ObjectAnimator widthAnimation = ObjectAnimator.ofInt(new WidthWrapper(mAnimationBtn1), "Width", 0, 600);
widthAnimation.setDuration(2000);
widthAnimation.start();
ValueAnimator.ofInt 方式
該方式是屬性動畫運用的通用方式,重要性較高,對一些效果複雜或多個動畫關聯調用的情況處理比較有優勢,其核心是動畫回調的運用。核心代碼如下:
if (null != widthAnimation2 && widthAnimation2.isRunning()) {
return;
}
widthAnimation2 = ValueAnimator.ofInt(0, 600);
widthAnimation2.setDuration(2000);
widthAnimation2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimationBtn2.getLayoutParams().width = (int) animation.getAnimatedValue();
mAnimationBtn2.requestLayout();
}
});
widthAnimation2.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mAnimationBtn2.getLayoutParams().width = 0;
mAnimationBtn2.requestLayout();
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimationBtn2.getLayoutParams().width = 600;
mAnimationBtn2.requestLayout();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
widthAnimation2.start();
首先,如果動畫存在且正在執行則返回。如果動畫不存在則創建動畫,並設置監聽,在監聽中動態改變佈局參數並刷新佈局。最後啓動動畫。
ValueAnimator.ofObject方式
該方式主要是爲了演示TypeEvaluator的運用,針對沒有get/set這種場景並不常見,主要運用於一些類的特殊變化過程,實現過程有點像第一種方式和第二種方式的結合。實現步驟如下:
步驟一:自定義WidthObject
public class WidthObject {
private int mWidth;
WidthObject(int width) {
mWidth = width;
}
public int getWidth() {
return mWidth;
}
}
步驟二:自定義TypeEvaluator
public class WidthTypeEvaluator implements TypeEvaluator <WidthObject>{
@Override
public WidthObject evaluate(float fraction, WidthObject startValue, WidthObject endValue) {
return new WidthObject((int)(startValue.getWidth()+fraction*(endValue.getWidth()-startValue.getWidth())));
}
}
自定義TypeEvaluator需要實現TypeEvaluator接口,根據傳入參數返回對應值。計算方式爲
startValue+fraction*(endValue-startValue)。
步驟三:配置屬性動畫
if (null != widthAnimation3 && widthAnimation3.isRunning()) {
return;
}
widthAnimation3 = ValueAnimator.ofObject(new WidthTypeEvaluator(),new WidthObject(0), new WidthObject(600));
widthAnimation3.setDuration(2000);
widthAnimation3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimationBtn3.getLayoutParams().width = ((WidthObject)animation.getAnimatedValue()).getWidth();
mAnimationBtn3.requestLayout();
}
});
widthAnimation3.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mAnimationBtn3.getLayoutParams().width = 0;
mAnimationBtn3.requestLayout();
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimationBtn3.getLayoutParams().width = 600;
mAnimationBtn3.requestLayout();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
widthAnimation3.start();
上述代碼跟ValueAnimator.ofInt 方式大同小異,都是獲取動態佈局參數並請求屬性佈局。
AnimatorSet的使用
AnimatorSet用於控制動畫的播放,跟AnimationSet作用類似,但還是有一定的區別的,相對更加強大,可以控制多個動畫的播放順序。下面的代碼設置了四個不同效果的屬性動畫,動畫1爲平移動畫,動畫2爲透明度變化動畫,動畫3爲縮放動畫,動畫4爲旋轉動畫。
ObjectAnimator animation1 = ObjectAnimator.ofFloat(mAnimationBtn4, "translationX", 0, 500);
ObjectAnimator animation2 = ObjectAnimator.ofFloat(mAnimationBtn4, "alpha", 0, 1);
ObjectAnimator animation3 = ObjectAnimator.ofFloat(mAnimationBtn4, "scaleX", 2);
ObjectAnimator animation4 = ObjectAnimator.ofFloat(mAnimationBtn4,"rotationX",0,270,50);
AnimatorSet animatorSet=new AnimatorSet ();
animatorSet.play(animation1).with(animation2).after(animation3).before(animation4);
animatorSet.setDuration(2000);
animatorSet.start();
AnimatorSet 對其進行了組合調用,將動畫1和動畫2一起播放,並且在動畫3之後,在動畫4之前。所以其播放順序爲:動畫3>(動畫1==動畫2)>動畫4;效果如下:
自定義控件switcher
除了上述知識點之外,屬性動畫還有一個非常重要的知識點那就是Interpolator,下面通過一個常用的自定義控件switcher來演示Interpolator的用法。先上一個慢放的效果圖:
先處理我們比較熟悉的繪製過程,很明顯這個switcher無論什麼狀態都是由兩個圖形組成,外圍的橢圓和內部的圖形(圓或橢圓),且外圍橢圓的背景是根據狀態變化的。
外圍的圖形所在矩形座標好確認,如果不考慮pading之類的參數,左上x、y的起始值都爲0,右下x、y值分別爲其寬度和高度。內部圖形是座標是變化的,我們定義了mInnerStartX、mInnerStartY、 mInnerEndX、 mInnerEndY 這4個值來代表其所在矩形的左上和右下的x、y值。另外,需要根據當前的背景繪製設置外圍橢圓的背景。繪製過程如下:
/**
* 根據當前背景和計算出的座標信息繪製switcher
* @param canvas
*/
private void drawSwitcher(Canvas canvas) {
RectF rect = new RectF(0, 0, mWidth, mHeight);
mPaint.setColor(mBgOnCurrent);
canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, mPaint);//畫外部橢圓
RectF innerRect = new RectF(mInnerStartX, mInnerStartY, mInnerEndX, mInnerEndY);
mPaint.setColor(Color.WHITE);
canvas.drawRoundRect(innerRect, innerRect.height() / 2, innerRect.height() / 2, mPaint);//畫內部橢圓
}
@Override
protected void onDraw(Canvas canvas) {
drawSwitcher(canvas);
super.onDraw(canvas);
}
寬度和高度的獲取是在onSizeChanged中獲得的,代碼如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mInnerStartY = mInterval;
mInnerEndY = mHeight - mInterval;
if (isOpen) {
buildOpenStatusParams();
} else {
buildCloseStatusParams();
}
}
isOpen標識當前switcher是否爲打開狀態, buildOpenStatusParams爲構建打開時的狀態, buildCloseStatusParams爲構建關閉時的狀態,實現過程後面會有介紹。
剩下的主要工作就是構建switcher每個變化過程的繪製參數,這也是難點所在。
首先將switcher變化動畫分解,通過觀察可以看到,動畫主要分爲4個狀態:起始狀態、移動狀態1、移動狀態2、結束狀態。移動狀態1和2內部爲一個橢圓,其中狀態1橢圓的起始X跟起始狀態X一致,狀態2橢圓的結束X與結束狀態X一致。所謂的起始狀態和結束狀態是相對的,也就是說,開關啓動前狀態爲起始狀態,結束時爲結束狀態,可以是內部圓在左側的時候,也可以是內部圓在右側的時候。
將上述分析轉換爲代碼形式:
if (null == mAnimator) {
mAnimator = ValueAnimator.ofInt(0, 3);
mAnimator.setDuration(DURATION_TIME);
mAnimator.setInterpolator(new SwitcherInterpolator());
//設置監聽,動畫結束改變按鈕的當前狀態值
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
isOpen = !isOpen;
if (null != mCallBack) {
mCallBack.IsOpen(isOpen);
}
}
});
//根據進度判斷處於何種狀態,計算對應的繪製參數,0-起始狀態,1-移動過程1,2-移動過程2,3-結束狀態
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
switch ((int) animation.getAnimatedValue()) {
case 0:
if (isOpen) {
buildOpenStatusParams();
} else {
buildCloseStatusParams();
}
break;
case 1:
if (isOpen) {
buildRightToCloseStatusParams();
} else {
buildLeftToOpenStatusParams();
}
break;
case 2:
if (isOpen) {
buildLeftToCloseStatusParams();
} else {
buildRightToOpenStatusParams();
}
break;
case 3:
if (isOpen) {
buildCloseStatusParams();
} else {
buildOpenStatusParams();
}
break;
}
invalidate();
}
});
} else if (mAnimator.isRunning()) {
return;
}
mAnimator.start();
如果動畫不存在則構建動畫,值的變化範圍爲0~3,分別代表不同的狀態:0-起始狀態,1-移動過程1,2-移動過程2,3-結束狀態。添加監聽,在動畫結束時改變按鈕的狀態。添加進度監聽,根據不同的AnimatedValue(實際爲不同的狀態),構建不同狀態的繪製參數。如果動畫存在且正在運行則不執行任何操作,否則啓動已有動畫。動畫通過 setInterpolator方法設置了一個自定義Interpolator——SwitcherInterpolator,其代碼如下:
public class SwitcherInterpolator implements Interpolator {
@Override
public float getInterpolation(float input) {
if(input<0.1){
return 0;
}else if(input<0.5){
return 0.34f;
}else if(input<0.9){
return 0.67f;
}else{
return 1.0f;
}
}
}
SwitcherInterpolator中根據傳入的input(動畫執行的比例,取值從0到1)計算出當前動畫的進行程度。這裏將動畫的前1/10時間返回值設爲0,1/10-1/2設置爲0.34f, 1/2-9/10設置爲0.67f,後1/10時間設置爲1.0f.
- 當進行程度爲0時,AnimatedValue爲0,爲起始狀態
- 當進行程度爲0.34f時,AnimatedValue爲1,爲移動過程1
- 當進行程度爲0.67f時,AnimatedValue爲2,爲移動過程2
- 當進行程度爲1.0f時,AnimatedValue爲3,爲結束狀態
AnimatedValue值需要結合上面所講的TypeEvaluator知識來計算,例如移動狀態1的AnimatedValue=0+0.34f*(3-0).
另外,我們也可以通過自定義TypeEvaluator的方式來達到這種效果,TypeEvaluator是用來計算其對應對象的屬性在某個插值下的值。我們完全可以在TypeEvaluator下判斷當前的插值,然後返回對應的值(0,1,2,3),跟SwitcherInterpolator中的邏輯類似。
哪Interpolator和TypeEvaluator能否相互取代呢?答案肯定是否定的,因爲它倆的職責本身就不同,Interpolator主要是改變插值,而TypeEvaluator是根據插值改變屬性的值。當然,如果在TypeEvaluator增加一定的邏輯也能取代Interpolator達到特定的效果,但是這與其設計初衷想背離的。
mAnimator返回值表示當前的動畫狀態,根據動畫狀態和switcher的開關狀態構建繪製參數,構建方法如下:
/***
* 構建switcher關閉狀態參數
*/
private void buildCloseStatusParams() {
mInnerStartX = mInnerStartY;
mInnerEndX = mInnerStartX + (mHeight - 2 * mInterval);
mBgOnCurrent = mBgOnClose;
}
/***
* 構建switcher打開狀態參數
*/
private void buildOpenStatusParams() {
mBgOnCurrent = mBgOnOpen;
mInnerEndX = mWidth - mInterval;
mInnerStartX = mInnerEndX - (mHeight - mInterval * 2);
}
/****
* 構建switcher打開過程1參數,啓動起始X位置爲間隔,
* 結束X位置爲寬度的2/3,當前背景爲關閉
*/
private void buildLeftToOpenStatusParams() {
mInnerStartX = mInnerStartY;
mInnerEndX = mWidth / 3 * 2;
mBgOnCurrent = mBgOnClose;
}
/****
* 構建switcher打開過程2參數,啓動起始X位置爲寬度的1/3,
* 結束X位置爲寬度-間隔,當前背景爲打開
*/
private void buildRightToOpenStatusParams() {
mInnerStartX = mWidth / 3;
mInnerEndX = mWidth - mInterval;
mBgOnCurrent = mBgOnOpen;
}
/***
* 構建switcher關閉過程1參數,啓動起始X位置爲寬度的1/3,
* 結束X位置爲寬度-間隔,當前北京爲打開
*/
private void buildRightToCloseStatusParams() {
mInnerStartX = mWidth / 3;
mInnerEndX = mWidth - mInterval;
mBgOnCurrent = mBgOnOpen;
}
/****
* 構建switcher關閉過程2參數,啓動起始X位置爲間隔,
* 結束X位置爲寬度的2/3,當前背景爲關閉
*/
private void buildLeftToCloseStatusParams() {
mInnerStartX = mInnerStartY;
mInnerEndX = mWidth / 3 * 2;
mBgOnCurrent = mBgOnClose;
}
打開狀態和關閉狀態可能爲起始狀態也可能是結束狀態,需要根據switcher的開關狀態來判斷,如果當前switcher爲關閉,則關閉狀態爲起始狀態,打開狀態爲結束狀態;如果當前switcher爲打開則打開狀態爲起始狀態,關閉狀態爲結束狀態。switcher的打開和關閉過程邏輯跟上面的邏輯類似,結合代碼非常容易理解,就不詳細講解了。
剩下的就是一些完善性工作了
設置開關狀態的監聽,獲取開關的狀態變化
/***
* 設置狀態改變回調
* @param callback
*/
public void SetCallBack(SwitcherCallback callback) {
mCallBack = callback;
}
public interface SwitcherCallback {
void IsOpen(boolean aIsOpen);
}
設置開關的狀態
/***
* 設置開關狀態,不回調狀態改變接口
* @param isOpen 開關狀態
*/
public void setOpen(boolean isOpen) {
setOpen(isOpen,false);
}
/***
* 設置開關狀態,根據需要回調狀態
* @param isOpen 開關狀態
* @param isCallBack 是否需要回調
*/
public void setOpen(boolean isOpen, boolean isCallBack) {
this.isOpen = isOpen;
if (isOpen) {
buildOpenStatusParams();
} else {
buildCloseStatusParams();
}
invalidate();
if (isCallBack && null != mCallBack) {
mCallBack.IsOpen(isOpen);
}
}
switcher的核心代碼就這些,主要是對屬性動畫的靈活運用,以及對Interpolator的理解。這兩篇文章對應的代碼都放在了一個項目裏,有需要的可以下載。項目地址