到达终点时如何以编程方式取消对 RecyclerView 的投掷
How to programmatically cancel fling on a RecyclerView when reaching its end
我在 CoordinatorLayout 中使用 ViewPager,每个页面中都有 RecyclerView(发布了 small sample project on GitHub 作为演示)。我注意到在 ViewPager 中滑动 left/right 在跳到 RecyclerView 的末尾后一段时间会被忽略。缩小问题的范围,我得出结论(实际上更多的是假设),即在到达 - 相当短 - RecyclerView 的末尾后,投掷仍在继续一段时间,并且只有在此之后才能在 ViewPager 上滑动fling 已停止。
以下是该问题的演示 gif:只有滚动才能让 ViewPager 立即滑动,而滑动需要 2 次尝试(或只是一些时间)。
有没有一种干净的方法可以阻止到达 RecyclerView 两端的投掷?我的解决方法是在到达终点时分派一个 MotionEvent,但这感觉非常 hack-ish。
我通过如下子类化 RecyclerView 设法解决了这个问题(当然,仍然对其他建议持开放态度):
/**
* RecyclerView dispatching an ACTION_DOWN MotionEvent when reaching either its beginning
* or end and consuming a fling gesture when that fling is in the (vertical) direction
* the RecyclerView can't scroll anymore anyway.
*
* Background: in following setup
*
* <CoordinatorLayout>
* <NestedScrollView>
* <ViewPager>
* <Fragments containing RecyclerView/>
* </ViewPager>
* </NestedScrollView>
* </CoordinatorLayout>
*
* a vertical fling on the RecyclerView will prevent the viewpager to swipe right/left
* immediately after reaching the end (on scroll down) or beginning (on scroll up) of the RV.
* It seems the RV is intercepting the touch until the fling has worn off. TouchyRecyclerView
* is a workaround for this phenomenon by both
* a) cancelling the fling on reaching either end of the RecyclerView by dispatching a
* MotionEvent ACTION_DOWN and
* b) consuming a detected fling gesture when that fling is in the direction the RV is
* at the respective end.
*/
class TouchyRecyclerView : RecyclerView {
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
private val gestureDetector: GestureDetector by lazy {
GestureDetector(context, VerticalFlingListener(this))
}
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val llm: LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
if (dy > 0) { // we're scrolling down the RecyclerView
val adapter = adapter
val position = llm.findLastCompletelyVisibleItemPosition()
if (adapter != null && position == adapter.itemCount - 1) {
// and we're at the bottom
dispatchActionDownMotionEvent()
}
} else if (dy < 0) { // we're scrolling up the RecyclerView
val position = llm.findFirstCompletelyVisibleItemPosition()
if (position == 0) {
// and we're at the very top
dispatchActionDownMotionEvent()
}
}
}
}
init {
this.addOnScrollListener(scrollListener)
}
private fun dispatchActionDownMotionEvent() {
val los = intArrayOf(0, 0)
this.getLocationOnScreen(los)
val e = MotionEvent.obtain(
0,
0,
ACTION_DOWN,
los[0].toFloat(),
los[1].toFloat(),
0)
dispatchTouchEvent(e)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent?): Boolean {
return if (gestureDetector.onTouchEvent(e)) {
true
} else {
super.onTouchEvent(e)
}
}
/**
* Listener to consume unnecessary vertical flings (i.e. when the RecyclerView is at the respective end).
*/
inner class VerticalFlingListener(private val recyclerView: RecyclerView) :
GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean {
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
val adapter = recyclerView.adapter
val llm = recyclerView.layoutManager as LinearLayoutManager
if (velocityY < 0) { // we're flinging down the RecyclerView
if (adapter != null &&
llm.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) {
// but we're already at the bottom - consume the fling
return true
}
} else if (velocityY > 0) { // we're flinging up the RecyclerView
if (0 == llm.findFirstCompletelyVisibleItemPosition()) {
// but we're already at the top - consume the fling
return true
}
}
return false
}
}
}
我在 CoordinatorLayout 中使用 ViewPager,每个页面中都有 RecyclerView(发布了 small sample project on GitHub 作为演示)。我注意到在 ViewPager 中滑动 left/right 在跳到 RecyclerView 的末尾后一段时间会被忽略。缩小问题的范围,我得出结论(实际上更多的是假设),即在到达 - 相当短 - RecyclerView 的末尾后,投掷仍在继续一段时间,并且只有在此之后才能在 ViewPager 上滑动fling 已停止。
以下是该问题的演示 gif:只有滚动才能让 ViewPager 立即滑动,而滑动需要 2 次尝试(或只是一些时间)。
有没有一种干净的方法可以阻止到达 RecyclerView 两端的投掷?我的解决方法是在到达终点时分派一个 MotionEvent,但这感觉非常 hack-ish。
我通过如下子类化 RecyclerView 设法解决了这个问题(当然,仍然对其他建议持开放态度):
/**
* RecyclerView dispatching an ACTION_DOWN MotionEvent when reaching either its beginning
* or end and consuming a fling gesture when that fling is in the (vertical) direction
* the RecyclerView can't scroll anymore anyway.
*
* Background: in following setup
*
* <CoordinatorLayout>
* <NestedScrollView>
* <ViewPager>
* <Fragments containing RecyclerView/>
* </ViewPager>
* </NestedScrollView>
* </CoordinatorLayout>
*
* a vertical fling on the RecyclerView will prevent the viewpager to swipe right/left
* immediately after reaching the end (on scroll down) or beginning (on scroll up) of the RV.
* It seems the RV is intercepting the touch until the fling has worn off. TouchyRecyclerView
* is a workaround for this phenomenon by both
* a) cancelling the fling on reaching either end of the RecyclerView by dispatching a
* MotionEvent ACTION_DOWN and
* b) consuming a detected fling gesture when that fling is in the direction the RV is
* at the respective end.
*/
class TouchyRecyclerView : RecyclerView {
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
private val gestureDetector: GestureDetector by lazy {
GestureDetector(context, VerticalFlingListener(this))
}
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val llm: LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
if (dy > 0) { // we're scrolling down the RecyclerView
val adapter = adapter
val position = llm.findLastCompletelyVisibleItemPosition()
if (adapter != null && position == adapter.itemCount - 1) {
// and we're at the bottom
dispatchActionDownMotionEvent()
}
} else if (dy < 0) { // we're scrolling up the RecyclerView
val position = llm.findFirstCompletelyVisibleItemPosition()
if (position == 0) {
// and we're at the very top
dispatchActionDownMotionEvent()
}
}
}
}
init {
this.addOnScrollListener(scrollListener)
}
private fun dispatchActionDownMotionEvent() {
val los = intArrayOf(0, 0)
this.getLocationOnScreen(los)
val e = MotionEvent.obtain(
0,
0,
ACTION_DOWN,
los[0].toFloat(),
los[1].toFloat(),
0)
dispatchTouchEvent(e)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent?): Boolean {
return if (gestureDetector.onTouchEvent(e)) {
true
} else {
super.onTouchEvent(e)
}
}
/**
* Listener to consume unnecessary vertical flings (i.e. when the RecyclerView is at the respective end).
*/
inner class VerticalFlingListener(private val recyclerView: RecyclerView) :
GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean {
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
val adapter = recyclerView.adapter
val llm = recyclerView.layoutManager as LinearLayoutManager
if (velocityY < 0) { // we're flinging down the RecyclerView
if (adapter != null &&
llm.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) {
// but we're already at the bottom - consume the fling
return true
}
} else if (velocityY > 0) { // we're flinging up the RecyclerView
if (0 == llm.findFirstCompletelyVisibleItemPosition()) {
// but we're already at the top - consume the fling
return true
}
}
return false
}
}
}