RecyclerView的辅助类----SnapHelper使用及源码浅析 上

本意是要了解一下Scroller的,但SnapHelper也算是Scroller的应用之一,本着先学会使用再尝试理解的用意,顺便看看这东西是怎么工作的。
先放一个自定义SimpleLinearSnapHelper,使用比较简单,看下注释就行。

SnapHelper的用处

用处?
其实没什么用,硬要说有的话…
拯救强迫症算不算…

所以官方命名为"helper"不是没有道理的,这代表着,只负责打辅助…也就是说,如果你不玩下路的话,其实是不需要的。

咳,回正题。
SnapHelper主要的作用是辅助操控RecyclerView的Fling过程
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表示惹不起,干脆自己新建了两个内部专用监听类OnFlingListenerOnScrollListener

总之,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不再自由滚动,而是总是中心对齐:
LinearSnapHelper
PagerSnapHelper同样可以中心对齐,但滑动干预较大,它会抑制Fling过程,迫使滑动行为偏向于一个一个Item展示,类似ViewPager:
PagerSnapHelper

SnapHelper源码

先甩一张用plantuml画的类图,看看知道大概关系就行。
SnapHelper类结构
看后只想说—“贵圈真乱”。

不看图也没关系,只需要知道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方法主要做了三件事:

  1. 通过setupCallbacks添加了OnFlingListener与OnScrollListener
  2. 创建了专属的Scroller
  3. 调用了snapToTargetExistingView

snapToTargetExistingView方法中依次调用了findSnapViewcalculateDistanceToFinalSnap,这里的逻辑很清晰:

  1. 通过findSnapView找到目标targetView
  2. 通过calculateDistanceToFinalSnap测算出与目标的距离snapDistance
  3. 调用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);

虽然只有两行代码,但逻辑却不少,这里看到滑动的速度velocityYvelocityY必须有一个要大于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;
    }

这里的逻辑也很清晰:

  1. 通过createSnapScroller得到一个Scroller
  2. 调用findTargetSnapPosition方法得到最终需要滑动到的Item位置索引
  3. 设置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为
真机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负责驱动:
action

不太会画这玩意,大概是这意思,结合之前的那张时序图一起看,会发现这三者其实在接力合作,条件满足的情况下,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相关的内容逻辑太杂,疑问点较多,慢也正常,慢慢来吧。

以上。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
传人记-fx2n码是用于Mitsubishi FX2N系列可编程逻辑控制器(PLC)的代码。在使用之前,有一些必要的说明需要知道。 首先,要使用传人记-fx2n码,您需要具备一定的PLC编程知识和经验。这包括了解PLC的原理和基本功能,了解PLC编程语言(通常是LD、STL或者FBD)以及掌握相应的PLC编程软件(如Mitsubishi GX Developer等)。 其次,您需要熟悉FX2N系列PLC的硬件和软件环境。这包括了解PLC的输入输出模块、通信接口和其它特性,并在PLC上正确配置码所需的硬件和参数。此外,确保PLC的固件已经更新到最新版本,以避免软硬件兼容性问题。 在使用传人记-fx2n码之前,请先阅读相关文档和使用说明。这些文档通常包括码编写过程、程序功能和使用方法等内容。特别是查看注释部分,了解码的各个功能模块的作用和使用方法,以及相关的编程标准和规范。 在使用传人记-fx2n码时,务必遵守软件开发的最佳实践和PLC编程的规范。确保码逻辑清晰、模块化,并考虑其可扩展性和可维护性。同时,进行必要的测试和验证,确保码在实际应用中能够正常运行和达到预期的效果。 最后,如果在使用过程中遇到问题或者需要进一步的支持,建议参考相关的技术文档、论坛和学习资,或者与供应商或相关专业人士进行咨询和交流。这样可以更好地理解和应用传人记-fx2n码,提高开发效率和质量。 总之,使用传人记-fx2n码之前,需要具备PLC编程知识和经验,熟悉FX2N系列PLC的硬件和软件环境,并仔细阅读相关文档和使用说明。遵循最佳实践和规范,进行测试和验证,以确保码的正确使用和有效应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值