【问题标题】:Android MotionLayout OnSwipe not working when touch region contains a RecyclerView当触摸区域包含 RecyclerView 时,Android MotionLayout OnSwipe 不起作用
【发布时间】:2020-09-27 12:42:30
【问题描述】:

我正在尝试实现这个播放器动画

我还希望能够在折叠和展开时同时滑动歌曲。所以这个想法是使用MotionLayout 和RecyclerView,并且还让RecyclerView 的每个项目都成为MotionLayout。这样我就可以在RecyclerView 上应用展开动画,并在它的孩子上应用过渡。

过渡本身可以正常工作,如附件视频所示。但是让RecyclerView 本身的阻力不起作用。 仅当触摸从RecyclerView 外部开始时才会检测到拖动,如视频中突出显示的触摸所示,其中触摸从RecyclerView 下方开始。

如果触摸从RecyclerView 开始,则滚动歌曲会消耗该事件。即使在附加的LinearLayoutManager 中禁用滚动也不起作用。我还尝试为RecyclerView 覆盖onTouch 以始终返回false 并且不消耗任何触摸事件(理论上),但这也没有用。

该项目可以在这里找到https://github.com/vlatkozelka/PlayerAnimation2 它并不意味着是一个生产就绪的应用程序,只是一个测试游乐场。

这里是相关代码

布局:

<?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)
      }

    }


  }

}

知道如何让RecyclerView 与MotionLayout 拖动手势配合得很好吗?

【问题讨论】:

    标签: android android-recyclerview android-animation android-motionlayout


    【解决方案1】:

    我遇到了同样的问题,我被困了超过 5 天,最后,我找到了一个可能适合您的简单解决方案。 问题在于,当用户触摸屏幕并且没有将其转发到运动布局以应用滑动动画时,回收器视图会获得焦点。

    所以,我只是在回收器视图上添加了一个触摸侦听器,并将其转发给运动布局类的 on touch 方法。检查代码

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

    只需从 onTouchListener 中获取运动事件,并将其转发给运动布局中的 onTouchEvent 方法

    希望对你有所帮助;)

    【讨论】:

      【解决方案2】:

      为潜在的修复创建了一个拉取请求here

      需要进行两个主要更改

      1. 在运动屏幕文件中从使用 touchRegionId 更改为同时使用 touchAnchorId 和 touchAnchorSide
        <OnSwipe
           app:dragDirection="dragUp"
           app:touchAnchorId="@id/layout_player"
           app:touchAnchorSide="top"/>
      
      1. 将RecyclerView 的onTouch 委托给MotionLayout
      songsRecycler.setOnTouchListener { _, motionEvent ->
              if (mainLayout.onTouchEvent(motionEvent).not()) {
                  songsRecycler.onTouchEvent(motionEvent)
              } else {
                  songsRecycler.onTouchEvent(motionEvent)
              }
          }
      

      【讨论】:

        【解决方案3】:

        方法应该正好相反。在这种情况下,您应该拦截父视图中的触摸事件,即 MotionLayout。每当您向上或向下滑动时,您都应该显式返回 true,然后此触摸不会传递到子视图,即 RecyclerView。如果您返回 false,它将照常传递给 recyclerview。有关更多信息,请查看此resource。对不起,我连续工作了 20 个小时,所以我需要小睡一下。如果您需要代码,我可以明天发布。

        编码愉快!

        【讨论】:

        • 是的,如果您可以随时提供代码,请提供。因为我确实尝试在父动作布局上的任何触摸事件上返回 true,但回收器视图仍然消耗滚动。
        猜你喜欢
        • 2020-06-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-11-20
        • 1970-01-01
        • 2019-09-07
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多