How I struggled with the Shared Element Transition and wrote my first opensource library
There is no sadder story in the world,
what the story about ViewPager'e and SET'e
I would like to warn that the author is a novice android, so the article contains so many technical inaccuracies that you rather need to be warned that the article may contain technically accurate statements.
Where the backend leads
All my life I sawed the backend. Beginning in 2019, behind one already very ambitious, but unfinished project. Barren trip to Zurich for an interview in one search company. Winter, dirt, no mood. Forces and the desire to pull the project no further.
I wanted to forget this terrible backend forever. Fortunately, fate gave me an idea - it was a mobile application. Its main feature was to be non-standard use of the camera. Work has begun to boil. Some time passed, and now the prototype is ready. The release of the project was nearing and everything was fine and slim, until I decided to make the user “ comfortable ”.
ViewPager and Shared Element Transition. We are looking for ways of reconciliation
Nobody wants to click on the small menu buttons in 2019, everyone wants to swap screens to the right and left. Said, done, done, broken. So the first ViewPager appeared on my project (I deciphered some terms for the same backenders as me - just move the cursor). And Shared Element Transition (hereinafter ViewPager to as SET or transition) - the Material Design signature element, ViewPager refused to work with the ViewPager , leaving me with a choice: either swipes, or beautiful transition animations between screens. I did not want to give up either one or the other. So began my quest.
Hours of study: dozens of topics on forums and questions on StackOverflow without an answer. Whatever I opened, I was offered to make a transition from RecyclerView in ViewPager or “attach plantain Fragment.postponeEnterTransition() ”.
Folk remedies did not help, and I decided to reconcile the ViewPager and the Shared Element Transition own.
ViewPager: First Blood
I began to think: “The problem appears at the moment when the user moves from one page to another ...”. And then it dawned on me: "You will not have problems with SET during the page change, if you do not change the page."
We can make a transition on the same page, and then simply substitute the current page for the target page in ViewPager .
To begin with, we will create fragments with which we will work.
SmallPictureFragment small_picture_fragment = new SmallPictureFragment(); BigPictureFragment big_picture_fragment = new BigPictureFragment();
Let's try to change the fragment in the current page to something else.
Run the application and ... smoothly go to an empty screen. What is the reason?
It turns out that the container for each of the pages is the ViewPager itself, without any intermediaries like Page1Container , Page2Container . Therefore, simply changing one page to another will not work, the entire pager will be replaced.
Well, to change the content of each page separately, we create several container fragments for each page.
RootSmallPictureFragment root_small_pic_fragment = new RootSmallPictureFragment(); RootBigPictureFragment root_big_pic_fragment = new RootBigPictureFragment();
Something will not start again.
java.lang.IllegalStateException: Cannot change BigPictureFragment {...}: was 2131165289 now 2131165290
We cannot attach a fragment of the second page ( BigPictureFragment ) to the first, because it is already attached to the container of the second page.
Gritting your teeth add more fragments, doublers.
SmallPictureFragment small_picture_fragment_fake = new SmallPictureFragment(); BigPictureFragment big_picture_fragment_fake = new BigPictureFragment();
Earned! The transition code, which I once copied from the GitHub, already contained fade in and fade out animations. Therefore, before the transition, all the static elements from the first fragment disappeared, then the pictures were moved, and only then the elements of the second page appeared. To the user, this looks like a real movement between pages.
All the animations have passed, but there is one problem. The user is still on the first page, but should be on the second.
To fix this, we carefully substitute the visible ViewPager page for the second. And then restore the contents of the first page to its original state.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Let's sum up. The code began to look much more solid: instead of 2 original fragments, I got as much as 6, it appeared instructions that control the performance, replacing the fragments at the right time. And this is only in the demo.
In the present project in the code one by one, props began to appear in the most unexpected places. They did not allow the application to collapse when the user pushed buttons from the “wrong” pages or held back the background work of duplicate fragments.
It turned out that there are no callbacks in the android to complete the transition , and its execution time is quite arbitrary and depends on many factors (for example, how quickly RecyclerView will load in the resulting fragment). This led to the fact that the substitution of fragments in handler.postDelayed() often performed too early or too late, which only aggravated the previous problem.
The last nail was that during the animation, the user could simply swipe to another page and watch two twin screens, after which the application also pulled it to the desired screen.
Interesting artifacts of this approach (animation - 2.7 mb)
This situation did not suit me, and I, full of righteous anger, began to search for another solution.
How to make Shared Element Transition correctly in the viewpager
We try PageTransformer
There were still no answers on the Internet, and I wondered: how else can I turn this transition. Something on the subcortex of consciousness was whispering to me: “Use PageTransformer , Luke”. The idea seemed promising to me and I decided to listen.
The idea is to make PageTransformer , which, unlike Android SET , will not require multiple repetitions of setTransitionName(transitionName) and FragmentTransaction.addSharedElement(sharedElement,name) on both sides of the transition. It will move the elements after the swipe and have a simple interface of the form:
publicvoidaddSharedTransition(int fromViewId, int toViewId)
Let's start the development. I will save the data from the addSharedTransition(fromId, toId) method addSharedTransition(fromId, toId) to the Set from Pair and get it in the PageTransfomer method
Inside, I'll go through all the saved pairs of View , between which you need to make an animation. And I will try to filter them so that only visible elements are animated.
To begin with, let's check if we had time to create the elements that need to be animated. We are not picky, and if the View not created before the start of the animation, we will not break the entire animation (as a Shared Element Transition ), but pick it up when the element is created.
In the past snippet, I asked slideToTheRight , and already in this it will come in handy for me. It depends on the sign in the translation , which will determine whether the View will fly to its place or somewhere off the screen.
Interestingly, the formulas for X and Y offsets in both View , on the start page and the resulting page, turned out to be the same, despite the different initial offsets.
But with scale, unfortunately, such a trick will not work - you need to consider whether the given View starting point or the end point of the animation.
For some, it may come as a surprise, but the transformPage(@NonNull View page, float position) is called many times: for each cached page (cache size is configured). And, in order not to redraw the animated View several times, for each call transformPage() , we change only those that are on the current page .
We set the position and scale of the animated elements.
ViewPager no hurry to share information between which pages are scrolling. As I promised, I’ll tell you how we get this information. In our PageTransformer we implement another ViewPager.OnPageChangeListener interface. After learning the onPageScrolled() output via System.out.println() I came up with the following formula:
publicvoidonPageScrolled( int position, float positionOffset, int positionOffsetPixels ){ Set<Integer> visiblePages = new HashSet<>(); visiblePages.add(position); visiblePages.add(positionOffset >= 0 ? position + 1 : position - 1); visiblePages.remove(fromPageNumber); toPageNumber = visiblePages.iterator().next(); if (pages == null || toPageNumber >= pages.size()) toPageNumber = null; } publicvoidonPageSelected(int position){ this.position = position; } publicvoidonPageScrollStateChanged(int state){ if (state == SCROLL_STATE_IDLE) { // , fromPageNumber = position; resetViewPositions(); } }
That's all. We did it! Animation follows the user's gestures. Why choose between svaypami and Shared Element Transition , when you can leave everything.
At the time of writing, I added the effect of the disappearance of static elements - it is still very raw, so it has not been added to the library.
See what happened in the end (animation - 2.4 mb)
Whole source code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
onClick , including all transitions, might look like this:
smallCatImageView.setOnClickListener( v -> activity.viewPager.setCurrentItem(2) );
So that the code does not disappear, I put the library in the JCenter repository and on GitHub . So I came into contact with the world opensource. You can try it on your project by simply adding
Even if the Internet does not know the answer, it does not mean that it does not exist. Look for workarounds, try until it works. Perhaps you will be the first to get to the bottom of the story and share it with the community.