niqueco
niqueco

Reputation: 2408

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.. =/

Upvotes: 0

Views: 1376

Answers (2)

niqueco
niqueco

Reputation: 2408

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

Upvotes: 1

ianhanniballake
ianhanniballake

Reputation: 200120

This is precisely what the ViewGroupCompat.setTransitionGroup() API (for API 14+ devices when using AndroidX Transition) or android:transitionGroup="true" XML attribute (for API 21+ devices) is for - by setting that flag to true, that entire ViewGroup is used as a single item when it comes to shared element transitions.

Note that you must also set a transition name on the same element you set as a transition group (using ViewCompat.setTransitionName() / android:transitionName depending on whether you want to support back to API 14 or only API 21+).

Upvotes: 1

Related Questions