Search code examples
androidandroid-layoutandroid-coordinatorlayout

Expand/Collapse Toolbar pulling on a view


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


Solution

  • 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