[From the sandbox] How I struggled with the Shared Element Transition and wrote my first opensource library

[From the sandbox] How I struggled with the Shared Element Transition and wrote my first opensource library


There is no sadder story,
What is the story about ViewPager and SET'e



I want 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 can meet 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 project release 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


No one 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, on my project, the first ViewPager (I deciphered some terms for the same backendators as I did - just sum up cursor). A Shared Element Transition (hereinafter referred to as SET or transition) is a signature element Material Design , flatly refused to work with 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 search.


Hours of study: dozens of topics on forums and questions on StackOverflow without an answer. No matter what I’ve opened, I’ve been asked to make transition from RecyclerView screen in the ViewPager or“ attach plantain Fragment.postponeEnterTransition () ”.



Folk remedies did not help, and I decided to reconcile the ViewPager and Shared Element Transition on my 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 pages.”



We can do transition on the same page, and then just replace the current page with the target page in ViewPager .


To begin, create fragments that we will work with.

  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.


  FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction ();
//take the View which contains the current fragment
 int fragmentContId = previousFragment.getView (). getParent (). getId ();
//and change the contents of this container to the next fragment
 fragmentTransaction.replace (fragmentContId, nextFragment);
 fragmentTransaction.commit ();  

Run the application and ... smoothly go to a blank 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 whole 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 does not start again.


java.lang.IllegalStateException: Cannot change the BigPictureFragment {...}: was 2131165289 now 2131165290

We cannot attach a second page fragment ( 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 that I once copied from the GitHub expanses already contained the animations fade in and fade out . Therefore, before the transition , all 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 replace the visible ViewPager page with a second one. And then restore the contents of the first page to its original state.


  handler.postDelayed (
  () - & gt;  {

//Gently change the displayed page to the desired
  activity.viewPager.setCurrentItem (nextPage, false);
  FragmentTransaction transaction = fragmentManager.beginTransaction ();

//Restore the previous fragment.
//It contains the content of another page after
//our hack with Shared Element Transition
  transaction.replace (fragmentContainer, previousFragment);
  transaction.commitAllowingStateLoss ();
  },
//Trying to find the right moment to change fragments back
  FADE_DEFAULT_TIME + MOVE_DEFAULT_TIME + FADE_DEFAULT_TIME
  );
  }  

What was the result? (animation - 2.7 mb)

Entire source code

You can view the project at GitHub .


To summarize. The code began to look much more solid: instead of the 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 pressed buttons from the “wrong” pages or held back the background work of duplicate fragments.


It turned out that there are no callbacks on the end of transition , and its execution time is quite arbitrary and depends on many factors (for example, how fast RecyclerView loads in the resulting fragment) . This led to the fact that the substitution of fragments in handler.postDelayed () was often performed too early or too late, which only aggravated the previous problem.


The final 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 the Shared Element Transition correctly in the Viewpager


Trying 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 the 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. Will move the elements after the swipe and have a simple interface of the form:


  public void addSharedTransition (int fromViewId, int toViewId)  

Let's get started developing. I’ll save the data from the addSharedTransition (fromId, toId) method to Set from Pair and get it in the PageTransfomer

 /**
 Here you can move the interface elements based on how much the user has shifted the screen.
  */
 public void transformPage (@NonNull View page, float position)  

Inside, I’ll go through all the View pairs that I need to animate between. And I will try to filter them so that only visible elements are animated.


First we’ll check to see if we managed to create elements that need to be animated. We are not picky, and if View was not created before the animation, we will not break the entire animation (as Shared Element Transition ), but pick it up when the element is created .


  for (Pair & lt; Integer, Integer & gt; idPair: sharedElementIds) {
  Integer fromViewId = idPair.first;
  Integer toViewId = idPair.second;

  View fromView = activity.findViewById (fromViewId);
  View toView = activity.findViewById (toViewId);

  if (fromView! = null & amp; & amp; toView! = null) { 

I find the pages between which the movement takes place (as I define the page number I will tell below).


  View fromPage = pages.get (fromPageNumber);
 View toPage = pages.get (toPageNumber);  

If both pages have already been created, then I’m looking for a couple of View to animate.


  If (fromPage! = null & amp; & amp; toPage! = null) {
  fromView = fromPage.findViewById (fromViewId);
  toView = toPage.findViewById (toViewId);  

At this stage, we chose View, which lie on the pages between which the user scrolls.


It's time to make a lot of variables. Calculate pivot points:


 //Calculate the initial position of each element of the pair on the screen//and the difference between them
 float fromX = fromView.getX () - fromView.getTranslationX ();
 float fromY = fromView.getY () - fromView.getTranslationY ();
 float toX = toView.getX () - toView.getTranslationX ();
 float toY = toView.getY () - toView.getTranslationY ();
 float deltaX = toX - fromX;
 float deltaY = toY - fromY;
//Calculate the original size and difference
 float fromWidth = fromView.getWidth ();
 float fromHeight = fromView.getHeight ();
 float toWidth = toView.getWidth ();
 float toHeight = toView.getHeight ();
 float deltaWidth = toWidth - fromWidth;
 float deltaHeight = toHeight - fromHeight;
//Determine in which direction the svip goes
 boolean slideToTheRight = toPageNumber & gt;  fromPageNumber;  

In the last snippet, I asked slideToTheRight , and in that I’ll need it. It depends on the sign in translation , which will determine whether View will fly to its place or somewhere off the screen.


  float pageWidth = getScreenWidth ();
 float sign = slideToTheRight?  eleven;

 float translationY = (deltaY + deltaHeight/2) * sign * (-position);
 float translationX = (deltaX + sign * pageWidth + deltaWidth/2) * sign * (-position);  

Interestingly, the formulas for X and Y for both View were the same on the start page and the resulting page, despite different initial offsets .


But with a scale, unfortunately, this trick will not work - you need to consider whether this View is the starting point or the ending point of the animation.


It may come as a surprise to someone, but transformPage (@NonNull View page, float position) is called many times: for each cached page (cache size is customized). And in order not to redraw the animated View several times, for each call of transformPage () , we only change those that are on the current page .


Set the position and scale of the animated elements
 //If initial  View is on the current page.
 if (page.findViewById (fromId)! = null) {
//Expose it to the desired position.
  fromView.setTranslationX (translationX);
  fromView.setTranslationY (translationY);

//Calculate the required stretch
  float scaleX = (fromWidth == 0)?
  1://If View has zero width,
//you can not even try to stretch it
  (fromWidth + deltaWidth * sign * (-position))/fromWidth;

  float scaleY = (fromHeight == 0)?
  1://If View has zero length,
//you can not even try to stretch it
  (fromHeight + deltaHeight * sign * (-position))/fromHeight;

  fromView.setScaleX (scaleX);
  fromView.setScaleY (scaleY);
 }//If the final View is on the current page
 if (page.findViewById (toId)! = null) {

  toView.setTranslationX (translationX);
  toView.setTranslationY (translationY);
  float scaleX = (toWidth == 0)?
  one :
  (toWidth + deltaWidth * sign * (-position))/toWidth;
  float scaleY = (toHeight == 0)?
  one :
  (toHeight + deltaHeight * sign * (-position))/toHeight;

  toView.setScaleX (scaleX);
  toView.setScaleY (scaleY);
 }  

Select the pages to draw the animation


ViewPager is in 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. Having studied the output of onPageScrolled () through System.out.println () I came to the following formula:


  public void onPageScrolled (
  int position, float positionOffset, int positionOffsetPixels
 ) {
  Set & lt; Integer & gt;  visiblePages = new HashSet & lt; & gt; ();

  visiblePages.add (position);
  visiblePages.add (positionOffset & gt; = 0? position + 1: position - 1);
  visiblePages.remove (fromPageNumber);

  toPageNumber = visiblePages.iterator (). next ();

  if (pages == null || toPageNumber & gt; = pages.size ()) toPageNumber = null;
 }

 public void onPageSelected (int position) {this.position = position;  }

 public void onPageScrollStateChanged (int state) {
  if (state == SCROLL_STATE_IDLE) {
//Once all the transition animations are finished, remember the current page.
  fromPageNumber = position;
  resetViewPositions ();
  }
 }  

That's it. We did it! An 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’s still very raw, so it’s not added to the library.


See what happened in the end (animation - 2.4 mb)

Entire source code

How does the work with the library look like


The configuration was rather concise.


The complete configuration for our example looks like this
  ArrayList & lt; Fragment & gt;  fragments = new ArrayList & lt; & gt; ();

 fragments.add (hello_fragment);
 fragments.add (small_picture_fragment);
 fragments.add (big_picture_fragment);

 SharedElementPageTransformer transformer =
  new SharedElementPageTransformer (this, fragments);

 transformer.addSharedTransition (R.id.smallPic_image_cat2, R.id.bigPic_image_cat, true);
 transformer.addSharedTransition (R.id.smallPic_text_label3, R.id.bigPic_text_label, true);
 transformer.addSharedTransition (R.id.hello_text, R.id.smallPic_text_label3, true);

 viewPager.setPageTransformer (false, transformer);
 viewPager.addOnPageChangeListener (transformer);
  

onClick , including the entire transition, could look like this:


  smallCatImageView.setOnClickListener (
  v - & gt;  activity.viewPager.setCurrentItem (2)
 );  

To keep the code from being lost, 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


  dependencies {
//...
  implementation 'com.github.kirillgerasimov: shared-element-view-pager: 0.0.2-alpha'
 }  

All sources are available on GitHub


Conclusion


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 it and share it with the community.

Source text: [From the sandbox] How I struggled with the Shared Element Transition and wrote my first opensource library