애니메이션을 사용하여 프래그먼트 간 탐색

Fragment API는 탐색 중에 모션 효과와 변환을 사용하여 프래그먼트를 시각적으로 연결할 수 있는 두 가지 방법을 제공합니다. 그중 하나는 AnimationAnimator를 모두 사용하는 애니메이션 프레임워크입니다. 다른 하나는 공유 요소의 전환을 포함하는 전환 프레임워크입니다.

들어오거나 나가는 프래그먼트에 사용하거나 프래그먼트 간 공유 요소의 전환에 사용하기 위한 맞춤 효과를 지정할 수 있습니다.

  • 들어가기 효과는 프래그먼트가 화면에 들어가는 방법을 결정합니다. 예를 들어 특정 프래그먼트로 이동할 때 화면 가장자리에서 그 프래그먼트를 밀어넣는 효과를 만들 수 있습니다.
  • 나가기 효과는 프래그먼트가 화면에서 나가는 방법을 결정합니다. 예를 들어 특정 프래그먼트를 벗어날 때 그 프래그먼트를 페이드 아웃하는 효과를 만들 수 있습니다.
  • 공유 요소의 전환은 두 프래그먼트 사이에 공유된 뷰가 프래그먼트 간에 어떻게 이동되는지를 결정합니다. 예를 들어 프래그먼트 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%" />

또한 백 스택(사용자가 위로 버튼 또는 뒤로 버튼을 탭할 때 발생할 수 있음)을 표시할 때 실행할 들어가기 효과와 나가기 효과의 애니메이션도 지정할 수 있습니다. 이를 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

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

자바

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

자바

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 전환을 사용하는 것이 좋습니다. AndroidX 전환이 API 수준 14 이상에서 지원되고 이전 프레임워크 전환 버전에 없는 버그 수정을 포함하기 때문입니다.

공유 요소의 전환 사용

전환 프레임워크의 일부인 공유 요소의 전환은 프래그먼트 전환 중에 관련 뷰가 두 프래그먼트 사이에 어떻게 이동하는지를 결정합니다. 예를 들면 그림 3에서처럼 프래그먼트 A의 ImageView에 표시된 이미지를 프래그먼트 B가 표시되면 B로 전환하려고 할 수 있습니다.

공유 요소를 이용한 프래그먼트 전환
그림 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”)
    }
}

자바

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

자바

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

자바

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()을 사용하면 됩니다.

전환 연기

잠시 프래그먼트 전환을 연기해야 하는 경우가 있을 수 있습니다. 예를 들어 Android에서 전환의 시작 상태와 종료 상태가 정확히 파악되도록 들어오는 프래그먼트의 모든 뷰가 측정되고 배치될 때까지 기다려야 하는 경우가 있을 수 있습니다.

필요한 일부 데이터가 로드될 때까지 전환을 연기해야 하는 경우도 있을 수 있습니다. 예를 들어 공유 요소의 이미지가 로드될 때까지 기다려야 하는 경우입니다. 그러지 않고 전환 중이나 전환 후에 이미지 로드가 끝나면 전환이 불안정해질 수 있기 때문입니다.

전환을 연기하려면 먼저 프래그먼트 트랜잭션에서 프래그먼트 상태 변경의 재정렬을 허용하는지 확인해야 합니다. 프래그먼트 상태 변경의 재정렬을 허용하려면 다음 예에서와 같이 FragmentTransaction.setReorderingAllowed()를 호출합니다.

Kotlin

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

자바

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

자바

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

자바

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

자바

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

추가 리소스

프래그먼트 전환에 관한 자세한 내용은 다음 추가 리소스를 참조하세요.

샘플

블로그 게시물