Điều hướng giữa các mảnh bằng ảnh động

Fragment API cung cấp 2 cách sử dụng hiệu ứng chuyển động và biến đổi để kết nối trực quan các mảnh trong quá trình điều hướng. Một trong số đó là Khung ảnh động, sử dụng cả AnimationAnimator. Mục còn lại là Khung chuyển tiếp, bao gồm các chuyển đổi phần tử được chia sẻ.

Bạn có thể chỉ định các hiệu ứng tùy chỉnh để nhập và thoát khỏi các phân mảnh cũng như chuyển đổi của các phần tử được chia sẻ giữa các phân mảnh.

  • Hiệu ứng enter (nhập) xác định cách một phân mảnh dữ liệu xuất hiện trên màn hình. Ví dụ: bạn có thể tạo một hiệu ứng để trượt phân mảnh từ cạnh màn hình khi di chuyển đến nó.
  • Hiệu ứng exit (thoát) xác định cách một phân mảnh dữ liệu rời khỏi màn hình. Ví dụ: bạn có thể tạo một hiệu ứng để làm mờ phân mảnh khi di chuyển ra khỏi nó.
  • Chuyển đổi phần tử được chia sẻ xác định cách một chế độ xem được chia sẻ giữa hai phân mảnh di chuyển giữa chúng. Ví dụ: một hình ảnh hiển thị trong ImageView trong phân mảnh A sẽ chuyển đổi thành phân mảnh B khi B hiển thị.

Cài đặt ảnh động

Trước tiên, bạn cần tạo ảnh động cho các hiệu ứng nhập và thoát. Các hiệu ứng này sẽ chạy khi điều hướng đến một phân mảnh mới. Bạn có thể xác định ảnh động như tài nguyên ảnh động dành cho thanh thiếu niên. Những tài nguyên này cho phép bạn xác định cách các phân mảnh sẽ xoay, kéo giãn, làm mờ và di chuyển trong ảnh động. Ví dụ: bạn có thể muốn phân mảnh hiện tại mờ dần và phân mảnh mới trượt vào từ cạnh phải màn hình, như được hiển thị trong hình 1.

Nhập và thoát ảnh động. Phân mảnh hiện tại này mờ đi trong khi phân mảnh tiếp theo trượt vào từ bên phải.
Hình 1. Nhập và thoát ảnh động. Phân mảnh hiện tại mờ đi và phân mảnh tiếp theo trượt vào từ bên phải.

Bạn có thể xác định các ảnh động này trong thư mục 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%" />

Bạn cũng có thể chỉ định ảnh động cho các hiệu ứng nhập và thoát sẽ chạy khi bật ngăn xếp lui, điều này có thể xảy ra khi người dùng nhấn vào nút mũi tên Lên hoặc Quay lại. Đây được gọi là ảnh động popEnterpopExit. Ví dụ: khi người dùng quay lại màn hình trước đó, bạn có thể muốn phân mảnh hiện tại trượt ra khỏi cạnh phải của màn hình và phân mảnh trước đó sẽ mờ đi.

Ảnh động popEnter và popExit. Phân mảnh hiện tại trượt khỏi màn hình về bên phải khi phân mảnh trước đó mờ dần.
Hình 2. Ảnh động popEnterpopExit. Phân mảnh hiện tại trượt ra khỏi màn hình về bên phải trong khi phân mảnh trước đó mờ dần.

Những ảnh động này có thể được xác định như sau:

<!-- 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" />

Sau khi bạn xác định ảnh động, hãy sử dụng ảnh động đó bằng cách gọi FragmentTransaction.setCustomAnimations(), chuyển tài nguyên ảnh động theo mã tài nguyên của chúng như trong ví dụ sau:

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();

Cài đặt các chuyển đổi

Bạn cũng có thể sử dụng chuyển đổi để xác định hiệu ứng nhập và thoát. Bạn có thể xác định những chuyển đổi này trong tệp tài nguyên XML. Chẳng hạn, bạn có thể muốn phân mảnh hiện tại mờ dần và phân mảnh mới trượt vào từ cạnh phải của màn hình. Những chuyển đổi này có thể được xác định như sau:

<!-- 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" />

Sau khi bạn đã xác định các chuyển đổi, hãy áp dụng chúng bằng cách gọi setEnterTransition() trên phân mảnh vào và setExitTransition() trên phân mảnh thoát, chuyển vào tài nguyên chuyển đổi tăng cường của bạn bằng mã tài nguyên, như trong ví dụ sau:

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

Các phân mảnh hỗ trợ chuyển đổi AndroidX. Mặc dù các phân mảnh cũng hỗ trợ chuyển đổi khung, nhưng bạn nên sử dụng chuyển đổi AndroidX vì chúng được hỗ trợ trong API cấp độ 14 trở lên và có chứa các bản sửa lỗi không có trong phiên bản cũ của các phiên bản chuyển đổi khung.

Dùng các chuyển đổi phần tử dùng chung

Trong Khung chuyển đổi, chuyển đổi của các phần tử được chia sẻ sẽ xác định cách các chế độ xem tương ứng di chuyển giữa hai phân mảnh trong một chuyển đổi phân mảnh. Ví dụ: bạn có thể muốn một hình ảnh hiển thị trong ImageView trên phân mảnh A để chuyển đổi sang phân mảnh B sau khi B hiển thị như minh họa trong hình 3.

Chuyển đổi phân mảnh với phần tử được chia sẻ.
Hình 3. Chuyển đổi phân mảnh với phần tử được chia sẻ.

Ở cấp độ cao, dưới đây là cách thực hiện chuyển đổi phân mảnh với các phần tử dùng chung:

  1. Chỉ định tên chuyển đổi duy nhất cho mỗi chế độ xem thành phần được chia sẻ.
  2. Thêm chế độ xem phần tử dùng chung và tên chuyển đổi vào FragmentTransaction.
  3. Đặt một ảnh động chuyển đổi phần tử dùng chung.

Trước tiên, bạn phải chỉ định tên chuyển đổi duy nhất cho mỗi chế độ xem phần tử được chia sẻ để cho phép liên kết các chế độ xem từ một phân mảnh đến phân mảnh tiếp theo. Đặt tên lượt chuyển đổi cho các phần tử được chia sẻ trong mỗi bố cục phân mảnh bằng cách sử dụng ViewCompat.setTransitionName(), cung cấp khả năng tương thích cho các API cấp độ 14 trở lên. Ví dụ: tên chuyển đổi cho ImageView trong các phân mảnh A và B có thể được gán như sau:

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”);
    }
}

Để đưa các phần tử được chia sẻ trong chuyển đổi phân mảnh, FragmentTransaction của bạn phải biết cách liên kết các chế độ xem của mỗi phần tử được chia sẻ từ phân mảnh này đến phân mảnh tiếp theo. Thêm từng phần tử được chia sẻ vàoFragmentTransaction bằng cách gọi FragmentTransaction.addSharedElement(), chuyển chế độ xem và tên chuyển đổi của chế độ xem tương ứng trong phân mảnh tiếp theo như trong ví dụ sau:

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();

Để chỉ định cách các phần tử dùng chung của bạn chuyển đổi từ một phân mảnh sang phân mảnh tiếp theo, bạn phải đặt chuyển đổi enter (vào) trên phân mảnh sẽ được chuyển đến. Gọi Fragment.setSharedElementEnterTransition() trong phương thức onCreate() của phân mảnh như trong ví dụ sau:

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

Chuyển đổi shared_image được định nghĩa như sau:

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

Tất cả các lớp con của Transition đều được hỗ trợ dưới dạng chuyển đổi phần tử dùng chung. Nếu bạn muốn tạo Transition tuỳ chỉnh, hãy xem bài viết Tạo ảnh động chuyển đổi tuỳ chỉnh. changeImageTransform, được sử dụng trong ví dụ trước, là một trong các bản dịch được tạo sẵn mà bạn có thể sử dụng. Bạn có thể tìm thêm các lớp con Transition trong tệp tham chiếu API cho lớp Transition.

Theo mặc định, chuyển đổi enter (vào) của phần tử được chia sẻ cũng được dùng làm chuyển đổi return (quay lại) cho các phần tử được chia sẻ. Chuyển đổi trả về xác định cách các phần tử được chia sẻ chuyển trở lại phân mảnh trước đó khi giao dịch phân mảnh được bật lên từ ngăn xếp lui. Nếu muốn chỉ định một lượt chuyển đổi trả về khác, bạn có thể thực hiện bằng cách sử dụng Fragment.setSharedElementReturnTransition() trong phương thức onCreate() của phân mảnh.

Khả năng tương thích xem trước thao tác quay lại

Bạn có thể sử dụng tính năng xem trước thao tác quay lại với nhiều, nhưng không phải tất cả, ảnh động trên nhiều mảnh. Khi triển khai tính năng xem trước thao tác quay lại, hãy lưu ý những yếu tố cân nhắc sau đây:

  • Nhập Transitions 1.5.0 trở lên và Fragments 1.7.0 trở lên.
  • Lớp Animator, các lớp con và thư viện chuyển đổi AndroidX được hỗ trợ.
  • Lớp Animation và thư viện Transition của khung không được hỗ trợ.
  • Ảnh động mảnh xem trước chỉ hoạt động trên các thiết bị chạy Android 14 trở lên.
  • setCustomAnimations, setEnterTransition, setExitTransition, setReenterTransition, setReturnTransition, setSharedElementEnterTransitionsetSharedElementReturnTransition được hỗ trợ tính năng xem trước thao tác quay lại.

Để tìm hiểu thêm, hãy xem phần Thêm tính năng hỗ trợ cho ảnh động xem trước thao tác quay lại.

Hoãn chuyển đổi

Trong một số trường hợp, bạn có thể cần trì hoãn chuyển đổi phân mảnh trong khoảng thời gian ngắn. Ví dụ: bạn có thể cần phải chờ cho đến khi tất cả các chế độ xem trong phân mảnh vào được đo lường và bố trí để Android có thể nắm bắt chính xác trạng thái bắt đầu và kết thúc của chúng để phục vụ việc chuyển đổi.

Ngoài ra, chuyển đổi có thể cần được trì hoãn cho đến khi một số dữ liệu cần thiết được tải. Ví dụ: bạn có thể phải đợi đến khi hình ảnh được tải xong cho các phần tử dùng chung. Nếu không, chuyển đổi có thể gây khó chịu nếu một hình ảnh thôi không tải nữa trong hoặc sau khi chuyển đổi.

Để hoãn một lượt chuyển đổi, trước tiên bạn phải đảm bảo giao dịch phân mảnh cho phép thay đổi thứ tự các thay đổi trạng thái phân mảnh. Để cho phép sắp xếp lại các thay đổi về trạng thái phân mảnh, hãy gọi FragmentTransaction.setReorderingAllowed() như trong ví dụ sau:

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();

Để trì hoãn việc chuyển đổi enter (vào), hãy gọi Fragment.postponeEnterTransition() trong phương thức onViewCreated() của phân đoạn vào:

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();
    }
}

Sau khi đã tải dữ liệu và sẵn sàng bắt đầu chuyển đổi, hãy gọi Fragment.startPostponedEnterTransition(). Ví dụ sau đây sử dụng thư viện Glide để tải hình ảnh vào một ImageView chia sẻ, sau đó trì hoãn chuyển đổi tương ứng cho đến khi tải xong hình ảnh.

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)
    }
}

Khi xử lý các trường hợp như kết nối Internet của người dùng bị chậm, bạn có thể cần lượt chuyển đổi bị trì hoãn khởi động sau một khoảng thời gian nhất định hơn là chờ tất cả dữ liệu thực hiện việc tải. Đối với những trường hợp này, bạn có thể gọi Fragment.postponeEnterTransition(long, TimeUnit) trong phương thức onViewCreated() của phân mảnh vào, chuyển vào thời lượng và đơn vị thời gian. Chuyển đổi bị trì hoãn sẽ tự động khởi động sau khi thời gian đã chỉ định trôi qua.

Sử dụng các chuyển đổi phần tử dùng chung thông qua RecyclerView

Các chuyển đổi enter (vào) bị hoãn sẽ không bắt đầu cho đến khi tất cả các chế độ xem trong phân mảnh vào (entering fragment) đã được đo lường và bố trí. Khi sử dụng RecyclerView, bạn phải đợi dữ liệu tải và để các mục RecyclerView sẵn sàng vẽ trước khi bắt đầu chuyển đổi. Ví dụ:

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

Lưu ý ViewTreeObserver.OnPreDrawListener được đặt trên thư mục mẹ (parent) của chế độ xem phân mảnh. Việc này nhằm đảm bảo tất cả các chế độ xem của phân mảnh đều đã được đo lường và bố trí, do đó sẵn sàng được vẽ trước khi bắt đầu chuyển đổi bị hoãn.

Một điểm khác cần cân nhắc khi sử dụng chuyển đổi phần tử được chia sẻ với RecyclerView là bạn không thể đặt tên chuyển đổi trong bố cục XML của mục RecyclerView vì một số lượng bất định (arbitrary) các mục chia sẻ bố cục đó. Bạn phải chỉ định tên chuyển đổi duy nhất để ảnh động chuyển đổi sử dụng chế độ xem chính xác.

Bạn có thể đặt một tên chuyển đổi riêng biệt cho từng phần tử được chia sẻ bằng cách chỉ định chúng khi ViewHolder đã được liên kết. Ví dụ: nếu dữ liệu của mỗi mục bao gồm một mã nhận dạng duy nhất, thì dữ liệu đó có thể được dùng làm tên chuyển đổi, như hiển thị trong ví dụ sau:

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);
        ...
    }
}

Tài nguyên khác

Để tìm hiểu thêm về các chuyển đổi phân mảnh, xem các tài nguyên bổ sung sau đây.

Mẫu

Bài đăng trên blog