Skip to content Skip to sidebar Skip to footer

Shared Element Transition Is Not Exiting Properly

I have fragment from which I'm launching activity with shared element transition that has viewpager in it, the enter transition works fine but when i scroll in view pager and finis

Solution 1:

Basically, Android start the transition with your pre-defined View and transitionName and automatically use the same properties for the return transition. When you change your focused View in ViewPager, Android doesn't know about that and keep the transition on the previous one on its way back. So you need to inform Android about the changes:

  • Remap the transition properties: Use setEnterSharedElementCallback to change the transitionName and View to the new one before returning from Activity2.
  • Wait for the Activity1 to finish rendering addOnPreDrawListener.

It's a bit complex in the final implementation. But you can look at my sample code https://github.com/tamhuynhit/PhotoGallery. I try to implement the shared-element-transition from many simple to complex sections. Your problem appeared from Level 3 and solved in Level 4.

I am writing a tutorial about this but it's not in English so hope the code can help

UPDATE 1: Work flow

Here is how I implement it in my code:

  • Override finishAfterTransition in Activity2 and call setEnterSharedElementCallback method to re-map the current selected item in ViewPager. Also, call setResult to pass the new selected index back to previous activity here.

    @Override@TargetApi(Build.VERSION_CODES.LOLLIPOP) 
    publicvoidfinishAfterTransition() {
        setEnterSharedElementCallback(newSharedElementCallback() {
            @OverridepublicvoidonMapSharedElements(List<String> names, Map<String, View> sharedElements) {
                View selectedView = getSelectedView();
                if (selectedView == null)
                    return;
    
                // Clear all current shared views and names
                names.clear();
                sharedElements.clear();
    
                // Store new selected view and nameString transitionName = ViewCompat.getTransitionName(selectedView);
                names.add(transitionName);
                sharedElements.put(transitionName, selectedView);
    
                setExitSharedElementCallback((SharedElementCallback) null);
            }
        });
    
        Intent intent = newIntent();
        intent.putExtra(PHOTO_FOCUSED_INDEX, mCurrentIndex);
        setResult(RESULT_PHOTO_CLOSED, intent);
    
        super.finishAfterTransition();
    }
    
  • Write a custom ShareElementCallback so I can set the callback before knowing which View is going to be used.

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    privatestaticclassCustomSharedElementCallbackextendsSharedElementCallback {
        privateView mView;
    
        /**
         * Set the transtion View to the callback, this should be called before starting the transition so the View is not null
         */publicvoidsetView(View view) {
            mView = view;
        }
    
        @OverridepublicvoidonMapSharedElements(List<String> names, Map<String, View> sharedElements) {
            // Clear all current shared views and names
            names.clear();
            sharedElements.clear();
    
            // Store new selected view and nameString transitionName = ViewCompat.getTransitionName(mView);
            names.add(transitionName);
            sharedElements.put(transitionName, mView);
        }
    }
    
  • Override onActivityReenter in Activity1, get the selected index from the result Intent. Set setExitSharedElementCallback to re-map new selected View when the transition begins.Call supportPostponeEnterTransition to delay a bit because your new View may not be rendered at this point. Use getViewTreeObserver().addOnPreDrawListener to listen for the layout changes, find the right View by the selected index and continue the transition supportStartPostponedEnterTransition.

    @Override@TargetApi(Build.VERSION_CODES.LOLLIPOP)
    publicvoidonActivityReenter(int resultCode, Intent data) {
        if (resultCode != LevelFourFullPhotoActivity.RESULT_PHOTO_CLOSED || data == null)
            return;
    
        final int selectedIndex = data.getIntExtra(LevelFourFullPhotoActivity.PHOTO_FOCUSED_INDEX, -1);
        if (selectedIndex == -1)
            return;
    
        // Scroll to the new selected view in case it's not currently visible on the screen
        mPhotoList.scrollToPosition(selectedIndex);
    
        final CustomSharedElementCallback callback = newCustomSharedElementCallback();
        getActivity().setExitSharedElementCallback(callback);
    
        // Listen for the transition end and clear all registered callbackgetActivity().getWindow().getSharedElementExitTransition().addListener(newTransition.TransitionListener() {
            @OverridepublicvoidonTransitionStart(Transition transition) {}
    
            @OverridepublicvoidonTransitionPause(Transition transition) {}
    
            @OverridepublicvoidonTransitionResume(Transition transition) {}
    
            @OverridepublicvoidonTransitionEnd(Transition transition) {
                removeCallback();
            }
    
            @OverridepublicvoidonTransitionCancel(Transition transition) {
                removeCallback();
            }
    
            privatevoidremoveCallback() {
                if (getActivity() != null) {
                    getActivity().getWindow().getSharedElementExitTransition().removeListener(this);
                    getActivity().setExitSharedElementCallback((SharedElementCallback) null);
                }
            }
        });
    
        // Pause transition until the selected view is fully drawngetActivity().supportPostponeEnterTransition();
    
        // Listen for the RecyclerView pre draw to make sure the selected view is visible,//  and findViewHolderForAdapterPosition will return a non null ViewHolder
        mPhotoList.getViewTreeObserver().addOnPreDrawListener(newViewTreeObserver.OnPreDrawListener() {
            @OverridepublicbooleanonPreDraw() {
                mPhotoList.getViewTreeObserver().removeOnPreDrawListener(this);
    
                RecyclerView.ViewHolder holder = mPhotoList.findViewHolderForAdapterPosition(selectedIndex);
                if (holder instanceofViewHolder) {
                    callback.setView(((ViewHolder) holder).mPhotoImg);
                }
    
                // Continue the transitiongetActivity().supportStartPostponedEnterTransition();
    
                returntrue;
            }
        });
    }
    

UPDATE 2: getSelectedItem

To get selected View from the ViewPager, don't use getChildAt or you get the wrong View, use findViewWithTag instead

In the PagerAdapter.instantiateItem, use position as tag for each View:

@Overridepublic View instantiateItem(ViewGroup container, int position) {
    // Create the View
    view.setTag(position)

    // ...
}

Listen to onPageSelected event to get the selected index:

mViewPager.addOnPageChangeListener(newViewPager.OnPageChangeListener() {
    @OverridepublicvoidonPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @OverridepublicvoidonPageSelected(int position) {
        mSelectedIndex = position;
    }

    @OverridepublicvoidonPageScrollStateChanged(int state) {

    }
});

Call getSelectedView to get the current view by the selected index

private View getSelectedView() {
    try {
        return mPhotoViewPager.findViewWithTag(mSelectedIndex);
    } catch (IndexOutOfBoundsException | NullPointerException ex) {
        returnnull;
    }
}

Solution 2:

This is actually a default behavior, I was struggling SharedElementTransitions a lot, but I have nested fragments. I got my solution from an article (very recent article), it shows an implementation with a RecyclerView, which I assume you have. In short, the solution is to override onLayoutChange :

recyclerView.addOnLayoutChangeListener(
newOnLayoutChangeListener() {
  @OverridepublicvoidonLayoutChange(View view,
            int left, 
            int top, 
            int right, 
            int bottom, 
            int oldLeft, 
            int oldTop, 
            int oldRight, 
            int oldBottom) {
     recyclerView.removeOnLayoutChangeListener(this);
     final RecyclerView.LayoutManagerlayoutManager=
        recyclerView.getLayoutManager();
     ViewviewAtPosition= 
        layoutManager.findViewByPosition(MainActivity.currentPosition);
     // Scroll to position if the view for the current position is null (not   // currently part of layout manager children), or it's not completely// visible.if (viewAtPosition == null 
         || layoutManager.isViewPartiallyVisible(viewAtPosition, false, true)){
        recyclerView.post(() 
           -> layoutManager.scrollToPosition(MainActivity.currentPosition));
     }
 }
});

Here is the article, and you will also find the project on GitHub.

Post a Comment for "Shared Element Transition Is Not Exiting Properly"