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.
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 ”.
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.
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.
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); // View int fragmentContId = previousFragment.getView().getParent().getId(); // fragmentTransaction.replace(fragmentContId, nextFragment); fragmentTransaction.commit();
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.
handler.postDelayed( () -> { // activity.viewPager.setCurrentItem(nextPage, false); FragmentTransaction transaction = fragmentManager.beginTransaction(); // . // // Shared Element Transition transaction.replace(fragmentContainer, previousFragment); transaction.commitAllowingStateLoss(); }, // , FADE_DEFAULT_TIME + MOVE_DEFAULT_TIME + FADE_DEFAULT_TIME ); }
The project can be viewed on GitHub .
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.
This situation did not suit me, and I, full of righteous anger, began to search for another solution.
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:
public void addSharedTransition(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
/** , , */ public void transformPage(@NonNull View page, float position)
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.
for (Pair<Integer,Integer> idPair : sharedElementIds) { Integer fromViewId = idPair.first; Integer toViewId = idPair.second; View fromView = activity.findViewById(fromViewId); View toView = activity.findViewById(toViewId); if (fromView != null && 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 are already created, then I look for a pair of View
on them, which needs to be animated.
If (fromPage != null && 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. I calculate the pivot points:
// // 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; // float fromWidth = fromView.getWidth(); float fromHeight = fromView.getHeight(); float toWidth = toView.getWidth(); float toHeight = toView.getHeight(); float deltaWidth = toWidth - fromWidth; float deltaHeight = toHeight - fromHeight; // boolean slideToTheRight = toPageNumber > fromPageNumber;
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.
float pageWidth = getScreenWidth(); float sign = slideToTheRight ? 1 : -1; float translationY = (deltaY + deltaHeight / 2) * sign * (-position); float translationX = (deltaX + sign * pageWidth + deltaWidth / 2) * sign * (-position);
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
.
// View if (page.findViewById(fromId) != null) { // fromView.setTranslationX(translationX); fromView.setTranslationY(translationY); / / float scaleX = (fromWidth == 0) ? 1 : // View , // (fromWidth + deltaWidth * sign * (-position)) / fromWidth; float scaleY = (fromHeight == 0) ? 1 : // View , // (fromHeight + deltaHeight * sign * (-position)) / fromHeight; fromView.setScaleX(scaleX); fromView.setScaleY(scaleY); } // View if (page.findViewById(toId) != null) { toView.setTranslationX(translationX); toView.setTranslationY(translationY); float scaleX = (toWidth == 0) ? 1 : (toWidth + deltaWidth * sign * (-position)) / toWidth; float scaleY = (toHeight == 0) ? 1 : (toHeight + deltaHeight * sign * (-position)) / toHeight; toView.setScaleX(scaleX); toView.setScaleY(scaleY); }
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:
public void onPageScrolled( 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; } public void onPageSelected(int position) { this.position = position; } public void onPageScrollStateChanged(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.
The configuration turned out pretty concise.
ArrayList<Fragment> fragments = new ArrayList<>(); 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 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
dependencies { //... implementation 'com.github.kirillgerasimov:shared-element-view-pager:0.0.2-alpha' }
All sources are available on GitHub.
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.
Source: https://habr.com/ru/post/451116/
All Articles