Search code examples
androidxmlkotlintouch-eventandroid-motionlayout

Motion Layout: Handling click on swipeable view


I am trying to set a touch listener to a cardView (which is also swipeable because of the motion layout) the problem is that if i return true in the OnTouchListener when the event.action == ACTION_DOWN (so i can receive the callback when the user lifts his finger from the cardView) the swiping animation on this cardView does not work. I need to execute my logic when the user lifts his finger because if i do my logic on the ACTION_DOWN, then in the case when the user swipes the cardView my logic is also executed (i want the logic to be executed only when the user taps on the cardView).
Here is the xml layout:

<androidx.constraintlayout.motion.widget.MotionLayout 
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"
app:motionDebug="SHOW_ALL"
android:id="@+id/matchingMotionLayout"
android:layout_width="match_parent"
app:layoutDescription="@xml/matching_scene"
android:layout_height="match_parent">

<com.google.android.material.card.MaterialCardView
    android:id="@+id/cardTwo"
    android:layout_width="320dp"
    android:layout_height="480dp"
    app:cardBackgroundColor="@color/design_default_color_secondary"
    app:cardCornerRadius="24dp"
    app:layout_constraintBottom_toTopOf="@id/dislikeButton"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/matchingWithTitle"
    app:layout_constraintVertical_bias="0.4">

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

        <ImageView
            android:id="@+id/bottomImage"
            android:layout_height="0dp"
            android:layout_width="match_parent"
            android:scaleType="centerCrop"
            app:layout_constraintBottom_toTopOf="@id/bottomInterestPointName"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/bottomInterestPointName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:elevation="1dp"
            android:gravity="center_horizontal"
            android:paddingVertical="24dp"
            android:textColor="@android:color/white"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Restaurant Name" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</com.google.android.material.card.MaterialCardView>

<com.google.android.material.card.MaterialCardView
    android:id="@+id/cardOne"
    android:layout_width="320dp"
    android:layout_height="480dp"
    app:layout_constraintBottom_toTopOf="@id/dislikeButton"
    app:cardBackgroundColor="@color/md_theme_light_primary"
    app:cardCornerRadius="24dp"
    app:layout_constraintTop_toBottomOf="@id/matchingWithTitle"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintVertical_bias="0.4">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/cardOneLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/topImage"
            android:layout_height="0dp"
            android:scaleType="centerCrop"
            android:layout_width="match_parent"
            app:layout_constraintBottom_toTopOf="@id/topInterestPointName"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/topInterestPointName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:elevation="1dp"
            android:gravity="center_horizontal"
            android:paddingVertical="24dp"
            android:textColor="@android:color/white"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Restaurant Name" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</com.jhn.restaurantpicker.ui.core.TestCardView>

...

</com.jhn.restaurantpicker.ui.core.TestMotionLayout>

This is my motion scene transitions:

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

<Transition
    android:id="@+id/startToLeft3"
    app:constraintSetEnd="@+id/unlike"
    app:constraintSetStart="@+id/start">

    <OnSwipe
        app:dragDirection="dragLeft"
        app:touchAnchorId="@id/cardOne"
        app:touchAnchorSide="left"
        app:touchRegionId="@id/cardOne" />

    <KeyFrameSet>

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/cardOne"
            app:percentY="0.3" />

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/topInterestPointName"
            app:percentY="0.3" />

    </KeyFrameSet>

</Transition>

<Transition
    android:id="@+id/startToRight3"
    app:constraintSetEnd="@+id/like"
    app:constraintSetStart="@+id/start">

    <OnSwipe
        app:dragDirection="dragRight"
        app:touchAnchorId="@id/cardOne"
        app:touchAnchorSide="right"
        app:touchRegionId="@id/cardOne" />

    <KeyFrameSet>

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/cardOne"
            app:percentY="-0.3" />

        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="pathRelative"
            app:motionTarget="@id/topInterestPointName"
            app:percentY="-0.3" />

    </KeyFrameSet>

</Transition>
...
This is the transition listener in the fragment:
matchingMotionLayout.setTransitionListener(object : TransitionAdapter() {
            override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {
                when (currentId) {
                    R.id.offScreenUnlike -> {
                        if (vm.shouldResetScene) {
                            motionLayout.progress = 0f
                            motionLayout.setTransition(R.id.start, R.id.start)
                        }
                        vm.unlike()
                    }

                    R.id.offScreenLike -> {
                        if (vm.shouldResetScene) {
                            motionLayout.progress = 0f
                            motionLayout.setTransition(R.id.start, R.id.start)
                        }
                        vm.like()
                    }

                    R.id.start -> {
                        motionLayout.progress = 0f
                        motionLayout.setTransition(R.id.start, R.id.start)
                    }
                }
                super.onTransitionCompleted(motionLayout, currentId)
            }
        })

Solution

  • Solution which worked for me: So after trying different methods for 1 week straight i finally managed to find something which worked.

    1. I created a custom MotionLayout class where i override the onInterceptTouchEvent(). In this method i return true only when the action is equal to ACTION_MOVE.
    class ClickableMotionLayout @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0
    ) : MotionLayout(context, attrs, defStyleAttr) {
    
        override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
            return event?.action == MotionEvent.ACTION_MOVE
        }
    }
    
    1. In the fragment i set a touchListener on the view which we (swipe/click):
        cardOne.setOnTouchListener(imageSeekListener)
    

    In the touchListener i call the onTouchEvent of the motionLayout by passing the event received by the touchListener and when the action is ACTION_DOWN i return true.

        private var imageSeekListener: View.OnTouchListener? = View.OnTouchListener { v, event ->
        binding.matchingMotionLayout.onTouchEvent(event)
        if (event.action == MotionEvent.ACTION_DOWN) {
            return@OnTouchListener true
        }
        if (event.action == MotionEvent.ACTION_UP) {
            when (getTouchSide(v, event)) {
                TouchSide.LEFT -> vm.peekPreviousPhoto()
                TouchSide.RIGHT -> vm.peekNextPhoto()
            }
        }
        return@OnTouchListener false
        }
    

    Now i can click on the card to peek the photos, and swipe on it to display the other card without triggering the click. Hope it helps someone.