Expand/Collapse Toolbar pulling on a view

Viewed 813

I'm trying to create the following behavior using a CollapsingToolbarLayout and other view in bottom of the AppBarLayout but the bar isn't collapsing/expanding when I scroll/pull on the PullView, that means isn't possible to open the bar when collapsed or close using the view.

I've already tried to use NestedScrollView in the PullView root but without success

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:app="http://schemas.android.com/apk/res-auto"
             android:layout_width="match_parent"
             android:layout_height="match_parent">

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

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appBarLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:expanded="true">

            <com.google.android.material.appbar.CollapsingToolbarLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

                <FrameLayout
                    android:id="@+id/frmMenu"
                    android:layout_width="match_parent"
                    android:layout_height="300dp"
                    android:clickable="true"
                    android:focusable="true"/>

            </com.google.android.material.appbar.CollapsingToolbarLayout>

        </com.google.android.material.appbar.AppBarLayout>

        <com.xpto.customview.PullView
            android:id="@+id/pullDownView"
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            app:layout_anchor="@+id/appBarLayout"
            app:layout_anchorGravity="bottom|center_horizontal"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"/>


    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</FrameLayout>

enter image description here enter image description here

1 Answers

I found a cool solution using the MotionLayout.

First of all, I'm extending the MotionLayout to change a bit the behavior, instead of dragging in the entire MotionLayout I only want to be possible to drag on the indicator and the menu.

TLDR: Example on Github

So my MotionLayout overrides the OnTouchEvent and checks if we're touching in one of the direct children.

class TouchableMotionLayout @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0)
    : MotionLayout(context, attrs, defStyleAttr), MotionLayout.TransitionListener {

    private val viewRect = Rect()
    private var touchStarted = false

    init {
        initListener()
    }

    private fun initListener() {
        setTransitionListener(this)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.actionMasked) {
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                touchStarted = false
                return super.onTouchEvent(event)
            }
        }

        if (!touchStarted) {
            touchStarted = verifyIfChildHasTouched(event)
        }

        return touchStarted && super.onTouchEvent(event)
    }

    /**
     * Verify if touching one fo the children.
     *
     * @param event The motion event.
     * @return True if touch its in one of the children.
     */
    private fun verifyIfChildHasTouched(event: MotionEvent): Boolean {
        for (index in 0 until childCount) {
            val view = getChildAt(index)
            view.getHitRect(viewRect)
            if (viewRect.contains(event.x.toInt(), event.y.toInt())) {
                return true
            }
        }

        return false
    }

    override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
        touchStarted = false
    }

    override fun allowsTransition(p0: MotionScene.Transition?) = true

    override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {}

    override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {}

    override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {}
}

In this layout I have a viewpager and then a custom MotionLayout, it's only possible to open/close the menu swipping in the direct children of the MotionLayout i.e. menuIndicatorand menu.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/baseViewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.extmkv.example.TouchableMotionLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" 
        app:layoutDescription="@xml/menu_scene">

        <FrameLayout
            android:id="@+id/menuIndicator"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:paddingBottom="8dp"
            android:paddingEnd="16dp"
            android:paddingStart="16dp"
            android:paddingTop="48dp"
            android:translationZ="1dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/menu">

            <FrameLayout
                android:layout_width="@dimen/menu_indicator_outside_width"
                android:layout_height="@dimen/menu_indicator_outside_height"
                android:layout_gravity="center"
                android:background="#F00"
                tools:ignore="UselessParent" />

        </FrameLayout>

        <LinearLayout
            android:id="@+id/menu"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center|top"
            android:background="#FFF"
            android:orientation="vertical"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent">

            <LinearLayout
                android:id="@+id/lnrMenuOptionsContainer"
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:orientation="horizontal" />

        </LinearLayout>

    </com.extmkv.example.TouchableMotionLayout>

</FrameLayout>

And now the scene: @xml/menu_scene.xml You can adjust the drag and changing the attrsin the end.

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/menu">
            <Layout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                motion:layout_constraintBottom_toTopOf="parent"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent" />
        </Constraint>

        <Constraint android:id="@id/menuIndicator">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toBottomOf="@id/menu" />
        </Constraint>

    </ConstraintSet>

    <ConstraintSet
        android:id="@+id/end"
        motion:deriveConstraintsFrom="@id/start">
        <Constraint android:id="@id/menu">
            <Layout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />
        </Constraint>

        <Constraint android:id="@id/menuIndicator">
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                motion:layout_constraintBottom_toBottomOf="@id/menu"
                motion:layout_constraintEnd_toEndOf="parent"
                motion:layout_constraintStart_toStartOf="parent" />
        </Constraint>

    </ConstraintSet>

    <!-- All the animations values are hardcoded for now. -->
    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="400">

        <OnSwipe
            motion:dragDirection="dragDown"
            motion:dragScale="0.5"
            motion:maxAcceleration="10"
            motion:maxVelocity="10.0" />

    </Transition>

</MotionScene>

And now the final result:

Menu Example

Related