Android 場景動畫 Scene 的一些剖析

這個 Android 官方源碼的注解:

A scene represents the collection of values that various properties in the View hierarchy will have when the scene is applied. A Scene can be configured to automatically run a Transition when it is applied, which will animate the various property changes that take place during the scene change.
大致的中文意思是:
場景表示應用場景時視圖層次結構中的各種屬性,將具有的值的集合。 可以將場景配置為在應用自動運行過渡時,這將為場景更改期間發生的各種屬性、更改設置動畫。

Scene 是Android 19 引入的轉換框架中一個場景 api ,幫我們友好的創建開始布局 Scene 和結束布局 Scene,有了開始 Scene 和結束 Scene,運用 Transition 框架來實現帶有動畫的場景切換。舉個例子,從 A 布局切換到 B 布局,一般情況下處理是 View.GONE 或者 View.VISIBLE,但是這樣太生硬了,沒有一點過度效果。那么 Android 的 Transition 框架就可以完美的解決切換場景帶來的生硬視覺感受。

其中 Scene是一個容器,就是放置你定義的布局,而真正去做場景之間切換這個動作是 Transition 框架中 TransitionManager 調用其中 go 方法或者 transitionTo 方法完成場景之間切換,而真正創建具體動畫交由Transition 子類來完成,開始動畫交給 Transition 來執行。

借助 Android 的過渡框架,您只需提供起始布局和結束布局,即可為界面中的各種運動添加動畫效果。您可以選擇所需的動畫類型(例如,淡入/淡出視圖或更改視圖尺寸),而過渡框架會確定如何為從起始布局到結束布局的運動添加動畫效果。

過渡框架包含以下功能:

  • 群組級動畫:將一個或多個動畫效果應用于視圖層次結構中的所有視圖。
  • 內置動畫:對淡出或移動等常見效果使用預定義動畫。
  • 資源文件支持:從布局資源文件加載視圖層次結構和內置動畫。
  • 生命周期回調:接收可控制動畫和層次結構更改流程的回調。

注意:如何在同一 Activity 的各個布局之間打造過渡效果。如果用戶在多個 Activity 之間移動,您應改為參閱啟動使用動畫的 Activity

在兩種布局之間添加動畫效果的基本流程如下所示:

  1. 為起始布局和結束布局創建一個 [Scene](https://developer.android.google.cn/reference/android/transition/Scene) 對象。然而,起始布局的場景通常是根據當前布局自動確定的。
  2. 創建一個 [Transition](https://developer.android.google.cn/reference/android/transition/Transition) 對象以定義所需的動畫類型。
  3. 調用 [TransitionManager.go()](https://developer.android.google.cn/reference/android/transition/TransitionManager#go(android.transition.Scene)),然后系統會運行動畫以交換布局。

圖 1 中的示意圖說明了布局、場景、過渡和最終動畫之間的關系。

圖 1. 過渡框架如何創建動畫的基本圖示

具體場景動畫 Scene 的創建場景、應用過渡等具體介紹,可以觀看 Android 官方文檔:https://developer.android.google.cn/training/transitions/

public class Scene {

    private Context mContext;
    private int mLayoutId = -1;
    private ViewGroup mSceneRoot;
    private View mLayout; // alternative to layoutId
    private Runnable mEnterAction, mExitAction;

    /**
     * 返回由與給定 layoutId 參數關聯的資源文件描述的場景。 如果已經為給定的 sceneRoot 創建了這樣的場    
     * 景,則將返回相同的場景。 這種基于 layoutId 的場景的緩存允許在代碼中創建的場景和由       
     * TransitionManager XML 資源文件引用的場景之間共享常見場景。
     * 
     *
     * @param sceneRoot The root of the hierarchy in which scene changes
     *                  and transitions will take place.
     * @param layoutId  The id of a standard layout resource file.
     * @param context   The context used in the process of inflating
     *                  the layout resource.
     * @return The scene for the given root and layout id
     */
    public static Scene getSceneForLayout(@NonNull ViewGroup sceneRoot, @LayoutRes int layoutId,
            @NonNull Context context) {
        SparseArray<Scene> scenes =
                (SparseArray<Scene>) sceneRoot.getTag(R.id.transition_scene_layoutid_cache);
        if (scenes == null) {
            scenes = new SparseArray<>();
            sceneRoot.setTag(R.id.transition_scene_layoutid_cache, scenes);
        }
        Scene scene = scenes.get(layoutId);
        if (scene != null) {
            return scene;
        } else {
            scene = new Scene(sceneRoot, layoutId, context);
            scenes.put(layoutId, scene);
            return scene;
        }
    }

    /**
     * 構造一個場景,但沒有關于應用此場景時值將如何變化的信息。 此構造函數可能在創建場景時使用,目 
     * 的是通過設置 setEnterAction(Runnable) 和可能的 setExitAction(Runnable) 進行動態配置。
     * {@link #setExitAction(Runnable)}.
     *
     * @param sceneRoot The root of the hierarchy in which scene changes
     *                  and transitions will take place.
     */
    public Scene(@NonNull ViewGroup sceneRoot) {
        mSceneRoot = sceneRoot;
    }

    /**
     * 構造一個場景,當進入該場景時,它將從 sceneRoot 容器中移除所有子節點,并將膨脹并添加 layoutId  
     * 資源文件指定的層次結構。這個方法是隱藏的,因為基于 layoutId 的場景應該由緩存工廠方法
     *  Scene.getCurrentScene(View) 創建。
     *
     * @param sceneRoot The root of the hierarchy in which scene changes
     *                  and transitions will take place.
     * @param layoutId  The id of a resource file that defines the view
     *                  hierarchy of this scene.
     * @param context   The context used in the process of inflating
     *                  the layout resource.
     */
    private Scene(ViewGroup sceneRoot, int layoutId, Context context) {
        mContext = context;
        mSceneRoot = sceneRoot;
        mLayoutId = layoutId;
    }

    /**
     * 構造一個 Scene ,當進入該場景時,將從 sceneRoot 容器中移除所有子級,并將布局對象添加為該容器 
     * 的新子級。
     *
     * @param sceneRoot The root of the hierarchy in which scene changes
     *                  and transitions will take place.
     * @param layout    The view hierarchy of this scene, added as a child
     *                  of sceneRoot when this scene is entered.
     */
    public Scene(@NonNull ViewGroup sceneRoot, @NonNull View layout) {
        mSceneRoot = sceneRoot;
        mLayout = layout;
    }

    /**
     * 獲取場景的根,它是由于該場景而受到更改影響的視圖層次結構的根,并且在進入該場景時將被動畫      
     * 化。
     *
     * @return The root of the view hierarchy affected by this scene.
     */
    @NonNull
    public ViewGroup getSceneRoot() {
        return mSceneRoot;
    }

    /**
     * 如果它是場景的場景根上的當前場景,則退出該場景。 進入場景時設置當前場景。 如果有一個場景,則 
      * 退出場景會運行退出動作。
     */
    public void exit() {
        if (getCurrentScene(mSceneRoot) == this) {
            if (mExitAction != null) {
                mExitAction.run();
            }
        }
    }

    /**
     * 進入此場景,這需要更改此場景指定的所有值。 這些可能是與現在將添加到場景根的布局視圖組或布局 
     * 資源文件相關聯的值,或者可能是由 setEnterAction(Runnable) enter action} 更改的值,或這些的組      
     * 合。 進入場景時不會運行任何過渡。 要在場景更改中獲得過渡行為,請改用 TransitionManager 中的方法之一。
     */
    public void enter() {
        // Apply layout change, if any
        if (mLayoutId > 0 || mLayout != null) {
            // empty out parent container before adding to it
            getSceneRoot().removeAllViews();

            if (mLayoutId > 0) {
                LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
            } else {
                mSceneRoot.addView(mLayout);
            }
        }

        // Notify next scene that it is entering. Subclasses may override to configure scene.
        if (mEnterAction != null) {
            mEnterAction.run();
        }

        setCurrentScene(mSceneRoot, this);
    }

    /**
     * 設置給定 ViewGroup 所在的場景。當前場景僅設置在場景的根 ViewGroup 上,而不是針對該層次結構  
     * 中的每個視圖。 Scene 使用此信息來確定是否存在應在進入新場景之前退出的先前場景。
     *
     * @param sceneRoot The ViewGroup on which the current scene is being set
     */
    static void setCurrentScene(@NonNull ViewGroup sceneRoot, @Nullable Scene scene) {
        sceneRoot.setTag(R.id.transition_current_scene, scene);
    }

    /**
     * 獲取給定 ViewGroup 上的當前場景集。 只有當 ViewGroup 是場景根時,才會在 ViewGroup 上設置場   
     * 景。
     *
     * @param sceneRoot The ViewGroup on which the current scene will be returned
     * @return The current Scene set on this ViewGroup. A value of null indicates that
     * no Scene is currently set.
     */
    @Nullable
    public static Scene getCurrentScene(@NonNull ViewGroup sceneRoot) {
        return (Scene) sceneRoot.getTag(R.id.transition_current_scene);
    }

    /**
     * 未使用布局資源或層次結構定義的場景,或者在這些層次結構更改為之后需要執行額外步驟的場景,應  
     * 設置進入動作,也可能設置退出動作。 輸入動作將導致場景回調到應用程序代碼中,以便在轉換捕獲預 
     * 更改值之后以及在應用任何其他場景更改之后執行應用程序所需的任何其他操作,例如將布局(如果      
     * 有) 添加到視圖中 等級制度。 調用此方法后,將播放過渡。
     *
     * @param action The runnable whose {@link Runnable#run() run()} method will
     *               be called when this scene is entered
     * @see #setExitAction(Runnable)
     * @see Scene#Scene(ViewGroup)
     */
    public void setEnterAction(@Nullable Runnable action) {
        mEnterAction = action;
    }

    /**
     * 未使用布局資源或層次結構定義的場景,或者在這些層次結構更改為之后需要執行額外步驟的場景,應  
     * 設置進入動作,也可能設置退出動作。 退出操作將導致場景回調到應用程序代碼中,以在適用的轉換已   
     * 捕獲更改前值之后但在應用任何其他場景更改之前執行應用程序需要執行的任何操作,例如新布局(如   
     * 果有) 添加到視圖層次結構中。 調用此方法后,將進入下一個場景,如果設置了進入操作,則調用       
     * setEnterAction(Runnable)。
     *
     * @see #setEnterAction(Runnable)
     * @see Scene#Scene(ViewGroup)
     */
    public void setExitAction(@Nullable Runnable action) {
        mExitAction = action;
    }

    /**
     * 返回此場景是否由布局資源文件創建,由傳遞給 getSceneForLayout(ViewGroup, int, Context) 的           
     * layoutId 確定。
     */
    boolean isCreatedFromLayoutResource() {
        return (mLayoutId > 0);
    }

}

對場景動畫 Scene 的具體代碼實踐,可以參考 Android 官方開發團隊的 Demo:
https://github.com/android/animation-samples/tree/main/BasicTransition

場景是視圖層次結構狀態的封裝,包括該層次結構中的視圖以及這些視圖具有的各種值(與布局相關的和其他的)。 場景可以直接通過布局層次結構定義,也可以通過在輸入場景時動態設置場景的代碼來定義。

Transition 是一種自動動畫化進入新場景時發生的變化的機制。 一些轉換功能是自動的。 也就是說,進入場景可能會導致動畫運行,淡出消失的視圖,更改邊界并調整已更改的現有視圖的大小,以及淡入可見的視圖。 還有一些額外的過渡可以為其他屬性設置動畫,例如顏色變化,并且可以選擇性地指定在特定場景變化期間發生。 最后,開發人員可以定義他們自己的 Transition 子類,這些子類監視特定的屬性更改并在這些屬性更改值時運行自定義動畫。

TransitionManager 用于為特定場景更改指定自定義轉換,并導致具有特定轉換的場景更改發生。
具體使用,可看如下代碼示例:

public class BasicTransitionFragment extends Fragment
        implements RadioGroup.OnCheckedChangeListener {

    // We transition between these Scenes
    private Scene mScene1;
    private Scene mScene2;
    private Scene mScene3;

    /** A custom TransitionManager */
    private TransitionManager mTransitionManagerForScene3;

    /** Transitions take place in this ViewGroup. We retain this for the dynamic transition on scene 4. */
    private ViewGroup mSceneRoot;

    public static BasicTransitionFragment newInstance() {
        return new BasicTransitionFragment();
    }

    public BasicTransitionFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_basic_transition, container, false);
        assert view != null;
        RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.select_scene);
        radioGroup.setOnCheckedChangeListener(this);
        mSceneRoot = (ViewGroup) view.findViewById(R.id.scene_root);

        // BEGIN_INCLUDE(instantiation_from_view)
        // A Scene can be instantiated from a live view hierarchy.
        mScene1 = new Scene(mSceneRoot, (ViewGroup) mSceneRoot.findViewById(R.id.container));
        // END_INCLUDE(instantiation_from_view)

        // BEGIN_INCLUDE(instantiation_from_resource)
        // You can also inflate a generate a Scene from a layout resource file.
        mScene2 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene2, getActivity());
        // END_INCLUDE(instantiation_from_resource)

        // Another scene from a layout resource file.
        mScene3 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene3, getActivity());

        // BEGIN_INCLUDE(custom_transition_manager)
        // We create a custom TransitionManager for Scene 3, in which ChangeBounds and Fade
        // take place at the same time.
        mTransitionManagerForScene3 = TransitionInflater.from(getActivity())
                .inflateTransitionManager(R.transition.scene3_transition_manager, mSceneRoot);
        // END_INCLUDE(custom_transition_manager)

        return view;
    }

    @Override
    public void onCheckedChanged(RadioGroup group, int checkedId) {
        switch (checkedId) {
            case R.id.select_scene_1: {
                // BEGIN_INCLUDE(transition_simple)
                // You can start an automatic transition with TransitionManager.go().
                TransitionManager.go(mScene1);
                // END_INCLUDE(transition_simple)
                break;
            }
            case R.id.select_scene_2: {
                TransitionManager.go(mScene2);
                break;
            }
            case R.id.select_scene_3: {
                // BEGIN_INCLUDE(transition_custom)
                // You can also start a transition with a custom TransitionManager.
                mTransitionManagerForScene3.transitionTo(mScene3);
                // END_INCLUDE(transition_custom)
                break;
            }
            case R.id.select_scene_4: {
                // BEGIN_INCLUDE(transition_dynamic)
                // Alternatively, transition can be invoked dynamically without a Scene.
                // For this, we first call TransitionManager.beginDelayedTransition().
                TransitionManager.beginDelayedTransition(mSceneRoot);
                // Then, we can just change view properties as usual.
                View square = mSceneRoot.findViewById(R.id.transition_square);
                ViewGroup.LayoutParams params = square.getLayoutParams();
                int newSize = getResources().getDimensionPixelSize(R.dimen.square_size_expanded);
                params.width = newSize;
                params.height = newSize;
                square.setLayoutParams(params);
                // END_INCLUDE(transition_dynamic)
                break;
            }
        }
    }

}
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容