简体   繁体   English

当触摸区域包含 RecyclerView 时,Android MotionLayout OnSwipe 不起作用

[英]Android MotionLayout OnSwipe not working when touch region contains a RecyclerView

I'm trying to implement this player animation我正在尝试实现这个播放器动画

在此处输入图片说明

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 .这样的想法是使用一个MotionLayoutRecyclerView ,也有每个项目RecyclerViewMotionLayout This way I could apply an expand animation on the RecyclerView and also apply transitions on it's children.这样我就可以在RecyclerView上应用展开动画,并在它的子项上应用过渡。

The transition itself works fine as seen in the attached video.如附加视频所示,过渡本身运行良好。 But getting the drag to work on the RecyclerView itself doesn't.但是让阻力在RecyclerView本身上工作并没有。 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 .仅当触摸从RecyclerView外部开始时才会检测到拖动,如视频中突出显示的触摸所示,其中触摸从RecyclerView下方开始。

If the touch starts on the RecyclerView , the scrolling of songs consumes the event.如果在RecyclerView上开始触摸,则歌曲的滚动会消耗该事件。 Even disabling the scroll in the attached LinearLayoutManager doesn't work.即使在附加的LinearLayoutManager中禁用滚动也不起作用。 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.我还尝试覆盖RecyclerView onTouch以始终返回false并且不消耗任何触摸事件(理论上),但这也不起作用。

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.该项目可以在这里找到https://github.com/vlatkozelka/PlayerAnimation2它并不意味着成为一个生产就绪的应用程序,只是一个测试场。

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?知道如何让RecyclerViewMotionLayout拖动手势配合使用吗?

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.我遇到了同样的问题,我被困了超过 5 天,最后,我找到了一个可能适合您的简单解决方案。 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只需从 onTouchListener 获取动作事件并将其转发到动作布局中的 onTouchEvent 方法

Hope that helped you ;)希望对你有帮助;)

Created a pull request for a potential fix here在此处创建了一个潜在修复的拉取请求

Two main changes needed需要进行两个主要更改

  1. Change from using touchRegionId to use both touchAnchorId and touchAnchorSide in the motion screen file从使用touchRegionId更改为在运动屏幕文件中同时使用touchAnchorIdtouchAnchorSide
  <OnSwipe
     app:dragDirection="dragUp"
     app:touchAnchorId="@id/layout_player"
     app:touchAnchorSide="top"/>
  1. Delegate the RecyclerView 's onTouch to the MotionLayoutRecyclerViewonTouchMotionLayout
songsRecycler.setOnTouchListener { _, motionEvent ->
        if (mainLayout.onTouchEvent(motionEvent).not()) {
            songsRecycler.onTouchEvent(motionEvent)
        } else {
            songsRecycler.onTouchEvent(motionEvent)
        }
    }

The approach should be just the opposite.方法应该正好相反。 You should intercept the touch event in the parent view ie MotionLayout in this case.在这种情况下,您应该拦截父视图中的触摸事件,即 MotionLayout。 You should explicitly return true whenever your swipe is in the up or down direction, then this touch will not pass onto the child view ie RecyclerView.每当您向上或向下滑动时,您都应该明确返回 true,然后这种触摸不会传递到子视图,即 RecyclerView。 And if you return false it will be passed on to the recyclerview as usual.如果您返回 false,它将像往常一样传递给 recyclerview。 For more info check this resource .有关更多信息,请查看此资源 Sorry I was working 20 hours straight so I am in need of a nap.抱歉,我连续工作了 20 个小时,所以我需要小睡一下。 If you require the code, I can post it tomorrow.如果您需要代码,我可以明天发布。

Happy Coding!快乐编码!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 MotionLayout - OnSwipe 不适用于 ViewPager2/RecyclerView - MotionLayout - OnSwipe not working with ViewPager2/RecyclerView 在 RecyclerView 的项目上使用 MotionLayout OnSwipe - Using MotionLayout OnSwipe on Item of a RecyclerView MotionLayout - OnSwipe 不适用于可点击的孩子 - MotionLayout - OnSwipe not working on clickable children OnSwipe 方法在 RecyclerView 中不起作用 - OnSwipe method not working in RecyclerView Android Kotlin onClickListener 似乎隐藏了 MotionLayout 的 OnSwipe - Android Kotlin onClickListener seems to hide OnSwipe of MotionLayout 我们可以同时使用OnSwipe和OnClick吗 <Transition> 适用于Android MotionLayout? - Can we use OnSwipe and OnClick in the same <Transition> for Android MotionLayout? MotionLayout OnSwipe spring 物理在 transitionToStart/End() 后停止工作 - MotionLayout OnSwipe spring physics stop working after transitionToStart/End() MotionLayout onSwipe 自动完成速度 - MotionLayout onSwipe autocomplete speed 如何设置多个OnSwipe Add Android MotionLayout Single Transition? - How to set multiple OnSwipe Add Android MotionLayout Single Transition? Android 使用 MotionLayout 折叠工具栏 - 当 RecyclerView 为空/不可滚动时禁用运动 - Android collapsing toolbar with MotionLayout - disable motion when RecyclerView is empty/not scrollable
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM