Android 使用RecyclerView SnapHelper详解
简介
这两个子类使用方式也很简单,只需要创建对象之后调用attachToRecyclerView()
附着到对应的RecyclerView对象上就可以了。
new LinearSnapHelper().attachToRecyclerView(mRecyclerView);
//或者
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
原理剖析
Fling操作
首先来了解一个概念,手指在屏幕上滑动RecyclerView然后松手,RecyclerView中的内容会顺着惯性继续往手指滑动的方向继续滚动直到停止,这个过程叫做Fling。Fling操作从手指离开屏幕瞬间被触发,在滚动停止时结束。
三个抽象方法
SnapHelper是一个抽象类,它有三个抽象方法:
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY)
public abstract View findSnapView(LayoutManager layoutManager)
该方法会找到当前layoutManager上最接近对齐位置的那个view,该view称为SanpView,对应的position称为SnapPosition。如果返回null,就表示没有需要对齐的View,也就不会做滚动对齐调整。
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);
这个方法会计算第二个参数对应的ItemView当前的坐标与需要对齐的坐标之间的距离。该方法返回一个大小为2的int数组,分别对应x轴和y轴方向上的距离。
attachToRecyclerView()
现在来看attachToRecyclerView()
这个方法,SnapHelper正是通过该方法附着到RecyclerView上,从而实现辅助RecyclerView滚动对齐操作。源码如下:
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
//如果SnapHelper之前已经附着到此RecyclerView上,不用进行任何操作
if (mRecyclerView == recyclerView) {
return;
}
//如果SnapHelper之前附着的RecyclerView和现在的不一致,清理掉之前RecyclerView的回调
if (mRecyclerView != null) {
destroyCallbacks();
}
//更新RecyclerView对象引用
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
//设置当前RecyclerView对象的回调
setupCallbacks();
//创建一个Scroller对象,用于辅助计算fling的总距离,后面会涉及到
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
//调用snapToTargetExistingView()方法以实现对SnapView的对齐滚动处理
snapToTargetExistingView();
}
}
attachToRecyclerView()
方法中会清掉SnapHelper之前保存的RecyclerView对象的回调(如果有的话),对新设置进来的RecyclerView对象设置回调,然后初始化一个Scroller对象,最后调用snapToTargetExistingView()
方法对SnapView进行对齐调整。snapToTargetExistingView()
该方法的作用是对SnapView进行滚动调整,以使得SnapView达到对齐效果。源码如下:
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
//找出SnapView
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
//计算出SnapView需要滚动的距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
//如果需要滚动的距离不是为0,就调用smoothScrollBy()使RecyclerView滚动相应的距离
if (snapDistance[] != || snapDistance[] != ) {
mRecyclerView.smoothScrollBy(snapDistance[], snapDistance[]);
}
}
可以看到,snapToTargetExistingView()
方法就是先找到SnapView,然后计算SnapView当前坐标到目的坐标之间的距离,然后调用RecyclerView.smoothScrollBy()
方法实现对RecyclerView内容的平滑滚动,从而将SnapView移到目标位置,达到对齐效果。RecyclerView.smoothScrollBy()
这个方法的实现原理这里就不展开了 ,它的作用就是根据参数平滑滚动RecyclerView的中的ItemView相应的距离。
setupCallbacks()和destroyCallbacks()
再看下SnapHelper对RecyclerView设置了哪些回调:
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
} private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
可以看出RecyclerView设置的回调有两个:一个是OnScrollListener对象mScrollListener.还有一个是OnFlingListener对象。由于SnapHelper实现了OnFlingListener接口,所以这个对象就是SnapHelper自身了.
先看下mScrollListener这个变量在怎样实现的.
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//mScrolled为true表示之前进行过滚动.
//newState为SCROLL_STATE_IDLE状态表示滚动结束停下来
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
} @Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != || dy != ) {
mScrolled = true;
}
}
};
该滚动监听器的实现很简单,只是在正常滚动停止的时候调用了snapToTargetExistingView()
方法对targetView进行滚动调整,以确保停止的位置是在对应的坐标上,这就是RecyclerView添加该OnScrollListener的目的。
public static abstract class OnFlingListener {
/**
* Override this to handle a fling given the velocities in both x and y directions.
* Note that this method will only be called if the associated {@link LayoutManager}
* supports scrolling and the fling is not handled by nested scrolls first.
*
* @param velocityX the fling velocity on the X axis
* @param velocityY the fling velocity on the Y axis
*
* @return true if the fling washandled, false otherwise.
*/
public abstract boolean onFling(int velocityX, int velocityY);
}
这个接口中就只有一个onFling()
方法,该方法会在RecyclerView开始做fling操作时被调用。我们来看看SnapHelper怎么实现onFling()
方法:
@Override
public boolean onFling(int velocityX, int velocityY) {
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter == null) {
return false;
}
//获取RecyclerView要进行fling操作需要的最小速率,
//只有超过该速率,ItemView才会有足够的动力在手指离开屏幕时继续滚动下去
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
//这里会调用snapFromFling()这个方法,就是通过该方法实现平滑滚动并使得在滚动停止时itemView对齐到目的坐标位置
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
注释解释得很清楚。看下snapFromFling()
怎么操作的:
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
int velocityY) {
//layoutManager必须实现ScrollVectorProvider接口才能继续往下操作
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
} //创建SmoothScroller对象,这个东西是一个平滑滚动器,用于对ItemView进行平滑滚动操作
RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
if (smoothScroller == null) {
return false;
} //通过findTargetSnapPosition()方法,以layoutManager和速率作为参数,找到targetSnapPosition
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
//通过setTargetPosition()方法设置滚动器的滚动目标位置
smoothScroller.setTargetPosition(targetPosition);
//利用layoutManager启动平滑滚动器,开始滚动到目标位置
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
snapFromFling()
方法会先判断layoutManager是否实现了ScrollVectorProvider接口,如果没有实现该接口就不允许通过该方法做滚动操作。那为啥一定要实现该接口呢?待会再来解释。接下来就去创建平滑滚动器SmoothScroller的一个实例,layoutManager可以通过该平滑滚动器来进行滚动操作。SmoothScroller需要设置一个滚动的目标位置,我们将通过findTargetSnapPosition()
方法来计算得到的targetSnapPosition给它,告诉滚动器要滚到这个位置,然后就启动SmoothScroller进行滚动操作。setTargetPosition()
方法设置的SmoothScroller只能将对应位置的ItemView滚动到与RecyclerView的边界对齐,那怎么实现将该ItemView滚动到我们需要对齐的目标位置呢?就得对SmoothScroller进行一下处理了。createSnapScroller()
方法创建得到的:@Nullable
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
//同样,这里也是先判断layoutManager是否实现了ScrollVectorProvider这个接口,
//没有实现该接口就不创建SmoothScroller
if (!(layoutManager instanceof ScrollVectorProvider)) {
return null;
}
//这里创建一个LinearSmoothScroller对象,然后返回给调用函数,
//也就是说,最终创建出来的平滑滚动器就是这个LinearSmoothScroller
return new LinearSmoothScroller(mRecyclerView.getContext()) {
//该方法会在targetSnapView被layout出来的时候调用。
//这个方法有三个参数:
//第一个参数targetView,就是本文所讲的targetSnapView
//第二个参数RecyclerView.State这里没用到,先不管它
//第三个参数Action,这个是什么东西呢?它是SmoothScroller的一个静态内部类,
//保存着SmoothScroller在平滑滚动过程中一些信息,比如滚动时间,滚动距离,差值器等
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
//calculateDistanceToFinalSnap()方法上面解释过,
//得到targetSnapView当前坐标到目的坐标之间的距离
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[];
final int dy = snapDistances[];
//通过calculateTimeForDeceleration()方法得到做减速滚动所需的时间
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > ) {
//调用Action的update()方法,更新SmoothScroller的滚动速率,使其减速滚动到停止
//这里的这样做的效果是,此SmoothScroller用time这么长的时间以mDecelerateInterpolator这个差值器的滚动变化率滚动dx或者dy这么长的距离
action.update(dx, dy, time, mDecelerateInterpolator);
}
} //该方法是计算滚动速率的,返回值代表滚动速率,该值会影响刚刚上面提到的
//calculateTimeForDeceleration()的方法的返回返回值,
//MILLISECONDS_PER_INCH的值是100,也就是说该方法的返回值代表着每dpi的距离要滚动100毫秒
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
通过以上的分析可以看到,createSnapScroller()
创建的是一个LinearSmoothScroller,并且在创建该LinearSmoothScroller的时候主要考虑两个方面:
- 第一个是滚动速率,由
calculateSpeedPerPixel()
方法决定; - 第二个是在滚动过程中,targetView即将要进入到视野时,将匀速滚动变换为减速滚动,然后一直滚动目的坐标位置,使滚动效果更真实,这是由
onTargetFound()
方法决定。
刚刚不是留了一个疑问么?就是正常模式下SmoothScroller通过setTargetPosition()
方法设置的ItemView只能滚动到与RecyclerView边缘对齐,而解决这个局限的处理方式就是在SmoothScroller的onTargetFound()
方法中了。onTargetFound()
方法会在SmoothScroller滚动过程中,targetSnapView被layout出来时调用。而这个时候利用calculateDistanceToFinalSnap()
方法得到targetSnapView当前坐标与目的坐标之间的距离,然后通过Action.update()
方法改变当前SmoothScroller的状态,让SmoothScroller根据新的滚动距离、新的滚动时间、新的滚动差值器来滚动,这样既能将targetSnapView滚动到目的坐标位置,又能实现减速滚动,使得滚动效果更真实。
onTargetFound()
方法被调用)并不是紧挨着界面上的Item,而是会有一定的提前,这是由于RecyclerView为了优化性能,提高流畅度,在滑动滚动的时候会有一个预加载的过程,提前将Item给layout出来了,这个知识点涉及到的内容很多,这里做个理解就可以了,不详细细展开了,以后有时间会专门讲下RecyclerView的相关原理机制。onFling()
方法会在RecyclerView触发Fling操作时调用。在onFling()
方法中判断当前方向上的速率是否足够做滚动操作,如果速率足够大就调用snapFromFling()
方法实现滚动相关的逻辑。在snapFromFling()
方法中会创建一个SmoothScroller,并且根据速率计算出滚动停止时的位置,将该位置设置给SmoothScroller并启动滚动。而滚动的操作都是由SmoothScroller全权负责,它可以控制Item的滚动速度(刚开始是匀速),并且在滚动到targetSnapView被layout时变换滚动速度(转换成减速),以让滚动效果更加真实。所以,SnapHelper辅助RecyclerView实现滚动对齐就是通过给RecyclerView设置OnScrollerListenerh和OnFlingListener这两个监听器实现的。
LinearSnapHelper
SnapHelper辅助RecyclerView滚动对齐的框架已经搭好了,子类只要根据对齐方式实现那三个抽象方法就可以了。以LinearSnapHelper为例,看它到底怎么实现SnapHelper的三个抽象方法,从而让ItemView滚动居中对齐:
calculateDistanceToFinalSnap()
@Override
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[];
//水平方向滚动,则计算水平方向需要滚动的距离,否则水平方向的滚动距离为0
if (layoutManager.canScrollHorizontally()) {
out[] = distanceToCenter(layoutManager, targetView,
getHorizontalHelper(layoutManager));
} else {
out[] = ;
} //竖直方向滚动,则计算竖直方向需要滚动的距离,否则水平方向的滚动距离为0
if (layoutManager.canScrollVertically()) {
out[] = distanceToCenter(layoutManager, targetView,
getVerticalHelper(layoutManager));
} else {
out[] = ;
}
return out;
}
该方法是返回第二个传参对应的view到RecyclerView中间位置的距离,可以支持水平方向滚动和竖直方向滚动两个方向的计算。最主要的计算距离的这个方法distanceToCenter()
:
private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//找到targetView的中心坐标
final int childCenter = helper.getDecoratedStart(targetView) +
(helper.getDecoratedMeasurement(targetView) / );
final int containerCenter;
//找到容器(RecyclerView)的中心坐标
if (layoutManager.getClipToPadding()) {
containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / ;
} else {
containerCenter = helper.getEnd() / ;
}
//两个中心坐标的差值就是targetView需要滚动的距离
return childCenter - containerCenter;
}
https://www.jianshu.com/p/e54db232df62
Android 使用RecyclerView SnapHelper详解的更多相关文章
- Android 高级UI设计笔记07:RecyclerView 的详解
1. 使用RecyclerView 在 Android 应用程序中列表是一个非常重要的控件,适用场合非常多,如新闻列表.应用列表.消息列表等等,但是从Android 一出生到现在并没有非常 ...
- Android RecyclerView使用详解(三)
在上一篇(RecyclerView使用详解(二))文章中介绍了RecyclerView的多Item布局实现,接下来要来讲讲RecyclerView的Cursor实现,相较于之前的实现,Cursor有更 ...
- Android RecyclerView使用详解(二)
在上一篇(RecyclerView使用详解(一))文章中简单的介绍了RecyclerView的基本用法,接下来要来讲讲RecyclerView的更多用法,要实现不同的功能效果,大部分都还是在于Recy ...
- ANDROID L——Material Design详解(UI控件)
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! Android L: Google已经确认Android L就是Android Lolli ...
- 《Android NFC 开发实战详解 》简介+源码+样章+勘误ING
<Android NFC 开发实战详解>简介+源码+样章+勘误ING SkySeraph Mar. 14th 2014 Email:skyseraph00@163.com 更多精彩请直接 ...
- Android开发之InstanceState详解
Android开发之InstanceState详解 本文介绍Android中关于Activity的两个神秘方法:onSaveInstanceState() 和 onRestoreInstanceS ...
- android bundle存放数据详解
转载自:android bundle存放数据详解 正如大家所知道,Activity之间传递数据,是将数据存放在Intent或者Bundle中 例如: 将数据存放倒Intent中传递: 将数据放到Bun ...
- Cordova 打包 Android release app 过程详解
Cordova 打包 Android release app 过程详解 时间 -- :: SegmentFault 原文 https://segmentfault.com/a/119000000517 ...
- Android中Service(服务)详解
http://blog.csdn.net/ryantang03/article/details/7770939 Android中Service(服务)详解 标签: serviceandroidappl ...
随机推荐
- 关于如何食用Xcode——用mac的小蒟蒻
前言QwQ 对于一只用Mac的小蒟蒻,没有Dev_c++简直太难受了,用在线IDE写代码又没法保存,那么我们怎么办呢? 好在App Store里有这个好东西 所以我们今天来介绍一下 “如何使用Xcod ...
- 课程一(Neural Networks and Deep Learning),第三周(Shallow neural networks)—— 0、学习目标
Learn to build a neural network with one hidden layer, using forward propagation and backpropagation ...
- vue通过webpack打包后怎么运行
1. 成功使用webpack打包完成后会默认得到dist的文件夹 2. dist文件夹中有html与其他的静态文件 3. 在dist文件夹中打开命令窗口或者git,开一个服务器(像anywhere) ...
- Log4j最佳实践
本文是结合项目中使用Log4j总结的最佳实践,非转载.网上可以找到的是这一篇<Log4j最佳实践>.本来Log4j使用是非常简单的,无需多介绍其用法,这只是在小型项目中:但在大型的项目中使 ...
- This Gradle plugin requires Studio 3.0 minimum
从github上下载的项目遇到一个问题:Error:This Gradle plugin requires Studio 3.0 minimum 意思就是说studio版本不高,导入的项目的版本是3. ...
- 多线程编程(六)-Executor与ThreadPoolExecutor的使用
使用Executors工厂类创建线程池 1.使用newCachedThreadPool()方法创建无界线程池 newCachedThreadPool()方法创建的是无界线程池,可以进行线程自动回收,此 ...
- Deep learning with Python 学习笔记(6)
本节介绍循环神经网络及其优化 循环神经网络(RNN,recurrent neural network)处理序列的方式是,遍历所有序列元素,并保存一个状态(state),其中包含与已查看内容相关的信息. ...
- core Animation之CAAnimationGroup(动画群组)
可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行 属性解析: animations:用来保存一组动画对象的NSArray 默认情况下,一组动画对象 ...
- 自定义MVC的Helper扩展方法
记得在开发ASP.NET时候,也经常性使用C#可以写自己义的扩展方法,如:http://www.cnblogs.com/insus/p/3154363.html 或http://www.cnblogs ...
- windows下nodejs监听80端口
windows下nodejs监听80端口时提示端口被占用报错,解决方案如下: 1.cmd---netstat -ano查看是什么程序占用了80端口: 2.控制面板--管理工具--服务--停止 SQL ...