如何在RecyclerView和ViewPager上解决捕捉功能时获取即将被选择的页面

How to get soon-to-be-selected page while settling snapping feature on RecyclerView and ViewPager

背景

ViewPager 在您执行一些滚动后捕捉到视图,RecyclerView 也可以,如果您使用这样的东西:

LinearSnapHelper().attachToRecyclerView(recyclerView)

或者使用库捕捉到某个边缘,如this library. It can mimic ViewPager almost entirely if you wish, as shown on this library, mentioned by CommonsWare here所示。

如果需要,它们都可以以相同的方式运行(捕捉到单个视图,可选择获取全部可用 space),所以这个问题是关于它们的。

ViewPager 有一些回收视图的问题,但这些问题可以解决,例如使用 this library.

问题

我需要更新新显示视图的 UI,一旦 RecyclerView/ViewPager 即将在滚动中空闲。

当然,由于用户仍然可以触摸它,因此知道将要发生什么是一个问题,所以我需要在设置滚动状态时也阻止触摸事件。

这意味着,例如,当用户从第 0 页翻到第 1 页时,一旦捕捉开始,触摸事件就会被阻止,我将能够知道它捕捉到第 1 页并更新此页面。

这里的问题是 RecyclerView 和 ViewPager 都不提供这个功能。我只能在它停止滚动后才能获得选中的项目,而不是在它稳定时。

我试过的

对于ViewPager,adapter只有setPrimaryItem , so sadly it tells me which item is selected after it finished settling. I do have addOnPageChangeListener function, which tells me about the scrolling position at any given time (using onPageScrolled),但是并没有告诉我往哪个方向走(left/right),也不知道是哪个数据以及它的哪个 viewHolder 将被选中。 addOnPageChangeListener 还提供了滚动的状态(空闲、沉降、拖动)。

对于RecyclerView,我可以获得滚动状态的回调,关于滚动状态何时结束以及何时变为空闲,但我看不到如何获得即将结束的项目。

关于阻止触摸事件,我想我可以在它稳定时在它上面放一个可点击的视图(没有内容,所以它对用户不可见),然后隐藏它(将可见性设置为GONE) 空闲的时候,不知道有没有更好的办法

我尝试将 setOnTouchListener 用于 RecyclerView 空闲和稳定状态,但是当我尝试在稳定状态下触摸时,它卡在了当前滚动位置。

所以 RecyclerViewViewPager 都有完成这一切的障碍...

这是我目前的工作:

ViewPager:

回收视图:

我尝试过的 POC 代码(ViewPagerRecyclerView):

MainActivity.kt

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val inflater = LayoutInflater.from(this)

        //viewPager area

        viewPager.adapter = object : RecyclerPagerAdapter<RecyclerPagerAdapter.ViewHolder>() {
            var selectedHolder: RecyclerPagerAdapter.ViewHolder? = null
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
                return object : RecyclerPagerAdapter.ViewHolder(inflater.inflate(R.layout.cell, parent, false)) {}
            }

            override fun getItemCount(): Int = 100

            override fun onBindViewHolder(holder: ViewHolder, position: Int) {
                (holder.itemView as TextView).text = position.toString()
            }

            override fun setPrimaryItem(container: ViewGroup?, position: Int, obj: Any?) {
                super.setPrimaryItem(container, position, obj)
                //TODO get the soon-to-be-selected page sooner
                val holder = obj as RecyclerPagerAdapter.ViewHolder
                if (selectedHolder != null && selectedHolder != holder)
                    (selectedHolder!!.itemView as TextView).text = position.toString()
                (holder.itemView as TextView).text = "selected:${position.toString()}"
                selectedHolder = holder
            }

        }
        viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
            override fun onPageScrollStateChanged(state: Int) {
                when (state) {
                    ViewPager.SCROLL_STATE_DRAGGING -> Log.d("AppLog", "onPageScrollStateChanged: SCROLL_STATE_DRAGGING")
                    ViewPager.SCROLL_STATE_IDLE -> Log.d("AppLog", "onPageScrollStateChanged: SCROLL_STATE_IDLE")
                    ViewPager.SCROLL_STATE_SETTLING -> Log.d("AppLog", "onPageScrollStateChanged: SCROLL_STATE_SETTLING")
                }
            }

            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                Log.d("AppLog", "onPageScrolled: position:$position positionOffset :$positionOffset positionOffsetPixels:$positionOffsetPixels")
            }

            override fun onPageSelected(position: Int) {
                Log.d("AppLog", "onPageSelected:" + position)
            }
        })

        //recyclerView area

        // Not needed, as I use a library for this: LinearSnapHelper().attachToRecyclerView(recyclerView)
        recyclerView.setHasFixedSize(true)
        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            override fun getItemCount(): Int = 100

            override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(inflater.inflate(R.layout.cell, parent, false)) {}
            }

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                (holder.itemView as TextView).text = position.toString()
            }
        }
        recyclerView.addOnPageChangedListener { oldPosition, newPosition -> Log.d("AppLog", "OnPageChanged:$oldPosition->$newPosition") }
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            @SuppressLint("ClickableViewAccessibility")
            override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                when (newState) {
                    RecyclerView.SCROLL_STATE_IDLE -> {
                        Log.d("AppLog", "state: SCROLL_STATE_IDLE")
                        recyclerViewStateTextView.text = "state: SCROLL_STATE_IDLE"
                        //setOnTouchListener doesn't really work well. It makes the scrolling stuck
                        //                        recyclerView!!.setOnTouchListener(null)
                    }
                    RecyclerView.SCROLL_STATE_SETTLING -> {
                        //TODO when settling, block touches, and update the soon-to-be-focused page
                        Log.d("AppLog", "state: SCROLL_STATE_SETTLING")
                        recyclerViewStateTextView.text = "state: SCROLL_STATE_SETTLING"
                        //                        recyclerView!!.setOnTouchListener(object : View.OnTouchListener {
                        //                            override fun onTouch(v: View?, event: MotionEvent?): Boolean {
                        //                                return true
                        //                            }
                        //
                        //                        })
                    }
                    RecyclerView.SCROLL_STATE_DRAGGING -> {
                        Log.d("AppLog", "state: SCROLL_STATE_DRAGGING")
                        recyclerViewStateTextView.text = "state: SCROLL_STATE_DRAGGING"
                    }
                }
            }
        })
        recyclerViewStateTextView.text = "state: SCROLL_STATE_IDLE"
    }

cell.xml

<TextView
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:id="@android:id/text1"
    android:textSize="36sp" tools:text="@tools:sample/lorem"/>

activity_main.xml

<LinearLayout
    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:orientation="vertical"
    tools:context="com.example.user.snapblockertest.MainActivity">

    <TextView
        android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="ViewPager :"/>

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="0px"
        android:layout_weight="1"/>

    <TextView
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:text="RecyclerView with snapping:"/>

    <com.lsjwzh.widget.recyclerviewpager.RecyclerViewPager
        android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="0px"
        android:layout_weight="1" android:orientation="horizontal"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager" app:rvp_singlePageFling="true"
        app:rvp_triggerOffset="0.1"/>

    <TextView
        android:id="@+id/recyclerViewStateTextView" android:layout_width="match_parent" android:layout_height="wrap_content"/>

</LinearLayout>

gradle 文件 "non-standard" 依赖,来自这里和这里

//https://github.com/henrytao-me/recycler-pager-adapter
implementation "me.henrytao:recycler-pager-adapter:2.1.0"
//https://github.com/lsjwzh/RecyclerViewPager
implementation 'com.github.lsjwzh.RecyclerViewPager:lib:v1.1.2@aar'

问题

  1. 如何获得ViewPager/RecyclerView结算时的回调,包括即将捕捉到哪个项目?

  2. 如何阻止触摸事件从稳定到空闲的时间?有没有比我写的更好的方法(在顶部有一个可点击的视图)?


更新:

似乎对于 ViewPager,我可以使用 onPageSelected 回调来获取它要定位的项目。想知道以这种方式获取其页面的 ViewHolder 的最佳方法是什么。我可以将需要的数据保存在onBindViewHolder中,然后自己查看,但不知道有没有更好的方法

现在缺少的是如何为 RecyclerView 做到这一点以及如何阻止触摸事件(如果有比我写的更好的方法)。库里有一个函数叫 addOnPageChangedListener ,但是是在 setting 完成后调用的,所以这里帮不上忙。

似乎对于 ViewPager,我可以使用 onPageSelected 回调来获取它要定位的项目。

所以这是 ViewPager 的解决方案(使用我使用的库):

    val inflater = LayoutInflater.from(this)
    val viewPagerHolders = HashMap<Int, ViewPagerViewHolder>()
    viewPager.adapter = object : RecyclerPagerAdapter<ViewPagerViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerViewHolder {
            return ViewPagerViewHolder(inflater.inflate(R.layout.cell, parent, false))
        }

        override fun getItemCount(): Int = 100

        override fun onBindViewHolder(holder: ViewPagerViewHolder, position: Int) {
            holder.textView.text = position.toString()
            viewPagerHolders.remove(holder.adapterPosition)
            holder.adapterPosition = position
            viewPagerHolders[position] = holder

        }
    }
    viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
        var selectedHolder: ViewPagerViewHolder? = null

        override fun onPageScrollStateChanged(state: Int) {}
        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}

        override fun onPageSelected(position: Int) {
            val holder = viewPagerHolders[position]
            if (holder != null) {
                if (selectedHolder != null && selectedHolder != holder)
                    selectedHolder!!.textView.text = selectedHolder!!.adapterPosition.toString()
                holder.textView.text = "selected:${position.toString()}"
                selectedHolder = holder
            }
        }
    })

还有一个 ViewHolder class,它只记住与其关联的适配器位置(因为由于某种原因缺少它):

class ViewPagerViewHolder(itemView: View) : RecyclerPagerAdapter.ViewHolder(itemView) {
    val textView: TextView = itemView.findViewById(android.R.id.text1)
    var adapterPosition: Int = Int.MIN_VALUE
}

由于它运行良好,我决定使用它,即使不阻止触摸事件功能。

仍然很高兴知道如何为 RecyclerView 执行此操作。

编辑:对于 RecyclerView,可以这样做: