[英]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
.这样的想法是使用一个
MotionLayout
与RecyclerView
,也有每个项目RecyclerView
是MotionLayout
。 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?知道如何让
RecyclerView
与MotionLayout
拖动手势配合使用吗?
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需要进行两个主要更改
touchRegionId
to use both touchAnchorId
and touchAnchorSide
in the motion screen filetouchRegionId
更改为在运动屏幕文件中同时使用touchAnchorId
和touchAnchorSide
<OnSwipe
app:dragDirection="dragUp"
app:touchAnchorId="@id/layout_player"
app:touchAnchorSide="top"/>
RecyclerView
's onTouch
to the MotionLayout
RecyclerView
的onTouch
给MotionLayout
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.