主要是实现类似于抖音翻页的效果,但是有有点不同,需要在底部漏出后面的view,这样说可能不好理解,看下Demo,按页滑动,后面的View有放大缩放的动画,滑动速度过小时会有回到原位的效果,下滑也是按页滑动的效果。

有的小伙伴可能说这个用 SnapHelper就可以了,没错,翻页是要结合这个,但是也不是纯粹靠这个,因为底部需要漏出来后面的view,所以LayoutManager就不能简单的使用LinearLayoutManager,需要去自定义LayoutManager,然后再自定义SnapHelper

先看下自定义LayoutManager

1.自定义LayoutManager

Android系统给我们提供了常用的几个LayoutManager,比如LinearLayoutManager:用于水平或者竖直滑动
GridLayoutManager:用于表格布局,一行可以有多列
StaggeredGridLayoutManager:瀑布流布局

但是在我们上面那个界面就用不了,因为在第一页界面底部需要漏出后面的Item,所以我们就需要自定义。

一般自定义LayoutManager需要实现三个方法:

第一个方法是generateDefaultLayoutParams,这个用来定义布局参数的,一般宽高都WRAP_CONTENT就行。

    @Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,RecyclerView.LayoutParams.WRAP_CONTENT);}
复制代码

第二个方法根据需要水平或者竖直滑动区分,我们这里是竖直滑动,重写canScrollVertically

    @Overridepublic boolean canScrollVertically() {return true;}
复制代码

聪明的你肯定已经知道如果水平滑动,就是重写canScrollHorizontally.

前面两个方法都很简单,最麻烦的就是第三个方法,重写LayoutManager就是需要自己去布局,所以需要重写

    @Overridepublic void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {if (state.getItemCount() == 0 || state.isPreLayout()) {return;}if (!hasChild) {hasChild = true;}mItemCount = getItemCount();// 滑动过的距离mScrollOffset = Math.min(Math.max(0, mScrollOffset), (mItemCount - 1) * itemHeight);layoutChild(recycler);}
复制代码

首先如果没有item或者第一次layout进来就直接返回,mScrollOffset是滑动过的距离,初始值为0,滑动到最后一个就不能进行滑动,这里其实可以加个下拉刷新,以后有机会再加,不是今天的主题。

接下来大头就是layoutChild.

    private void layoutChild(RecyclerView.Recycler recycler) {if (getItemCount() == 0) {return;}int firstItemPosition = (int) Math.floor(mScrollOffset / itemHeight);if (firstItemPosition > commonAdapter.getItemCount() - 1) {return;}int firstItemScrolledHeight = mScrollOffset % itemHeight;final float firstItemScrolledHeightPercent = firstItemScrolledHeight * 1.0f / itemHeight;ArrayList<PageItemViewInfo> layoutInfos = new ArrayList<>();// 计算view位置int tmpCount = Math.min(VISIBLE_EMOTICON_COUNT, commonAdapter.getItemCount() - firstItemPosition - 1);for (int i = 0; i <= tmpCount; i++) {// 用于计算偏移量int tmp = i + 1;double maxOffset = (getVerticalSpace()- itemHeight - firstItemScrolledHeightPercent) / 2 * Math.pow(0.65, tmp);if (maxOffset <= 0) {break;}int start;if (i == 0) {start = getPaddingTop() - firstItemScrolledHeight;} else {start = (int) (getPaddingTop() + i * maxOffset + i * ITEM_OFFSET);}float mScale = 0.95f;float scaleXY = (float) (Math.pow(mScale, i) * (1 - firstItemScrolledHeightPercent * (1 - mScale)));PageItemViewInfo info = new PageItemViewInfo(start, scaleXY);layoutInfos.add(0, info);}// 回收Viewint layoutCount = layoutInfos.size();final int endPos = firstItemPosition + VISIBLE_EMOTICON_COUNT;final int childCount = getChildCount();for (int i = childCount - 1; i >= 0; i--) {View childView = getChildAt(i);if (childView == null) {continue;}int pos;try {pos = getPosition(childView);} catch (NullPointerException e) {e.printStackTrace();continue;}if (pos > endPos + 1 || pos < firstItemPosition - 1) {removeAndRecycleView(childView, recycler);}}detachAndScrapAttachedViews(recycler);// 添加Itemfor (int i = layoutCount - 1; i >= 0; i--) {int pos = firstItemPosition + i;if (pos > commonAdapter.getItemCount() - 1) {break;}// If a ViewHolder must be constructed and not enough time remains, null is returned, 不进行layoutView view;try {view = recycler.getViewForPosition(pos);} catch (IndexOutOfBoundsException e) {e.printStackTrace();return;}PageItemViewInfo layoutInfo = layoutInfos.get(layoutCount - 1 - i);view.setTag(pos);addView(view);measureChildWithExactlySize(view);int left = (getHorizontalSpace() - itemWidth) / 2;layoutDecoratedWithMargins(view, left,layoutInfo.getTop(),left + itemWidth,layoutInfo.getTop() + itemHeight);view.setPivotX(view.getWidth() / 2);view.setPivotY(view.getHeight() / 2);view.setScaleX(layoutInfo.getScaleXY());view.setScaleY(layoutInfo.getScaleXY());}}
复制代码

主要分成三部分,

计算Item位置
回收Item
添加Item
下面分别看下。

计算Item位置

就是根据当前滑动过的距离来计算当前可见的几个Item(这里我设置为3)的位置,这里由于宽高都定死的,所以需要的其实就是top位置。根据根据注释看代码应该比较简单了。

        // 第一个可见Item位置int firstItemPosition = (int) Math.floor(mScrollOffset / itemHeight);// 如果第一个可见Item位置是最后一个Item,返回if (firstItemPosition > commonAdapter.getItemCount() - 1) {return;}// 第一个可见Item划过的距离,也就是不可见int firstItemScrolledHeight = mScrollOffset % itemHeight;// 第一个可见Item划过的距离占自身高度的百分比final float firstItemScrolledHeightPercent = firstItemScrolledHeight * 1.0f / itemHeight;ArrayList<PageItemViewInfo> layoutInfos = new ArrayList<>();// 计算view位置int tmpCount = Math.min(VISIBLE_EMOTICON_COUNT, commonAdapter.getItemCount() - firstItemPosition - 1);for (int i = 0; i <= tmpCount; i++) {// 用于计算偏移量int tmp = i + 1;double maxOffset = (getVerticalSpace()- itemHeight - firstItemScrolledHeightPercent) / 2 * Math.pow(0.65, tmp);if (maxOffset <= 0) {break;}int start;if (i == 0) {start = getPaddingTop() - firstItemScrolledHeight;} else {start = (int) (getPaddingTop() + i * maxOffset + i * ITEM_OFFSET);}float mScale = 0.95f;float scaleXY = (float) (Math.pow(mScale, i) * (1 - firstItemScrolledHeightPercent * (1 - mScale)));PageItemViewInfo info = new PageItemViewInfo(start, scaleXY);layoutInfos.add(0, info);}
复制代码

回收Item

RecyclerView提供了三级缓存,简单看下Recycler

 public final class Recycler {final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();RecycledViewPool mRecyclerPool;...}
复制代码

其中

mAttachedScrap缓存当前屏幕上可见的ViewHolder,

mCachedViews缓存复用即将入屏的 item ,

RecycledViewPool可以缓存多个RecyclerView需要共用的ViewHolder,内部维护了一个SparseArray, key为ViewHolder的ViewType,也就是每一套ViewHolder都有自己的缓存数据,value为ScrapData类型

public static class RecycledViewPool {private static final int DEFAULT_MAX_SCRAP = 5;static class ScrapData {ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();int mMaxScrap = DEFAULT_MAX_SCRAP;long mCreateRunningAverageNs = 0;long mBindRunningAverageNs = 0;}SparseArray<ScrapData> mScrap = new SparseArray<>();
}
复制代码

ArrayList 的默认大小被限制为 5,但是这个值是可以通过 RecycledViewPool#setMaxRecycledViews(viewType, max) 来替换

多个RecycledView复用可以通过public void putRecycledView(ViewHolder scrap)向内主动填充数据。

关于缓存可以参考手摸手第二弹,可视化 RecyclerView 缓存机制
上面简单看了下RecyclerView的缓存机制,在这里我们需要都不在屏幕可见范围内的Item进行回收,放到mCachedViews中,然后把屏幕可见范围内的Item放到mAttachedScrap中,后面进行重新add。最后看下回收的代码:

        int layoutCount = layoutInfos.size();final int endPos = firstItemPosition + VISIBLE_EMOTICON_COUNT;final int childCount = getChildCount();for (int i = childCount - 1; i >= 0; i--) {View childView = getChildAt(i);if (childView == null) {continue;}int pos;try {pos = getPosition(childView);} catch (NullPointerException e) {e.printStackTrace();continue;}if (pos > endPos + 1 || pos < firstItemPosition - 1) {removeAndRecycleView(childView, recycler);}}detachAndScrapAttachedViews(recycler);
复制代码

最后就是根据计算的位置重新addView就行,View从view = recycler.getViewForPosition(pos);中取出,RecyclerView会从缓存中拿。

        // 添加Itemfor (int i = layoutCount - 1; i >= 0; i--) {int pos = firstItemPosition + i;if (pos > commonAdapter.getItemCount() - 1) {break;}// If a ViewHolder must be constructed and not enough time remains, null is returned, 不进行layoutView view;try {view = recycler.getViewForPosition(pos);} catch (IndexOutOfBoundsException e) {e.printStackTrace();return;}PageItemViewInfo layoutInfo = layoutInfos.get(layoutCount - 1 - i);view.setTag(pos);addView(view);measureChildWithExactlySize(view);int left = (getHorizontalSpace() - itemWidth) / 2;layoutDecoratedWithMargins(view, left,layoutInfo.getTop(),left + itemWidth,layoutInfo.getTop() + itemHeight);view.setPivotX(view.getWidth() / 2);view.setPivotY(view.getHeight() / 2);view.setScaleX(layoutInfo.getScaleXY());view.setScaleY(layoutInfo.getScaleXY());}
复制代码

以上就是自定义LayoutManager的过程,现在的实现是Item会随着手指移动,没有按页滑动的效果,要实现按页滑动的效果就需要SnapHelper出场了。

2.自定义SnapHelper

SnapHelper有三个抽象方法需要实现:

    @SuppressWarnings("WeakerAccess")@Nullablepublic abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,@NonNull View targetView);@SuppressWarnings("WeakerAccess")@Nullablepublic abstract View findSnapView(LayoutManager layoutManager);public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,int velocityY);
复制代码

三个方法的作用:

findTargetSnapPosition用来找到最终的目标位置,在fling操作刚触发的时候会根据速度计算一个最终目标位置,然后开始fling操作
calculateDistanceToFinalSnap 这个用来计算滑动到最终位置还需要滑动的距离,在一开始attachToRecyclerView或者targetView layout的时候会调用
findSnapView用来找到上面的targetView,就是需要对其的view,在calculateDistanceToFinalSnap 调用之前会调用该方法。

在LayoutManager中需要把SnapHelper和RecyclerView关联起来:

    @Overridepublic void onAttachedToWindow(RecyclerView view) {super.onAttachedToWindow(view);this.snapHelper.attachToRecyclerView(view);}
复制代码

attachToRecyclerView中会调用snapToTargetExistingView方法:

    void snapToTargetExistingView() {if (mRecyclerView == null) {return;}RecyclerView.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]);}}
复制代码

一开始应该是不需要对齐的,会调用findSnapView找到需要对齐的View,看下log:

I: ===========attachToRecyclerView=====================snapToTargetExistingView=====================findSnapView==========pos = -1
复制代码

返回-1就是没有需要对齐的。

看下findSnapView的实现:

    @Nullable@Overridepublic View findSnapView(RecyclerView.LayoutManager layoutManager) {if (layoutManager instanceof SlidePageLayoutManager) {int pos = ((SlidePageLayoutManager) layoutManager).getFixedScrollPosition(mDirection);mDirection = 0;if (pos != RecyclerView.NO_POSITION) {View view = layoutManager.findViewByPosition(pos);return view;}}return null;}
复制代码

其实就是调用layoutmanager获得位置pos, 代码实现很简单:

    public int getFixedScrollPosition(int direction) {if (hasChild) {if (mScrollOffset % itemHeight == 0) {return RecyclerView.NO_POSITION;}float position = mScrollOffset * 1.0f / itemHeight;if (direction > 0) {position =  (int) Math.ceil(position);} else {position =  (int) Math.floor(position);}return (int) position;}return RecyclerView.NO_POSITION;}
复制代码

第一次进来ScrollOffset为0,所以返回-1.后面根据ScrollOffset计算位置,如果向下滑,那就向下取整,比如现在第一个滑动到一半,那么position是小于1的小数,向下取整就得到1,所以SnapView就是位置是1的ViewHolder。

然后手指按下滑动,此时scrollState状态就是SCROLL_STATE_DRAGGING,看下log:

I: ===========onScrollStateChanged==========newState = 1
I: ===========******onFling******==========
I: ===========findTargetSnapPosition=====================getFixedScrollPosition==========
I: ScrollOffset = 461, itemHeight = 1116, position = 1.0, direction = 578
I: ===========onTargetFound==========targetView = 1===========calculateDistanceToFinalSnap=====================calculateDistanceToPosition==========
I: targetPos = 1, distance = 655, scrollOffset = 461y = 655===========onScrollStateChanged==========newState = 2
I: ===========onScrollStateChanged==========newState = 0
I: ===========snapToTargetExistingView=====================findSnapView=====================getFixedScrollPosition==========
I: pos = -1复制代码

然后就会触发onFling状态,首先会去回调另外一个findTargetSnapPosition找到fling操作需要滚动到什么位置,我们这里是按页滑动,所以需要滚动到下一个View的位置,看下该方法实现

    @Overridepublic int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {if (layoutManager.canScrollVertically()) {mDirection = velocityY;return ((SlidePageLayoutManager) layoutManager).getFixedScrollPosition(mDirection);} else {mDirection = velocityX;}return RecyclerView.NO_POSITION;}
复制代码

调用的其实还是getFixedScrollPosition计算得到位置。通过log也可以看出往下滑direction是正值,同时下一个位置就是1,因为我们拖拽的是0位置。

在我们需要找的位置layout出来的时候(在这里是1位置), 会回调onTargetFound方法,其中入参targetView就是上面找到的1位置对应的View,我在layout的时候给每个View加了个其对应位置的tag,通过log也可以看出targetView.getTag = 1。

在这里我们需要计算一个RecyclerView还需要滚动的距离给系统,这就需要实现最后一个抽象函数calculateDistanceToFinalSnap,把需要滚动的距离通过方法calculateTimeForDeceleration换算成时间,然后通过Action.update通知RecyclerView进行减速滚动到最终位置。

看下calculateDistanceToFinalSnap的实现:

    @Nullable@Overridepublic int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,@NonNull View targetView) {if (layoutManager instanceof SlidePageLayoutManager) {int[] out = new int[2];if (layoutManager.canScrollHorizontally()) {out[0] = ((SlidePageLayoutManager) layoutManager).calculateDistanceToPosition(layoutManager.getPosition(targetView));out[1] = 0;} else {out[0] = 0;out[1] = ((SlidePageLayoutManager) layoutManager).calculateDistanceToPosition(layoutManager.getPosition(targetView));}LogUtils.LogSlide(null, new String[]{"y"}, out[1]);return out;}return null;}public int calculateDistanceToPosition(int targetPos) {int distance = itemHeight * targetPos - mScrollOffset;return distance;}
复制代码

代码炒鸡简单,就是通过下一个位置的layout位置减去当前已经滑动过的距离,就得到RecyclerView剩下还需要滑动的距离,小学的计算题了大家一看就懂。

看下上面的log也可以看出,当前已经滑动过的距离ScrollOffset是461,每个item的高度是1116,那么1116 - 461 = 655就是RecyclerView还需要滑动的距离。

不知不觉我们已经把三个需要实现的抽象方法都实现了,onFling操作过程中会把scrollstate设置为2,就是SCROLL_STATE_SETTLING。滚动过上面计算得到的距离后停止,状态变为0,就是SCROLL_STATE_IDLE

再看下下滑的log:

I: ===========onScrollStateChanged==========
I: newState = 1
I: ===========******onFling******==========
I: ===========findTargetSnapPosition==========
I: ===========getFixedScrollPosition==========ScrollOffset = 579, itemHeight = 1116, position = 0.0, direction = -446
I: ===========onTargetFound==========targetView = 0===========calculateDistanceToFinalSnap==========
I: ===========calculateDistanceToPosition==========targetPos = 0, distance = -579, scrollOffset = 579y = -579===========onScrollStateChanged==========newState = 2
I: ===========onScrollStateChanged==========
I: newState = 0===========snapToTargetExistingView=====================findSnapView==========
I: ===========getFixedScrollPosition==========pos = -1
复制代码

scrollstate还是差不多,先是1 = SCROLL_STATE_DRAGGING,然后进入Fling状态,scrollstate变为2 = SCROLL_STATE_SETTLING, 最后停下来状态变为0 = SCROLL_STATE_IDLE
在Fling状态开始的时候会通过findTargetSnapPosition,调用getFixedScrollPosition方法计算, direction = -446 < 0,所以向下取整得到0,目标位置就是0位置了。
然后会回调onTargetFound,通过方法calculateDistanceToFinalSnap得到滑动到0目标位置还需要滑动的距离。

自定义SnapHelper就是这样,理清三个接口的作用就简单了。

3.总结

因为要实现PM的需求所以有了这一次的自定义LayoutManagerSnapHelper之旅。自定义LayoutManager也是实现三个方法,最主要的是要实现onLayoutChildren,然后根据具体需求去layout每个item。自定义SnapHelper也是主要实现三个方法,主要是告诉需要最终滑动到的位置,需要对齐的View,然后在最终位置对应的View在layout出来后RecyclerView还需要滑动的距离。

坚持写博客不容易,从网上博客学到很多东西,所以也一再告诉自己要坚持下去。

+qq群457848807:。获取以上高清技术思维图,以及相关技术的免费视频学习资料

RecyclerView之自定义LayoutManager和SnapHelper相关推荐

  1. viewpager 自定义翻页效果_Android RecyclerView自定义LayoutManager

    在第一篇中已经讲过,LayoutManager主要用于布局其中的Item,在LayoutManager中能够对每个Item的大小,位置进行更改,将它放在我们想要的位置,在很多优秀的效果中,都是通过自定 ...

  2. android弧形左右滑动空间,自定义LayoutManager 实现弧形以及滑动放大效果RecyclerView...

    我们都知道RecyclerView可以通过将LayoutManager设置为StaggeredGridLayoutManager来实现瀑布流的效果.默认的还有LinearLayoutManager用于 ...

  3. 自定义LayoutManager实现最美应用列表

    RecyclerView的使用大家都很熟悉了,这里偶带大家来实现下面这种效果. 对的,你猜的不错.这种效果只要操作LayoutManager就可以实现,然后就这样 mRecyclerView.setL ...

  4. Android 常见界面控件(ListView、RecyclerView、自定义View篇)

    Android 常见界面控件(ListView.RecyclerView.自定义View篇) 目录 3.3 ListView的使用 3.3.1 ListView控件的简单使用 3.3.2 常用数据适配 ...

  5. 使用RecyclerView实现旋转3D画廊效果

    3D旋转画廊效果实现有哪些方式? 1.Gallery实现(官方已不推荐使用). 2.RecyclerView通过自定义LayoutManager实现. 一.简介 RecyclerView是google ...

  6. Android 仿抖音视频播放列表和评论列表

    Android 汇集CSDN.GitHub等最实用的良心之作-KING Android最实用的各种技能点的网址链接(每天都会更新,希望大家用的上) Android 仿抖音系列之视频播放列表和评论列表 ...

  7. 由旋转画廊,看自定义RecyclerView.LayoutManager

    一.简介 前段时间需要一个旋转木马效果用于展示图片,于是第一时间在github上找了一圈,找了一个还不错的控件,但是使用起来有点麻烦,始终觉得很不爽,所以寻思着自己做一个轮子.想起旋转画廊的效果不是和 ...

  8. 针对Android TV端使用的自定义RecyclerView

    在Android TV中开发, 需要处理焦点的移动, 为了达到醒目的作用一般都会添加焦点移动动画和焦点选中放大效果, 为了方便在TV端使用RecyclerView, 故自定义TvRecyclerVie ...

  9. 自定义 FlowLayout流式布局搜索框 加 GreenDao存取搜索记录,使用RecyclerView展示

    输入框布局的shape <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android ...

最新文章

  1. Android 7.0 源码分析项目一期竣工啦
  2. 导航模块自带的rtk算法_这款百元国产RTK板卡要改变高精度定位市场格局吗?
  3. html缓存在本地缓存,HTML5 本地缓存 window.localStorage
  4. windows系统如何cmake_Windows操作系统如何快速增加分区?建议收藏
  5. c#字符相似度对比通用类
  6. Leaflet笔记-Leaflet与echarts结合使用(在地图上绘图表)
  7. .NET Framework 4 中的新增功能【转载】
  8. android框架xUtils使用介绍
  9. 电机与拖动(电、磁、机械运动)
  10. 怎么把png批量转换jpg格式?
  11. python绘制图像频谱_python傅里叶变换FFT绘制频谱图
  12. shell中各种括号用法
  13. java订单超时取消设计_订单超时30分钟自动取消
  14. 技术经理成长复盘-聊聊核心骨干
  15. 京东云主机 mysql_京东云所有地域正式支持 MySQL 8.0!
  16. 图片按日期批量导入WPS表格
  17. 利用U盘里的GHOST文件恢复系统
  18. 【动态规划】字符串类型动态规划
  19. 产品:《人人都是产品经理》读书笔记
  20. 【无标题】 6UVPX 总线架构的高性能实时信号处理

热门文章

  1. 10句编程箴言 每个程序员都应该知道
  2. activex控件方法和事件
  3. 华为模拟器如何实现不同Vlan不同网段之间的互通
  4. 一条长为L的绳子,一面靠墙,另外三边组成矩形,问此矩形最大面积能是多少?...
  5. 使用servlet+jdbc+MD5实现用户加密登录
  6. phpstudy apache设置伪静态
  7. channel9.msdn.com 微软虚拟学院MVA系列视频课程
  8. HSRP与VRRP以及GLBP区别
  9. 解析淘宝商城缘何更名“天猫”
  10. Andriod 从源码的角度详解View,ViewGroup的Touch事件的分发机制