1

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 finish transition the shared image comes from left side which is not desired it should reposition itself to where it was launched, here is my code:

Intent myIntent = new Intent(getActivity(), EnlargeActivity.class);

            ActivityOptionsCompat options = ActivityOptionsCompat.
                    makeSceneTransitionAnimation(getActivity(),
                            imageView,
                            ViewCompat.getTransitionName(imageView));
            startActivity(myIntent, options.toBundle());

I'm updating view and its name in activity that contains viewpager when finishing activity, but its going with blink:

public void finishAfterTransition() {
    setEnterSharedElementCallback(new SharedElementCallback() {
        @Override
        public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
            // Clear all current shared views and names
            names.clear();
            sharedElements.clear();

            ViewGroup viewGroup = (ViewGroup) viewPagerDetail.getAdapter()
                    .instantiateItem(viewPagerDetail, viewPagerDetail.getCurrentItem());

            if (viewGroup == null) {
                return;
            }

            // Map the first shared element name to the child ImageView.
            sharedElements.put(viewGroup.findViewById(R.id.img).getTransitionName(), viewGroup.findViewById(R.id.img));

           // setExitSharedElementCallback((SharedElementCallback) this);
        }
    });

    super.finishAfterTransition();
blackHawk
  • 6,047
  • 13
  • 57
  • 100

2 Answers2

3

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) 
    public void finishAfterTransition() {
        setEnterSharedElementCallback(new SharedElementCallback() {
            @Override
            public void onMapSharedElements(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 name
                String transitionName = ViewCompat.getTransitionName(selectedView);
                names.add(transitionName);
                sharedElements.put(transitionName, selectedView);
    
                setExitSharedElementCallback((SharedElementCallback) null);
            }
        });
    
        Intent intent = new Intent();
        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)
    private static class CustomSharedElementCallback extends SharedElementCallback {
        private View mView;
    
        /**
         * Set the transtion View to the callback, this should be called before starting the transition so the View is not null
         */
        public void setView(View view) {
            mView = view;
        }
    
        @Override
        public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
            // Clear all current shared views and names
            names.clear();
            sharedElements.clear();
    
            // Store new selected view and name
            String 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)
    public void onActivityReenter(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 = new CustomSharedElementCallback();
        getActivity().setExitSharedElementCallback(callback);
    
        // Listen for the transition end and clear all registered callback
        getActivity().getWindow().getSharedElementExitTransition().addListener(new Transition.TransitionListener() {
            @Override
            public void onTransitionStart(Transition transition) {}
    
            @Override
            public void onTransitionPause(Transition transition) {}
    
            @Override
            public void onTransitionResume(Transition transition) {}
    
            @Override
            public void onTransitionEnd(Transition transition) {
                removeCallback();
            }
    
            @Override
            public void onTransitionCancel(Transition transition) {
                removeCallback();
            }
    
            private void removeCallback() {
                if (getActivity() != null) {
                    getActivity().getWindow().getSharedElementExitTransition().removeListener(this);
                    getActivity().setExitSharedElementCallback((SharedElementCallback) null);
                }
            }
        });
    
        // Pause transition until the selected view is fully drawn
        getActivity().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(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                mPhotoList.getViewTreeObserver().removeOnPreDrawListener(this);
    
                RecyclerView.ViewHolder holder = mPhotoList.findViewHolderForAdapterPosition(selectedIndex);
                if (holder instanceof ViewHolder) {
                    callback.setView(((ViewHolder) holder).mPhotoImg);
                }
    
                // Continue the transition
                getActivity().supportStartPostponedEnterTransition();
    
                return true;
            }
        });
    }
    

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:

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

    // ...
}

Listen to onPageSelected event to get the selected index:

mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int position) {
        mSelectedIndex = position;
    }

    @Override
    public void onPageScrollStateChanged(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) {
        return null;
    }
}
Tam Huynh
  • 2,026
  • 1
  • 16
  • 20
  • You are right transition name is not setting properly thats why problem happening, I'm tying your approach – blackHawk May 05 '18 at 17:58
  • where should apply setEnterSharedElementCallback – blackHawk May 05 '18 at 18:24
  • @blackHawk it's explained in the [same article](https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html) that you read – Suleyman May 05 '18 at 22:54
  • @blackHawk I've added an update above in my answer to explain more about my code – Tam Huynh May 06 '18 at 04:58
  • @LieForBananas yes, trying to understand bit more, – blackHawk May 06 '18 at 10:31
  • @TamHuynh when onMapSharedElements will be called? actually i have fragment and through fragment im launching activity with shared element transition that has viewpager in it, – blackHawk May 06 '18 at 10:34
  • @LieForBananas I'm getting out of way properly set transition name because viewpager's instantiate item called 3 times and transition name got disturb – blackHawk May 06 '18 at 10:53
  • @blackHawk it's called 3 times probably because ViewPager is preloading the pages for performance reasons – Suleyman May 06 '18 at 10:55
  • where i use onMapSharedElements, or i need not to use – blackHawk May 06 '18 at 10:57
  • @TamHuynh I tried, but i cant access view to get its transition name because its in Viewpage adapter not in activity – blackHawk May 06 '18 at 14:51
  • `onMapSharedElements` will be called on both Activities when enter/ exit the transition. That's why we need to set the callback back to null right after finishing the work so it won't mess up the next transition. Shared element from Fragment to Activity is the same as Activity to activity because fragment can access its Activity by `getActivity()`. You just need to pass the call from `onActivityReenter` to the fragment. I will update the answer to include the ViewPager current view selection. – Tam Huynh May 06 '18 at 15:06
  • Uncomment the last line: `setEnterSharedElementCallback(null)` is important. And did you setup `onActivityReenter` in the first Activity? The remap process needs to be done on both sides – Tam Huynh May 06 '18 at 16:04
  • where onActivityReenter? in fragment? – blackHawk May 06 '18 at 16:09
  • It's in `Activity` only, it's called when Activity2 is finished to get back on Activity1 but before the transition, write your own `onActivityReenter` in the fragment and call it from Activity. The implementation is shared above – Tam Huynh May 06 '18 at 16:14
1

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(
new OnLayoutChangeListener() {
  @Override
  public void onLayoutChange(View view,
            int left, 
            int top, 
            int right, 
            int bottom, 
            int oldLeft, 
            int oldTop, 
            int oldRight, 
            int oldBottom) {
     recyclerView.removeOnLayoutChangeListener(this);
     final RecyclerView.LayoutManager layoutManager =
        recyclerView.getLayoutManager();
     View viewAtPosition = 
        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.

Suleyman
  • 2,765
  • 2
  • 18
  • 31
  • I used the same article and i learnt alot from it, but here is different situation, I'm opening activity with shared element from fragment, when i finish that activity it works fine, but when i add viewpager in activity and then finish activity its behaving like transition name is not set properly, it comes from right hand side where as when there is no viewpager the image comes straight like zoom in and goes back like zoom out which is desired – blackHawk May 05 '18 at 13:00
  • can you figure out whats being happening when im adding viewpager in activity and finishing activity – blackHawk May 05 '18 at 13:01
  • @blackHawk So if you have the viewpager, open the activity, and close it without swiping the viewpager it still does the same? – Suleyman May 05 '18 at 13:03
  • adding viewpager in activity causing difference, no matter i swap or not – blackHawk May 05 '18 at 13:06
  • @blackHawk as you said, it may be due to transition names, what does your viewpager contain? Are you sure that the names are unique? I had a problem with [conflicting transition names](https://stackoverflow.com/questions/49806441/shared-element-transition-fragment-recyclerview-viewpager), I was using a tablayout with fragments that were displaying similar data, so I had to add fragment numbers to my transition names to differentiate them. – Suleyman May 05 '18 at 13:11
  • in instantiateitem of viewpager im doing like this: img.setTransitionName("simple_activity_transition"); – blackHawk May 05 '18 at 13:15
  • @blackHawk Are you using `FragmentStatePagerAdapter`? It's probably because your transition name is the same for all viewpager pages, try appending the position when you are setting transition name, like this `img.setTransitionName("simple_activity_transition" + position)` – Suleyman May 05 '18 at 13:22
  • I have noticed, suppose i have 5 in getCount of viewpager, when i scrolled to 5th item and then exit activity it works fine, and when i scroll to 3 or 4 it does not work – blackHawk May 05 '18 at 13:27
  • img.setTransitionName("simple_activity_transition" + position) does work but it it coming or going with blink – blackHawk May 05 '18 at 13:33
  • @blackHawk it's good it's working :) the whole image is blinking or the status bar and action bar? – Suleyman May 05 '18 at 13:35
  • whole image is blinking – blackHawk May 05 '18 at 13:35
  • @blackHawk I believe it's a common problem, check this [answer](https://stackoverflow.com/a/28591442/7915814) it should help, it has to do with activities animating in and out. – Suleyman May 05 '18 at 13:39
  • I think problem still lies in transition name, Im unable to assign unique name for each image – blackHawk May 05 '18 at 13:43
  • @blackHawk I thought you said it's working when you append the position? If not then you have to think about creating unique names, in my case I was using image paths as transition names, maybe you can do the same, maybe pass the image path to the constructor and add it as a transition name. – Suleyman May 05 '18 at 13:47
  • I did the same for my previous work :) for this unfortunately(for the time being) I have only one url, what about connect personally? I really like talking to you, you understand better :D – blackHawk May 05 '18 at 13:51
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/170426/discussion-between-lieforbananas-and-blackhawk). – Suleyman May 05 '18 at 13:56
  • I believe what this code snippet does is to just scroll the recyclerview to ensure that the currently displayed view from the viewpager is visible when the user decides to go back to the recyclerview. It has nothing to do with transitions – Rafsanjani Jul 22 '19 at 11:51