天天看點

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

       最近又用到了動畫,決定把幾次項目裡用到的動畫走過的彎路總結一下,順便梳理下android的動畫體系。衆所周知,android動畫分三類:一是View 動畫,又叫Tween動畫,二是frame 動畫(幀動畫),又叫drawable 動畫,三是屬性動畫,即property animation.

       View動畫,根據作用又分為縮放動畫ScaleAnimation/移位動畫TranslateAnimation / 透明度動畫AlphaAnimation / 旋轉動畫RotateAnimation,這四個動畫都繼承android.view.animation下的Animation類。繼承Animation的除了這四個類外,還有AnimationSet,關系圖如下所示:

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

       幀動畫 對應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 
Sets 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 Attributes
android:translationX
Parameters
translationX    The horizontal position of this view relative to its left position, in pixels.
           

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

LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)text.getLayoutParams();
        params.leftMargin = ;
        params.rightMargin = ;
        params.setMargins(, , , );
        text.setLayoutParams(params);
           

       之是以說有時需要建立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,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>
           

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 = ;
        params.rightMargin = ;
        params.setMargins(, , , );
        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[];
            text.getLocationInWindow(pos);
            Log.i(TAG, "location, x = " + pos[] + " y = " + pos[]);
            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", , 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();
        transAnim.start();;
    }

    private void playAnim2(){
        int w = text.getWidth();
        int screenW = UiUtil.getScreenWidth();
        int transX = screenW - w;
        TranslateAnimation transAnim = new TranslateAnimation(, transX, , );
        transAnim.setDuration();
        text.setAnimation(transAnim);
        transAnim.start();

    }

    private void resetPos(){
        ViewHelper.setTranslationX(text, );
    }
}
           

用到了一個輔助類獲得螢幕的寬高和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 = ;
    private static int screenHeight = ;
    private static float screenDensity = ;
    private static int densityDpi = ;
    private static int statusBarHeight = ;



    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 + f);
    }

    public static int px2dip(float pxValue){

        return (int)(pxValue / screenDensity + f);
    }

    public static int getScreenWidth() {
        return screenWidth;
    }

    public static int getScreenHeight() {
        return screenHeight;
    }

}
           

運作界面:

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

       大概說下裡面核心的幾個函數:

1,使用view動畫TranslateAnimation:

private void playAnim2(){
        int w = text.getWidth();
        int screenW = UiUtil.getScreenWidth();
        int transX = screenW - w;
        TranslateAnimation transAnim = new TranslateAnimation(, transX, , );
        transAnim.setDuration();
        text.startAnimation(transAnim);
    }
           

2,使用屬性動畫移位:

private void playAnim1(){
        int w = text.getWidth();
        int screenW = UiUtil.getScreenWidth();
        int transX = screenW - w;
        ObjectAnimator transAnim = ObjectAnimator.ofFloat(text, "translationX", , 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();
        transAnim.start();;
    }
           

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[];
            text.getLocationInWindow(pos);
            Log.i(TAG, "location, x = " + pos[] + " y = " + pos[]);
            Toast.makeText(getApplicationContext(), s, Toast.LENGTH_LONG).show();
        }
    }
           

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

private void resetPos(){
        ViewHelper.setTranslationX(text, );
    }
           

       直接将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(, transX, , );
        transAnim.setDuration();
//        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);
    }
           

       即使用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 Attributes
android:fillAfter
Parameters
fillAfter   true if the animation should apply its transformation after it ends
           

       如果為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.

文中測試代碼下載下傳:http://download.csdn.net/detail/yanzi1225627/9034125

--—–--本文系原創,轉載注明作者yanzi1225627

繼續閱讀