本意是要了解一下Scroller的,但SnapHelper也算是Scroller的应用之一,本着先学会使用再尝试理解的用意,顺便看看这东西是怎么工作的。
先放一个自定义SimpleLinearSnapHelper,使用比较简单,看下注释就行。
SnapHelper的用处
用处?
其实没什么用,硬要说有的话…
拯救强迫症算不算…
所以官方命名为"helper"不是没有道理的,这代表着,只负责打辅助…也就是说,如果你不玩下路的话,其实是不需要的。
咳,回正题。
SnapHelper主要的作用是辅助操控RecyclerView的Fling过程。
Fling是什么意思??
有道词典了解一下。
对于屏幕来说,Fling就是一种滑动手势,一种有惯性的滑动。
Android本身提供了一个相当集全的手势监听器,其中就有fling动作的监听;
没有错,就是GestureDetector:
public class GestureDetector {
public interface OnGestureListener {
boolean onDown(MotionEvent e);
void onShowPress(MotionEvent e);
boolean onSingleTapUp(MotionEvent e);
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
void onLongPress(MotionEvent e);
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
public interface OnDoubleTapListener {
boolean onSingleTapConfirmed(MotionEvent e);
boolean onDoubleTap(MotionEvent e);
boolean onDoubleTapEvent(MotionEvent e);
}
public interface OnContextClickListener {
boolean onContextClick(MotionEvent e);
}
}
SnapHelper面对GestureDetector这样的老大哥,只能瑟瑟发抖:
//SnapHelper.java
public abstract class SnapHelper extends RecyclerView.OnFlingListener {
private final RecyclerView.OnScrollListener mScrollListener;
//各种代码
}
//RecyclerView.java
public abstract static class OnFlingListener {
public abstract boolean onFling(int velocityX, int velocityY);
}
public abstract static class OnScrollListener {
public void onScrollStateChanged(RecyclerView recyclerView, int newState){}
public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
}
RecyclerView表示惹不起,干脆自己新建了两个内部专用监听类OnFlingListener和OnScrollListener 。
总之,SnapHelper就是一个RecyclerView的Fling监听器,这么理解就可以了。
SnapHelper的用法
用法很简单,提供RecyclerView一个,SnapHelper一个,然后建立关系,收工。
就像这样:
rvDemo = findViewById(R.id.rv_demo);
rvDemo.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
SnapHelper lineSnapHelper = new LinearSnapHelper();
lineSnapHelper.attachToRecyclerView(rvDemo);
大多数应用场景下,RecyclerView本身提供的LinearSnapHelper和PagerSnapHelper就足够用了。
使用LinearSnapHelper对RecyclerView本身的滑动行为干预不明显,主要作用是让RecyclerView的Item不再自由滚动,而是总是中心对齐:
而PagerSnapHelper同样可以中心对齐,但滑动干预较大,它会抑制Fling过程,迫使滑动行为偏向于一个一个Item展示,类似ViewPager:
SnapHelper源码
先甩一张用plantuml画的类图,看看知道大概关系就行。
看后只想说—“贵圈真乱”。
…
不看图也没关系,只需要知道SnapHepler不仅本身继承了RecyclerView.OnFlingListener,还添加了一个RecyclerView.OnScrollListener对RecyclerView的滑动状态随时进行监听就可以了。
SnapHepler有三个抽象方法:
/**
* Override this method to snap to a particular point within the target view or the container
*
* @return the output coordinates the put the result into. out[0] is the distance
* on horizontal axis and out[1] is the distance on vertical axis.
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);
/**
* Override this method to provide a particular target view for snapping.
*
* @return the target view to which to snap on fling or end of scroll
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract View findSnapView(LayoutManager layoutManager);
/**
* Override to provide a particular adapter target position for snapping.
*
* @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION} if no snapping should happen
*/
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY);
大概理解下注释的意思就是:
- View findSnapView(LayoutManager layoutManager)
得到触发Fling或滚动结束(Fling结束)时的View,这意味着这个方法可能会在两种场景下调用 - int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY)
得到预计滚动结束时的Item索引,比如以RecyclerView水平中心的位置作为参考坐标,当前的Item为0,Fling后根据速度等参数计算后预计得出滚动结束时处于参考位置的Item索引为5 - int[] calculateDistanceToFinalSnap(LayoutManager layoutManager, View targetView)
得到一个大小为2数组,分别存储x轴与y轴的距离数值,此处“距离”是指targetView与参考位置的差值
(这里的【参考位置】是一个相对概念,比如LinearSnapHelper/PagerSnapHelper的参考位置都是设定为RecyclerView的中心位置坐标,横向列表为水平居中的位置坐标,竖向则为垂直居中的位置坐标)
那么这三个方法分别在哪里调用了?调用顺序又是怎样的?
先看看最开始建立关系的attachToRecyclerView有没有调用吧:
//SnapHelper.java
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
//就是这里了
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();
}
}
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);
}
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
attachToRecyclerView方法主要做了三件事:
- 通过setupCallbacks添加了OnFlingListener与OnScrollListener
- 创建了专属的Scroller
- 调用了snapToTargetExistingView
而snapToTargetExistingView方法中依次调用了findSnapView与calculateDistanceToFinalSnap,这里的逻辑很清晰:
- 通过findSnapView找到目标targetView
- 通过calculateDistanceToFinalSnap测算出与目标的距离snapDistance
- 调用smoothScrollBy滚动相应距离snapDistance
(可以想象,在一般的操作中,初次来到此方法,snapView大概率为null,并不会产生滚动行为。)
在此之前不是通过setupCallbacks添加过两个监听器么,来看看监听器中会有什么动作:
//SnapHelper.java
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
@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;
}
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
重点果然在两个监听器中。
首先看OnScrollListener ,这里只监听了一种状态并作出反馈,
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
复习RecyclerView的三种状态:
- public static final int SCROLL_STATE_IDLE = 0
RecyclerView未滚动或停止滚动时 - public static final int SCROLL_STATE_DRAGGING = 1
RecyclerView被拖拽时,此时处于被触摸状态 - public static final int SCROLL_STATE_SETTLING = 2
RecyclerView自动滚动状态,此时不处于被触摸状态
所以,OnScrollListener这里主要是为了在RecyclerView停止滚动时去调用snapToTargetExistingView方法,自然也就又调用到了findSnapView与calculateDistanceToFinalSnap方法。
接着看OnFlingListener:
//SnapHelper.java
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && snapFromFling(layoutManager, velocityX, velocityY);
虽然只有两行代码,但逻辑却不少,这里看到滑动的速度velocityY或velocityY必须有一个要大于minFlingVelocity 才能成功调用到snapFromFling方法,意思就是用户滑动RecyclerView的速度得”够快“才能触发snapFromFling方法。
那么看看snapFromFling究竟会做什么:
//SnapHelper.java
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
}
RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
这里的逻辑也很清晰:
- 通过createSnapScroller得到一个Scroller
- 调用findTargetSnapPosition方法得到最终需要滑动到的Item位置索引
- 设置Scroller属性,开始滚动startSmoothScroll
findTargetSnapPosition自不必说,需要自行实现,那么createSnapScroller呢?
源码中其实已经给出了一个:
//SnapHelper.java
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
if (!(layoutManager instanceof 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, mDecelerateInterpolator);
}
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
可以看到,calculateDistanceToFinalSnap再次出现了,那么这里的calculateDistanceToFinalSnap会不会调用?
答案是会的。
因为LinearSmoothScroller的onTargetFound会被调用。
至于为什么会调用到这里,说来话长~~
之前看到snapFromFling方法中的末尾调用一个方法:
layoutManager.startSmoothScroll(smoothScroller);
这个方法只是冰山一角,他真实的调用是这样的:
开不开心?
其中ViewFlinger实现了Runnable接口。
至此,三个抽象方法的调用处都知道了。
小结一下:
- findSnapView
- attachToRecyclerView–>snapToTargetExistingView–>
- onScrollStateChanged–>snapToTargetExistingView–>
- calculateDistanceToFinalSnap
- attachToRecyclerView–>snapToTargetExistingView–>
- onScrollStateChanged–>snapToTargetExistingView–>
- onFling–>snapFromFling–>createSnapScroller–>onTargetFound–>
- findTargetSnapPosition
- onFling–>snapFromFling–>
可以看到,基本都是由监听发起,这几个方法也必须用在监听中才能起到作用,尽在意料之中。忽略attachToRecyclerView的影响,就目前来看工作流程是这样的:
滑动速度相关
上面看到了onTargetFound方法的回调,至于另一个方法calculateSpeedPerPixel,可以看源码中的注释:
//LinearSmoothScroller.java
/**
* Calculates the scroll speed.
*
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
*/
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
这个方法返回值代表着滑动每像素耗时多少ms,那么真正的速度值就是这个值的倒数,也就是:
speed = displayMetrics.densityDpi / MILLISECONDS_PER_INCH;
densityDpi就是常说的DPI = dots-per-inch,屏幕密度,代表了每英寸多少个像素点。
这个DisplayMetrics也算是一个知识点,复习一下:
//ResourcesImpl.java
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
mConfiguration.setToDefaults();
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
mAssets.ensureStringBlocks();
}
//DisplayMetrics .java
public static int DENSITY_DEVICE = getDeviceDensity();
public void setToDefaults() {
widthPixels = 0;
heightPixels = 0;
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
xdpi = DENSITY_DEVICE;
ydpi = DENSITY_DEVICE;
noncompatWidthPixels = widthPixels;
noncompatHeightPixels = heightPixels;
noncompatDensity = density;
noncompatDensityDpi = densityDpi;
noncompatScaledDensity = scaledDensity;
noncompatXdpi = xdpi;
noncompatYdpi = ydpi;
}
private static int getDeviceDensity() {
// 当使用模拟器时qemu.sf.lcd_density可能会覆盖ro.sf.lcd_density
return SystemProperties.getInt("qemu.sf.lcd_density",
SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
}
别的不必深究,只需要知道DisplayMetrics在资源Resource初始化时就已经被赋值,这个值可以从系统属性中拿到,使用
adb shell getprop ro.sf.lcd_density
adb shell getprop qemu.sf.lcd_density
其中一个就可以拿到,我这里使用的是真机,拿到DPI为
在DisplayMetrics类中对应的
public static final int DENSITY_XXHIGH = 480;
LinearSmoothScroller中的MILLISECONDS_PER_INCH 值为25f,但是是private的,这个值其实是SnapHelper重新定的:
//SnapHelper.java
static final float MILLISECONDS_PER_INCH = 100f;
也就是我这里的真机在运行RecylcerView时,在SnapHelper的控制下,Scroller会将其滑动速度定为
480/100 = 4.8 px/ms
即4.8个像素每毫秒;
换言之,4800像素每秒…
emmmm,这应该是个理论速度值。
请务必忘掉这个数字,源码中用的不是它,而是它的倒数。
calculateSpeedPerPixel这个方法在LinearSmoothScroller中有明显的调用,其中包括了之前在OnTargetFound中调用的calculateTimeForDeceleration方法:
//LinearSmoothScroller.java
private static final float MILLISECONDS_PER_INCH = 25f;
private final float MILLISECONDS_PER_PX;
public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}
/**
* Calculates the time it should take to scroll the given distance (in pixels)
*
* @param dx Distance in pixels that we want to scroll
* @return Time in milliseconds
* @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
// In a case where dx is very small, rounding may return 0 although dx > 0.
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
// time.
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
protected int calculateTimeForDeceleration(int dx) {
// we want to cover same area with the linear interpolator for the first 10% of the
// interpolation. After that, deceleration will take control.
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
// which gives 0.100028 when x = .3356
// this is why we divide linear scrolling time with .3356
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
}
这里的MILLISECONDS_PER_PX就是之前的滑动每像素耗时多少ms,是速度的倒数,并不是速度;
Math.ceil,没什么复习的,向上取整,8.4取成9,-8.4取成-8这样子。
看一下原来的onTargetFound方法:
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, mDecelerateInterpolator);
}
}
总结一下,就可以得到公式:
time = distance * MILLISECONDS_PER_INCH / displayMetrics.densityDpi / 0.3356
依照之前的情景,在MILLISECONDS_PER_INCH = 100的情况下,DPI为480的手机上滑动1080像素耗时应为:
time = 1080 * 100/480 / 0.3356 = 670.44100...
这里计算出来的应该是ms值。
代表着1080像素的距离将在减速器的作用下使用670ms滑过。
当然这个0.3356很神秘,calculateTimeForDeceleration中的注释我暂时还不是太明白,似乎是为了顾及另外的一种算法,才在(1-(1-x)^2)与(1 - x/3) * x * x取了一个特殊值0.3356,(1-(1-x)^2)可以猜到是DecelerateInterpolator的算法:
//DecelerateInterpolator.java
public float getInterpolation(float input) {
float result;
if (mFactor == 1.0f) {
result = (float)(1.0f - (1.0f - input) * (1.0f - input));
} else {
result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
}
return result;
}
但另一个就不知道了,留待后来吧。
而action.update本身只负责更新值,其他都由之前的SmoothScroller与ViewFlinger负责驱动:
不太会画这玩意,大概是这意思,结合之前的那张时序图一起看,会发现这三者其实在接力合作,条件满足的情况下,ViewFlinger会不断的run,由此调用SmoothScroller的动画,Action.update的值也就有了用处。
因此,滑动速度可以通过改变calculateSpeedPerPixel的返回值来调整,准确地说,只能调整滑动时间,因为速度是由DecelerateInterpolator计算的,随时都在减小。
也就是说,我们可以过后重新创建新的LinearSmoothScroller来覆写calculateSpeedPerPixel,由此改变滑动时间,间接达到改变滑动速度的作用:
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
最好的方法当然是改变MILLISECONDS_PER_INCH 的值,SnapHelper中默认值为100,改成什么数字都可以马上计算比例,比如改为40,那么同样的DPI同样的滑动距离下,滑动耗时会缩短至40%,也就是说滑动速度平均会增大2.5倍。
我尝试将其改为400,可慢死我了:
看了下日志,把dx和time都打印出来:
com.xter.slimidea D/(HorizontalLinearSnapHelper.java:152)#calculateDistanceToFinalSnap-->: out=2016
com.xter.slimidea W/(HorizontalLinearSnapHelper.java:467)#onTargetFound-->: dx=2016,time=5006
按照公式应该为:
time = 2016 * 400/480 /0.3356 = 5005.9594...
误差非常小,问题不大。
这时应想到前面OnFling方法中的有一个值叫做minFlingVelocity:
@Override
public boolean onFling(int velocityX, int velocityY) {
//略过部分代码
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
这个值是作为一个下限值存在的,意为必须大于此下限值才能触发snapFromFling。
那么究竟是多少?
//RecyclerView.java
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//略过
final ViewConfiguration vc = ViewConfiguration.get(context);
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
}
//ViewConfiguration.java
private ViewConfiguration(Context context) {
//略过
mMinimumFlingVelocity = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewMinFlingVelocity);
mMaximumFlingVelocity = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewMaxFlingVelocity);
}
在android-26中有两个值,一个是在values/config.xml中,普通设备都是用的这里:
<!-- Minimum velocity to initiate a fling, as measured in dips per second. -->
<dimen name="config_viewMinFlingVelocity">50dp</dimen>
<!-- Maximum velocity to initiate a fling, as measured in dips per second. -->
<dimen name="config_viewMaxFlingVelocity">8000dp</dimen>
另一个在values-watch/config.xml中,应该是手表之类的设备专用,看起来下限很高:
<!-- Minimum velocity to initiate a fling, as measured in dips per second. -->
<dimen name="config_viewMinFlingVelocity">500dp</dimen>
<!-- Maximum velocity to initiate a fling, as measured in dips per second. -->
<dimen name="config_viewMaxFlingVelocity">8000dp</dimen>
结合Android屏幕适配的基础知识可以知道,dp的测量与DPI是成正相关的,在dpi=160的屏幕中,1dp=1sp=1px;同理,dpi=320的时候,1dp=1sp=2px。
在源码中体现出来就是:
//TypedValue.java
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
//DisplayMetrics.java
/**Thus on a 160dpi screen
* this density value will be 1; on a 120 dpi screen it would be .75; etc.
*
* @see #DENSITY_DEFAULT
*/
public float density;
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
public static final int DENSITY_MEDIUM = 160;
源码注释中说得很清楚了,metrics.density其实就是以160为基准的倍数,DP的计算就是与这个倍数作乘法。
那么在DPI为480的手机上,最小滑动速度必须达到50*3 = 150,才能触发snapFromFling。
至于速度的单位?
好吧,其实这又是个知识点,和VelocityTracker有关,当然还是从事情的开始讲起,开始的情况是——毫无意外地来到RecyclerView的onTouchEvent:
//RecyclerView.java
@Override
public boolean onTouchEvent(MotionEvent e) {
//略过部分代码
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final MotionEvent vtev = MotionEvent.obtain(e);
final int action = e.getActionMasked();
switch (action) {
//略过部分代码
case MotionEvent.ACTION_UP: {
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
final float xvel = canScrollHorizontally
? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically
? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
滑动速度确实是在手指抬起后触发ACTION_UP才开始计算的,注意一下computeCurrentVelocity,这个方法会根据传入的值计算单位速度,看源码注释:
//VelocityTracker.java
/**
*
* @param units The units you would like the velocity in. A value of 1
* provides pixels per millisecond, 1000 provides pixels per second, etc.
* @param maxVelocity The maximum velocity that can be computed by this method.
* This value must be declared in the same unit as the units parameter. This value
* must be positive.
*/
public void computeCurrentVelocity(int units, float maxVelocity) {
nativeComputeCurrentVelocity(mPtr, units, maxVelocity);
}
private static native void nativeComputeCurrentVelocity(long ptr, int units, float maxVelocity);
units影响到返回值,传入1则返回pixels per millisecond = ppms多少像素每毫秒,传入1000则返回pixels per second = pps,多少像素每秒。看起来也是可以传入其他值的,不过1和1000相对比较常用,传入其他值会显得反人类…比如传入2000,就是多少像素每2秒,emmmm…
很明显,RecyclerView选择的是pps,那么Fling中监听的速度值几乎可以确定也是pps了。想来应该不会中途再换单位,应该不会那么变态…
接上之前的问题,综合说一下:
在DPI为480的手机上,最小滑动速度必须达到50*3 = 150pps,也就是1秒内必须滑动过150个像素才能触发snapFromFling。
讲了这么多,结果还是没讲到怎样实现SnapHelper,怎样计算与参考位置对齐…下一篇再说吧,这种UI相关的内容逻辑太杂,疑问点较多,慢也正常,慢慢来吧。
以上。