RV怎么翻滚到指定索引
前语
看到标题或许有同学有点疑问,这不是有手就行? 且慢,听我渐渐道来。
确实,RV 内部提供了一系列的翻滚办法:
scrollTo,scrollBy,scrollToPosition ,还有一系列的 smoothScrollTo,smoothScrollBy,smoothScrollToPosition。甚至还有嵌套的 nestedScrollBy,nestedScrollByInternal等等。
难道这些都不能完结指定到翻滚索引的逻辑?额,当然能,可是又不是那么能!
为什么这么说?或许咱们对 翻滚到指定索引
这个需求的理解有所误差。
谷歌理解的 翻滚到指定索引
是当时索引在屏幕上可见了就到达意图,而咱们需求的作用是展现到指定索引并且在顶部展现。
那这又有什么区别?
比方咱们要翻滚到第 75 的索引,那么当这个 Item 在屏幕中间,或许在屏幕上面,或许在屏幕下面,三种状况翻滚到索引的作用都是不同的。
从屏幕上翻滚到 75 索引,是契合咱们的预期,展现出来也是在顶部展现,可是假如从屏幕下翻滚到 75 索引,就只会出现在底部,而假如 75 索引的 Item 本来就在屏幕中间,那么点击回到索引则无反响。在谷歌看来它现已是在屏幕中了。
所以为了完结 翻滚到指定索引并在顶部展现
这个作用,本文才对 RV 的翻滚做了一些兼容操作,尝试性的出一篇文章讨论一下。
本文并没有涉及到源码,全程轻松愉快容易理解,下面开端正文 ↓
一、scrollToPosition的运用
首要不管是 scrollToPosition 还是 smoothScrollToPosition 都是由 LayoutManager 办理与完结的。
所以关于 scrollToPosition 咱们其实调用 LayoutManager 的办法也是能完结的:
layoutManager.scrollToPositionWithOffset(position, 0) layoutManager.scrollToPosition(position)
其次,scrollToPosition 与 smoothScrollToPosition 的基本是有区别的。
scrollToPosition 内部其实仅仅 requestLayout 从头布局罢了,办法写的是scroll,可是并没有滚。能够理解为仅仅相当于改写了布局罢了。
而 smoothScrollToPosition 是实在的翻滚了,由 RecyclerView.SmoothScroller 办理,而咱们常用的 LinearLayoutManager 内部也是用的默许完结的 LinearSmoothScroller 来办理翻滚的。
大部分状况下都是够咱们用的了,假如想要一些特殊作用也能够自定义 LinearLayoutManager 与 LinearSmoothScroller 自己办理翻滚,也能够重写部分办法到达想要的作用,比方翻滚的间隔操控,翻滚的速度操控等。
咱们先看看前语中的三种作用,究竟是不是对的,下面给出简易代码:
val datas = arrayListOf<String>()
for (i in 0..99) {
datas.add("Item 内容 $i")
}
//RV绑定Adapter
mBinding.recyclerView.vertical()
.bindData(datas, R.layout.item_custom_jobs) { holder, t, _ ->
holder.setText(R.id.tv_job_text, t)
}
.divider(Color.BLACK)
.scrollToPosition(50)
mBinding.btnScollTo.click {
mBinding.recyclerView.scrollToPosition(75)
}
下面给出 GIF 的图片演示:
我要翻滚的是第 75 个索引,可是这个 Item 要么就不收效,要么就在底部展现,这并不契合我(产品)的要求。
没办法,只能对翻滚作用对这三种状况别离做处理,(我知道 scrollToPositionWithOffset
好用),可是或许部分同学的RV版本并没有那么高,还是分状况判别兼容性更好一点。
修正代码如下:
private fun rvScrollToPosition(rv: RecyclerView, layoutManager: LinearLayoutManager, position: Int) {
val firstPos = layoutManager.findFirstVisibleItemPosition()
val lastPos: Int = layoutManager.findLastVisibleItemPosition()
YYLogUtils.w("firstPos:$firstPos lastPos:$lastPos position:$position")
if (position <= firstPos) {
//当要置顶的项在当时显现的第一个项的前面时
rv.scrollToPosition(position)
} else if (position <= lastPos) {
//当要置顶的项现已在屏幕上显现时
val childAt: View? = layoutManager.findViewByPosition(position)
var top = childAt?.top ?: 0
rv.scrollBy(0, top)
} else {
//当要置顶的项在当时显现的最后一项之后
layoutManager.scrollToPositionWithOffset(position, 0)
}
}
那么咱们经过这个办法去翻滚的话,那么作用如下:
没错这样才是我(产品)想要的作用!
二、smoothScrollToPosition的运用
虽然能完结作用了,可是有些时分,我(产品)更喜爱用一些翻滚作用,这中选中作用太突兀了,只适合一些初始化选中的作用,当用户点击按钮或操作之后,咱们的 RV 缓缓翻滚到指定的索引位置,看起来很美!
咱们先试试原生的 smoothScrollToPosition 运用作用,还是分为上面的三种状况,那么作用便是如下:
还是会有相同的问题,那么咱们能不能经过像上面相同的办法来判别呢?能,又不能。
思路是一个思路,可是完结的进程不同了,由于不同的间隔的翻滚进程与翻滚时长是不同的,所以咱们至少需求在翻滚完结之后的监听中进行处理,可是咱们有翻滚完结的监听吗?没有!
所以咱们只能间接的经过RV的翻滚监听来完结是否现已完结翻滚
mBinding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (smoothScrolling || newState == SCROLL_STATE_IDLE) {
val lastPos: Int = layoutManager.findLastVisibleItemPosition()
if (smoothScrollPosition >= 0 && lastPos == smoothScrollPosition) {
val childAt: View? = layoutManager.findViewByPosition(lastPos)
var top = childAt?.top ?: 0
recyclerView.scrollBy(0, top)
mBinding.recyclerView.removeOnScrollListener(this)
smoothScrollPosition = -1
}
smoothScrolling = false
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
}
})
作用为:
这不就行了吗?
下面给出完好的工具类办法,假如咱们想要横向的翻滚或许其他 LayoutManager 的作用,稍作修正即可:
object RVScrollUtils {
/**
* 缓慢翻滚
*/
fun rvSmoothScrollToPosition(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int) {
var smoothScrolling = true
val firstPos: Int = layoutManager.findFirstVisibleItemPosition()
val lastPos: Int = layoutManager.findLastVisibleItemPosition()
if (position in (firstPos + 1) until lastPos) {
val childAt: View? = layoutManager.findViewByPosition(position)
var top = childAt?.top ?: 0
recyclerView.smoothScrollBy(0, top)
} else {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (smoothScrolling || newState == RecyclerView.SCROLL_STATE_IDLE) {
if (position in layoutManager.findFirstVisibleItemPosition() + 1..layoutManager.findLastVisibleItemPosition()) {
val childAt: View? = layoutManager.findViewByPosition(position)
val top = childAt?.top ?: 0
recyclerView.scrollBy(0, top)
recyclerView.removeOnScrollListener(this)
}
smoothScrolling = false
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
}
})
recyclerView.smoothScrollToPosition(position)
}
}
/**
* 直接跳转改写Layout
*/
fun rvScrollToPosition(rv: RecyclerView, layoutManager: LinearLayoutManager, position: Int) {
val firstPos = layoutManager.findFirstVisibleItemPosition()
val lastPos: Int = layoutManager.findLastVisibleItemPosition()
if (position <= firstPos) {
//当要置顶的项在当时显现的第一个项的前面时
rv.scrollToPosition(position)
} else if (position <= lastPos) {
//当要置顶的项现已在屏幕上显现时,经过LayoutManager
val childAt: View? = layoutManager.findViewByPosition(position)
var top = childAt?.top ?: 0
rv.scrollBy(0, top)
} else {
//当要置顶的项在当时显现的最后一项之后
layoutManager.scrollToPositionWithOffset(position, 0)
}
}
}
三、smoothScroll的速度操控
产品:不错,作用不错,可是还差了那么一丢丢。 开发:这不挺好的吗?翻滚作用不错。 产品:你这个隔的远的翻滚时刻长,隔的近的翻滚时刻短,作用不统一,我想要的是不管远近都要翻滚时刻统一。 开发:你这什么鬼需求,就不契合物理学规律,牛顿的棺材… 哎哎哎,有话好好说,快把刀放下,又没说不能做,急什么…
虽然说体系的默许翻滚作用以及能满足绝大部分的需求了,可是总有一些奇葩的需求需求一些定制,咱们也能经过重写一些 LayoutManager 等类,能够自己操控股翻滚的间隔与翻滚的速度。
LayoutManager 本身是负责 RV 的布局展现的,内部的 翻滚
逻辑是交由LinearSmoothScroller 来完结的。
那么怎么获取翻滚的间隔呢?咱们需求重写 onTargetFound 办法,内部的参数是需求翻滚到的 ItemView 目标,然后经过体系办法 calculateDyToMakeVisible 级能够核算出需求翻滚的间隔。
计划一:指定翻滚时刻
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
//获取翻滚间隔
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
//依据翻滚间隔核算时刻
final int time = calculateTimeForDeceleration(distance);
YYLogUtils.w("打印需求翻滚的时刻与间隔,distance:"+distance + " time:"+time);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}
打印成果如下:
能够看到确实是翻滚的间隔越长,所需求的时刻也是越长的。假如咱们需求修正翻滚的时刻,那么还需求修正翻滚的速度,应该这个 calculateTimeForDeceleration 办法,假如想定死翻滚的时长咱们能够直接重写 calculateTimeForDeceleration 或 calculateTimeForScrolling 即可。
@Override
protected int calculateTimeForDeceleration(int dx) {
return 5000;
}
打印日志:
作用便是:
咱们改为实在的 250ms 之后感觉还行,可是假如翻滚间隔太长,而实际动画时刻太短,会导致更难看的作用:
产品看了这个作用直拍脑门。。。这作用不太行啊。那能不能动态的改动翻滚速度呢?
计划二:指定翻滚速度
先说怎么改变翻滚速度,咱们只需求重写 calculateSpeedPerPixel 办法即可,内部完结滑动一个像素需求多少毫秒。
比方:
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
//滑动一个像素需求多少毫秒
return 25f / displayMetrics.density;
}
作用为:
假如想更快或更慢,就能够自己调试。那么再接上面的需求,咱们就能够修正速度不就行了吗?当间隔隔得比较远的时分咱们就设置速度快一些,当隔的比较近的时分咱们设置速度慢一些。
public class SmoothLinearLayoutManager extends LinearLayoutManager {
private float MILLISECONDS_PER_INCH = 25f;
private Context contxt;
public SmoothLinearLayoutManager(Context context) {
super(context);
this.contxt = context;
}
public SmoothLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
this.contxt = context;
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
private int distance = 0;
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
//获取翻滚间隔
distance = (int) Math.sqrt(dx * dx + dy * dy);
//依据翻滚间隔核算时刻
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return super.computeScrollVectorForPosition(targetPosition);
}
@Override
protected int calculateTimeForDeceleration(int dx) {
return 250;
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 15f / displayMetrics.densityDpi;
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
}
再从远处滚到到一个长间隔的索引的作用:
计划三:自定义翻滚与改写
其实到这儿现已基本满足产品的需求了,可是咱们寻求细节的话,其实也能够看到从 0 到 75 的索引是稍微大于 250ms 的。
为什么呢?这就要看源码…,好吧直接讲结论。
其实 RV 的翻滚原理便是从第一帧的动画回调开端就开端找 View ,检查当时 Position 是否在屏幕上了。假如指定的 View 没有在屏幕上,那么就履行 onSeekTargetStep 持续找,假如不在就持续找,一直到找到View在屏幕上了才会调用 onTargetFound 办法。所以咱们上面的办法直接从 onTargetFound 拿参数就现已是晚了。现已履行了N次 onTargetFound 和动画办法了。仅仅咱们设置了动画时刻短显得比较快罢了。
//太远了,没有找到View
@Override
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
YYLogUtils.w("太远了,没有找到View dy:"+dy);
super.onSeekTargetStep(dx, dy, state, action);
}
//渐渐滚渐渐找,找到了!
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
YYLogUtils.w("渐渐滚渐渐找,找到了");
//下面才开端翻滚到实在的位置
}
能够看到调用的次序:
所以假如真的要针对性的优化这一点话,咱们能够绕过这些流程
,直接做到另一种作用:假如需求翻滚的间隔大于一屏高度,咱们就只翻滚一屏的高度,然后直接改写到指定的位置,比方:scrollToPositionWithOffset 。
咱们修正代码如下:
LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
boolean startScrolling = false;
//太远了,没有找到View
@Override
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
if (!startScrolling) {
startScrolling = true;
int height = recyclerView.getMeasuredHeight();
recyclerView.smoothScrollBy(0, height);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {
if (startScrolling && newState == RecyclerView.SCROLL_STATE_IDLE) {
scrollToPositionWithOffset(position, 0);
recyclerView.removeOnScrollListener(this);
startScrolling = false;
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
}
});
}
}
//渐渐滚渐渐找,找到了!
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
//获取翻滚间隔
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
//依据翻滚间隔核算时刻
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return super.computeScrollVectorForPosition(targetPosition);
}
@Override
protected int calculateTimeForDeceleration(int dx) {
return 250;
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return 15f / displayMetrics.densityDpi;
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
这样便是先滚着,翻滚到指定间隔之后再改写到指定的索引:
看似还行,可是这个计划有一点不完美,便是翻滚完结之后改写的那一下卡顿作用有一点突兀。
计划四:自定义改写与翻滚
那其实咱们换一个思路,先改写到离当时 Position 的一屏幕间隔然后再滚过去不就行了吗?
听起来就比较靠谱,这儿分为索引的完结办法与间隔的完结办法:
@Override
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
YYLogUtils.w("太远了,没有找到View dy:" + dy + " action-dy:" + action.getDy() + " action-du:" + action.getDuration());
//实在场景需求判别索引与方向
if (!startScrolling) {
startScrolling = true;
int firstPos = findFirstVisibleItemPosition();
//依据实在场景判别是否超越索引边界与展现边界
if (firstPos < position) {
scrollToPositionWithOffset(position - 10, 0);
} else {
scrollToPositionWithOffset(position + 10, 0);
}
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView rv, int newState) {
if (startScrolling && newState == RecyclerView.SCROLL_STATE_IDLE) {
recyclerView.removeOnScrollListener(this);
startScrolling = false;
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
}
});
recyclerView.smoothScrollToPosition(position);
}
}
下面一种是依据间隔来完结:
@Override
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
YYLogUtils.w("太远了,没有找到View dy:" + dy + " action-dy:" + action.getDy() + " action-du:" + action.getDuration());
//实在场景需求判别索引与方向
int firstPos = findFirstVisibleItemPosition();
int lastPos = findLastVisibleItemPosition();
PointF pointF = computeScrollVectorForPosition(position);
int height = recyclerView.getMeasuredHeight();
float distance = Math.abs((position - firstPos) * getDecoratedMeasuredHeight(getChildAt(0))) / pointF.y;
if (distance > 0) {
recyclerView.scrollBy(0, (int) distance - height);
} else {
recyclerView.scrollBy(0, (int) distance + height);
}
recyclerView.smoothScrollToPosition(position);
}
作用,从0 翻滚到 75 索引:
这生成的都是什么鬼GIF 。原谅我这录制工具…由于不是录屏是MP4转的,作用欠好,咱们有条件能够去自行完结或运行Demo。
总结
看到这儿咱们应该对这些翻滚作用有所了解,怎么 scrollToPosition 并置顶,怎么 smoothScrollToPosition 并置顶。
这也是咱们常用的作用,一般来说咱们只用到上面的几种办法即可,假如要完结产品这种固定时长的翻滚的类似作用,咱们也能够参阅第三点的四种计划来完结。
由于这些翻滚作用是跟事务逻辑关联的,许多当地都是伪代码,并没有完善也没有解决索引越界之类的问题,假如咱们有需求还是需求参阅来完结的。
惯例了,我如有解说不到位或错漏的当地,期望同学们能够指出。
我知道各位大神都有各种骚操作完结这些作用,假如有更好的办法或其他办法,或许你有遇到的坑也都能够在谈论区沟通一下,咱们互相学习进步嘛。
本文的部分代码能够在我的 Kotlin 测验项目中看到,【传送门】。你也能够关注我的这个Kotlin项目,我有时刻都会持续更新。
Ok,这一期就此完结。
本文正在参加「金石计划」