標籤雲

搜尋此網誌

2015/09/03

Animation - Scene Trasitions

關於 layout 動畫轉場的實現
可以用 Property Animation 去做
Android 4.0 (API level 14)後也可以在 xml 裡使用 android:animateLayoutChanges="true" 這樣的方式來做

但 4.4.2 KitKat (API level 19) 新增了 Scene Transition 的功能
從官方範例 Basic Transition 來看非常的簡單好用
以下就簡單介紹一下

mSceneRoot = (ViewGroup) view.findViewById(R.id.scene_root);
// 用 new 的方式建立 Scene 物件
// (這個建構子在 API level 21 起已經被標為 deprecated, Lollipop 以後應該用 Scene(ViewGroup, View) 代替).
mScene1 = new Scene(mSceneRoot, (ViewGroup) mSceneRoot.findViewById(R.id.container));
// 也可以呼叫Scene.getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context)
// 從 layout 的 xml 來建立 scene
mScene2 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene2, getActivity());

mScene3 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene3, getActivity());
// 自訂 TransitionManager 來設定轉場效果
mTransitionManagerForScene3 = TransitionInflater.from(getActivity()).inflateTransitionManager(
 R.transition.scene3_transition_manager, SceneRoot
);


/res/transition/scene3_transition_manager.xml
<transitionManager xmlns:android="http://schemas.android.com/apk/res/android">
    <transition
        android:toScene="@layout/scene3"
        android:transition="@transition/changebounds_fadein_together"/>
</transitionManager>

/res/transition/changebounds_fadein_together.xml
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeBounds/>
    <fade android:fadingMode="fade_in">
        <targets>
            <target android:targetId="@id/transition_title" />
        </targets>
    </fade>
</transitionSet>
上面的寫法動畫會同時發生
如果要照順序的話可以在transitionSet tag 加上 android:transitionOrdering="sequential" 屬性
(TransitionSet 類別裡 ORDERING_SEQUENTIAL = 1, ORDERING_TOGETHER = 0, 所以沒設這屬性的話預設是 together)

進行轉場的方法
在不同的 Scene 中若有 id 相同的元件,Transition就會自動產生動畫去改變位置及大小
若有新增或消失的元件,則會 fade in/fade out

內建的 Transition:
ClassTagAttributesEffect
AutoTransition<autoTransition/> - 預設的 transition.
按順序執行 Fade out -> move -> resize -> fade in
Fade<fade/>android:fadingMode=
"[fade_in|fade_out|fade_in_out]"
fade_in
fade_out
fade_in_out (預設) 先fade_outfade_in
ChangeBounds<changeBounds/> - Moves and resizes views.

不過 transition 對一些元件是無法正常作用的:
SurfaceView - SurfaceView 是在 Non-UI thread 更新的
TextureView - 有些動畫無法套用
繼承 AdapterView 的類別(如 ListView) - AdapterView 有其管理 child 的方式, 如果套用 transition 可能會導致畫面無反應
TextView - 無法動畫呈現 resize

可以呼叫 public Transition removeTarget (target) 方法來把不想要進行轉場動畫的或是上述這些無法正常作用的 view 從 transition 給移除
target 參數可以是 Class, View, int targetId, String targetName
另外還有
public Transition excludeTarget (target, boolean exclude)
public Transition addTarget (target)
感覺使用上還蠻自由的

// 可以用 TransitionManager.go() 讓轉場自動進行
TransitionManager.go(mScene2);
// TransitionManager.go 有一個 overload 的方法 可以傳入指定的 transition
// public static void go (Scene scene, Transition transition)

// 或是用剛剛自訂的 TransitionManager 呼叫 transitionTo(Scene scene)
mTransitionManagerForScene3.transitionTo(mScene3);

// 轉場也可以不靠 Scene 來進行
// 這裡先呼叫 TransitionManager.beginDelayedTransition(final ViewGroup sceneRoot)
// 他會把目前 View 的狀態跟屬性記下來
// 然後就開始改變 View 吧, TransitionManager 也會把新的狀態記下來
// 當系統 redraw UI的時候動畫就會進行了
TransitionManager.beginDelayedTransition(mSceneRoot);
View square = mSceneRoot.findViewById(R.id.transition_square);
ViewGroup.LayoutParams params = square.getLayoutParams();
int newSize = getResources().getDimensionPixelSize(R.dimen.square_size_expanded);
params.width = params.height = newSize;
square.setLayoutParams(params);

// 也可以用 ViewGroup.removeView() 和 ViewGroup.addView() 來移除/增加 View
mLabelText = new TextView();
mLabelText.setText("Label").setId("1");
mSceneRoot.addView(mLabelText);

Transition 生命週期事件可以透過 android.transition.Transition.TransitionListener 監聽

onTransitionStart(Transition transition)
onTransitionCancel(Transition transition)
onTransitionPause(Transition transition)
onTransitionResume(Transition transition)
onTransitionEnd(Transition transition)

另外如果在轉場前後有需要利用 callback 做些事情
可以透過 Scene.setExitAction(Runnable action) 和 public void setEnterAction (Runnable action) 這兩個方法來定義

starting scene 的 setExitAction(Runnable action) 會在轉場執行前被呼叫
ending scene 的 setEnterAction (Runnable action) 會在轉場完成後被呼叫

不過官方強調請不要用這兩個方法來在場景間傳遞資料...
正確方式是把 View 的 data 先存起來,等到 transition 完成以後會觸發 onTransitionEnd(Transition transition),這時再把 data 塞回 View

2015/9/11 更新:
由於 scene 轉換後,裡面的 view 跟原本的不會是同一個 instance
這樣會導致原本設定的 listener 都無法正常動作(如 OnclickListener)
而官方的簡單範例當然沒有關於這部分的 code

所以我自己想了一些解決的方法:
方法一 - 將事件寫在 xml 裡
android:onClick="onMyViewClicked"
這方法限制很多,首先這必須是簡單而且官方已經有的事件,其次必須在 Activity 裡建立相對應的 onMyViewClicked(View) 方法
所以這解法基本上並不好用

方法二 - 將 listener 的指派放在 TransitionManager.go(mEndingScene) 之後
這樣問題可以解決且很直接,但是這樣 code 看起來不美觀且有點繁雜(個人對程式碼有點龜毛...)

所以我最後把方法二改良成方法三 - 利用 Transition 的 onTransitionEnd 來做
好處是 view 的 listener 指派都集中在一個方法
要用哪個解法就看各人自由選擇嚕

AutoTransition defaultTransition;
private Scene mCurrentScene;

private void initViewListener() {
    //將 view 的 listener 指派都集中在一個方法裡設定
    View square = mSceneRoot.findViewById(R.id.transition_square);
    //由於有的 view 可能已經不在畫面上,所以還是先判斷是否為 null 比較保險
    //(當然也可以用目前所在的 scene 來判斷畫面上會有那些 view,不過那樣寫會比較多 if 判斷,個人不喜歡)
    if(square != null) { 
        square.setOnClickListener(onSquareClick);
    }
    View oval = mSceneRoot.findViewById(R.id.transition_oval);
    if(oval != null) {
        oval.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 省略...
            }
        });
    }
}

Transition.TransitionListener transitionListener = new Transition.TransitionListener() {
    @Override
    public void onTransitionStart(Transition transition) { }
    @Override
    public void onTransitionEnd(Transition transition) {
        initViewListener(); //在轉場完成後進行 listener 指派
    }
    @Override
    public void onTransitionCancel(Transition transition) { }
    @Override
    public void onTransitionPause(Transition transition) { }
    @Override
    public void onTransitionResume(Transition transition) { }
};

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
    // 省略...

    //在 onCreateView 將 Transition 給 new 出來並加上 transitionListener
    //這裡我使用預設動作的 AutoTransition
    defaultTransition = new AutoTransition();
    defaultTransition.addListener(transitionListener);

    // 省略...

    // TransitionManager 的 instance 可以透過 setTransition(scene, transition) 為場景指定 transition
    mTransitionManagerForScene3 = TransitionInflater.from(getActivity())
        .inflateTransitionManager(R.transition.scene3_transition_manager, mSceneRoot);
    mTransitionManagerForScene3.setTransition(mScene3, defaultTransition);
}

public void switchScene(int sceneIndex) {
    switch (sceneIndex) {
        case 1:
            // 傳入 transition 參數指定使用已經加了 listener 的 transition
            // (這裡統一都用 defaultTransition)
            TransitionManager.go(mScene1, defaultTransition); 
            mCurrentScene = mScene1; //如果同一個 id 的 view 在不同 scene 要有不同動作的話,可以把目前的 scene 記住
            break;
        case 2:
            TransitionSet tranSet = new TransitionSet();
            tranSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
            tranSet.addTransition(defaultTransition);
            CustomTrans customTrans = new CustomTrans();
            tranSet.addTransition(customTrans);

            //tranSet.addListener(transitionListener);

            //TransitionSet 可以直接加 listener 但由於我們已經在 defaultTransition 加了,不需要重複做
            //(TransitionSet 與裡面單一 Transition 的 onTransitionEnd 在意義上是不同的,雖然在本例中是沒差,但還是應該注意)

            TransitionManager.go(mScene2, tranSet);
            mCurrentScene = mScene2; //如果同一個 id 的 view 在不同 scene 要有不同動作的話,可以把目前的 scene 記住
            break;
        case 3:
            //前面已經在 TransitionManager 實體將 mScene3 的 transition 指定為加了 listener 的 defaultTransition
            //這裡直接用 transitionTo
            mTransitionManagerForScene3.transitionTo(mScene3);
            mCurrentScene = mScene3; //如果同一個 id 的 view 在不同 scene 要有不同動作的話,可以把目前的 scene 記住
            break;
        case 4:
            //由於 listener 是監聽 Transition 的狀態,所以不轉換 scene 的 transition 也可以用
            TransitionManager.beginDelayedTransition(mSceneRoot, defaultTransition);
            // 省略...
            break;
    }
}
// 省略...

相關資料:
Android sample code - Basic Transition
Animating Views Using Scenes and Transitions
YouTube - DevBytes: Android 4.4 Transitions

沒有留言: