Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android shared element transition ToolBar overlap

I've implemented shared element transitions for my app, where the transition begins on an image from a Fragment (with RecyclerView) inside a ViewPager on the home screen and expands into full screen gallery view, again within a Fragment in a ViewPager. This is all working fine except that if the image is not fully visible it goes on top of the TabBar before expanding into full screen. Here's what's happening:

enter image description here

My enter transition looks like this:

<?xml version="1.0" encoding="utf-8"?>
<fade xmlns:android="http://schemas.android.com/apk/res/android">
    <targets>
        <target android:excludeId="@android:id/statusBarBackground"/>
        <target android:excludeId="@android:id/navigationBarBackground"/>
    </targets>
</fade>

And exit:

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
android:transitionOrdering="together"
android:duration="500">
    <fade>
        <targets>
            <target android:excludeId="@android:id/statusBarBackground" />
            <target android:excludeId="@android:id/navigationBarBackground" />
        </targets>
    </fade>
</transitionSet>

And in the shared element callback of the calling activity I've got this:

View navigationBar = activity.findViewById(android.R.id.navigationBarBackground);
View statusBar = activity.findViewById(android.R.id.statusBarBackground);
if (navigationBar != null) {
    names.add(navigationBar.getTransitionName());
    sharedElements.put(navigationBar.getTransitionName(), navigationBar);
}
if (statusBar != null) {
    names.add(statusBar.getTransitionName());
    sharedElements.put(statusBar.getTransitionName(), statusBar);
}

Finally in styles.xml for the activity theme:

<item name="android:windowContentTransitions">true</item>
<item name="android:windowEnterTransition">@transition/details_window_enter_transition</item>
<item name="android:windowReturnTransition">@transition/details_window_return_transition</item>

I don't really understand how the toolbar (or actionbar) can be excluded by the transition without getting this overlap. Perhaps a way to do it would be to somehow force the image to be clipped at the top part so that it doesn't become fully visible when under the ToolBar and expands only from the visible rectangle.

I've tried adding <target android:excludeId="@id/action_bar_container"/> to the targets of the animation but the same thing happens still.

Any suggestions are welcome.

like image 504
Georgi Avatar asked Jun 11 '17 12:06

Georgi


4 Answers

I searched everywhere and couldn't find any solution so I figured myself. Here is a recorded demo (at half the speed) to show the result (with and without the fix).

With fix    Without fix

Please checkout the full working demo here: https://github.com/me-abhinav/shared-element-overlap-demo

Steps

Let us say we have two activities viz. MainActivity which has a scrolling container with a grid/list of thumbnails, and we have a SecondActivity which shows the image in a slideshow in fullscreen.

Please checkout the full code to completely understand the solution.

  1. Inside your MainActivity which hosts the scrolling container, set a click listener on your thumbnail to open SecondActivity:
ImageView imageView = findViewById(R.id.image_view);
imageView.setOnClickListener(v -> {
    // Set the transition name. We could also do it in the xml layout but this is to demo
    // that we can choose any name generated dynamically.
    String transitionName = getString(R.string.transition_name);
    imageView.setTransitionName(transitionName);

    // This part is important. We first need to clip this view to only its visible part.
    // We will also clip the corresponding view in the SecondActivity using shared element
    // callbacks.
    Rect localVisibleRect = new Rect();
    imageView.getLocalVisibleRect(localVisibleRect);
    imageView.setClipBounds(localVisibleRect);
    mClippedView = imageView;

    Intent intent = new Intent(MainActivity.this, SecondActivity.class);
    intent.putExtra(SecondActivity.EXTRA_TRANSITION_NAME, transitionName);
    intent.putExtra(SecondActivity.EXTRA_CLIP_RECT, localVisibleRect);
    ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
                    MainActivity.this,
                    Pair.create(imageView, transitionName));
    startActivity(intent, options.toBundle());
});
  1. Restore the clip in onResume() of your MainActivity.
@Override
protected void onResume() {
    super.onResume();

    // This is also important. When we come back to this activity, we need to reset the clip.
    if (mClippedView != null) {
        mClippedView.setClipBounds(null);
    }
}
  1. Create transition resource in your res folder like this: app/src/main/res/transition/shared_element_transition.xml The contents should be similar to this:
<?xml version="1.0" encoding="utf-8"?>
<transitionSet
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="375"
    android:interpolator="@android:interpolator/fast_out_slow_in"
    android:transitionOrdering="together">

    <!-- This is needed to clip the invisible part of the view being transitioned. Otherwise we
         will see weird transitions when the image is partially hidden behind appbar or any other
         view. -->
    <changeClipBounds/>

    <changeTransform/>
    <changeBounds/>

</transitionSet>
  1. Set the transition in your SecondActivity.
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_second);

    // Setup transition
    Transition transition =
            TransitionInflater.from(this)
                    .inflateTransition(R.transition.shared_element_transition);
    getWindow().setSharedElementEnterTransition(transition);

    // Postpone the transition. We will start it when the slideshow is ready.
    ActivityCompat.postponeEnterTransition(this);

    // more code ... 
    // See next step below.
}
  1. Now we need to clip the shared view in the SecondActivity as well.
// Setup the clips
String transitionName = getIntent().getStringExtra(EXTRA_TRANSITION_NAME);
Rect clipRect = getIntent().getParcelableExtra(EXTRA_CLIP_RECT);
setEnterSharedElementCallback(new SharedElementCallback() {
    @Override
    public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
        for (int i = 0; i < sharedElementNames.size(); i++) {
            if (Objects.equals(transitionName, sharedElementNames.get(i))) {
                View view = sharedElements.get(i);
                view.setClipBounds(clipRect);
            }
        }
        super.onSharedElementStart(sharedElementNames, sharedElements, sharedElementSnapshots);
    }

    @Override
    public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
        for (int i = 0; i < sharedElementNames.size(); i++) {
            if (Objects.equals(transitionName, sharedElementNames.get(i))) {
                View view = sharedElements.get(i);
                view.setClipBounds(null);
            }
        }
        super.onSharedElementEnd(sharedElementNames, sharedElements, sharedElementSnapshots);
    }
});
like image 176
Abhinav Chauhan Avatar answered Oct 25 '22 17:10

Abhinav Chauhan


I found a similar problem in my project.Add below code in your style.

<item name="android:windowSharedElementsUseOverlay">false</item>

It works for me.

like image 26
Chan Myae Aung Avatar answered Oct 25 '22 18:10

Chan Myae Aung


I came up with a temporary workaround. Before the shared element transition is executed, the calling activity checks whether the target view is within the bounds of the RecyclerView. If not, the RecyclerView is smooth-scrolled to show the full view and once that is finished the transition runs. If the view is fully visible, then transitions runs normally.

// Scroll to view and run animation when scrolling completes.
recycler.smoothScrollToPosition(adapterPosition);
recycler.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {

    @Override
    public boolean onPreDraw() {
        recycler.getViewTreeObserver().removeOnPreDrawListener(this);
        // Open activity here.
        return true;
    }
});

Here's the result, not too bad until I find a better solution:

enter image description here

like image 39
Georgi Avatar answered Oct 25 '22 18:10

Georgi


I guess i have the proper answer for this question. This issue happening because on the second activity you do not have toolbar at all, so it can not be added to transition as shared element. So in my case i added some 'fake toolbar' to my second activity, which have 0dp height. Then you can add toolbar from first activity as a shared element, and give him change bounds animation, so toolbar will collapse at the same time as image and image will no longer be 'over' toolbar.

My 'fake toolbar' view:

    <View
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:background="@color/colorPrimary"
    android:elevation="10dp"
    android:outlineProvider="none"
    android:transitionName="toolbar_transition" />

important notes:

-view have to have non-transparent background

-i added elevation to be sure that my view i 'over' image

-elevation causes shadow, whick i did not wanted, so i set outilenProvider as none

Next all you have to do is add your toolbar to shared elements

    sharedElements.add(new Pair<>(toolbar, "toolbar_transition"));
like image 41
KaMyLL Avatar answered Oct 25 '22 17:10

KaMyLL