如何使用 ViewGroup 作为过渡动画的共享元素?

How to use a ViewGroup as a shared element for a transition animation?

我正在尝试设置两个片段之间的“共享元素” 过渡动画。然而,我想要的目的地不是单一的视图,而是一个 FrameLayout 有两个共享大小的重叠元素(一个箭头和一个旋转地图)并且必须同时移动和收缩。

我的目标布局如下所示:

    <FrameLayout
        android:id="@+id/container_arrow"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/map_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />

        <ar.com.lichtmaier.antenas.ArrowView
            android:id="@+id/arrow"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />

    </FrameLayout>

我想把这一切当成一件事情。

在过渡之前,我在 container_arrow 上使用缩放和平移属性制作此动画,效果很好。

但是,当我使用过渡时,尺寸动画只影响外部 FrameLayout,而不影响它的 children。内部箭头移动,但不是从小开始变大,而是从大开始并保持大。如果我改为瞄准箭头,它会起作用。

查看 ChangeBounds 转换代码,它似乎使用 setFrame() 直接调整目标元素的边界。这不会传播到它的 children.

我需要平移+收缩动画来影响两个元素,但转换名称必须是唯一的。有什么办法可以实现我想要的吗?

编辑:

我已经尝试通过调用将 FrameLayout 设置为一个组:

    ViewCompat.setTransitionName(arrowContainer, "animatedArrow")
    ViewGroupCompat.setTransitionGroup(arrowContainer, true) // <-- this

同样的事情.. =/

这正是 ViewGroupCompat.setTransitionGroup() API (for API 14+ devices when using AndroidX Transition) or android:transitionGroup="true" XML 属性(对于 API 21+ 设备)的用途 - 通过将该标志设置为 true,整个 ViewGroup 用作单个关于共享元素转换的项目。

请注意,您还必须在设置为过渡组的同一元素上设置过渡名称(使用 ViewCompat.setTransitionName() / android:transitionName 取决于您是否要支持回 API 14 岁或只有 API 21+).

我最终创建了自己的 Transition 子类,它类似于 ChangeBounds 但使用平移和缩放视图属性来移动目标而不是调整边界。计算平移增量并将其动画化为 0,还计算初始比例并将其动画化为 1。

代码如下:

class MoveWithScaleAndTranslation : Transition() {

    override fun captureStartValues(transitionValues: TransitionValues) {
        captureValues(transitionValues)
    }

    override fun captureEndValues(transitionValues: TransitionValues) {
        captureValues(transitionValues)
    }

    override fun getTransitionProperties() = properties

    private fun captureValues(transitionValues: TransitionValues) {
        val view = transitionValues.view
        val values = transitionValues.values

        val screenLocation = IntArray(2)
        view.getLocationOnScreen(screenLocation)
        values[PROPNAME_POSX] = screenLocation[0]
        values[PROPNAME_POSY] = screenLocation[1]

        values[PROPNAME_WIDTH] = view.width
        values[PROPNAME_HEIGHT] = view.height
    }

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

        val leftDelta = ((startValues.values[PROPNAME_POSX] as Int) - (endValues.values[PROPNAME_POSX] as Int)).toFloat()
        val topDelta = ((startValues.values[PROPNAME_POSY] as Int) - (endValues.values[PROPNAME_POSY] as Int)).toFloat()

        val scaleWidth = (startValues.values[PROPNAME_WIDTH] as Int).toFloat() / (endValues.values[PROPNAME_WIDTH] as Int).toFloat()
        val scaleHeight = (startValues.values[PROPNAME_HEIGHT] as Int).toFloat() / (endValues.values[PROPNAME_HEIGHT] as Int).toFloat()

        val view = endValues.view
        val anim = ObjectAnimator.ofPropertyValuesHolder(view,
                PropertyValuesHolder.ofFloat("scaleX", scaleWidth, 1f),
                PropertyValuesHolder.ofFloat("scaleY", scaleHeight, 1f),
                PropertyValuesHolder.ofFloat("translationX", leftDelta, 0f),
                PropertyValuesHolder.ofFloat("translationY", topDelta, 0f)
        )
        anim.doOnStart {
            view.pivotX = 0f
            view.pivotY = 0f
        }
        return anim
    }

    companion object {
        private const val PROPNAME_POSX = "movewithscaleandtranslation:posX"
        private const val PROPNAME_POSY = "movewithscaleandtranslation:posY"
        private const val PROPNAME_WIDTH = "movewithscaleandtranslation:width"
        private const val PROPNAME_HEIGHT = "movewithscaleandtranslation:height"
        val properties = arrayOf(PROPNAME_POSX, PROPNAME_POSY, PROPNAME_WIDTH, PROPNAME_HEIGHT)
    }
}