Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shared Element Transition on CardView with radius

I've been working on this problem for weeks and I'm still unable to solve this problem.

So, I have a CardView that contains a LinearLayout with an ImageView.

without radius

Without that radius Shared Element Transition works seamlessly. However, when I add radius (app:cardCornerRadius="25dp") to that CardView, the Shared Element Transition looks ugly because it remove the radius first and then start the animation.

with radius transition

1st Approach: ObjectAnimator

I create ObjectAnimator to animate the radius value on card, and after the animation end it start the transition.

ObjectAnimator animator = ObjectAnimator
            .ofFloat(view, "radius", AppUtil.dpAsPixel(this, 25), 0);
animator.setDuration(150);
animator.addListener( // start new Activity with Transition );
animator.start();

This works but it doesn't looks great, because the transition wait the animation to finish before starting the transition. What I need is the radius is animating while doing transition to new Activity (something like ORDERING_TOGETHER in TransitionSet).

2nd Approach - ChangeImageTransform

I've read a StackOverflow post to use Transformation Class like ChangeImageTransform and ChangeBounds.

I did define my application theme like it was suggested (my_transition contains ChangeImageTransform transitionSet)

<item name="android:windowSharedElementEnterTransition">@transition/my_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/my_transition</item>

But it doesn't work..

3rd Approach - Naive

My last attempt is to force the target ImageView to have a radius of 25dp too. Because maybe my CardView is transformed into square because the target ImageView is square, but as you can guess, it doesn't work.

4th Approach - Not Using CardView

As you can see, I'm using Penguin images and use CardView to make a radius. I can make the image rounded by using Image Transformation, but I still don't think that's the right way to create a Shared Element Transition..

And here is my question, is there a way to make a Shared Element Transition with CardView radius works without removing the radius first?

like image 469
aldok Avatar asked Feb 16 '17 12:02

aldok


3 Answers

I finally able to solve it. For those who are interested, here's how:

Why it remove radius before starting transition? Because the target ImageView doesn't have any radius.

activity_detail.xml

<ImageView
    android:id="@+id/iv_image_cover"
    android:layout_width="match_parent"
    android:layout_height="250dp"
    android:scaleType="centerCrop"
    android:src="@{animal.imageRes}"
    android:transitionName="animalImage"
    tools:src="@drawable/acat"
/>

When I use CardView without radius, it's not noticable, but it's actually turned into target Shared View.

  1. To achieve radius to no-radius transition you have to set the target Shared View to be rounded. I'm simply wrap it using a Card View (with radius).

activity_detail.xml

<android.support.v7.widget.CardView
    android:id="@+id/card"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:transitionName="card"
    app:cardCornerRadius="25dp"
>

    <ImageView
        android:id="@+id/iv_image_cover"
        android:layout_width="match_parent"
        android:layout_height="250dp"
        android:scaleType="centerCrop"
        android:src="@{animal.imageRes}"
        android:transitionName="animalImage"
        tools:src="@drawable/acat"
    />

</android.support.v7.widget.CardView>
  1. Be sure to change your makeSceneTransition to use "card" instead of "animalImage"

ListActivity.class

ActivityOptionsCompat option = ActivityOptionsCompat
.makeSceneTransitionAnimation(ListActivity.this, cardView, "card");

startActivity(intent, option.toBundle());
  1. In the DetailActivity, you can start a radius animation when the transition start.

DetailActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().getSharedElementEnterTransition()
        .addListener(new Transition.TransitionListener() {
            @Override
            public void onTransitionStart(Transition transition) {
                ObjectAnimator animator = ObjectAnimator
                    .ofFloat(activityDetailBinding.card, "radius", 0);
                animator.setDuration(250);
                animator.start();
            }
        });
}
  1. Enjoy the smooth transition

final animation

Note: gist for layout and activities

like image 74
aldok Avatar answered Oct 26 '22 11:10

aldok


Based on Ovidiu's answer, here is a working transition which can animate the corners

https://gist.github.com/StefanDeBruijn/d45807d386af0e066a03186fe00366e8

This can either be added programatically to the enter shared transition set or through xml:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <transitionSet>
        <targets>
            <target android:targetId="@id/backdrop" />
        </targets>
        <!-- Custom transition to take care of rounded corner to square corners transition -->
        <transition
            class=".ChangeOutlineRadius"
            app:endRadius="@dimen/square_corner_radius"
            app:startRadius="@dimen/default_corner_radius" />
        <!-- Default shared element transitions -->
        <changeBounds />
        <changeTransform />
        <changeClipBounds />
        <changeImageTransform />
    </transitionSet>
</transitionSet>

Don't forget to add to your attrs.xml:

   <declare-styleable name="ChangeOutlineRadius">
        <attr name="startRadius" format="dimension" />
        <attr name="endRadius" format="dimension" />
    </declare-styleable>
like image 32
Stefan de Bruijn Avatar answered Oct 26 '22 12:10

Stefan de Bruijn


I couldn't get that to work with Fragment Shared Element Transitions. The CardView's corner radius would just be ignored during the animation. Here's something that does work:

fragment2.setEnterSharedElementCallback(new SharedElementCallback() {
    @Override
    public void onSharedElementStart(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {}

    @Override
    public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
        ImageView sharedImageView = null;

        for (View view : sharedElements) {
            if (view instanceof ImageView) {
                sharedImageView = (ImageView) view;
                break;
            }
        }

        if (sharedImageView != null) {
            sharedImageView.setClipToOutline(true);

            ObjectAnimator.ofInt(sharedImageView, new Property<ImageView, Integer>(Integer.class, "outlineRadius") {
                @Override
                public Integer get(ImageView object) {
                    return 0;
                }

                @Override
                public void set(ImageView object, final Integer value) {
                    object.setOutlineProvider(new ViewOutlineProvider() {
                        @Override
                        public void getOutline(View view, Outline outline) {
                            outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), value);
                        }
                    });
                }
            }, 150, 0).setDuration(duration).start();
        }
    }

    @Override
    public void onRejectSharedElements(List<View> rejectedSharedElements) {}

    @Override
    public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {}
});

Basically, instead of animating the rounded corners of the CardView, the ImageView itself has its own rounded corners that are being animated instead.

The ImageView's corners are rounded with a ViewOutlineProvider, and an ObjectAnimator can be used to animate the corner radius while the shared element transition is playing. Note that calling setClipToOutline(true) on the ImageView is also required, otherwise the corners will not be clipped.

The onSharedElementEnd method of the callback will be called with a list of all shared elements. Note that my example code handles animating the corners of just one of the ImageViews that are being shared. If your transition shares multiple ImageViews, you will need to take those into account as well.

Also note that for some reason the same callback is also called when the reverse transition is played.

With some amount of effort, this could be turned into a regular Transition that you just add to the set of shared element transitions that automatically figure out what to do with the shared elements.

like image 1
Ovidiu Avatar answered Oct 26 '22 10:10

Ovidiu