📜 ⬆️ ⬇️

Continuous transitions between common elements: from RecyclerView to ViewPager

Using transitions in Material Design gives the application visual continuity. While the user walks through the application, the interface elements in it change state. The animations of transitions of relevant elements from one screen to another underline the idea that interfaces are tangible.


The purpose of this article is to provide guidelines and implementations for certain continuous transitions between fragments of the Android OS. We will show how to make the transition from the image to the RecyclerView into the image inside the ViewPager and vice versa, using “shared elements” to determine how and what elements are involved in the transition. We will also handle the difficult case of going back to the grid after paging on the page to an element that was originally outside the screen in the grid.


Here is the result we want to achieve (animated version under the cut).



If you want to skip the explanation and start learning the code right away, you will find it here .


From the translator. Further, there will be quite a lot of code and gifs (by estimation, megabytes at 20).



What are common elements?


A transition using a common element defines how the views that are present on two fragments move between them. For example, a picture that is shown in the ImageView, and in fragment A and fragment B, changes from A to B when B becomes visible.


There are many previously published examples that explain how common elements work, and how to implement a basic transition between fragments. In this article, the basics are omitted, and instead the conversation will go about the features of the implementation of the transition to the ViewPager and back. However, if you want to learn more about transitions, I recommend starting reading about transitions on the Android developer site and take time to view this presentation on Google I / O 2016.


Difficulties


Mapping common items


We want to provide seamless back and forth transitions. This is both a transition from the grid to the details screen (in the original pager, and the term “page” will be used below), and a transition back to the relevant image in the grid when the user has scrolled through the page to another image.


In order to do this, we need to find a way to dynamically reassign the mapping of common elements in order to provide the Android transition system with everything needed to do the magic!


Delayed loading


Transitions of common elements have powerful functionality, but it is still difficult to work with elements that need to be loaded before we can go to them. The transition may simply not work as expected, if the views in the target fragment have not yet completed layout and are not loaded, for example, images.


There are two areas in this project in which load time affects transitions between common elements:


  1. The ViewPager takes several milliseconds to load its nested fragments. It also takes time to upload a picture to the displayed fragment of the page (which can also include the time to download the picture over the network).
  2. RecyclerView also faces latency when loading images into its views.

Design demo application


Basic structure


Before we dive into the very pulp of transitions, let's discuss a little the structure of the demo application.



MainActivity loads a GridFragment to display a RecyclerView consisting of images. The RecyclerView adapter loads a list of images (an immutable array defined in the ItemData class) and manages onClick events by replacing the GridFragment fragment with an ImagePagerFragment fragment on the screen.


The ImagePagerFragment adapter loads nested ImageFragments to display individual images while the user scrolls through the pages.


Note : The demo application implementation uses the Glide library to load images asynchronously into the view. Pictures in the demo application are delivered with it. However, you can easily correct the ImageData class for storing URLs pointing to images on the Internet.


Sync selected and displayed positions


To transfer the position of the selected picture between the fragments, we will use the MainActivity as a synchronization point.


When clicking on an element or changing a page, the corresponding element number is transferred to MainActivity.


The saved position is then used in several places:



Conversion setup


As mentioned earlier, we need to find a way to dynamically change the mapping of common elements in order to give the transition system everything that is necessary for witchcraft.


Using static mapping using the transitionName attributes for an ImageView in XML markup does not work, since we are dealing with a set of views that use the same layout (for example, views collected by the RecyclerView adapter or viewers collected by ImageFragment).


To achieve this, we will use the methods provided by the transition system.


  1. We set the transition ID for the images by calling setTransitionName . This will allow the view to be associated with a unique transition name. setTransitionName is called when a view is binded in the RecyclerView adapter in the GridFragment grid, as well as in onCreateView in the ImageFragment. In both places we use unique names or links to images as an identifier for the presentation.
  2. We customize SharedElementCallbacks to intercept onMapSharedElements and fix the mapping of common element names to the views. This will be done when exiting the GridFragment and when entering the ImagePagerFragment.

Configuring the FragmentManager transaction


To initiate a transition to replace fragments, we first tweak something in preparation for the FragmentManager transaction. We need to inform the system that we have a transition with common elements.


 fragment.getFragmentManager() .beginTransaction() .setReorderingAllowed(true) // setAllowOptimization before 26.1.0 .addSharedElement(imageView, imageView.getTransitionName()) .replace(R.id.fragment_container, new ImagePagerFragment(), ImagePagerFragment.class.getSimpleName()) .addToBackStack(null) .commit(); 

In the code above, setReorderingAllowed to true . This allows you to reorder state changes of the fragments to make the transition look better. For the added fragment, onCreate(Bundle) will be called before the onDestroy call onDestroy the deleted fragment, which will allow you to create and place a common interface element before the transition begins.


Transition pictures


To determine how the image is transformed during the animation of the transition to a new location, we will set up the TransitionSet in the XML file and load it into the ImagePagerFragment.


<ImagePagerFragment.java>


 Transition transition = TransitionInflater.from(getContext()) .inflateTransition(R.transition.image_shared_element_transition); setSharedElementEnterTransition(transition); 

<image_shared_element_transition.xml>


 <?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="375" android:interpolator="@android:interpolator/fast_out_slow_in" android:transitionOrdering="together"> <changeClipBounds/> <changeTransform/> <changeBounds/> </transitionSet> 

Changing the compliance of common elements


Let's start with the changes when exiting the GridFragment. To do this, we will call setExitSharedElementCallback() and pass in it a SharedElementCallback which will map the names of the transitions to the views that we would like to include in the transition.


It is important to note that this handler will be called upon output from the fragment during the processing of the fragment transaction, as well as upon entering the fragment, when it is pushed out of the backstack (during reverse navigation). We use this behavior to make the mapping match the common views and fix the transition so as to handle the case when the view has changed after paging through the pages with pictures.


In this special case, we are interested only in the transition of one ImageView from the grid to the fragment that now shows the ViewPager, so you only need to change the mapping for the first named element obtained in the onMapSharedElements handler.


<GridFragment.java>


 setExitSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements( List<String> names, Map<String, View> sharedElements) { //  ViewHolder   . RecyclerView.ViewHolder selectedViewHolder = recyclerView .findViewHolderForAdapterPosition(MainActivity.currentPosition); if (selectedViewHolder == null || selectedViewHolder.itemView == null) { return; } //        ImageView. sharedElements .put(names.get(0), selectedViewHolder.itemView.findViewById(R.id.card_image)); } }); 

We also need to change the mapping of common elements when entering the ImagePagerFragment.
To do this, we call setEnterSharedElementCallback() .


 setEnterSharedElementCallback( new SharedElementCallback() { @Override public void onMapSharedElements( List<String> names, Map<String, View> sharedElements) { //  ImageView    ( ImageFragment, //     ).   ,  // instantiateItem   . //           //     . Fragment currentFragment = (Fragment) viewPager.getAdapter() .instantiateItem(viewPager, MainActivity.currentPosition); View view = currentFragment.getView(); if (view == null) { return; } //        ImageView. sharedElements.put(names.get(0), view.findViewById(R.id.image)); } }); 

Postpone transition


Images that we would like to move during the transition process take some time to load into the grid and onto the page. In order for everything to work correctly, we need to postpone the transition until the moment when the participating views are ready (for example, when layout is completed for them and pictures are loaded).


To do this, we call postponeEnterTransition() in the onCreateView() method of our fragments, and when the picture is loaded, we start the transition by calling startPostponedEnterTransition() .


Note : "defer" is called for both the grid and the page to provide forward and backward transitions when navigating the application.


Since we use Glide to load images, we set up handlers that will trigger the transition after loading images.


This must be done in two places:


  1. When a picture is loaded into an ImageFragment, the parent ImagePagerFragment is called to start the transition.
  2. When returning to the grid, the transition starts when the selected image is loaded.

This is how ImageFragment loads a picture and notifies its parent of readiness.


Note that the postponeEnterTransition call is made in the ImagePagerFragment, although startPostponedEnterTransition is called from a child ImageFragment


 Glide.with(this) .load(arguments.getInt(KEY_IMAGE_RES)) //   .listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { getParentFragment().startPostponedEnterTransition(); return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { getParentFragment().startPostponedEnterTransition(); return false; } }) .into((ImageView) view.findViewById(R.id.image)); 

As you can see, we also cause the start of the delayed transition in case the loading of the picture failed. This is to prevent the UI from stalling when loading fails.


Final touches


To make our transitions even smoother, we would like to darken the elements of the grid when the image goes to the page screen.


To achieve this, we will create a TransitionSet and use it as a transition to exit the GridFragment.


 setExitTransition(TransitionInflater.from(getContext()) .inflateTransition(R.transition.grid_exit_transition)); 

 <?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="375" android:interpolator="@android:interpolator/fast_out_slow_in" android:startDelay="25"> <fade> <targets android:targetId="@id/card_view"/> </fade> </transitionSet> 

Here's what the transition will look like after setting up the output animation:



As you can see, the transition is not fully polished yet. The dimming animation starts for all grid cards, including the one that contains the image that moves to the page.


To fix this, we exclude the selected card from the exit transition before committing the fragment transaction to the GridAdapter.


 // view -  ,    ,  . ((TransitionSet) fragment.getExitTransition()).excludeTarget(view, true); 

After this change, the animation looks much better (the selected card is not obscured as part of the exit transition, unlike other cards):



And the final touch, we configure the GridFragment to scroll and display a card, which we switch to when navigating backward from the page (this is done in onViewCreated ):


 recyclerView.addOnLayoutChangeListener( new OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { recyclerView.removeOnLayoutChangeListener(this); final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); View viewAtPosition = layoutManager.findViewByPosition(MainActivity.currentPosition); //   ,       null (.. //      layout-)   //    . if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(viewAtPosition, false, true)){ recyclerView.post(() -> layoutManager.scrollToPosition(MainActivity.currentPosition)); } } }); 

Summing up


In this article, we implemented a smooth transition from RecyclerView to ViewPager and back.


We showed how to postpone the transition and start it after all views are ready. We also remap the common elements in order to provide a transition animation when the common views change dynamically as we navigate through the application.


These changes in the transitions between fragments of our application made them visually more continuous for the user.



The code of the demo application is attached .


')

Source: https://habr.com/ru/post/349650/


All Articles