使用動畫以瀏覽方式切換內容片段

使用 Fragment API 時,您可以透過兩種方式使用動作特效和轉換功能,在瀏覽期間透過視覺效果連結片段。其中一種是同時使用 AnimationAnimator 的「Animation Framework」 (動畫架構)。另一種是 「Transition Framework」(轉換架構),其中包含共用元素轉換功能。

您可以指定自訂進入和離開片段的效果以及在片段之間轉換共用元素。

  • 「進入」效果可決定片段進入螢幕的方式。例如,瀏覽片段時,您可以建立一個使片段從畫面邊緣滑入的效果。
  • 「離開」效果可決定片段離開畫面的方式。例如,瀏覽離開片段時,您可以建立一個淡出片段的效果。
  • 「共用元素轉換功能」可決定兩個片段之間切換移動時共用檢視畫面的方式。例如,A 片段 ImageView 中顯示的圖片在 B 片段顯示後就立即轉換為 B 片段的圖片。

設定動畫

首先,需要建立動畫,用於瀏覽新片段時執行的進入和離開效果。您可以將動畫定義為補間動畫資源。這些資源可讓您定義片段在動畫期間的旋轉、縮放、淡出和移動方式。例如,如要讓目前片段淡出,並將新的片段從畫面右側邊緣滑入,如圖 1 所示。

進入及離開動畫。目前的片段淡出,同時下一個片段從右側滑入。
圖 1. 進入及離開動畫。目前的片段淡出,同時下一個片段從右側滑入。

可在 res/anim 目錄中定義這些動畫:

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

此外,還可以為彈出返回堆疊時執行的進入和離開效果指定動畫,當使用者輕觸「Up」(向上) 或「Back」(返回) 按鈕時就會出現動畫。這些動畫稱為 popEnterpopExit 動畫。舉例來說,當使用者返回上一個畫面時,如果要讓目前的片段從畫面右側邊緣滑出,然後上一個片段淡入。

popEnter 和 popExit 動畫。目前的片段向右滑出畫面,同時上一個片段淡入。
圖 2. popEnterpopExit 動畫。目前的片段向右滑出畫面,同時上一個片段淡入。

這些動畫可以定義如下:

<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

定義動畫後,請呼叫 FragmentTransaction.setCustomAnimations() 來使用動畫,並透過動畫資源 ID 傳入您的動畫資源中,如下列範例所示:

Kotlin

supportFragmentManager.commit {
    setCustomAnimations(
        R.anim.slide_in, // enter
        R.anim.fade_out, // exit
        R.anim.fade_in, // popEnter
        R.anim.slide_out // popExit
    )
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in,  // enter
        R.anim.fade_out,  // exit
        R.anim.fade_in,   // popEnter
        R.anim.slide_out  // popExit
    )
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

設定轉場效果

您也可以使用轉場效果定義進入效果和離開效果。這些轉場效果可以在 XML 資源檔案中定義。舉例來說,如果要讓目前的片段淡出,再將新的片段從畫面右側邊緣滑入。可以按照以下步驟定義這些轉換:

<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

轉場效果後定義完成後,即可對進入片段呼叫 setEnterTransition() 並對離開片段呼叫 setExitTransition(),以套用這些轉場效果,然後透過資源 ID 傳入加載的轉場效果中,如以下範例所示:

Kotlin

class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setExitTransition(inflater.inflateTransition(R.transition.fade));
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
    }
}

片段支援 AndroidX 轉場效果。雖然片段也支援 架構轉換,但我們強烈建議您使用 AndroidX 轉場效果,因為 API 級別 14 以上可支援,而且其中包含舊版本的架構轉換中沒有的錯誤修正。

使用共用元素轉換功能

Transition Framework (轉換架構) 中,共用元素轉換功能可決定在片段轉換期間對應檢視畫面在兩個片段之間移動的方法。例如,如果希望 A 片段 ImageView 中顯示的圖片在 B 片段顯示後就立即轉換為 B 片段的圖片 (如圖 3 所示)。

使用共用元素進行片段轉換。
圖 3. 使用共用元素進行片段轉換。

大致來說,以下是使用共用元素進行片段轉換的方法:

  1. 為每個共用元素檢視畫面指派不重複使用的轉換名稱。
  2. 將共用元素檢視畫面和轉換名稱新增至 FragmentTransaction
  3. 設定共用元素轉換動畫。

您必須先為每個共用元素檢視畫面指派一個不重複使用的轉換名稱,以允許系統將檢視畫面從一個片段對應至下一個片段。使用 ViewCompat.setTransitionName() 為每個片段配置的共用元素設定轉換名稱,以提供 API 級別 14 以上級別的相容性。舉例來說,可以按照以下方式指派 A 片段和 B 片段中的 ImageView 轉換名稱:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, item_image)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, hero_image)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView itemImageView = view.findViewById(R.id.item_image);
        ViewCompat.setTransitionName(itemImageView, item_image);
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView heroImageView = view.findViewById(R.id.hero_image);
        ViewCompat.setTransitionName(heroImageView, hero_image);
    }
}

如要在片段轉換中加入共用元素,FragmentTransaction 必須瞭解每個共用元素的檢視畫面如何從下一個片段對應至下一個片段。呼叫 FragmentTransaction.addSharedElement(),將每個分享元素新增至 FragmentTransaction,然後將對應檢視畫面的檢視畫面和轉換名稱傳入下一個片段中,如以下範例所示:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(...)
    addSharedElement(itemImageView, hero_image)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(...)
    .addSharedElement(itemImageView, hero_image)
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

如要指定共用元素如何從一個片段轉換到下一個片段的方法,必須在即將前往的片段上設定「進入」轉換。呼叫片段的 onCreate() 方法中的 Fragment.setSharedElementEnterTransition(),如以下範例所示:

Kotlin

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Transition transition = TransitionInflater.from(requireContext())
            .inflateTransition(R.transition.shared_image);
        setSharedElementEnterTransition(transition);
    }
}

shared_image 轉換的定義如下:

<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

共用元素轉換時,系統支援 Transition 的所有子類別。如要建立自訂的 Transition,請參閱建立自訂轉換動畫一文。上一個範例中使用的 changeImageTransform 是可以使用的其中一個可用預建平移。您可以在 Transition API 參考資料中找到其他的 Transition 子類別。

根據預設,共用元素進入轉換也用作共用元素的「回傳」轉換。片段轉換從返回堆疊中彈開時,回傳轉換決定共用元素返回上一個片段的方式。如要指定不同的回傳轉換,請使用片段的 onCreate() 方法中的 Fragment.setSharedElementReturnTransition() 指定。

預測返回相容性

預測返回功能可以與許多 (但非全部的跨片段) 動畫搭配使用。 實作預測返回手勢時,請注意下列事項:

  • 請匯入 Transitions 1.5.0 以上版本,以及 Fragments 1.7.0 以上版本。
  • Animator 類別、子類別和 AndroidX Transition 程式庫為 支援。
  • 不支援 Animation 類別和架構 Transition 程式庫。
  • 預測片段動畫僅適用於搭載 Android 14 或 更高。
  • setCustomAnimationssetEnterTransitionsetExitTransitionsetReenterTransitionsetReturnTransitionsetSharedElementEnterTransitionsetSharedElementReturnTransition 預測返回功能可支援

詳情請參閱: 新增預測返回動畫支援功能

延遲轉場效果

在某些情況下,您可能需要將片段轉換時間延後一段短時間。舉例來說,可能需要等到已經測量並排版進入片段中的所有檢視畫面,Android 才能準確擷取轉換的開始和結束狀態。

此外,您可能還需要將轉換延後到已載入某些必要資料時。例如,您可能需要等待直到系統為共用元素載入圖片。否則,如果圖片在轉換期間或結束後載入完成,轉換可能引起畫面晃動。

您必須先確保片段交易允許重新排序片段狀態變更,才能延遲轉換。如要允許重新排序片段狀態,請呼叫 FragmentTransaction.setReorderingAllowed(),如以下範例所示:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setReorderingAllowed(true)
    setCustomAnimation(...)
    addSharedElement(view, view.transitionName)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setReorderingAllowed(true)
    .setCustomAnimations(...)
    .addSharedElement(view, view.getTransitionName())
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

如要延遲進入轉換,請在輸入片段的 onViewCreated() 方法中呼叫 Fragment.postponeEnterTransition()

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        postponeEnterTransition()
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        postponeEnterTransition();
    }
}

已載入資料並準備開始轉換時,請呼叫 Fragment.startPostponedEnterTransition()。以下範例使用 Glide 程式庫,將映像檔載入一個共用的 ImageView 中,然後將對應的轉換延遲到映像檔已載入完成時。

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        Glide.with(this)
            .load(url)
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(...) {
                    startPostponedEnterTransition();
                    return false;
                }

                @Override
                public boolean onResourceReady(...) {
                    startPostponedEnterTransition();
                    return false;
                }
            })
            .into(headerImage)
    }
}

如果是處理使用者網路連線緩慢等情況,可能需要在一段時間後啟動延遲轉換,而不是等待系統載入所有資料。在這種情況下,可以改為呼叫進入片段 onViewCreated() 方法中的 Fragment.postponeEnterTransition(long, TimeUnit),然後傳入時間長度和時間單位。指定的時間過了之後,延遲時間統隨即自動開始執行。

透過 RecyclerView 使用共用元素轉換

在測量並排版進入片段的所有檢視畫之前,不應啟動已延遲的進入轉換。使用 RecyclerView 時,必須先等待任何資料載入和已準備好進行繪圖的 RecyclerView 項目,然後才開始轉換。範例如下:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // Wait for the data to load
        viewModel.data.observe(viewLifecycleOwner) {
            // Set the data on the RecyclerView adapter
            adapter.setData(it)
            // Start the transition once all views have been
            // measured and laid out
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        postponeEnterTransition();

        final ViewGroup parentView = (ViewGroup) view.getParent();
        // Wait for the data to load
        viewModel.getData()
            .observe(getViewLifecycleOwner(), new Observer<List<String>>() {
                @Override
                public void onChanged(List<String> list) {
                    // Set the data on the RecyclerView adapter
                    adapter.setData(it);
                    // Start the transition once all views have been
                    // measured and laid out
                    parentView.getViewTreeObserver()
                        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                            @Override
                            public boolean onPreDraw(){
                                parentView.getViewTreeObserver()
                                        .removeOnPreDrawListener(this);
                                startPostponedEnterTransition();
                                return true;
                            }
                    });
                }
        });
    }
}

請注意,片段檢視畫面的父項上設有 ViewTreeObserver.OnPreDrawListener。這是為了確保所有片段的檢視畫面都已經過測量和排版,並因此在開始延遲進入轉換前準備好進行繪圖。

將共用元素轉換搭配 RecyclerView 一起使用時,要考慮的另一點是,無法在 RecyclerView 項目的 XML 版面配置中設定轉換名稱,因為任意數量的項目會共用該版面配置。請務必指派不重複使用的轉換名稱,轉換動畫才能使用正確的檢視畫面。

ViewHolder 綁定時,您可以為每個項目的共用元素指派不重複的轉換名稱,藉此為每個項目提供不重複的轉換名稱。舉例來說,如果每個項目的資料都包含一個不重複的 ID,這個 ID 即可用作轉換名稱,如以下範例所示:

Kotlin

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

Java

public class ExampleViewHolder extends RecyclerView.ViewHolder {
    private final ImageView image;

    ExampleViewHolder(View itemView) {
        super(itemView);
        image = itemView.findViewById(R.id.item_image);
    }

    public void bind(String id) {
        ViewCompat.setTransitionName(image, id);
        ...
    }
}

其他資源

如要進一步瞭解片段轉換,請參閱以下其他資源。

範例

部落格文章