Search code examples
androidandroid-fragmentsandroid-recyclerviewnavigationcontrollerandroid-motionlayout

How to animate RecyclerView item to fragment by scaling with Motion Layout


I'm trying to recreate the same motion as exemplified here: https://material.io/develop/android/theming/motion

Specifically the example 2 under MaterialContainerTransform

enter image description here

The detailed explanation suggests an example that transitions from RecyclerView to ViewPager, but this does not apply to my case which is to a Fragment.

As it stands I am unable to make this work properly and since this is such a generic search term any possible examples I hope to find are in a sea of generic results that contain RecyclerView and Fragment.

This is as far as I got, but only the Hold animation on exit is working.

For this project I am using the Navigation Architecture Component in Java

Fragment A

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setExitTransition(new Hold());
}

// this is the recycler view item click listener
private ItemClickCallback getClickCallback() {
    return (view) -> {

        FragmentNavigator.Extras extras = new FragmentNavigator.Extras
            .Builder()
            .addSharedElement(view, view.getTransitionName())
            .build();

        NavHostFragment
            .findNavController(this)
            .navigate(R.id.action_navigation, null, null, extras);

    };
}

Fragment B

@Override
public void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setSharedElementEnterTransition(new MaterialContainerTransform());

}

That is it. There is no difference when clicking an item with that implemented or without, except for the delay as a result of the Hold animation.

Is this a bug in the animation library or is this not being done right? The documentation does not show clearly how to implement this for this case specifically and I assume this is not being done right as a result of the lack of documentation.


Solution

  • It seems that the available documentation is lacking important steps. In my case the Motion Layout needed to know how to connect the clicked item to the target view to be animated.

    I am not aware if that step is usually automated when given a 1:1 view relation from the origin fragment and the target fragment, but in this case it is an n:1 view relation and as such I had to pass the view.getTransitionName() in a bundle (other ways make use of the navargs) so it can tell how to connect both views.

    Using the code in the question, the changes required are as follows:

    Fragment A

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
        // if hold is not used then the origin fragment might
        // disappear too soon before the target fragment is
        // fully visible
        setExitTransition(new Hold());
    
    }
    
    @Override
    public void onViewCreated(
        @NonNull View view,
        @Nullable Bundle savedInstanceState
    ) {
    
        // this is required to animate correctly when the user returns
        // to the origin fragment, gives a chance for the layout
        // to be fully laid out before animating it
        ViewGroup viewGroup = (ViewGroup) view.getParent();
        viewGroup
            .getViewTreeObserver()
            .addOnPreDrawListener(() -> {
                startPostponedEnterTransition();
                return true;
            });
    
        super.onViewCreated(view, savedInstanceState);
    
    }
    
    // this is the recycler view item click listener
    private ItemClickCallback getClickCallback() {
        return (view) -> {
    
            FragmentNavigator.Extras extras = new FragmentNavigator.Extras
                .Builder()
                .addSharedElement(view, view.getTransitionName())
                .build();
    
            Bundle bundle = new Bundle();
            bundle.putString("itemTransitionName", view.getTransitionName());
    
            NavHostFragment
                .findNavController(this)
                .navigate(R.id.action_navigation, bundle, null, extras);
    
        };
    }
    

    Fragment B

    String sharedViewId = "";
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
    
        if (getArguments() != null) {
            sharedViewId = getArguments().getString("itemTransitionName");
        }
    
        setSharedElementEnterTransition(new MaterialContainerTransform());
        postponeEnterTransition();
    
    }
    
    @Override
    public void onViewCreated(
        @NonNull View view,
        @Nullable Bundle savedInstanceState
    ) {
    
        // binding.getRoot() should be whichever view target you want to animate
        // in the target fragment
        ViewCompat.setTransitionName(binding.getRoot(), sharedViewId);
        super.onViewCreated(view, savedInstanceState);
    
    }