Как программно отменить бросок на RecyclerView при достижении его конца - PullRequest
0 голосов
/ 05 июня 2019

Я использую ViewPager внутри CoordinatorLayout с RecyclerViews на каждой из его страниц (в качестве демонстрации опубликован небольшой пример проекта на GitHub ).Я заметил, что пролистывание влево / вправо в ViewPager игнорируется в течение некоторого времени после перехода к концу RecyclerView.Сужая проблему, я пришел к выводу (на самом деле, скорее из предположения), что бросание все еще продолжается в течение еще некоторого времени после достижения конца - довольно короткого - RecyclerView, и пролистывание ViewPager возможно только после этогоfling остановился.

Ниже приведен демонстрационный пример проблемы: только прокрутка позволяет ViewPager смахивать сразу, тогда как fling требует 2 попытки (или просто некоторое время).

img

Есть ли чистый способ остановить бросок при достижении любого конца RecyclerView?Мой обходной путь - отправить MotionEvent по достижении конца, но это выглядит очень взломанным.

1 Ответ

0 голосов
/ 05 июня 2019

Мне удалось обойти эту проблему, подклассифицировав 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
        }
    }
}
...