and*_*per 16 android snapping android-viewpager android-recyclerview
可以使用以下方法将RecyclerView捕捉到其中心:
LinearSnapHelper().attachToRecyclerView(recyclerView)
Run Code Online (Sandbox Code Playgroud)
例:
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)
}
}
Run Code Online (Sandbox Code Playgroud)
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"/>
Run Code Online (Sandbox Code Playgroud)
它也可以将其捕捉到其他方面,就像在某些库中所做的那样,例如这里.
还有一些库允许使用可以像ViewPager一样工作的RecyclerView,例如此处.
假设我有一个包含许多项目的RecyclerView(在我的情况下为水平),我希望它将每个X项目(X是常量)视为一个单元,并对齐每个单元.
例如,如果我滚动一下,它可以捕捉到0项或X项,但不能捕捉到它们之间的某些内容.
在某种程度上,它的行为与普通ViewPager的情况类似,只是每个页面中都有X个项目.
例如,如果我们从上面编写的示例代码继续,假设X == 3,则捕捉将来自此空闲状态:
到这个空闲状态(如果我们滚动得足够多,否则将保持在以前的状态):
更多的投掷或滚动应该像在ViewPager上一样处理,就像我上面提到的库一样.
向下一个捕捉点滚动更多(朝同一方向)将是到达项目"6","9",依此类推......
我试图搜索替代库,我也尝试阅读有关此问题的文档,但我没有找到任何可能有用的文档.
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.
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.
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()
}
})
}
}
Run Code Online (Sandbox Code Playgroud)
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"
}
}
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)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.
更新:关于第3点,这似乎是因为recyclelerView有一个视图池,它会很快被填充.为了解决这个问题,我们可以通过使用recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE)我们在其中的每种视图类型来简单地扩大池大小.真的需要这个奇怪的事情.我已经将它发布到谷歌(这里和这里),但被拒绝,默认情况下池应该是无限的.最后,我决定至少要求为所有视图类型(这里)提供更方便的功能.
Che*_*amp 28
SnapHelper为您尝试的内容提供必要的框架,但需要扩展它以处理视图块.SnapToBlock下面的类扩展SnapHelper为捕捉到视图块.在这个例子中,我对一个块使用了四个视图,但它可以更多或更少.
更新:代码已更改以适应GridLayoutManager以及LinearLayoutManager.现在禁止投掷,因此捕捉更有效ViewPager.现在支持水平和垂直滚动以及LTR和RTL布局.
更新:更改了平滑滚动插补器更像ViewPager.
更新:为前/后捕捉添加回调.
更新:添加对RTL布局的支持.
以下是示例应用的快速视频:
如下设置布局管理器:
// 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));
Run Code Online (Sandbox Code Playgroud)
添加以下内容以附加SnapToBlock到RecyclerView.
SnapToBlock snapToBlock = new SnapToBlock(mMaxFlingPages);
snapToBlock.attachToRecyclerView(recyclerView);
Run Code Online (Sandbox Code Playgroud)
mMaxFlingPages 是允许一次抛出的最大块数(rowsCols*spans).
对于即将完成快照并已完成快照的回叫,请添加以下内容:
snapToBlock.setSnapBlockCallback(new SnapToBlock.SnapBlockCallback() {
@Override
public void onBlockSnap(int snapPosition) {
...
}
@Override
public void onBlockSnapped(int snapPosition) {
...
}
});
Run Code Online (Sandbox Code Playgroud)
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";
}
Run Code Online (Sandbox Code Playgroud)
SnapBlockCallback上面定义的接口可用于报告要捕捉的块开始处的视图的适配器位置.如果视图不在屏幕上进行调用,则可能无法实例化与该位置关联的视图.
| 归档时间: |
|
| 查看次数: |
6845 次 |
| 最近记录: |