内容简介:很久没有写Android控件了,正好最近项目有个自定义控件的需求,整理了下做个总结,主要是实现类似于抖音翻页的效果,但是有有点不同,需要在底部漏出后面的view,这样说可能不好理解,看下Demo,按页滑动,后面的View有放大缩放的动画,滑动速度过小时会有回到原位的效果,下滑也是按页滑动的效果。有的小伙伴可能说这个用如果把自定义
很久没有写Android控件了,正好最近项目有个自定义控件的需求,整理了下做个总结,主要是实现类似于抖音翻页的效果,但是有有点不同,需要在底部漏出后面的view,这样说可能不好理解,看下Demo,按页滑动,后面的View有放大缩放的动画,滑动速度过小时会有回到原位的效果,下滑也是按页滑动的效果。
有的小伙伴可能说这个用 SnapHelper
就可以了,没错,翻页是要结合这个,但是也不是纯粹靠这个,因为底部需要漏出来后面的view,所以 LayoutManager
就不能简单的使用 LinearLayoutManager
,需要去自定义 LayoutManager
,然后再自定义 SnapHelper
。
如果把自定义 LayoutManager
和 SnapHelper
放在一篇里面会太长,所以我们今天主要分析 SnapHelper
。
本文分析的源码是基于 recyclerview-v7-26.1.0
1. Scroll
和 Fling
这方面参考我的上篇分享: RecyclerView之Scroll和Fling
总结一下调用栈就是:
SnapHelper onFling ---> snapFromFling 复制代码
上面得到最终位置 targetPosition
,把位置给 RecyclerView.SmoothScroller
, 然后就开始滑动了:
RecyclerView.SmoothScroller start --> onAnimation 复制代码
在滑动过程中如果 targetPosition
对应的 targetView
已经layout出来了,就会回调 SnapHelper
,然后计算得到到当前位置到 targetView
的距离 dx,dy
SnapHelper onTargetFound ---> calculateDistanceToFinalSnap 复制代码
然后把距离 dx,dy
更新给 RecyclerView.Action
:
RecyclerView.Action update --> runIfNecessary --> recyclerView.mViewFlinger.smoothScrollBy 复制代码
最后调用 RecyclerView.ViewFlinger
, 然后又回到 onAnimation
class ViewFlinger implements Runnable public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) { if (mInterpolator != interpolator) { mInterpolator = interpolator; mScroller = new OverScroller(getContext(), interpolator); } setScrollState(SCROLL_STATE_SETTLING); mLastFlingX = mLastFlingY = 0; mScroller.startScroll(0, 0, dx, dy, duration); postOnAnimation(); } 复制代码
2. SnapHelper
源码分析
上面其实已经接触到部分的 SnapHelper
源码, SnapHelper
其实是一个抽象类,有三个抽象方法:
/** * Override to provide a particular adapter target position for snapping. * * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView} * @param velocityX fling velocity on the horizontal axis * @param velocityY fling velocity on the vertical axis * * @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); /** * Override this method to snap to a particular point within the target view or the container * view on any axis. * <p> * This method is called when the {@link SnapHelper} has intercepted a fling and it needs * to know the exact distance required to scroll by in order to snap to the target view. * * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView} * @param targetView the target view that is chosen as the view to snap * * @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. * <p> * This method is called when the {@link SnapHelper} is ready to start snapping and requires * a target view to snap to. It will be explicitly called when the scroll state becomes idle * after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap * after a fling and requires a reference view from the current set of child views. * <p> * If this method returns {@code null}, SnapHelper will not snap to any view. * * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached * {@link RecyclerView} * * @return the target view to which to snap on fling or end of scroll */ @SuppressWarnings("WeakerAccess") @Nullable public abstract View findSnapView(LayoutManager layoutManager); 复制代码
上面三个方法就是我们重写 SnapHelper
需要实现的,很重要,简单介绍下它们的作用和调用时机:
findTargetSnapPosition
用来找到最终的目标位置,在fling操作刚触发的时候会根据速度计算一个最终目标位置,然后开始fling操作 calculateDistanceToFinalSnap
这个用来计算滑动到最终位置还需要滑动的距离,在一开始 attachToRecyclerView
或者targetView layout的时候会调用 findSnapView
用来找到上面的targetView,就是需要对其的view,在 calculateDistanceToFinalSnap
调用之前会调用该方法。
我们看下 SnapHelper
怎么用的,其实就一行代码:
this.snapHelper.attachToRecyclerView(view); 复制代码
SnapHelper
正是通过该方法附着到RecyclerView上,从而实现辅助RecyclerView滚动对齐操作,那我们就从上面的 attachToRecyclerView
开始入手:
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(); } } 复制代码
在 attachToRecyclerView()
方法中会清掉 SnapHelper
之前保存的 RecyclerView
对象的回调(如果有的话),对新设置进来的 RecyclerView
对象设置回调,然后初始化一个 Scroller
对象,最后调用 snapToTargetExistingView()
方法对SnapView进行对齐调整。
snapToTargetExistingView()
该方法的作用是对SnapView进行滚动调整,以使得SnapView达到对齐效果。
看下源码:
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]); } } 复制代码
snapToTargetExistingView()
方法就是先找到 SnapView
,然后计算 SnapView
当前坐标到目的坐标之间的距离,然后调用 RecyclerView.smoothScrollBy()
方法实现对 RecyclerView
内容的平滑滚动,从而将 SnapView
移到目标位置,达到对齐效果。
其实这个时候 RecyclerView
还没进行layout,一般 findSnapView
会返回null,不需要对齐。
回调
SnapHelper
要有对齐功能,肯定需要知道 RecyclerView
的滚动scroll和fling过程的,这个就是通过回调接口实现。再看下 attachToRecyclerView
的源码:
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(); } } 复制代码
一开始会先清空之前的回调接口然后再注册接口,先看下 destroyCallbacks
:
/** * Called when the instance of a {@link RecyclerView} is detached. */ private void destroyCallbacks() { mRecyclerView.removeOnScrollListener(mScrollListener); mRecyclerView.setOnFlingListener(null); } 复制代码
可以看出 SnapHelper
对 RecyclerView
设置了两个回调,一个是 OnScrollListener
对象 mScrollListener
,另外一个就是 OnFlingListener
对象。
再看下 setupCallbacks
:
/** * Called when an instance of a {@link RecyclerView} is attached. */ private void setupCallbacks() throws IllegalStateException { if (mRecyclerView.getOnFlingListener() != null) { throw new IllegalStateException("An instance of OnFlingListener already set."); } mRecyclerView.addOnScrollListener(mScrollListener); mRecyclerView.setOnFlingListener(this); } 复制代码
SnapHelper
实现了 RecyclerView.OnFlingListener
接口,所以 OnFlingListener
就是 SnapHelper
自身。
先来看下 RecyclerView.OnScrollListener
对象 mScrollListener
RecyclerView.OnScrollListener
先看下 mScrollListener
是怎么实现的:
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; } } }; 复制代码
mScrolled = true
表示之前滚动过, RecyclerView.SCROLL_STATE_IDLE
表示滚动停止,这个不清楚的可以看考之前的博客 RecyclerView之Scroll和Fling 。这个监听器的实现其实很简单,就是在滚动停止的时候调用 snapToTargetExistingView
对目标View进行滚动调整对齐。
RecyclerView.OnFlingListener
RecyclerView.OnFlingListener
接口只有一个方法,这个就是在 Fling
操作触发的时候会回调,返回true就是已处理,返回false就会交给系统处理。
/** * This class defines the behavior of fling if the developer wishes to handle it. * <p> * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior. * * @see #setOnFlingListener(OnFlingListener) */ public abstract static 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 was handled, false otherwise. */ public abstract boolean onFling(int velocityX, int velocityY); } 复制代码
看下 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; } int minFlingVelocity = mRecyclerView.getMinFlingVelocity(); return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && snapFromFling(layoutManager, velocityX, velocityY); } 复制代码
首先会获取 mRecyclerView.getMinFlingVelocity()
需要进行fling操作的最小速率,只有超过该速率,Item才能在手指离开的时候进行 Fling
操作。 关键就是调用 snapFromFling
方法实现平滑滚动。
snapFromFling
看下怎么实现的:
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) { if (!(layoutManager instanceof ScrollVectorProvider)) { return false; } SmoothScroller smoothScroller = createScroller(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; } 复制代码
- 首先判断是不是实现了
ScrollVectorProvider
接口,系统提供的Layoutmanager默认都实现了该接口 - 创建SmoothScroller对象,默认是
LinearSmoothScroller
对象,会用LinearInterpolator
进行平滑滚动,在目标位置成为Recyclerview
的子View时会用DecelerateInterpolator
进行减速停止。 - 通过
findTargetSnapPosition()
方法,以layoutManager和速率作为参数,找到targetSnapPosition,这个方法就是自定义SnapHelper
需要实现的。 - 把targetSnapPosition设置给平滑滚动器,然后开始进行滚动操作。
很明显重点就是要看下平滑滚动器了。
LinearSmoothScroller
看下系统怎么实现:
@Nullable 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; } }; } 复制代码
在通过 findTargetSnapPosition()
方法找到的targetSnapPosition成为 Recyclerview
的子View时(根据 Recyclerview
的缓存机制,这个时候可能该View在屏幕上还看不到),会回调 onTargetFound
,看下系统定义:
/** * Called when the target position is laid out. This is the last callback SmoothScroller * will receive and it should update the provided {@link Action} to define the scroll * details towards the target view. * @param targetView The view element which render the target position. * @param state Transient state of RecyclerView * @param action Action instance that you should update to define final scroll action * towards the targetView */ protected abstract void onTargetFound(View targetView, State state, Action action); 复制代码
传入的第一个参数 targetView
就是我们希望滚动到的位置对应的View,最后一个参数就是我们可以用来通知滚动器要减速滚动的距离。
其实就是我们要在这个方法里面告诉滚动器在目标子View layout出来后还需要滚动多少距离, 然后通过 Action
通知滚动器。
第二个方法是计算滚动速率,返回值会影响 onTargetFound
中的 calculateTimeForDeceleration
方法,看下源码:
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); } /** * <p>Calculates the time for deceleration so that transition from LinearInterpolator to * DecelerateInterpolator looks smooth.</p> * * @param dx Distance to scroll * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning * from LinearInterpolation */ 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); } 复制代码
可以看到,第二个方法返回值越大,需要滚动的时间越长,也就是滚动越慢。
3.总结
到这里, SnapHelper
的源码就分析完了,整理下思路, SnapHelper
辅助 RecyclerView
实现滚动对齐就是通过给 RecyclerView
设置 OnScrollerListener
和 OnFlingListener
这两个监听器实现的。 整个过程如下:
- 在
onFling
操作触发的时候首先通过findTargetSnapPosition
找到最终需要滚动到的位置,然后启动平滑滚动器滚动到指定位置, - 在指定位置需要渲染的View -targetView layout出来后,系统会回调
onTargetFound
,然后调用calculateDistanceToFinalSnap
方法计算targetView需要减速滚动的距离,然后通过Action
更新给滚动器。 - 在滚动停止的时候,也就是state变成
SCROLL_STATE_IDLE
时会调用snapToTargetExistingView
,通过findSnapView
找到SnapView
,然后通过calculateDistanceToFinalSnap
计算得到滚动的距离,做最后的对齐调整。
前面分享的Demo就留到下一篇博客再说了,其实只要理解了 SnapHelper
的源码,自定义就很简单了。
对Demo感兴趣的欢迎关注下一篇博客了。
完。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Zero to One
Peter Thiel、Blake Masters / Crown Business / 2014-9-16 / USD 27.00
“This book delivers completely new and refreshing ideas on how to create value in the world.” - Mark Zuckerberg, CEO of Facebook “Peter Thiel has built multiple breakthrough companies, and ......一起来看看 《Zero to One》 这本书的介绍吧!