androidandroid-animationandroid-transitionsshared-element-transition

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


I'm trying to set up a "shared element" transition animation among two fragments. However, the destination I want is not a single view, but a FrameLayout with two overlapped elements that share size (an arrow and a rotating map) and must move and shrink at the same time.

My target layout looks like this:

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

I want to treat all this as a single thing.

Before transitions I was doing this animation on container_arrow using scale and translation properties, and it worked fine.

However, when I use a transition the size animation only affects the outer FrameLayout, but not its children. The inner arrow moves, but doesn't start small and grows, it start big and stays big. If I target the arrow instead, it works.

Looking at ChangeBounds transition code it seems it uses setFrame() to directly adjust the bounds of the target element. That doesn't propagate to its children.

I would need the translation+shrink animation to affect two elements, but transition names must be unique. Is there any way to achieve what I want?

EDIT:

I'm already trying to set the FrameLayout as a group by calling:

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

Same thing.. =/


Solution

  • I ended up creating my own Transition subclass which is similar to ChangeBounds but uses translation and scale view properties to move the target instead of adjusting bounds. A delta for translation is calculated and it's animated to 0, and an initial scale is also calculated and animated to 1.

    Here's the code:

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