Scene transition with nested shared element
Asked Answered
A

3

10

I'm trying to use the Transitions API to animate a shared element between two ViewGroups. The goal is that the green view travels 'out of its parent's bounds' towards the new position.

enter image description here enter image description here enter image description here

I have the following layouts:

first.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <FrameLayout
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="#f00" />

  <FrameLayout
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_alignParentBottom="true"
    android:layout_alignParentRight="true"
    android:background="#00f">

    <View
      android:id="@+id/myview"
      android:layout_width="100dp"
      android:layout_height="100dp"
      android:layout_gravity="center"
      android:background="#0f0" />

  </FrameLayout>
</RelativeLayout>

second.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <FrameLayout
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="#f00">

    <View
      android:id="@+id/myview"
      android:layout_width="100dp"
      android:layout_height="100dp"
      android:layout_gravity="center"
      android:background="#0f0" />

  </FrameLayout>

  <FrameLayout
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_alignParentBottom="true"
    android:layout_alignParentRight="true"
    android:background="#00f" />

</RelativeLayout>

However, I can't get this to work properly. The default transition just fades everything out and in, the ChangeBounds transition does nothing at all, and the ChangeTransform does not look right either.

The code I'm using:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final ViewGroup root = (ViewGroup) findViewById(android.R.id.content);

    setContentView(R.layout.first);
    View myView1 = findViewById(R.id.myview);
    myView1.setTransitionName("MYVIEW");

    new Handler().postDelayed(new Runnable() {
      @Override
      public void run() {
        View second = LayoutInflater.from(MainActivity.this).inflate(R.layout.second, root, false);
        View myView2 = second.findViewById(R.id.myview);
        myView2.setTransitionName("MYVIEW");

        Scene scene = new Scene(root, second);
        Transition transition = new ChangeTransform();
        transition.addTarget("MYVIEW");
        transition.setDuration(3000);

        TransitionManager.go(scene, transition);
      }
    }, 2000);
  }

Now I am manually able to do this by creating the animation myself using a ViewOverlay. However, I'm looking for a solution using the Transitions API. Is this even possible?

Also, I am not looking to 'flatten' the hierarchy. I am intentionally nesting the View to account for more complex use cases.

Abie answered 29/5, 2016 at 17:32 Comment(0)
G
4

Yes, reparenting is possible with the Android transition API. There is one major restriction to consider when reparenting views:

Usually, every child view is only allowed to draw within it's parents bounds. This will result in the view disappearing when it hits the border of its initial parent and reappear in the new parent after a short while.

Turn off child clipping all the way up your view hierarchies of the relevant scenes by setting android:clipChildren="false" on all relevant parents.

In more complex hierarchies, such as adapter backed views, it's more performant to toggle child clipping dynamically via a TransitionListener.

You'll also have to use ChangeTransform in order to reparent the view, either instead of ChangeBounds or add it to a TransitionSet.

There is no need for transition name or target at this level of transitions since the framework will figure that out itself. If things get more complicated you'll want to have either

  • matching resource ids or
  • matching transition names

for views participating in the transition.

Grice answered 2/6, 2016 at 13:4 Comment(2)
Ah thanks, clipChildren does the trick in this case. Why doesn't it use an overlay though? From the ChangeTransform.setReparentWithOverlay doc: Sets whether changes to parent should use an overlay or not. When the parent change doesn't use an overlay, it affects the transforms of the child. *The default value is true*.Abie
Take a look at the Android Device Monitor before you've made a transition and after. There should be a ViewOverlay in the hierarchy after the transition.Grice
A
0

The ChangeBounds class has a deprecated setReparent(Boolean) method which refers to ChangeTransform "to handle transitions between different parents". This ChangeTransform class doesn't give the desired effect at all.

From the ChangeBounds source we can see that, if reparent is set to true (plus some other conditions), the overlay of the sceneroot is used to add an overlay. For some reason though, this doesn't work: probably due to the fact that it is deprecated.

I've been able to work around this by extending ChangeBounds to use an overlay when I want to (in Kotlin):

class OverlayChangeBounds : ChangeBounds() {

    override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
        if (startValues == null || endValues == null) return super.createAnimator(sceneRoot, startValues, endValues)

        val startView = startValues.view
        val endView = endValues.view

        if (endView.id in targetIds || targetNames?.contains(endView.transitionName) == true) {
            startView.visibility = View.INVISIBLE
            endView.visibility = View.INVISIBLE

            endValues.view = startValues.view.toImageView()
            sceneRoot.overlay.add(endValues.view)

            return super.createAnimator(sceneRoot, startValues, endValues)?.apply {
                addListener(object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator) {
                        endView.visibility = View.VISIBLE
                        sceneRoot.overlay.remove(endValues.view)
                    }
                })
            }
        }

        return super.createAnimator(sceneRoot, startValues, endValues)
    }
}

private fun View.toImageView(): ImageView {
    val v = this
    val drawable = toBitmapDrawable()
    return ImageView(context).apply {
        setImageDrawable(drawable)
        scaleType = ImageView.ScaleType.CENTER_CROP
        layout(v.left, v.top, v.right, v.bottom)
    }
}

private fun View.toBitmapDrawable(): BitmapDrawable {
    val b = Bitmap.createBitmap(layoutParams.width, layoutParams.height, Bitmap.Config.ARGB_8888);
    draw(Canvas(b));
    return BitmapDrawable(resources, b)
}
Abie answered 30/5, 2016 at 12:23 Comment(1)
While this may be a solution which works for you, I recommend taking a look into my answer and considering going the way the Android transitions API is intended.Grice
K
0

here is my workaround for such case (it's just part of ChangeBounds in separate class)

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.transition.Transition;
import android.transition.TransitionValues;
import android.util.Property;
import android.view.View;
import android.view.ViewGroup;

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class AbsoluteChangeBounds extends Transition {

    private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
    private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";

    private int[] tempLocation = new int[2];

    private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
            new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
                private Rect mBounds = new Rect();

                @Override
                public void set(Drawable object, PointF value) {
                    object.copyBounds(mBounds);
                    mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
                    object.setBounds(mBounds);
                }

                @Override
                public PointF get(Drawable object) {
                    object.copyBounds(mBounds);
                    return new PointF(mBounds.left, mBounds.top);
                }
            };

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    private void captureValues(TransitionValues values) {
        View view = values.view;
        if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
            values.view.getLocationInWindow(tempLocation);
            values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
            values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
        }
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null || endValues == null) {
            return null;
        }

        final View view = endValues.view;
        sceneRoot.getLocationInWindow(tempLocation);
        int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
        int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
        int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
        int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
        // TODO: also handle size changes: check bounds and animate size changes
        if (startX != endX || startY != endY) {
            final int width = view.getWidth();
            final int height = view.getHeight();
            Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            view.draw(canvas);
            final BitmapDrawable drawable = new BitmapDrawable(bitmap);
            view.setAlpha(0);
            drawable.setBounds(startX, startY, startX + width, startY + height);
            sceneRoot.getOverlay().add(drawable);
            Path topLeftPath = getPathMotion().getPath(startX, startY, endX, endY);
            PropertyValuesHolder origin = PropertyValuesHolder.ofObject(
                    DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath);
            ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
            anim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    sceneRoot.getOverlay().remove(drawable);
                    view.setAlpha(1);
                }
            });
            return anim;
        }
        return null;
    }

}
Kea answered 23/12, 2016 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.