Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android MotionLayout OnSwipe not working when touch region contains a RecyclerView

I'm trying to implement this player animation

enter image description here

I also want to be able to both swipe on songs while collapsed and while expanded. So the idea was to use a MotionLayout with a RecyclerView, and also have each item of the RecyclerView be a MotionLayout. This way I could apply an expand animation on the RecyclerView and also apply transitions on it's children.

The transition itself works fine as seen in the attached video. But getting the drag to work on the RecyclerView itself doesn't. The drag is detected only if the touch starts from outside of the RecyclerView as shown in the highlighted touch in the video, where the touch starts from below the RecyclerView.

If the touch starts on the RecyclerView, the scrolling of songs consumes the event. Even disabling the scroll in the attached LinearLayoutManager doesn't work. I also tried overriding onTouch for the RecyclerView to always return false and not consume any touch events (in theory) but that also didn't work.

The project can be found here https://github.com/vlatkozelka/PlayerAnimation2 It's not meant to be a production ready application, just a testing playground.

Here is the relevant code

Layout:

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    app:layoutDescription="@xml/player_scene"
    tools:context=".MainActivity"
    android:id="@+id/layout_main"
    >

    <FrameLayout
        android:id="@+id/layout_player"
        android:layout_width="match_parent"
        android:layout_height="@dimen/mini_player_height"
        android:elevation="2dp"
        app:layout_constraintBottom_toTopOf="@id/layout_navigation"
        app:layout_constraintStart_toStartOf="parent"
        android:background="@color/dark_grey"
        android:focusable="true"
        android:clickable="true"
        >

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_songs"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:focusable="false"
            android:clickable="false"
            />
    </FrameLayout>


    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/layout_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/dark_grey"
        android:padding="5dp"
        android:weightSum="3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <ImageView
            android:id="@+id/iv_home"
            android:layout_width="0dp"
            android:layout_height="34dp"
            android:layout_weight="1"
            android:tint="#fff"
            app:layout_constraintEnd_toStartOf="@id/iv_search"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_home_24px" />

        <ImageView
            android:id="@+id/iv_search"
            android:layout_width="0dp"
            android:layout_height="34dp"
            android:layout_weight="1"
            android:tint="#fff"
            app:layout_constraintEnd_toStartOf="@id/iv_library"
            app:layout_constraintStart_toEndOf="@id/iv_home"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_search_24px" />

        <ImageView
            android:id="@+id/iv_library"
            android:layout_width="0dp"
            android:layout_height="34dp"
            android:layout_weight="1"
            android:tint="#fff"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/iv_search"
            app:layout_constraintTop_toTopOf="parent"
            app:srcCompat="@drawable/ic_library_music_24px" />


    </androidx.constraintlayout.widget.ConstraintLayout>



</androidx.constraintlayout.motion.widget.MotionLayout>

MotionScene:

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

    <Transition
        android:id="@+id/dragUp"
        app:constraintSetEnd="@id/expanded"
        app:constraintSetStart="@id/collapsed">

        <OnSwipe
            app:dragDirection="dragUp"
            app:touchRegionId="@id/layout_player" />

        <OnClick
            app:clickAction="transitionToEnd"
            app:targetId="@id/layout_player" />

    </Transition>

    <Transition
        android:id="@+id/dragDown"
        app:constraintSetEnd="@id/collapsed"
        app:constraintSetStart="@id/expanded">


        <OnSwipe
            app:dragDirection="dragDown"
            app:touchRegionId="@id/layout_player" />

        <OnClick
            app:clickAction="transitionToEnd"
            app:targetId="@id/layout_player" />

    </Transition>

    <ConstraintSet android:id="@+id/collapsed">


        <Constraint
            android:id="@+id/layout_navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/dark_grey"
            android:orientation="horizontal"
            android:padding="5dp"
            android:weightSum="3"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

        <Constraint
            android:id="@+id/layout_player"
            android:layout_width="match_parent"
            android:layout_height="@dimen/mini_player_height"
            android:elevation="2dp"
            app:layout_constraintBottom_toTopOf="@id/layout_navigation"
            app:layout_constraintStart_toStartOf="parent" />

    </ConstraintSet>

    <ConstraintSet android:id="@+id/expanded">


        <Constraint
            android:id="@+id/layout_navigation"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:background="@color/dark_grey"
            android:orientation="horizontal"
            android:padding="5dp"
            android:weightSum="3"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="parent" />

        <Constraint
            android:id="@+id/layout_player"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:elevation="2dp"
            app:layout_constraintBottom_toTopOf="@id/layout_navigation"
            app:layout_constraintStart_toStartOf="parent" />


    </ConstraintSet>

</MotionScene>

MainActivity:

package com.example.playeranimation2

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import io.reactivex.subjects.PublishSubject
import org.notests.sharedsequence.Driver


data class AppState(
    val songs: List<Song> = Song.getRandomSongs(),
    val currentSong: Int = 0,
    val expandedPercent: Float = 0f
)

class MainActivity : AppCompatActivity() {

  companion object {
    var appState = AppState()
    val appStateObservable = PublishSubject.create<AppState>()
    val appStateDriver = Driver(appStateObservable.startWith(appState))
  }

  lateinit var mainLayout: MotionLayout
  lateinit var songsRecycler: RecyclerView
  lateinit var playerLayout : ViewGroup
  lateinit var adapter: SongsAdapter
  lateinit var snapHelper: PagerSnapHelper

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    mainLayout = findViewById(R.id.layout_main)
    songsRecycler = findViewById(R.id.recycler_songs)
    playerLayout = findViewById(R.id.layout_player)

    songsRecycler.layoutManager = LinearLayoutManager(this).apply { orientation = LinearLayoutManager.HORIZONTAL }

    adapter = SongsAdapter()

    songsRecycler.adapter = adapter

    adapter.refreshData(appState.songs)

    snapHelper = PagerSnapHelper()
    snapHelper.attachToRecyclerView(songsRecycler)




    mainLayout.setTransitionListener(object : MotionLayout.TransitionListener {
      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) {
        if (p1 == R.id.expanded) {
          appState = appState.copy(expandedPercent = 1f - p3)
        } else {
          appState = appState.copy(expandedPercent = p3)
        }

        emitNewAppState()
        adapter.expandedPercent = appState.expandedPercent

        updateAllRecyclerChildren()
      }

      override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {

      }

    })

    songsRecycler.addOnScrollListener(object:  RecyclerView.OnScrollListener(){
      override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        updateAllRecyclerChildren()
      }
    })
  }

  fun updateAllRecyclerChildren(){
    for (i in appState.songs.indices) {
      val childView = songsRecycler.getChildAt(i)
      if(childView != null){
        val songViewHolder = songsRecycler.getChildViewHolder(childView) as? SongsAdapter.SongViewHolder
        songViewHolder?.setExpandPercent(appState.expandedPercent)
      }
    }
  }

  fun emitNewAppState() {
    appStateObservable.onNext(appState)
  }

  class SongsAdapter : RecyclerView.Adapter<SongsAdapter.SongViewHolder>() {

    val data = arrayListOf<Song>()
    var expandedPercent : Float = 0f

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SongViewHolder {
      val view = LayoutInflater.from(parent.context).inflate(R.layout.item_song, parent, false)
      return SongViewHolder(view)
    }

    override fun getItemCount(): Int {
      return data.size
    }

    override fun onBindViewHolder(holder: SongViewHolder, position: Int) {
      holder.bind(data[position], expandedPercent)
    }

    fun refreshData(data: List<Song>) {
      this.data.clear()
      this.data.addAll(data)
    }


    class SongViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
      var songImageView: ImageView? = itemView.findViewById(R.id.iv_cover_art)
      var songTitleView: TextView? = itemView.findViewById(R.id.tv_song_title)
      var rootView: MotionLayout? = itemView.findViewById(R.id.root_view)

      fun bind(song: Song, expandedPercent: Float) {
        songImageView?.setImageResource(song.imageRes)
        songTitleView?.text = song.title
        setExpandPercent(expandedPercent)
      }

      fun setExpandPercent(percent: Float) {
        rootView?.setInterpolatedProgress(percent)
      }

    }


  }

}

Any idea how I can get the RecyclerView to play nice with MotionLayout drag gesture?

like image 318
vlatkozelka Avatar asked Sep 27 '20 12:09

vlatkozelka


1 Answers

I faced the same problem and I was stuck for more than 5 days and finally, I found a simple solution that may fit for you. the problem is that the recycler view gets focused when the user touches the screen and did not forward it to the motion layout to apply the swipe animation.

So, simply I added a touch listener on the recycler view and forward it to the on touch method on the motion layout class. check the code

recyclerView.setOnTouchListener { _, event ->
            binding.motionLayout.onTouchEvent(event)
            return@setOnTouchListener false
        }

simply take the motion event from onTouchListener and forward it to the onTouchEvent method in motion layout

Hope that helped you ;)

like image 114
Mina Samir Avatar answered Jan 03 '23 23:01

Mina Samir