⬆️ ⬇️

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.



 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 ); } 


What was the result? (animation - 2.7 mb)


Whole source code
public class FragmentTransitionUtil {
private static final long FADE_DEFAULT_TIME = 500;
private static final long MOVE_DEFAULT_TIME = 1000;
public static void perform(
MainActivity activity,
Fragment previousFragment,
Fragment nextFragment,
Map<View, String> sharedElements,
int nextPage
) {
FragmentManager fragmentManager = activity.getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
if (previousFragment != null) {
// 1. Exit for Previous Fragment
Fade exitFade = new Fade();
exitFade.setDuration(FADE_DEFAULT_TIME);
previousFragment.setExitTransition(exitFade);
// 2. Shared Elements Transition
TransitionSet enterTransitionSet = new TransitionSet();
enterTransitionSet.addTransition(
new TransitionSet() {
{
setOrdering(ORDERING_TOGETHER);
addTransition(new ChangeBounds()).
addTransition(new ChangeTransform()).
addTransition(new ChangeImageTransform());
}
}
);
enterTransitionSet.setDuration(MOVE_DEFAULT_TIME);
enterTransitionSet.setStartDelay(FADE_DEFAULT_TIME);
nextFragment.setSharedElementEnterTransition(enterTransitionSet);
// 3. Enter Transition for New Fragment
Fade enterFade = new Fade();
enterFade.setStartDelay(MOVE_DEFAULT_TIME + FADE_DEFAULT_TIME);
enterFade.setDuration(FADE_DEFAULT_TIME);
nextFragment.setEnterTransition(enterFade);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (sharedElements != null) {
for (Map.Entry<View, String> viewStringEntry : sharedElements.entrySet()) {
View view = viewStringEntry.getKey();
String transName = viewStringEntry.getValue();
view.setTransitionName(transName);
fragmentTransaction.addSharedElement(
view,
transName
);
}
}
}
int fragmentContId = ((ViewGroup) previousFragment.getView().getParent()).getId();
fragmentTransaction.replace(fragmentContId, nextFragment);
fragmentTransaction.commit();
final Handler handler = new Handler();
handler.postDelayed(
() -> {
// Stealthy changing page visible to user. He won’t notice!
activity.viewPager.setCurrentItem(nextPage, false);
FragmentTransaction transaction = fragmentManager.beginTransaction();
// Restore previous fragment. It contains inappropriate view now
transaction.replace(fragmentContId, previousFragment);
transaction.commitAllowingStateLoss();
},
FADE_DEFAULT_TIME + MOVE_DEFAULT_TIME + FADE_DEFAULT_TIME
);
}
}
}
public class FragmentTransitionUtil {
private static final long FADE_DEFAULT_TIME = 500;
private static final long MOVE_DEFAULT_TIME = 1000;
public static void perform(
MainActivity activity,
Fragment previousFragment,
Fragment nextFragment,
Map<View, String> sharedElements,
int nextPage
) {
FragmentManager fragmentManager = activity.getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
if (previousFragment != null) {
// 1. Exit for Previous Fragment
Fade exitFade = new Fade();
exitFade.setDuration(FADE_DEFAULT_TIME);
previousFragment.setExitTransition(exitFade);
// 2. Shared Elements Transition
TransitionSet enterTransitionSet = new TransitionSet();
enterTransitionSet.addTransition(
new TransitionSet() {
{
setOrdering(ORDERING_TOGETHER);
addTransition(new ChangeBounds()).
addTransition(new ChangeTransform()).
addTransition(new ChangeImageTransform());
}
}
);
enterTransitionSet.setDuration(MOVE_DEFAULT_TIME);
enterTransitionSet.setStartDelay(FADE_DEFAULT_TIME);
nextFragment.setSharedElementEnterTransition(enterTransitionSet);
// 3. Enter Transition for New Fragment
Fade enterFade = new Fade();
enterFade.setStartDelay(MOVE_DEFAULT_TIME + FADE_DEFAULT_TIME);
enterFade.setDuration(FADE_DEFAULT_TIME);
nextFragment.setEnterTransition(enterFade);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (sharedElements != null) {
for (Map.Entry<View, String> viewStringEntry : sharedElements.entrySet()) {
View view = viewStringEntry.getKey();
String transName = viewStringEntry.getValue();
view.setTransitionName(transName);
fragmentTransaction.addSharedElement(
view,
transName
);
}
}
}
int fragmentContId = ((ViewGroup) previousFragment.getView().getParent()).getId();
fragmentTransaction.replace(fragmentContId, nextFragment);
fragmentTransaction.commit();
final Handler handler = new Handler();
handler.postDelayed(
() -> {
// Stealthy changing page visible to user. He won’t notice!
activity.viewPager.setCurrentItem(nextPage, false);
FragmentTransaction transaction = fragmentManager.beginTransaction();
// Restore previous fragment. It contains inappropriate view now
transaction.replace(fragmentContId, 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.



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:



  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 .



We set the position and scale of the animated elements.
 //   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); } 


Select the pages to draw the animation.



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.



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


Whole source code
/**
* PageTransformer that allows you to do shared element transitions between pages in ViewPager.
* It requires view pager sides match screen sides to function properly. I.e. ViewPager page width
* must be equal to screen width. <br/>
* Usage:<br/>
* <code>
* sharedElementPageTransformer.addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code>
* </code>
*
*
*/
public class SharedElementPageTransformer implements ViewPager.PageTransformer, ViewPager.OnPageChangeListener {
/** Android need the correction while view scaling for some reason*/
private static float MAGICAL_ANDROID_RENDERING_SCALE = 1;
// private static float MAGICAL_ANDROID_RENDERING_SCALE = 0.995f;
// External variables
private final Activity activity;
List<Fragment> fragments;
private Set<Pair<Integer,Integer>> sharedElementIds = new HashSet<>();
//Internal variables
private List<View> pages;
private Map<View, Integer> pageToNumber = new HashMap<>();
private Integer fromPageNumber = 0;
private Integer toPageNumber;
/** current view pager position */
private int position;
/**
* @param activity activity that hosts view pager
* @param fragments fragment that are in view pager in the SAME ORDER
*/
public SharedElementPageTransformer(Activity activity, List<Fragment> fragments) {
this.activity = activity;
this.fragments = fragments;
}
@Override
public void transformPage(@NonNull View page, float position) {
updatePageCache();
if (fromPageNumber == null || toPageNumber == null) return;
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) {
//Looking if current Shared element transition matches visible pages
View fromPage = pages.get(fromPageNumber);
View toPage = pages.get(toPageNumber);
if (fromPage != null && toPage != null) {
fromView = fromPage.findViewById(fromViewId);
toView = toPage.findViewById(toViewId);
// if both views are on pages user drag between apply transformation
if (
fromView != null
&& toView != null
) {
// saving shared element position on the screen
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;
// scaling
float fromWidth = fromView.getWidth();
float fromHeight = fromView.getHeight();
float toWidth = toView.getWidth();
float toHeight = toView.getHeight();
float deltaWidth = toWidth - fromWidth;
float deltaHeight = toHeight - fromHeight;
int fromId = fromView.getId();
int toId = toView.getId();
boolean slideToTheRight = toPageNumber > fromPageNumber;
if (position <= -1) {
} else if (position < 1) {
float pageWidth = getSceenWidth();
float sign = slideToTheRight ? 1 : -1;
float translationY = (deltaY + deltaHeight / 2) * sign * (-position);
float translationX = (deltaX + sign * pageWidth + deltaWidth / 2) * sign * (-position);
if (page.findViewById(fromId) != null) {
fromView.setTranslationX(translationX);
fromView.setTranslationY(translationY);
float scaleX = (fromWidth == 0) ? 1 : (fromWidth + deltaWidth * sign * (-position)) / fromWidth;
float scaleY = (fromHeight == 0) ? 1 : (fromHeight + deltaHeight * sign * (-position)) / fromHeight;
fromView.setScaleX(scaleX);
fromView.setScaleY(scaleY * MAGICAL_ANDROID_RENDERING_SCALE);
}
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);
}
} else {
}
}
}
}
}
}
private float getSceenWidth() {
Point outSize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(outSize);
return outSize.x;
}
/**
* Creating page cache array to determine if shared element on
* currently visible page
*/
private void updatePageCache() {
pages = new ArrayList<>();
for (int i = 0; i < fragments.size(); i++) {
View pageView = fragments.get(i).getView();
pages.add(pageView);
pageToNumber.put(pageView, i);
}
}
@Override
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;
}
@Override
public void onPageSelected(int position) {
this.position = position;
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == SCROLL_STATE_IDLE) {
fromPageNumber = position;
resetViewPositions();
}
}
private void resetViewPositions() {
for (Pair<Integer, Integer> idPair : sharedElementIds) {
View sharedElement = activity.findViewById(idPair.first);
if(sharedElement != null) {
sharedElement.setTranslationX(0);
sharedElement.setTranslationY(0);
sharedElement.setScaleX(1);
sharedElement.setScaleY(1);
}
sharedElement = activity.findViewById(idPair.second);
if(sharedElement != null) {
sharedElement.setTranslationX(0);
sharedElement.setTranslationY(0);
sharedElement.setScaleX(1);
sharedElement.setScaleY(1);
}
}
}
/**
* Set up shared element transition from element with <code>fromViewId</code> to
* element with <code>toViewId</code>. Note that you can setup each transition
* direction separately. e.g. <br/>
* <code>addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code><br/>
* and<br/>
* <code>addSharedTransition(R.id.SecondPageTextView, R.id.FirstPageTextView)</code><br/>
* are different.
* @param fromViewId
* @param toViewId
*/
public void addSharedTransition(int fromViewId, int toViewId) {
addSharedTransition(fromViewId, toViewId, false);
}
/**
* Set up shared element transition from element with <code>fromViewId</code> to
* element with <code>toViewId</code>. Note that you can setup each transition
* direction separately. e.g. <br/>
* <code>addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code><br/>
* and<br/>
* <code>addSharedTransition(R.id.SecondPageTextView, R.id.FirstPageTextView)</code><br/>
* are different.
* @param fromViewId
* @param toViewId
* @param bothDirections to include backward transition from toViewId to fromViewId aswell
*/
public void addSharedTransition(int fromViewId, int toViewId, boolean bothDirections) {
sharedElementIds.add(new Pair<>(fromViewId, toViewId));
if(bothDirections) {
sharedElementIds.add(new Pair<>(toViewId, fromViewId));
}
}
/**
* In case there is "ladder" appears between while transition.
* You may try to tune that magical scale to get rid of it.
* @param magicalAndroidRenderingScale float between 0 and infinity. Typically very close to 1.0
*/
public static void setMagicalAndroidRenderingScale(float magicalAndroidRenderingScale) {
MAGICAL_ANDROID_RENDERING_SCALE = magicalAndroidRenderingScale;
}
}
/**
* PageTransformer that allows you to do shared element transitions between pages in ViewPager.
* It requires view pager sides match screen sides to function properly. I.e. ViewPager page width
* must be equal to screen width. <br/>
* Usage:<br/>
* <code>
* sharedElementPageTransformer.addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code>
* </code>
*
*
*/
public class SharedElementPageTransformer implements ViewPager.PageTransformer, ViewPager.OnPageChangeListener {
/** Android need the correction while view scaling for some reason*/
private static float MAGICAL_ANDROID_RENDERING_SCALE = 1;
// private static float MAGICAL_ANDROID_RENDERING_SCALE = 0.995f;
// External variables
private final Activity activity;
List<Fragment> fragments;
private Set<Pair<Integer,Integer>> sharedElementIds = new HashSet<>();
//Internal variables
private List<View> pages;
private Map<View, Integer> pageToNumber = new HashMap<>();
private Integer fromPageNumber = 0;
private Integer toPageNumber;
/** current view pager position */
private int position;
/**
* @param activity activity that hosts view pager
* @param fragments fragment that are in view pager in the SAME ORDER
*/
public SharedElementPageTransformer(Activity activity, List<Fragment> fragments) {
this.activity = activity;
this.fragments = fragments;
}
@Override
public void transformPage(@NonNull View page, float position) {
updatePageCache();
if (fromPageNumber == null || toPageNumber == null) return;
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) {
//Looking if current Shared element transition matches visible pages
View fromPage = pages.get(fromPageNumber);
View toPage = pages.get(toPageNumber);
if (fromPage != null && toPage != null) {
fromView = fromPage.findViewById(fromViewId);
toView = toPage.findViewById(toViewId);
// if both views are on pages user drag between apply transformation
if (
fromView != null
&& toView != null
) {
// saving shared element position on the screen
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;
// scaling
float fromWidth = fromView.getWidth();
float fromHeight = fromView.getHeight();
float toWidth = toView.getWidth();
float toHeight = toView.getHeight();
float deltaWidth = toWidth - fromWidth;
float deltaHeight = toHeight - fromHeight;
int fromId = fromView.getId();
int toId = toView.getId();
boolean slideToTheRight = toPageNumber > fromPageNumber;
if (position <= -1) {
} else if (position < 1) {
float pageWidth = getSceenWidth();
float sign = slideToTheRight ? 1 : -1;
float translationY = (deltaY + deltaHeight / 2) * sign * (-position);
float translationX = (deltaX + sign * pageWidth + deltaWidth / 2) * sign * (-position);
if (page.findViewById(fromId) != null) {
fromView.setTranslationX(translationX);
fromView.setTranslationY(translationY);
float scaleX = (fromWidth == 0) ? 1 : (fromWidth + deltaWidth * sign * (-position)) / fromWidth;
float scaleY = (fromHeight == 0) ? 1 : (fromHeight + deltaHeight * sign * (-position)) / fromHeight;
fromView.setScaleX(scaleX);
fromView.setScaleY(scaleY * MAGICAL_ANDROID_RENDERING_SCALE);
}
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);
}
} else {
}
}
}
}
}
}
private float getSceenWidth() {
Point outSize = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(outSize);
return outSize.x;
}
/**
* Creating page cache array to determine if shared element on
* currently visible page
*/
private void updatePageCache() {
pages = new ArrayList<>();
for (int i = 0; i < fragments.size(); i++) {
View pageView = fragments.get(i).getView();
pages.add(pageView);
pageToNumber.put(pageView, i);
}
}
@Override
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;
}
@Override
public void onPageSelected(int position) {
this.position = position;
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == SCROLL_STATE_IDLE) {
fromPageNumber = position;
resetViewPositions();
}
}
private void resetViewPositions() {
for (Pair<Integer, Integer> idPair : sharedElementIds) {
View sharedElement = activity.findViewById(idPair.first);
if(sharedElement != null) {
sharedElement.setTranslationX(0);
sharedElement.setTranslationY(0);
sharedElement.setScaleX(1);
sharedElement.setScaleY(1);
}
sharedElement = activity.findViewById(idPair.second);
if(sharedElement != null) {
sharedElement.setTranslationX(0);
sharedElement.setTranslationY(0);
sharedElement.setScaleX(1);
sharedElement.setScaleY(1);
}
}
}
/**
* Set up shared element transition from element with <code>fromViewId</code> to
* element with <code>toViewId</code>. Note that you can setup each transition
* direction separately. e.g. <br/>
* <code>addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code><br/>
* and<br/>
* <code>addSharedTransition(R.id.SecondPageTextView, R.id.FirstPageTextView)</code><br/>
* are different.
* @param fromViewId
* @param toViewId
*/
public void addSharedTransition(int fromViewId, int toViewId) {
addSharedTransition(fromViewId, toViewId, false);
}
/**
* Set up shared element transition from element with <code>fromViewId</code> to
* element with <code>toViewId</code>. Note that you can setup each transition
* direction separately. e.g. <br/>
* <code>addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code><br/>
* and<br/>
* <code>addSharedTransition(R.id.SecondPageTextView, R.id.FirstPageTextView)</code><br/>
* are different.
* @param fromViewId
* @param toViewId
* @param bothDirections to include backward transition from toViewId to fromViewId aswell
*/
public void addSharedTransition(int fromViewId, int toViewId, boolean bothDirections) {
sharedElementIds.add(new Pair<>(fromViewId, toViewId));
if(bothDirections) {
sharedElementIds.add(new Pair<>(toViewId, fromViewId));
}
}
/**
* In case there is "ladder" appears between while transition.
* You may try to tune that magical scale to get rid of it.
* @param magicalAndroidRenderingScale float between 0 and infinity. Typically very close to 1.0
*/
public static void setMagicalAndroidRenderingScale(float magicalAndroidRenderingScale) {
MAGICAL_ANDROID_RENDERING_SCALE = magicalAndroidRenderingScale;
}
}


How does the work with the library



The configuration turned out pretty concise.



The complete setup for our example looks like this.
 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.



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



')

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



All Articles