屬性動畫是API 11加進來的一個新特性,其實在現在來說也沒什么新的了。屬性動畫可以對任意view的屬性做動畫,實現動畫的原理就是在給定的時間內把屬性從一個值變為另一個值。因此可以說屬性動畫什么都可以干,只要view有這個屬性。
所以我們這里對Button來做一個簡單的屬性動畫:改變這個Button的寬度。也可以用Tween Animation,但是明顯有一點不能滿足要求的地方是Tween Animation只能做Scale動畫,也就是縮放。你可以對這個button做縮放來達到增加寬度的效果,但是這個時候按鈕的文字也會跟着出現縮放和變形。同時很重要的一點,Tween Animation不改變view的本來位置和大小。看起來這個按鈕變大了,但是點擊動畫執行前的按鈕沒有覆蓋的位置是沒有效果的。
我們簡略的看一下這個Tween動畫是怎么樣的。
首先,用xml的文件定義一個Scale動畫,對寬度擴大為原來的兩倍,高度擴大為原來的六倍。在動畫之后填充。動畫執行完成后就可以清楚的看到按鈕文字跟着按鈕動畫執行完成后之后的效果。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:fillAfter="true"
android:interpolator="@android:anim/accelerate_decelerate_interpolator">
<scale
android:fromXScale="100%"
android:fromYScale="100%"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="200%"
android:toYScale="600%" />
</set>
執行這個Tween動畫:
var button = findViewById(R.id.tween_button) as Button
button.setOnClickListener { v ->
var anim = AnimationUtils.loadAnimation(this@TweenAnimActvity, R.anim.scale_anim)
v.startAnimation(anim)
}
這里必須說明,上面這段代碼是Kotlin語言寫的。自從用了之后就再不想用java了。只要你有一定的java基礎,閱讀這段代碼並沒有什么難度。
點擊原來按鈕區域以外的地方,按鈕是不會有任何的反應的。
看看效果:

試試Property動畫吧
直接看看按照同樣的縮放大小生成的效果吧:

前后兩者相差還是很明顯的。越明顯越突出了屬性動畫存在的必要。這種必要不止是效果上看到的,
還有交互和開發的時候代碼相關的。
所以無論如何都要使用屬性動畫了。這里使用最簡答的方法: ObjectAnimator來做這個動畫:
ObjectAnimator.ofInt(mAnimateButton, "width", mAnimateButton.getWidth(), 1000)
.setDuration(1000)
.start();
看起來很簡單就實現了按鈕的動畫。但是運行的時候就會出現問題。因為,屬性動畫在執行的時候需要改變指定的屬性,這里是width,的值。使用的就是屬性對應的getWidth和setWidth方法。getWidth在沒有給定動畫的初值時,使用這個方法獲得初始值。setWidth則在給定的時間內不斷地被用來修改屬性值來達到動畫的效果。注意,這個方法不是只是用一次。
但是來看看Button的getWidth和setWidth兩個方法的代碼:
/**
* Return the width of the your view.
*
* @return The width of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}
/**
* Makes the TextView exactly this many pixels wide.
* You could do the same thing by specifying this number in the
* LayoutParams.
*
* @see #setMaxWidth(int)
* @see #setMinWidth(int)
* @see #getMinWidth()
* @see #getMaxWidth()
*
* @attr ref android.R.styleable#TextView_width
*/
@android.view.RemotableViewMethod
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
顯然在setWidth的時候,並沒有用給定的值去修改按鈕layout param的寬度。
在這種情況下Google給了三種解決方法:
- 給你的view加上get和set方法。但是這需要你有這個權限。
- 用一個類來包裝目標view,間接的給這個view來添加get和set方法。
- 用
ValueAnimator和AnimatorUpdateListener監聽動畫,自己修改每個時間片的屬性修改。
給Button添加get和set方法不是很現實,所以只能選擇后兩者。
下面一一介紹后面兩個方法。
間接給出get、set方法
這個方法看起來很簡單,定義一個類間接給出get、set方法就是這樣的:
class ViewWrapper {
View mTargetView;
public ViewWrapper(View v) {
mTargetView = v;
}
public void setWidth(int width) {
mTargetView.getLayoutParams().width = width;
mTargetView.requestLayout();
}
// for view's width
public int getWidth() {
int width = mTargetView.getLayoutParams().width;
return width;
}
// for view's height
public void setHeight(int height) {
mTargetView.getLayoutParams().height = height;
mTargetView.requestLayout();
}
public int getHeight() {
int height = mTargetView.getLayoutParams().height;
return height;
}
}
- 既然動畫是需要修改layout params的寬度,那么我們在這個set方法里就修改layout params的寬度。
- 返回layout params的寬度。這個值是view在動畫之前的寬度。
然后在按鈕點擊之后開始這個修改寬度的動畫:
@Override
public void onClick(View v) {
Log.d("##ViewWrapperActivity", "width is " + v.getWidth());
// 1
ViewWrapper viewWrapper = new ViewWrapper(v);
// 2
ObjectAnimator animator = ObjectAnimator.ofInt(viewWrapper, "width", /*viewWrapper.getWidth(),*/ 1500);
// 3
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.d("##ANIM", "started");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.d("##ANIM", "stopped");
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
// 4
animator.setDuration(3000).start();
}
- 用包裝類包裝view,這里是按鈕。
- 開始動畫,動畫的對象現在為包裝類對象。這里可以修改屬性動畫的定義了,屬性動畫可以對任何對象修改屬性。這里的包裝類對象明顯不是一個view。
- 這里增加了一個監聽器,監聽動畫是剛開始還是已經結束。
- 開始動畫。在三秒鍾的時間內修改按鈕的寬度,從初始值修改為1500像素寬。
看起來已經很完美了,運行這個段代碼。點擊按鈕后。好吧,這個動畫很奇怪,並沒有運行“完全”。點一下動一點,但是沒有達到寬度為1500像素。雖然動畫監聽器AnimatorListener的方法onAnimationEnd已經執行,而且也打出了執行完成的log,但是寬度始終達不到。所以說動畫執行並不“完全”。
那么這是為什么呢?先給出正確的代碼各位可以參考着考慮一下:
public class ViewWrapperActivity extends Activity implements View.OnClickListener {
private Button mAnimateButton;
// 1
private ViewWrapper mWrapper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_wrapper);
mAnimateButton = (Button) findViewById(R.id.animate_button);
mAnimateButton.setOnClickListener(this);
// 2
mWrapper = new ViewWrapper(mAnimateButton);
}
@Override
public void onClick(View v) {
Log.d("##ViewWrapperActivity", "width is " + v.getWidth());
// 3
int width = v.getLayoutParams().width;
int height = v.getHeight(); // current height
// 4
PropertyValuesHolder widthHolder = PropertyValuesHolder.ofInt("width", width * 2);
PropertyValuesHolder heightHolder = PropertyValuesHolder.ofInt("height", height * 6);
// 5
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mWrapper, widthHolder, heightHolder);
animator.setInterpolator(new LinearInterpolator());
animator.addListener(new Animator.AnimatorListener() {
// ...
});
animator.setDuration(3000).start();
}
}
- 聲明包裝類對象類成員。
- 在
onCreate方法里初始化包裝類對象。 width和height獲取Button當前的寬度和高度。- 在這定義對寬度做2倍的擴大,對高度做6倍的擴大。兩個動畫的定義都存放在
PropertyValuesHolder中,並在后面的實現中使用。使用這個類存放對不同屬性的動畫定義,方便使用。這兩個動畫會同時並行執行。 - 對
mWrapper執行前面定義的兩個動畫。這兩個動畫同時執行。要使兩個動畫順序執行可以AnimatorSet來實現:
int width = v.getWidth();
int height = v.getHeight();
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator widthAnim = ObjectAnimator.ofInt(mSequenceWrapper, "width", width * 2);
ObjectAnimator heightAnim = ObjectAnimator.ofInt(mSequenceWrapper, "height", height * 6);
animSet.play(widthAnim).before(heightAnim);
animSet.setDuration(1000);
animSet.setInterpolator(new AccelerateDecelerateInterpolator());
animSet.start();
這樣就可以一次動畫達到指定寬度和高度了。具體是為什么呢?歡迎再后面的評論中一起討論。😉
用ValueAnimator和AnimatorUpdateListener的組合來實現動畫
這個就比較簡單了,直接看代碼:
private void performAnimation(final View targetView, final int start, final int end) {
// 1
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
// 2
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
private final static String ANIM_TAG = "##Value animator";
private IntEvaluator mIntEvaluator = new IntEvaluator();
@Override
public void onAnimationUpdate(ValueAnimator animator) {
int currentValue = (Integer) animator.getAnimatedValue();
Log.d(ANIM_TAG, "current value: " + currentValue);
// 3
float fraction = animator.getAnimatedFraction();
targetView.getLayoutParams().width = mIntEvaluator.evaluate(fraction, start, end);
targetView.requestLayout();
}
});
// 4
valueAnimator.setDuration(1000).start();
}
- 用
ValueAnimator來做動畫。ValueAnimator並不會實質的做什么。所以需要后面的AnimatorUpdateListener來做一些粗活兒。這里指定的從1到100也沒有什么實質的作用。並不是把按鈕的寬度從1變到100。后面的代碼很清晰的表達了這一點。 - 添加
AnimatorUpdateListener。最主要的就是在方法public void onAnimationUpdate(ValueAnimator animator)中做動畫。每一個時間片都會調用一次這個方法。每調用這個方法一次就給這個按鈕的寬度設定一個新的值。 - 第三步的算法是獲取當前動畫進行的時間片占整個動畫時間的百分比,這里是
fraction。然后根據這個百分比來計算當前時間片對應的按鈕寬度是多少。
當前寬度 = 初始寬度 + fraction * (結束寬度 - 初始寬度)。
這也就解釋了代碼mIntEvaluator.evaluate(fraction, start, end)的作用。
完整代碼看這里。
到這里全部解釋完。歡迎拍磚,歡迎討論!
