Background
ViewPager
snaps to a view after you perform some scrolling, and so can RecyclerView
, if you use something like this:
LinearSnapHelper().attachToRecyclerView(recyclerView)
Or by using a library to snap to a certain edge, as shown on this library. It can mimic ViewPager
almost entirely if you wish, as shown on this library, mentioned by CommonsWare here.
Both of them can function the same way if needed (snapping to a single view, optionally taking whole available space), so this question is about both of them.
ViewPager has some issues of recycling views, but those can be fixed, for example using this library.
The problem
I'm required to update the UI of the newly shown view, as soon as the RecyclerView/ViewPager is about to be idle in scrolling.
Of course, since the user can still touch it, it would be a problem of knowing what is about to occur, so I'm required to also block touch events when it's settling the scroll state.
This means, for example, that when the user flings a single page, from page 0 to page 1, as soon as the snapping starts, touch events are blocked, and I would be able to know that it snaps to page 1 and update this page.
The problem here is that both RecyclerView and ViewPager don't offer this functionality. I can get the item that's selected only after it stopped scrolling, not while it is settling.
What I've tried
For ViewPager
, the adapter only has 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), but it doesn't tell me which direction I go (left/right), and I can't know which data and which viewHolder of it is about to be selected. addOnPageChangeListener
also provides the state of the scrolling (idle, settling, dragging).
For RecyclerView
I can get a callback of the scrolling state, of when it's about to settle and when it becomes idle, but I can't see how to get the item it's about to settle on.
About blocking touch events, I think I could put a clickable view (that has no content, so it's invisible to the user) on top of it when it's settling, and hide it (set visibility to GONE
) when it's idle, but I wonder if there is a better way.
I tried using setOnTouchListener
for RecyclerView
idle and settling states, but when I tried touching while it's settling, it got stuck on its current scrolling location.
So both of RecyclerView
and ViewPager
have obstacles for getting it all done...
Here's what I got currently working :
ViewPager:
RecyclerView:
POC code of what I tried (both ViewPager
and RecyclerView
):
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 file "non-standard" dependencies, from here and here
//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'
The questions
How can I get a callback of when the ViewPager/RecyclerView is settling, including which item is about to be snapped to ?
How can I block the touch events from the time it's settling, till the time it's idle? Is there a better way than what I wrote (of having a clickable view on top) ?
Update:
Seems that for ViewPager, I could use the onPageSelected callback to get which item it's about to settle on. Wonder though what's the best way to get the ViewHolder of its page this way. I could save the needed data in onBindViewHolder
and then check it myself, but I wonder if there is a better way.
Now what's missing is how to do it for RecyclerView
and how to block touch events (if there is a better way than what I wrote). The library has a function called addOnPageChangedListener
, but it's called after the settling has finished, so it can't help here.
Seems that for ViewPager, I could use the onPageSelected callback to get which item it's about to settle on.
So here's the solution for ViewPager (using the library I used) :
And a ViewHolder class that just remembers which adapter position is associated with it (because this is missing for some reason) :
Since it works so well, I decided I will use it, even without blocking touch events functionality.
Still could be nice to know how to do it for RecyclerView.
EDIT: for RecyclerView, this can be done: https://stackoverflow.com/a/47580753/878126