场景转换中的嵌套共享元素

10

我正在尝试使用Transitions API来在两个ViewGroup之间动画共享元素。目标是使绿色视图朝着新位置“超出其父级边界”移动。

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

我有以下布局:

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>

然而,我无法使其正常工作。默认的转换只会淡入淡出所有内容,ChangeBounds 转换根本没有任何效果,ChangeTransform 也看起来不对劲。

我使用的代码:

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);
  }

现在我可以通过使用ViewOverlay自己创建动画来手动完成此操作。但是,我正在寻找使用Transitions API的解决方案。这个可能吗?
另外,我不想“铺平”层次结构。我有意嵌套视图以处理更复杂的用例。
3个回答

4

是的,使用Android转换API可以进行重新绑定。

在重新绑定视图时需要考虑一个主要限制:

通常,每个子视图只允许在它的父容器范围内绘制。这将导致视图在到达其初始父容器的边界时消失,并在短时间后重新出现在新的父容器中。

通过在所有相关父容器上设置android:clipChildren="false",可以关闭相关场景下整个视图层次结构中的子视图剪辑。

在更复杂的层次结构中(例如适配器支持的视图),通过设置TransitionListener以动态切换子视图剪辑的方式更为高效。

您还需要使用ChangeTransform来重新绑定视图,可以使用它替代ChangeBounds或将其添加到TransitionSet中。

在此级别的过渡中,没有必要使用过渡名称或目标,因为框架会自己找出。如果事情变得更加复杂,则需要使用匹配的资源 ID 或匹配的转换名称对参与过渡的视图进行标识。


啊,谢谢,clipChildren 在这种情况下起到了作用。但为什么它不使用叠加呢?从 ChangeTransform.setReparentWithOverlay 文档中可以看到:设置父级是否应该使用叠加或不使用叠加进行更改。当父级更改不使用叠加时,它会影响子级的变换。默认值为 true - nhaarman
1
在进行转换之前和之后,查看Android设备监视器。转换后层次结构中应该有一个ViewOverlay - Ben Weiss

0

ChangeBounds类有一个已弃用的setReparent(Boolean)方法,该方法引用ChangeTransform“处理不同父级之间的转换”。但是,这个ChangeTransform类根本没有给出期望的效果。

ChangeBounds源代码中,我们可以看到,如果将reparent设置为true(加上其他一些条件),则使用场景根的覆盖层来添加覆盖层。但由于它已被弃用,所以出现了某些问题。

我已经通过扩展ChangeBounds来解决了这个问题,在需要时使用覆盖层(在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)
}

虽然这可能是适合您的解决方案,但我建议您查看我的答案并考虑按照Android转换API的意图进行操作。 - Ben Weiss

0

这是我针对这种情况的解决方法(它只是ChangeBounds中的一部分,放在单独的类中)

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;
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接