1. 程式人生 > >Android動畫精講一 從setTranslationX談屬性動畫和view動畫的區別

Android動畫精講一 從setTranslationX談屬性動畫和view動畫的區別

                     

       最近又用到了動畫,決定把幾次專案裡用到的動畫走過的彎路總結一下,順便梳理下android的動畫體系。眾所周知,android動畫分三類:一是View 動畫,又叫Tween動畫,二是frame 動畫(幀動畫),又叫drawable 動畫,三是屬性動畫,即property animation.         View動畫,根據作用又分為縮放動畫ScaleAnimation/移位動畫TranslateAnimation / 透明度動畫AlphaAnimation / 旋轉動畫RotateAnimation,這四個動畫都繼承android.view.animation下的Animation類。繼承Animation的除了這四個類外,還有AnimationSet,關係圖如下所示: 這裡寫圖片描述

       幀動畫 對應AnimationDrawable類,繼承自DrawableContainer,通過載入多個Drawable來一幀一幀播放達到動畫效果。儘管很多人覺得這個不值一提,但是某些動畫效果,如顯示個小羊吃草還必須得用這個動畫。        接下來進入正題談屬性動畫,該動畫從android3.0引入,API11引入,是為了彌補view動畫的不足。正式專案裡用的話為了相容android2.3可以用NineOldAndroids,直接將生成的jar包放進去就ok了。        屬性動畫都在android.animation包下,基類是Animator類,子類為ValueAnimator和AnimatorSet(作用同view動畫的AnimationSet相同),ValueAnimator的子類有ObjectAnimator和TimeAnimator,一般我們用屬性動畫ObjectAnimator就ok了。不妨簡單對比下和view動畫架構上的異同:        View動畫,包名android.view.animation,基類為Animation,核心子類為TranslateAnimation,ScaleAnimation,AlphaAnimation,RotateAnimation及AnimationSet。        Property動畫,包名android.animation,基類為Animator,核心子類為AnimatorSet,ValueAnimator,ObjectAnimator,TimeAnimator。        在詳細對比屬性動畫和view動畫前,先介紹個函式setTranslationX和setTranslationY,api版本為11,是設定view相對原始位置的偏移量,正式專案用的話考慮到相容api11之前的用nineoldandroids裡的ViewHelper即可。

public void setTranslationX (float translationX)Added in API level 11Sets the horizontal location of this view relative to its left position. This effectively positions the object post-layout, in addition to wherever the object's layout placed it.Related XML Attributesandroid:translationXParameterstranslationX    The horizontal position of
this view relative to its left position, in pixels.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

       上面是api介紹,即相對left position的偏移,所謂left position也即getLeft(),同時可以在xml裡直接用android:translationX進行設定。關於view的位置,我們最常用的莫過於android:layoutMargin這一套,用來設定相對父佈局的偏移,在java程式碼裡可以通過新建或更新view的LayoutParams進行修改,如下所示:

   LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)text.getLayoutParams();        params.leftMargin = 0;        params.rightMargin = 0;        params.setMargins(0, 0, 0, 0);        text.setLayoutParams(params);
  • 1
  • 2
  • 3
  • 4
  • 5

       之所以說有時需要新建Params而有時候需要更新,是因為有時候從view取來的params是空的,這個日後開篇文章專門談這個問題。總之,通過view的LayoutParams設定margin最終影響了view的位置,這個同時會改變view的getLeft/getRight等變數。但通過setTranslationX改變view的位置,是不改變view的LayoutParams的,也即不改變getLeft等view的資訊。  但他確實改變了view的位置,這一點可以通過獲取其在window或screen的座標,或通過getLocationInWindow及如下所示的api等到view的精確位置:

    text.getLocationInWindow(pos);    text.getLocationOnScreen(pos);    text.getLocalVisibleRect()    text.getGlobalVisibleRect()
  • 1
  • 2
  • 3
  • 4

       總結: 1,setTranslationX改變了view的位置,但沒有改變view的LayoutParams裡的margin屬性; 2,它改變的是android:translationX 屬性,也即這個引數級別是和margin平行的。

       下面來看這個例子,通過點選按鍵讓一個view從最左邊移動到螢幕的最右邊,分別用view的TranslateAnimation和屬性動畫來實現。 佈局程式碼:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context=".MainActivity">    <LinearLayout        android:layout_width="match_parent"        android:layout_height="50dp"        android:background="@android:color/holo_green_light"        android:orientation="vertical">        <TextView            android:id="@+id/text"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="@string/hello_world" />    </LinearLayout>    <Button        android:id="@+id/btn_start_anim"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_centerInParent="true"        android:text="屬性動畫" />    <Button        android:id="@+id/btn_start_anim2"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_toLeftOf="@id/btn_start_anim"        android:layout_centerVertical="true"        android:layout_marginRight="40dp"        android:text="復位" />    <Button        android:id="@+id/btn_reset_pos"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_toRightOf="@id/btn_start_anim"        android:layout_centerVertical="true"        android:layout_marginLeft="40dp"        android:text="復位" /></RelativeLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

Java程式碼: MainActivity.java

package com.example.yanzi.myapplication;import android.os.Bundle;import android.support.v7.app.ActionBarActivity;import android.util.Log;import android.view.View;import android.view.animation.TranslateAnimation;import android.widget.Button;import android.widget.LinearLayout;import android.widget.TextView;import android.widget.Toast;import com.nineoldandroids.animation.Animator;import com.nineoldandroids.animation.ObjectAnimator;import com.nineoldandroids.view.ViewHelper;import com.yanzi.util.UiUtil;public class MainActivity extends ActionBarActivity implements View.OnClickListener{    private static final String TAG = "YanZi";    Button btn_start_anim;    Button btn_reset_pos;    Button btn_start_anim2;    TextView text;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initData();        initUI();    }    private void initData(){        UiUtil.initialize(getApplicationContext());    }    private void initUI(){        btn_start_anim = (Button)findViewById(R.id.btn_start_anim);        btn_start_anim.setOnClickListener(this);        btn_start_anim2 = (Button)findViewById(R.id.btn_start_anim2);        btn_start_anim2.setOnClickListener(this);        btn_reset_pos = (Button)findViewById(R.id.btn_reset_pos);        btn_reset_pos.setOnClickListener(this);        text = (TextView)findViewById(R.id.text);        text.setOnClickListener(this);        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)text.getLayoutParams();        params.leftMargin = 0;        params.rightMargin = 0;        params.setMargins(0, 0, 0, 0);        text.setLayoutParams(params);    }    @Override    public void onClick(View v) {        switch (v.getId()){            case R.id.btn_start_anim:                playAnim1();                break;            case R.id.btn_start_anim2:                playAnim2();                break;            case R.id.btn_reset_pos:                resetPos();                break;            case R.id.text:                printParams();                break;            default:break;        }    }    public void printParams(){        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)text.getLayoutParams();        if(params != null){            String s =  "leftMargin = " + params.leftMargin + " rightMargin = " + params.rightMargin                    + " getLeft = " + text.getLeft() + " getRight = " + text.getRight() + " getWidth = " + text.getWidth();            Log.i(TAG, s);            int[] pos = new int[2];            text.getLocationInWindow(pos);            Log.i(TAG, "location, x = " + pos[0] + " y = " + pos[1]);            Toast.makeText(getApplicationContext(), s, Toast.LENGTH_LONG).show();        }    }    private void playAnim1(){        int w = text.getWidth();        int screenW = UiUtil.getScreenWidth();        int transX = screenW - w;        ObjectAnimator transAnim = ObjectAnimator.ofFloat(text, "translationX", 0, transX);        transAnim.addListener(new Animator.AnimatorListener() {            @Override            public void onAnimationStart(Animator animator) {            }            @Override            public void onAnimationEnd(Animator animator) {            }            @Override            public void onAnimationCancel(Animator animator) {            }            @Override            public void onAnimationRepeat(Animator animator) {            }        });        transAnim.setDuration(300);        transAnim.start();;    }    private void playAnim2(){        int w = text.getWidth();        int screenW = UiUtil.getScreenWidth();        int transX = screenW - w;        TranslateAnimation transAnim = new TranslateAnimation(0, transX, 0, 0);        transAnim.setDuration(300);        text.setAnimation(transAnim);        transAnim.start();    }    private void resetPos(){        ViewHelper.setTranslationX(text, 0);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130

用到了一個輔助類獲得螢幕的寬高和dip轉px:

package com.yanzi.util;import android.content.Context;import android.util.DisplayMetrics;import android.util.Log;import android.view.View;import android.view.ViewGroup;import android.view.WindowManager;import android.widget.ListAdapter;import android.widget.ListView;public class UiUtil {    private static final String TAG =  "YanZi_UiUtil";    private static int screenWidth = 0;    private static int screenHeight = 0;    private static float screenDensity = 0;    private static int densityDpi = 0;    private static int statusBarHeight = 0;    public static void initialize(Context context){        if (context == null)            return;        DisplayMetrics metrics = new DisplayMetrics();        WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);        wm.getDefaultDisplay().getMetrics(metrics);        screenWidth = metrics.widthPixels;     // 螢幕寬度        screenHeight = metrics.heightPixels;   // 螢幕高度        screenDensity = metrics.density;      // 0.75 / 1.0 / 1.5 / 2.0 / 3.0        densityDpi = metrics.densityDpi;  //120 160 240 320 480        Log.i(TAG, "screenDensity = " + screenDensity + " densityDpi = " + densityDpi);    }    public static int dip2px(float dipValue){        return (int)(dipValue * screenDensity + 0.5f);    }    public static int px2dip(float pxValue){        return (int)(pxValue / screenDensity + 0.5f);    }    public static int getScreenWidth() {        return screenWidth;    }    public static int getScreenHeight() {        return screenHeight;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

執行介面: 這裡寫圖片描述

       大概說下里面核心的幾個函式: 1,使用view動畫TranslateAnimation:

   private void playAnim2(){        int w = text.getWidth();        int screenW = UiUtil.getScreenWidth();        int transX = screenW - w;        TranslateAnimation transAnim = new TranslateAnimation(0, transX, 0, 0);        transAnim.setDuration(300);        text.startAnimation(transAnim);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2,使用屬性動畫移位:

  private void playAnim1(){        int w = text.getWidth();        int screenW = UiUtil.getScreenWidth();        int transX = screenW - w;        ObjectAnimator transAnim = ObjectAnimator.ofFloat(text, "translationX", 0, transX);        transAnim.addListener(new Animator.AnimatorListener() {            @Override            public void onAnimationStart(Animator animator) {            }            @Override            public void onAnimationEnd(Animator animator) {            }            @Override            public void onAnimationCancel(Animator animator) {            }            @Override            public void onAnimationRepeat(Animator animator) {            }        });        transAnim.setDuration(300);        transAnim.start();;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

3,點選text列印它的座標:

    public void printParams(){        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)text.getLayoutParams();        if(params != null){            String s =  "leftMargin = " + params.leftMargin + " rightMargin = " + params.rightMargin                    + " getLeft = " + text.getLeft() + " getRight = " + text.getRight() + " getWidth = " + text.getWidth();            Log.i(TAG, s);            int[] pos = new int[2];            text.getLocationInWindow(pos);            Log.i(TAG, "location, x = " + pos[0] + " y = " + pos[1]);            Toast.makeText(getApplicationContext(), s, Toast.LENGTH_LONG).show();        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4,使用屬性動畫後如果想復位:

private void resetPos(){        ViewHelper.setTranslationX(text, 0);    }
  • 1
  • 2
  • 3

       直接將translationX設為0即可,而不是上次偏移量的相反數。正因為如此,重複點選屬性動畫,看到view每次都從最左邊到最右邊,並最終停在最右邊。因為屬性動畫的執行過程就是setTranslationX(0), 1, 2, 3, 4,……..N的過程,所以才會有看到的效果。

       另外,可以看到使用view的TranslateAnimation動畫播放完畢後,view瞬間又回到了原點;而使用屬性動畫移位後view位置確實發生了改變。但LayoutParams裡的margin和getLeft資訊並未改變。有沒有辦法讓view的TranslateAnimation播放完畢後,停在那個地方呢?

       肯定是有,加上這句話:transAnim.setFillAfter(true);之後執行發現view確實停在了螢幕的右側,但是點選右側的textview並沒有觸發列印引數的函式,而點選textview的初始位置才觸發。所以它並沒有改變view的位置,僅僅是繪製在了螢幕的右側。因此,如果使用view動畫但又想真正改變view位置需要如下程式碼:

private void playAnim2(){        int w = text.getWidth();        int screenW = UiUtil.getScreenWidth();        int transX = screenW - w;        TranslateAnimation transAnim = new TranslateAnimation(0, transX, 0, 0);        transAnim.setDuration(300);//        transAnim.setFillAfter(true);        transAnim.setAnimationListener(new Animation.AnimationListener() {            @Override            public void onAnimationStart(Animation animation) {            }            @Override            public void onAnimationEnd(Animation animation) {                updateParams();            }            @Override            public void onAnimationRepeat(Animation animation) {            }        });        text.startAnimation(transAnim);    }    private void updateParams(){        int w = text.getWidth();        int screenW = UiUtil.getScreenWidth();        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) text.getLayoutParams();        params.leftMargin = screenW - w;        text.setLayoutParams(params);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

       即使用LayoutParams在動畫結束後設置下就ok了,這樣也能達到屬性動畫改變view的位置的效果。view 動畫+updateParams 約等於property動畫效果。        但是切忌,使用view動畫+updateParams策略時,務必注意不要使用transAnim.setFillAfter(true);這句話,先看看setFillAfter的api:

If fillAfter is true, the transformation that this animation performed will persist when it is finished. Defaults to false if not set. Note that this applies to individual animations and when using an AnimationSet to chain animations.Related XML Attributesandroid:fillAfterParametersfillAfter   true if the animation should apply its transformation after it ends
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

       如果為true,動畫結束後關於view的變換會一直存在。在view動畫+updateParams+transAnim.setFillAfter(true)這種策略下,view最終的繪製位置等於將view先updateParams後在新的位置基礎上,再進行動畫移位,一般情況下這並不是我們想要的!        基本上可以這麼說,如果需要view位置真正改變setFillAfter一定不要設!        時間原因,很多東西只有下次再寫了,關於屬性動畫和view動畫詳細對比可以參考官方文件裡How Property Animation Differs from View Animation這一段,見後文。

       總之,要知其然並知其所以然,不要一味否定view動畫而肯定屬性動畫。很多多個介面間的複雜效果非view動畫不可,用屬性動畫只能掉坑裡,我是兩種坑都掉過。如果想改變動畫後view的屬性,如位置,可以用屬性動畫也可以用view動畫+updateParams,當然前者更省事。在有些情況下,僅僅是想得到動畫的呈現,動畫結束後的位置就是view的初始位置,如view從一個地方飛過來,動畫結束時view的位置就是view的位置時,此時view動畫最合適!

 

The view animation system provides the capability to only animate View objects, so if you wanted to animate non-View objects, you have to implement your own code to do so. The view animation system is also constrained in the fact that it only exposes a few aspects of a View object to animate, such as the scaling and rotation of a View but not the background color, for instance.

   

Another disadvantage of the view animation system is that it only modified where the View was drawn, and not the actual View itself. For instance, if you animated a button to move across the screen, the button draws correctly, but the actual location where you can click the button does not change, so you have to implement your own logic to handle this.

   

With the property animation system, these constraints are completely removed, and you can animate any property of any object (Views and non-Views) and the object itself is actually modified. The property animation system is also more robust in the way it carries out animation. At a high level, you assign animators to the properties that you want to animate, such as color, position, or size and can define aspects of the animation such as interpolation and synchronization of multiple animators.

   

The view animation system, however, takes less time to setup and requires less code to write. If view animation accomplishes everything that you need to do, or if your existing code already works the way you want, there is no need to use the property animation system. It also might make sense to use both animation systems for different situations if the use case arises.