Trigger animation shortly before end of previous

16 Views Asked by At

I have an expandable element that I'd like to have the following animation: enter image description here

I am using databinding.

It works fine for expanding, that the button hits the edge and then "pulls open" the drawer below. However, when collapsing again, it feels off, because the button gets animated as if it got pulled up by the drawer and then it sets of to the middle of the main part after that. I think that this is the case, because the distance from the lower part of the parent is smaller when being at the bottom than when being centered, so it feels as if it was unnaturally clipping into the bottom and then coming out of it again. I already set the delay of the transition to 1ms, so that it would factually not be delayed (also tried 0, no difference, but not sure, whether 0 as a delay gets applied).

So the structure is the following: I have the main part and below that I am using ExpandableLayout Both is constrained to the start of the button, so it would not invade it's space. I am using ExpandableLayout and not animateLayoutChanges, because recyclerview can't really deal well with animateLayoutChanges and other elements would overlay the close animation instantly, because their position change was not animated. I know there are fixes for that, but they require accessing the recyclerview and in my abstracted structure with clean division between business logic and UI I do not have access to the recyclerview in the displayed elements. At the right side I have the button with dynamically added constraints. When it should be collapsed, constraints are bottom and top. When it gets expanded, the top constraint gets unset and that change gets animated. Because both elements have the same parent and the transition is animated on the parent, it seems to not be able to play them at the same time. Currently the solution is, that I wait for one animation to finish, before I trigger the next one. But if I want the whole movement of the button to feel as one and as if it was pulling the rest, I think I have to trigger it's animation of the constraint change a little earlier than the full collapse. However, as soon as I start the delayedTransition on the parent, it kills the animation of the expandable and sets it to 0 scale instantly, even if I remove it as target of the transition.

My question now is, how can I set of the second animation while the first one is still playing? I understand that there may be the necessity to add another sublayout for the button and I would be fine with that, I'm just not sure how to tackle this best. My layout file (simplified):

                <androidx.constraintlayout.widget.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                    <include
                        android:id="@+id/item_main"
                        layout="@layout/main_layout"
                        android:layout_height="wrap_content"
                        android:layout_width="0dp"
                        bind:viewModel="@{viewModel}"
                        android:layout_marginEnd="@dimen/margin_end"
                        bind:layout_constraintTop_toTopOf="parent"
                        bind:layout_constraintStart_toStartOf="parent"
                        bind:layout_constraintEnd_toStartOf="@id/primary_action"/>

                    <androidx.appcompat.widget.AppCompatImageButton
                        android:id="@+id/primary_action"
                        style="@style/ButtonStyle"
                        android:layout_marginEnd="@dimen/journal_item_inset_button_margin"
                        android:padding="@dimen/item_journal_call_button_padding"
                        app:layout_constraintBottom_toBottomOf="parent"
                        app:layout_constraintTop_toTopOf="parent"
                        app:layout_constraintEnd_toEndOf="parent"
                        app:expandableBrother="@{expandableLayoutRoot}"
                        app:constraintTopActive="@{!viewModel.detailsVisible}"/>

                    <net.cachapa.expandablelayout.ExpandableLayout
                        android:id="@+id/expandable_layout_root"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        bind:layout_constraintTop_toBottomOf="@id/item_main"
                        bind:layout_constraintStart_toStartOf="parent"
                        bind:layout_constraintEnd_toStartOf="@id/primary_action"
                        android:layout_marginStart="@dimen/journal_item_image_view_type_icon_margin_start"
                        android:layout_marginEnd="@dimen/journal_item_inset_button_margin"
                        app:interpolator="@{viewModel.interpolator}"
                        app:el_duration="300"
                        app:el_expanded="false">

                       //ExpandableContent

                    </net.cachapa.expandablelayout.ExpandableLayout>
                </androidx.constraintlayout.widget.ConstraintLayout>

My binding adapter provides the following code:


@BindingAdapter("constraintTopActive", "expandableBrother", requireAll = false)
fun constraintTopActive(view : View, active : Boolean, expandableBrother : ExpandableLayout) {
    //There's something wrong if the parent is no ViewGroup, because that doesn't
    //exist in Android
    if(view.parent is ViewGroup) {
        if(active) {
            //If we add constraint again, first collapse
            expandableBrother.setOnExpansionUpdateListener(setUpOnExpansionUpdateListener(view))
            expandableBrother.collapse()
        } else {
            expandableBrother.setOnExpansionUpdateListener(null)
            val transition = ChangeBounds()
            transition.duration = 100
            transition.interpolator = LinearInterpolator()
            //We only do expandable animation after us, if we
            //expand (when collapse it happens first)
            transition.addListener (object : TransitionListener {
                override fun onTransitionStart(transition: Transition?) {}

                override fun onTransitionEnd(transition: Transition?) {
                    expandableBrother.expand()
                }

                override fun onTransitionCancel(transition: Transition?) {}

                override fun onTransitionPause(transition: Transition?) {}

                override fun onTransitionResume(transition: Transition?) {}
            })
            TransitionManager.beginDelayedTransition(view.parent as ViewGroup, transition)
            view.updateLayoutParams<ConstraintLayout.LayoutParams> {
                topToTop = ConstraintLayout.LayoutParams.UNSET
            }
        }
    }
}

private fun setUpOnExpansionUpdateListener(view : View) : OnExpansionUpdateListener {
    return OnExpansionUpdateListener { percentage, state ->
        if(state == ExpandableLayout.State.COLLAPSED) {
            //Reached end of expansion, animate boundaries now
            val transition = ChangeBounds()
            transition.duration = 100
            //Reducing start delay below human perception, so that it does not feel as if button stopped
            transition.startDelay = 1
            transition.interpolator = LinearInterpolator()
            TransitionManager.beginDelayedTransition(view.parent as ViewGroup, transition)
            view.updateLayoutParams<ConstraintLayout.LayoutParams> {
                topToTop = ConstraintLayout.LayoutParams.PARENT_ID
            }
        }
    }
}

Used LinearInterpolator, because the size may differ, so I need to calculate the duration manually and it's easiest for a linear interpolation. However, I also tried different combinations of AccelerateInterpolator, DecelerateInterpolator and AccelerateDecelerateInterpolator.

0

There are 0 best solutions below