How to snap RecyclerView items so that every X items would be considered like a single unit to snap to?(如何捕捉 RecyclerView 项目,以便将每个 X 项目视为一个要捕捉的单元?)
问题描述
背景
可以使用以下方法将 RecyclerView 捕捉到其中心:
LinearSnapHelper().attachToRecyclerView(recyclerView)
例子:
MainActivity.kt
类 MainActivity : AppCompatActivity() {覆盖 fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val inflater = LayoutInflater.from(this)recyclerView.adapter = 对象:RecyclerView.Adapter() {覆盖乐趣 onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {val textView = holder.itemView 作为 TextViewtextView.setBackgroundColor(如果 (位置 % 2 == 0) 0xffff0000.toInt() 否则 0xff00ff00.toInt())textView.text = position.toString()}覆盖有趣的 getItemCount(): Int {返回 100}覆盖乐趣 onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextViewval cellSize = recyclerView.width/3view.layoutParams.height = cellSizeview.layoutParams.width = cellSizeview.gravity = Gravity.CENTER返回对象:RecyclerView.ViewHolder(view) {}}}LinearSnapHelper().attachToRecyclerView(recyclerView)}}
activity_main.xml
到这个空闲状态(如果我们滚动得足够多,否则会停留在以前的状态):
应该像在 ViewPager 上一样处理更多的翻转或滚动,就像我上面提到的库一样.
滚动更多(沿相同方向)到下一个捕捉点将到达项目6".、9"等……
我尝试了什么
我尝试搜索替代库,也尝试阅读有关此的文档,但没有找到任何有用的东西.
使用 ViewPager 也有可能,但我认为这不是最好的方法,因为 ViewPager 不能很好地回收它的项目,而且我认为它在如何捕捉方面不如 RecyclerView 灵活.
问题
是否可以将 RecyclerView 设置为捕捉每个 X 项,将每个 X 项视为要捕捉到的单个页面?
当然,项目会平均占用整个 RecyclerView 的足够空间.
假设有可能,当 RecyclerView 即将捕捉到某个项目(包括拥有该项目)之前,我将如何获得回调?我问这个是因为它与我问
如下设置布局管理器:
//用于LinearLayoutManager 水平方向recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));//对于 GridLayoutManager 垂直方向recyclerView.setLayoutManager(new GridLayoutManager(this, SPAN_COUNT, RecyclerView.VERTICAL, false));
添加以下内容以将 SnapToBlock
附加到 RecyclerView
.
SnapToBlock snapToBlock = new SnapToBlock(mMaxFlingPages);snapToBlock.attachToRecyclerView(recyclerView);
mMaxFlingPages
是一次允许抛出的最大块数(rowsCols * spans).
对于即将进行快照并已完成时的回调,请添加以下内容:
snapToBlock.setSnapBlockCallback(new SnapToBlock.SnapBlockCallback() {@覆盖公共无效onBlockSnap(int snapPosition){...}@覆盖公共无效onBlockSnapped(int snapPosition){...}});
SnapToBlock.java
/* RecyclerView中的item个数应该是block size的倍数;否则,到达数据末尾时,额外的项目视图将不会定位在块边界上.如果需要,用空的项目视图填充.更新以适应 RTL 布局.*/公共类 SnapToBlock 扩展 SnapHelper {私有 RecyclerView mRecyclerView;//RecyclerView 中某块视图中的项目总数私有 int mBlocksize;//一次移动的最大位置数.私人 int mMaxPositionsToMove;//如果方向是水平的,则 RecyclerView 项目的宽度;垂直时项目的高度私有 int mItemDimension;//在最剧烈的投掷过程中,Maxim 会阻止移动.私有最终 int mMaxFlingBlocks;//捕捉块时的回调接口.私有 SnapBlockCallback mSnapBlockCallback;//捕捉时,用于确定捕捉的方向.私有 int mPriorFirstPosition = RecyclerView.NO_POSITION;//我们的私有滚动条私有滚动器 mScroller;//水平/垂直布局助手私人OrientationHelper mOrientationHelper;//LTR/RTL 助手私人 LayoutDirectionHelper mLayoutDirectionHelper;//借自 ViewPager.java私有静态最终插值器 sInterpolator = new Interpolator() {公共浮点 getInterpolation(float t) {//_o(t) = t * t * ((张力 + 1) * t + 张力)//o(t) = _o(t - 1) + 1t -= 1.0f;返回 t * t * t + 1.0f;}};SnapToBlock(int maxFlingBlocks) {极好的();mMaxFlingBlocks = maxFlingBlocks;}@覆盖public void attachToRecyclerView(@Nullable final RecyclerView recyclerView)抛出 IllegalStateException {if (recyclerView != null) {mRecyclerView = 回收器视图;最终 LinearLayoutManager 布局管理器 =(LinearLayoutManager) recyclerView.getLayoutManager();if (layoutManager.canScrollHorizontally()) {mOrientationHelper = OrientationHelper.createHorizontalHelper(layoutManager);mLayoutDirectionHelper =新的 LayoutDirectionHelper(ViewCompat.getLayoutDirection(mRecyclerView));} else if (layoutManager.canScrollVertically()) {mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);//RTL 与此类的垂直滚动无关.mLayoutDirectionHelper = new LayoutDirectionHelper(RecyclerView.LAYOUT_DIRECTION_LTR);} 别的 {throw new IllegalStateException("RecyclerView 必须是可滚动的");}mScroller = new Scroller(mRecyclerView.getContext(), sInterpolator);initItemDimensionIfNeeded(layoutManager);}super.attachToRecyclerView(recyclerView);}//当目标视图可用时调用,我们需要知道还有多少//滚动使其与 RecyclerView 的一侧对齐.@NonNull@覆盖public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,@NonNull 查看目标视图){int[] out = new int[2];if (layoutManager.canScrollHorizontally()) {out[0] = mLayoutDirectionHelper.getScrollToAlignView(targetView);}if (layoutManager.canScrollVertically()) {out[1] = mLayoutDirectionHelper.getScrollToAlignView(targetView);}if (mSnapBlockCallback != null) {if (out[0] == 0 && out[1] == 0) {mSnapBlockCallback.onBlockSnapped(layoutManager.getPosition(targetView));} 别的 {mSnapBlockCallback.onBlockSnap(layoutManager.getPosition(targetView));}}退出;}//我们正在逃跑,需要知道我们要去哪里.@覆盖公共 int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager,国际速度X,国际速度Y){线性布局管理器 lm = (线性布局管理器) 布局管理器;initItemDimensionIfNeeded(layoutManager);mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE,整数.MIN_VALUE,整数.MAX_VALUE);如果(速度X!= 0){返回 mLayoutDirectionHelper.getPositionsToMove(lm, mScroller.getFinalX(), mItemDimension);}如果(速度Y!= 0){返回 mLayoutDirectionHelper.getPositionsToMove(lm, mScroller.getFinalY(), mItemDimension);}返回 RecyclerView.NO_POSITION;}//我们已经滚动到我们将要捕捉的邻域.确定捕捉位置.@覆盖公共视图 findSnapView(RecyclerView.LayoutManager layoutManager) {//捕捉到 1) 靠近数据底部并因此在屏幕上的视图,//或者,2) 朝向数据的顶部并且可能在屏幕外.int snapPos = calcTargetPosition((LinearLayoutManager) layoutManager);查看 snapView = (snapPos == RecyclerView.NO_POSITION)?null : layoutManager.findViewByPosition(snapPos);if (snapView == null) {Log.d(TAG, "<<<<findSnapView 正在返回 null!");}Log.d(TAG, "<<<<findSnapView snapos=" + snapPos);返回快照视图;}//为 findSnapView 做繁重的工作.私人 int calcTargetPosition(LinearLayoutManager layoutManager) {int snapPos;int firstVisiblePos = layoutManager.findFirstVisibleItemPosition();if (firstVisiblePos == RecyclerView.NO_POSITION) {返回 RecyclerView.NO_POSITION;}initItemDimensionIfNeeded(layoutManager);if (firstVisiblePos >= mPriorFirstPosition) {//向数据底部滚动int firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition();if (firstCompletePosition != RecyclerView.NO_POSITION&&firstCompletePosition % mBlocksize == 0) {snapPos = firstCompletePosition;} 别的 {snapPos = roundDownToBlockSize(firstVisiblePos + mBlocksize);}} 别的 {//向数据顶部滚动snapPos = roundDownToBlockSize(firstVisiblePos);//检查目标视图是否存在.如果没有,请强制平滑滚动.//SnapHelper 只捕捉现有视图,不会滚动到不存在的视图.//如果将 fling 限制为单个块,则不需要以下内容,因为//视图很可能在 RecyclerView 池中.if (layoutManager.findViewByPosition(snapPos) == null) {int[] toScroll = mLayoutDirectionHelper.calculateDistanceToScroll(layoutManager, snapPos);mRecyclerView.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator);}}mPriorFirstPosition = firstVisiblePos;返回快照位置;}私人无效initItemDimensionIfNeeded(最终RecyclerView.LayoutManager layoutManager){如果(mItemDimension!= 0){返回;}查看孩子;if ((child = layoutManager.getChildAt(0)) == null) {返回;}if (layoutManager.canScrollHorizontally()) {mItemDimension = child.getWidth();mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getWidth()/mItemDimension);} else if (layoutManager.canScrollVertically()) {mItemDimension = child.getHeight();mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getHeight()/mItemDimension);}mMaxPositionsToMove = mBlocksize * mMaxFlingBlocks;}私有 int getSpanCount(RecyclerView.LayoutManager layoutManager) {返回(GridLayoutManager的layoutManager实例)?((GridLayoutManager) layoutManager).getSpanCount(): 1;}私人 int roundDownToBlockSize(int trialPosition) {返回 trialPosition - trialPosition % mBlocksize;}私人 int roundUpToBlockSize(int trialPosition) {return roundDownToBlockSize(trialPosition + mBlocksize - 1);}@Nullable受保护的 LinearSmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {返回空值;}返回新的 LinearSmoothScroller(mRecyclerView.getContext()) {@覆盖protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),目标视图);最终 int dx = snapDistances[0];最终 int dy = snapDistances[1];最终 int 时间 = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));如果(时间> 0){action.update(dx, dy, time, sInterpolator);}}@覆盖受保护的浮点calculateSpeedPerPixel(DisplayMetrics displayMetrics){返回 MILLISECONDS_PER_INCH/displayMetrics.densityDpi;}};}公共无效 setSnapBlockCallback(@Nullable SnapBlockCallback 回调){mSnapBlockCallback = 回调;}/*处理 LTR 和 RTL 布局计算的助手类.*/私有类 LayoutDirectionHelper {//布局是 RTL 的吗?私有最终布尔 mIsRTL;@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)LayoutDirectionHelper(int 方向) {mIsRTL = 方向 == View.LAYOUT_DIRECTION_RTL;}/*计算将目标视图与布局边缘对齐所需的滚动量.*/int getScrollToAlignView(查看 targetView){返回 (mIsRTL)?mOrientationHelper.getDecoratedEnd(targetView) - mRecyclerView.getWidth(): mOrientationHelper.getDecoratedStart(targetView);}/*** 计算snap对应view时到最终snap位置的距离* 职位目前不可用.** @param layoutManager LinearLayoutManager 或后代类* @param targetPos - 要捕捉到的适配器位置* @return int[2] {x-距离以像素为单位,y-距离以像素为单位}*/int[] calculateDistanceToScroll(LinearLayoutManager layoutManager, int targetPos) {int[] out = new int[2];int firstVisiblePos;firstVisiblePos = layoutManager.findFirstVisibleItemPosition();if (layoutManager.canScrollHorizontally()) {if (targetPos <= firstVisiblePos) {//向数据顶部滚动如果(mIsRTL){查看 lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition());out[0] = mOrientationHelper.getDecoratedEnd(lastView)+ (firstVisiblePos - targetPos) * mItemDimension;} 别的 {查看 firstView = layoutManager.findViewByPosition(firstVisiblePos);out[0] = mOrientationHelper.getDecoratedStart(firstView)- (firstVisiblePos - targetPos) * mItemDimension;}}}if (layoutManager.canScrollVertically()) {if (targetPos <= firstVisiblePos) {//向数据顶部滚动查看 firstView = layoutManager.findViewByPosition(firstVisiblePos);out[1] = firstView.getTop() - (firstVisiblePos - targetPos) * mItemDimension;}}退出;}/*给定滚动量,计算在 RecyclerView 中移动的位置数以及要滚动的项目的大小.不返回 mBlockSize 的整数倍等于零.*/int getPositionsToMove(LinearLayoutManager llm, int scroll, int itemSize) {int 位置移动;positionToMove = roundUpToBlockSize(Math.abs(scroll)/itemSize);if (positionsToMove < mBlocksize) {//必须至少移动一个方块positionToMove = mBlocksize;} else if (positionsToMove > mMaxPositionsToMove) {//限制要移动的位置数,这样我们就不会乱扔垃圾了.PositionToMove = mMaxPositionsToMove;}如果(滚动 < 0){位置移动 *= -1;}如果(mIsRTL){位置移动 *= -1;}if (mLayoutDirectionHelper.isDirectionToBottom(scroll < 0)) {//向数据底部滚动.返回 roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionToMove;}//向数据顶部滚动.return roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove;}boolean isDirectionToBottom(boolean velocityNegative) {//noinspection SimplifiableConditionalExpressionreturn mIsRTL ? velocityNegative : !velocityNegative;}}public interface SnapBlockCallback {void onBlockSnap(int snapPosition);void onBlockSnapped(int snapPosition);}private static final float MILLISECONDS_PER_INCH = 100f;@SuppressWarnings("unused")private static final String TAG = "SnapToBlock";}
The SnapBlockCallback
interface defined above can be used to report the adapter position of the view at the start of the block to be snapped. The view associated with that position may not be instantiated when the call is made if the view is off screen.
Background
It's possible to snap a RecyclerView to its center using :
LinearSnapHelper().attachToRecyclerView(recyclerView)
Example:
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView as TextView
textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
textView.text = position.toString()
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
val cellSize = recyclerView.width / 3
view.layoutParams.height = cellSize
view.layoutParams.width = cellSize
view.gravity = Gravity.CENTER
return object : RecyclerView.ViewHolder(view) {}
}
}
LinearSnapHelper().attachToRecyclerView(recyclerView)
}
}
activity_main.xml
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView" 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="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
It's also possible to snap it to other sides, as was done in some libraries, such as here.
There are also libraries that allow to have a RecyclerView that can work like a ViewPager, such as here.
The problem
Supposed I have a RecyclerView (horizontal in my case) with many items, and I want that it will treat every X items (X is constant) as a single unit, and snap to each of those units.
For example, if I scroll a bit, it could snap to either the 0-item, or the X-item, but not to something in between them.
In a way, it's similar in its behavior to a case of a normal ViewPager, just that each page would have X items in it.
For example, if we continue from the sample code I wrote above,suppose X==3 , the snapping would be from this idle state:
to this idle state (in case we scrolled enough, otherwise would stay in previous state) :
Flinging or scrolling more should be handled like on ViewPager, just like the library I've mentioned above.
Scrolling more (in the same direction) to the next snapping point would be to reach item "6" , "9", and so on...
What I tried
I tried to search for alternative libraries, and I also tried to read the docs regarding this, but I didn't find anything that might be useful.
It might also be possible by using a ViewPager, but I think that's not the best way, because ViewPager doesn't recycle its items well, and I think it's less flexible than RecyclerView in terms of how to snap.
The questions
Is it possible to set RecyclerView to snap every X items, to treat each X items as a single page to snap to?
Of course, the items will take enough space for the whole RecyclerView, evenly.
Supposed it is possible, how would I get a callback when the RecyclerView is about to snap to a certain item, including having this item, before it got snapped? I ask this because it's related to the same question I asked here.
Kotlin solution
A working Kotlin solution based on "Cheticamp" answer (here), without the need to verify that you have the RecyclerView size, and with the choice of having a grid instead of a list, in the sample:
MainActivity.kt
class MainActivity : AppCompatActivity() {
val USE_GRID = false
// val USE_GRID = true
val ITEMS_PER_PAGE = 4
var selectedItemPos = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView as TextView
textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
textView.text = if (selectedItemPos == position) "selected: $position" else position.toString()
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
view.layoutParams.width = if (USE_GRID)
recyclerView.width / (ITEMS_PER_PAGE / 2)
else
recyclerView.width / 4
view.layoutParams.height = recyclerView.height / (ITEMS_PER_PAGE / 2)
view.gravity = Gravity.CENTER
return object : RecyclerView.ViewHolder(view) {
}
}
}
recyclerView.layoutManager = if (USE_GRID)
GridLayoutManager(this, ITEMS_PER_PAGE / 2, GridLayoutManager.HORIZONTAL, false)
else
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
val snapToBlock = SnapToBlock(recyclerView, ITEMS_PER_PAGE)
snapToBlock.attachToRecyclerView(recyclerView)
snapToBlock.setSnapBlockCallback(object : SnapToBlock.SnapBlockCallback {
override fun onBlockSnap(snapPosition: Int) {
if (selectedItemPos == snapPosition)
return
selectedItemPos = snapPosition
recyclerView.adapter.notifyDataSetChanged()
}
override fun onBlockSnapped(snapPosition: Int) {
if (selectedItemPos == snapPosition)
return
selectedItemPos = snapPosition
recyclerView.adapter.notifyDataSetChanged()
}
})
}
}
SnapToBlock.kt
/**@param maxFlingBlocks Maxim blocks to move during most vigorous fling*/
class SnapToBlock constructor(private val maxFlingBlocks: Int) : SnapHelper() {
private var recyclerView: RecyclerView? = null
// Total number of items in a block of view in the RecyclerView
private var blocksize: Int = 0
// Maximum number of positions to move on a fling.
private var maxPositionsToMove: Int = 0
// Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
private var itemDimension: Int = 0
// Callback interface when blocks are snapped.
private var snapBlockCallback: SnapBlockCallback? = null
// When snapping, used to determine direction of snap.
private var priorFirstPosition = RecyclerView.NO_POSITION
// Our private scroller
private var scroller: Scroller? = null
// Horizontal/vertical layout helper
private var orientationHelper: OrientationHelper? = null
// LTR/RTL helper
private var layoutDirectionHelper: LayoutDirectionHelper? = null
@Throws(IllegalStateException::class)
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
if (recyclerView != null) {
this.recyclerView = recyclerView
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
orientationHelper = when {
layoutManager.canScrollHorizontally() -> OrientationHelper.createHorizontalHelper(layoutManager)
layoutManager.canScrollVertically() -> OrientationHelper.createVerticalHelper(layoutManager)
else -> throw IllegalStateException("RecyclerView must be scrollable")
}
scroller = Scroller(this.recyclerView!!.context, sInterpolator)
initItemDimensionIfNeeded(layoutManager)
}
super.attachToRecyclerView(recyclerView)
}
// Called when the target view is available and we need to know how much more
// to scroll to get it lined up with the side of the RecyclerView.
override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray {
val out = IntArray(2)
initLayoutDirectionHelperIfNeeded(layoutManager)
if (layoutManager.canScrollHorizontally())
out[0] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
if (layoutManager.canScrollVertically())
out[1] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
if (snapBlockCallback != null)
if (out[0] == 0 && out[1] == 0)
snapBlockCallback!!.onBlockSnapped(layoutManager.getPosition(targetView))
else
snapBlockCallback!!.onBlockSnap(layoutManager.getPosition(targetView))
return out
}
private fun initLayoutDirectionHelperIfNeeded(layoutManager: RecyclerView.LayoutManager) {
if (layoutDirectionHelper == null)
if (layoutManager.canScrollHorizontally())
layoutDirectionHelper = LayoutDirectionHelper()
else if (layoutManager.canScrollVertically())
// RTL doesn't matter for vertical scrolling for this class.
layoutDirectionHelper = LayoutDirectionHelper(false)
}
// We are flinging and need to know where we are heading.
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
initLayoutDirectionHelperIfNeeded(layoutManager)
val lm = layoutManager as LinearLayoutManager
initItemDimensionIfNeeded(layoutManager)
scroller!!.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE)
return when {
velocityX != 0 -> layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalX, itemDimension)
else -> if (velocityY != 0)
layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalY, itemDimension)
else RecyclerView.NO_POSITION
}
}
// We have scrolled to the neighborhood where we will snap. Determine the snap position.
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
// Snap to a view that is either 1) toward the bottom of the data and therefore on screen,
// or, 2) toward the top of the data and may be off-screen.
val snapPos = calcTargetPosition(layoutManager as LinearLayoutManager)
val snapView = if (snapPos == RecyclerView.NO_POSITION)
null
else
layoutManager.findViewByPosition(snapPos)
if (snapView == null)
Log.d(TAG, "<<<<findSnapView is returning null!")
Log.d(TAG, "<<<<findSnapView snapos=" + snapPos)
return snapView
}
// Does the heavy lifting for findSnapView.
private fun calcTargetPosition(layoutManager: LinearLayoutManager): Int {
val snapPos: Int
initLayoutDirectionHelperIfNeeded(layoutManager)
val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
if (firstVisiblePos == RecyclerView.NO_POSITION)
return RecyclerView.NO_POSITION
initItemDimensionIfNeeded(layoutManager)
if (firstVisiblePos >= priorFirstPosition) {
// Scrolling toward bottom of data
val firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
snapPos = if (firstCompletePosition != RecyclerView.NO_POSITION && firstCompletePosition % blocksize == 0)
firstCompletePosition
else
roundDownToBlockSize(firstVisiblePos + blocksize)
} else {
// Scrolling toward top of data
snapPos = roundDownToBlockSize(firstVisiblePos)
// Check to see if target view exists. If it doesn't, force a smooth scroll.
// SnapHelper only snaps to existing views and will not scroll to a non-existant one.
// If limiting fling to single block, then the following is not needed since the
// views are likely to be in the RecyclerView pool.
if (layoutManager.findViewByPosition(snapPos) == null) {
val toScroll = layoutDirectionHelper!!.calculateDistanceToScroll(layoutManager, snapPos)
recyclerView!!.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator)
}
}
priorFirstPosition = firstVisiblePos
return snapPos
}
private fun initItemDimensionIfNeeded(layoutManager: RecyclerView.LayoutManager) {
if (itemDimension != 0)
return
val child = layoutManager.getChildAt(0) ?: return
if (layoutManager.canScrollHorizontally()) {
itemDimension = child.width
blocksize = getSpanCount(layoutManager) * (recyclerView!!.width / itemDimension)
} else if (layoutManager.canScrollVertically()) {
itemDimension = child.height
blocksize = getSpanCount(layoutManager) * (recyclerView!!.height / itemDimension)
}
maxPositionsToMove = blocksize * maxFlingBlocks
}
private fun getSpanCount(layoutManager: RecyclerView.LayoutManager): Int = (layoutManager as? GridLayoutManager)?.spanCount ?: 1
private fun roundDownToBlockSize(trialPosition: Int): Int = trialPosition - trialPosition % blocksize
private fun roundUpToBlockSize(trialPosition: Int): Int = roundDownToBlockSize(trialPosition + blocksize - 1)
override fun createScroller(layoutManager: RecyclerView.LayoutManager): LinearSmoothScroller? {
return if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider)
null
else object : LinearSmoothScroller(recyclerView!!.context) {
override fun onTargetFound(targetView: View, state: RecyclerView.State?, action: RecyclerView.SmoothScroller.Action) {
val snapDistances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager, targetView)
val dx = snapDistances[0]
val dy = snapDistances[1]
val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)))
if (time > 0)
action.update(dx, dy, time, sInterpolator)
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float = MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
fun setSnapBlockCallback(callback: SnapBlockCallback?) {
snapBlockCallback = callback
}
/*
Helper class that handles calculations for LTR and RTL layouts.
*/
private inner class LayoutDirectionHelper {
// Is the layout an RTL one?
private val mIsRTL: Boolean
constructor() {
mIsRTL = ViewCompat.getLayoutDirection(recyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL
}
constructor(isRTL: Boolean) {
mIsRTL = isRTL
}
/*
Calculate the amount of scroll needed to align the target view with the layout edge.
*/
fun getScrollToAlignView(targetView: View): Int = if (mIsRTL)
orientationHelper!!.getDecoratedEnd(targetView) - recyclerView!!.width
else
orientationHelper!!.getDecoratedStart(targetView)
/**
* Calculate the distance to final snap position when the view corresponding to the snap
* position is not currently available.
*
* @param layoutManager LinearLayoutManager or descendent class
* @param targetPos - Adapter position to snap to
* @return int[2] {x-distance in pixels, y-distance in pixels}
*/
fun calculateDistanceToScroll(layoutManager: LinearLayoutManager, targetPos: Int): IntArray {
val out = IntArray(2)
val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
if (layoutManager.canScrollHorizontally()) {
if (targetPos <= firstVisiblePos) // scrolling toward top of data
if (mIsRTL) {
val lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition())
out[0] = orientationHelper!!.getDecoratedEnd(lastView) + (firstVisiblePos - targetPos) * itemDimension
} else {
val firstView = layoutManager.findViewByPosition(firstVisiblePos)
out[0] = orientationHelper!!.getDecoratedStart(firstView) - (firstVisiblePos - targetPos) * itemDimension
}
}
if (layoutManager.canScrollVertically() && targetPos <= firstVisiblePos) { // scrolling toward top of data
val firstView = layoutManager.findViewByPosition(firstVisiblePos)
out[1] = firstView.top - (firstVisiblePos - targetPos) * itemDimension
}
return out
}
/*
Calculate the number of positions to move in the RecyclerView given a scroll amount
and the size of the items to be scrolled. Return integral multiple of mBlockSize not
equal to zero.
*/
fun getPositionsToMove(llm: LinearLayoutManager, scroll: Int, itemSize: Int): Int {
var positionsToMove: Int
positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize)
if (positionsToMove < blocksize)
// Must move at least one block
positionsToMove = blocksize
else if (positionsToMove > maxPositionsToMove)
// Clamp number of positions to move so we don't get wild flinging.
positionsToMove = maxPositionsToMove
if (scroll < 0)
positionsToMove *= -1
if (mIsRTL)
positionsToMove *= -1
return if (layoutDirectionHelper!!.isDirectionToBottom(scroll < 0)) {
// Scrolling toward the bottom of data.
roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove
} else roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove
// Scrolling toward the top of the data.
}
fun isDirectionToBottom(velocityNegative: Boolean): Boolean = if (mIsRTL) velocityNegative else !velocityNegative
}
interface SnapBlockCallback {
fun onBlockSnap(snapPosition: Int)
fun onBlockSnapped(snapPosition: Int)
}
companion object {
// Borrowed from ViewPager.java
private val sInterpolator = Interpolator { input ->
var t = input
// _o(t) = t * t * ((tension + 1) * t + tension)
// o(t) = _o(t - 1) + 1
t -= 1.0f
t * t * t + 1.0f
}
private val MILLISECONDS_PER_INCH = 100f
private val TAG = "SnapToBlock"
}
}
Update
Even though I've marked an answer as accepted, as it works fine, I've noticed it has serious issues:
Smooth scrolling doesn't seem to work fine (doesn't scroll to correct place). Only scrolling that work is as such (but with the "smearing" effect) :
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)
When switching to RTL (Right to left) locale such as Hebrew ("עברית"), it doesn't let me scroll at all.
I've noticed that onCreateViewHolder
is called a lot. In fact it is called every time I scroll, even for times it should have recycled the ViewHolders. This means there is an excessive creation of views, and it might also mean there is a memory leak.
I've tried to fix those myself, but failed so far.
If anyone here knows how to fix it, I will grant the extra, new bounty
Update: as we got a fix for RTL/LTR, I've updated the Kotlin solution within this post.
Update: about point #3 , this seems to be because there is a pool of views for the recyclerView, which gets filled too soon. To handle this, we can simply enlarge the pool size, by using recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE)
for each view type we have in it. Weird thing that this is really needed. I've posted about it to Google (here and here) but was rejected that the pool should be unlimited by default. In the end, I decided to at least request to have a more convinient function to do it for all view types (here).
解决方案 SnapHelper
supplies the necessary framework for what you are attempting, but it needs to be extended to handle blocks of views. The class SnapToBlock
below extends SnapHelper
to snap to blocks of views. In the example, I have used four views to a block but it can be more or less.
Update: The code has been change to accommodate GridLayoutManager
as well as LinearLayoutManager
. Flinging is now inhibited so the snapping works more list a ViewPager
. Horizontal and vertical scrolling is now supported as well as LTR and RTL layouts.
Update: Changed smooth scroll interpolator to be more like ViewPager
.
Update: Adding callbacks for pre/post snapping.
Update: Adding support for RTL layouts.
Here is a quick video of the sample app:
Set up the layout manager as follows:
// For LinearLayoutManager horizontal orientation
recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
// For GridLayoutManager vertical orientation
recyclerView.setLayoutManager(new GridLayoutManager(this, SPAN_COUNT, RecyclerView.VERTICAL, false));
Add the following to attach the SnapToBlock
to the RecyclerView
.
SnapToBlock snapToBlock = new SnapToBlock(mMaxFlingPages);
snapToBlock.attachToRecyclerView(recyclerView);
mMaxFlingPages
is the maximum number of blocks (rowsCols * spans) to allow to be flung at one time.
For call backs when a snap is about to be made and has been completed, add the following:
snapToBlock.setSnapBlockCallback(new SnapToBlock.SnapBlockCallback() {
@Override
public void onBlockSnap(int snapPosition) {
...
}
@Override
public void onBlockSnapped(int snapPosition) {
...
}
});
SnapToBlock.java
/* The number of items in the RecyclerView should be a multiple of block size; otherwise, the
extra item views will not be positioned on a block boundary when the end of the data is reached.
Pad out with empty item views if needed.
Updated to accommodate RTL layouts.
*/
public class SnapToBlock extends SnapHelper {
private RecyclerView mRecyclerView;
// Total number of items in a block of view in the RecyclerView
private int mBlocksize;
// Maximum number of positions to move on a fling.
private int mMaxPositionsToMove;
// Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
private int mItemDimension;
// Maxim blocks to move during most vigorous fling.
private final int mMaxFlingBlocks;
// Callback interface when blocks are snapped.
private SnapBlockCallback mSnapBlockCallback;
// When snapping, used to determine direction of snap.
private int mPriorFirstPosition = RecyclerView.NO_POSITION;
// Our private scroller
private Scroller mScroller;
// Horizontal/vertical layout helper
private OrientationHelper mOrientationHelper;
// LTR/RTL helper
private LayoutDirectionHelper mLayoutDirectionHelper;
// Borrowed from ViewPager.java
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
// _o(t) = t * t * ((tension + 1) * t + tension)
// o(t) = _o(t - 1) + 1
t -= 1.0f;
return t * t * t + 1.0f;
}
};
SnapToBlock(int maxFlingBlocks) {
super();
mMaxFlingBlocks = maxFlingBlocks;
}
@Override
public void attachToRecyclerView(@Nullable final RecyclerView recyclerView)
throws IllegalStateException {
if (recyclerView != null) {
mRecyclerView = recyclerView;
final LinearLayoutManager layoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager.canScrollHorizontally()) {
mOrientationHelper = OrientationHelper.createHorizontalHelper(layoutManager);
mLayoutDirectionHelper =
new LayoutDirectionHelper(ViewCompat.getLayoutDirection(mRecyclerView));
} else if (layoutManager.canScrollVertically()) {
mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
// RTL doesn't matter for vertical scrolling for this class.
mLayoutDirectionHelper = new LayoutDirectionHelper(RecyclerView.LAYOUT_DIRECTION_LTR);
} else {
throw new IllegalStateException("RecyclerView must be scrollable");
}
mScroller = new Scroller(mRecyclerView.getContext(), sInterpolator);
initItemDimensionIfNeeded(layoutManager);
}
super.attachToRecyclerView(recyclerView);
}
// Called when the target view is available and we need to know how much more
// to scroll to get it lined up with the side of the RecyclerView.
@NonNull
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = mLayoutDirectionHelper.getScrollToAlignView(targetView);
}
if (layoutManager.canScrollVertically()) {
out[1] = mLayoutDirectionHelper.getScrollToAlignView(targetView);
}
if (mSnapBlockCallback != null) {
if (out[0] == 0 && out[1] == 0) {
mSnapBlockCallback.onBlockSnapped(layoutManager.getPosition(targetView));
} else {
mSnapBlockCallback.onBlockSnap(layoutManager.getPosition(targetView));
}
}
return out;
}
// We are flinging and need to know where we are heading.
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager,
int velocityX, int velocityY) {
LinearLayoutManager lm = (LinearLayoutManager) layoutManager;
initItemDimensionIfNeeded(layoutManager);
mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE,
Integer.MIN_VALUE, Integer.MAX_VALUE);
if (velocityX != 0) {
return mLayoutDirectionHelper
.getPositionsToMove(lm, mScroller.getFinalX(), mItemDimension);
}
if (velocityY != 0) {
return mLayoutDirectionHelper
.getPositionsToMove(lm, mScroller.getFinalY(), mItemDimension);
}
return RecyclerView.NO_POSITION;
}
// We have scrolled to the neighborhood where we will snap. Determine the snap position.
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
// Snap to a view that is either 1) toward the bottom of the data and therefore on screen,
// or, 2) toward the top of the data and may be off-screen.
int snapPos = calcTargetPosition((LinearLayoutManager) layoutManager);
View snapView = (snapPos == RecyclerView.NO_POSITION)
? null : layoutManager.findViewByPosition(snapPos);
if (snapView == null) {
Log.d(TAG, "<<<<findSnapView is returning null!");
}
Log.d(TAG, "<<<<findSnapView snapos=" + snapPos);
return snapView;
}
// Does the heavy lifting for findSnapView.
private int calcTargetPosition(LinearLayoutManager layoutManager) {
int snapPos;
int firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
if (firstVisiblePos == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
initItemDimensionIfNeeded(layoutManager);
if (firstVisiblePos >= mPriorFirstPosition) {
// Scrolling toward bottom of data
int firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
if (firstCompletePosition != RecyclerView.NO_POSITION
&& firstCompletePosition % mBlocksize == 0) {
snapPos = firstCompletePosition;
} else {
snapPos = roundDownToBlockSize(firstVisiblePos + mBlocksize);
}
} else {
// Scrolling toward top of data
snapPos = roundDownToBlockSize(firstVisiblePos);
// Check to see if target view exists. If it doesn't, force a smooth scroll.
// SnapHelper only snaps to existing views and will not scroll to a non-existant one.
// If limiting fling to single block, then the following is not needed since the
// views are likely to be in the RecyclerView pool.
if (layoutManager.findViewByPosition(snapPos) == null) {
int[] toScroll = mLayoutDirectionHelper.calculateDistanceToScroll(layoutManager, snapPos);
mRecyclerView.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator);
}
}
mPriorFirstPosition = firstVisiblePos;
return snapPos;
}
private void initItemDimensionIfNeeded(final RecyclerView.LayoutManager layoutManager) {
if (mItemDimension != 0) {
return;
}
View child;
if ((child = layoutManager.getChildAt(0)) == null) {
return;
}
if (layoutManager.canScrollHorizontally()) {
mItemDimension = child.getWidth();
mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getWidth() / mItemDimension);
} else if (layoutManager.canScrollVertically()) {
mItemDimension = child.getHeight();
mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getHeight() / mItemDimension);
}
mMaxPositionsToMove = mBlocksize * mMaxFlingBlocks;
}
private int getSpanCount(RecyclerView.LayoutManager layoutManager) {
return (layoutManager instanceof GridLayoutManager)
? ((GridLayoutManager) layoutManager).getSpanCount()
: 1;
}
private int roundDownToBlockSize(int trialPosition) {
return trialPosition - trialPosition % mBlocksize;
}
private int roundUpToBlockSize(int trialPosition) {
return roundDownToBlockSize(trialPosition + mBlocksize - 1);
}
@Nullable
protected LinearSmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, sInterpolator);
}
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
public void setSnapBlockCallback(@Nullable SnapBlockCallback callback) {
mSnapBlockCallback = callback;
}
/*
Helper class that handles calculations for LTR and RTL layouts.
*/
private class LayoutDirectionHelper {
// Is the layout an RTL one?
private final boolean mIsRTL;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
LayoutDirectionHelper(int direction) {
mIsRTL = direction == View.LAYOUT_DIRECTION_RTL;
}
/*
Calculate the amount of scroll needed to align the target view with the layout edge.
*/
int getScrollToAlignView(View targetView) {
return (mIsRTL)
? mOrientationHelper.getDecoratedEnd(targetView) - mRecyclerView.getWidth()
: mOrientationHelper.getDecoratedStart(targetView);
}
/**
* Calculate the distance to final snap position when the view corresponding to the snap
* position is not currently available.
*
* @param layoutManager LinearLayoutManager or descendent class
* @param targetPos - Adapter position to snap to
* @return int[2] {x-distance in pixels, y-distance in pixels}
*/
int[] calculateDistanceToScroll(LinearLayoutManager layoutManager, int targetPos) {
int[] out = new int[2];
int firstVisiblePos;
firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
if (layoutManager.canScrollHorizontally()) {
if (targetPos <= firstVisiblePos) { // scrolling toward top of data
if (mIsRTL) {
View lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition());
out[0] = mOrientationHelper.getDecoratedEnd(lastView)
+ (firstVisiblePos - targetPos) * mItemDimension;
} else {
View firstView = layoutManager.findViewByPosition(firstVisiblePos);
out[0] = mOrientationHelper.getDecoratedStart(firstView)
- (firstVisiblePos - targetPos) * mItemDimension;
}
}
}
if (layoutManager.canScrollVertically()) {
if (targetPos <= firstVisiblePos) { // scrolling toward top of data
View firstView = layoutManager.findViewByPosition(firstVisiblePos);
out[1] = firstView.getTop() - (firstVisiblePos - targetPos) * mItemDimension;
}
}
return out;
}
/*
Calculate the number of positions to move in the RecyclerView given a scroll amount
and the size of the items to be scrolled. Return integral multiple of mBlockSize not
equal to zero.
*/
int getPositionsToMove(LinearLayoutManager llm, int scroll, int itemSize) {
int positionsToMove;
positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize);
if (positionsToMove < mBlocksize) {
// Must move at least one block
positionsToMove = mBlocksize;
} else if (positionsToMove > mMaxPositionsToMove) {
// Clamp number of positions to move so we don't get wild flinging.
positionsToMove = mMaxPositionsToMove;
}
if (scroll < 0) {
positionsToMove *= -1;
}
if (mIsRTL) {
positionsToMove *= -1;
}
if (mLayoutDirectionHelper.isDirectionToBottom(scroll < 0)) {
// Scrolling toward the bottom of data.
return roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove;
}
// Scrolling toward the top of the data.
return roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove;
}
boolean isDirectionToBottom(boolean velocityNegative) {
//noinspection SimplifiableConditionalExpression
return mIsRTL ? velocityNegative : !velocityNegative;
}
}
public interface SnapBlockCallback {
void onBlockSnap(int snapPosition);
void onBlockSnapped(int snapPosition);
}
private static final float MILLISECONDS_PER_INCH = 100f;
@SuppressWarnings("unused")
private static final String TAG = "SnapToBlock";
}
The SnapBlockCallback
interface defined above can be used to report the adapter position of the view at the start of the block to be snapped. The view associated with that position may not be instantiated when the call is made if the view is off screen.
这篇关于如何捕捉 RecyclerView 项目,以便将每个 X 项目视为一个要捕捉的单元?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!
本文标题为:如何捕捉 RecyclerView 项目,以便将每个 X 项目视为一个要捕捉的单元?
- SetOnItemSelectedListener上的微调程序错误 2022-01-01
- 使用自动布局向 UIScrollView 添加动态大小的视图 2022-01-01
- 如何在 iPhone 模拟器中重置 NSUserDefaults 数据? 2022-01-01
- Xcode 7.3 中带有 UILabel 的 UIStackView 2022-01-01
- UITextView 内容插图 2022-01-01
- GPS状态的广播接收器? 2022-01-01
- URL编码Swift iOS 2022-01-01
- 网上有没有好的 UIScrollView 教程? 2022-01-01
- 类似于 Mail.app 的 iPad 模态视图控制器? 2022-01-01
- 在 Iphone SDK 的导航栏上添加多个按钮 2022-01-01