由旋转画廊,看自定义RecyclerView.LayoutManager
一、简介
前段时间需要一个旋转木马效果用于展示图片,于是第一时间在github上找了一圈,找了一个还不错的控件,但是使用起来有点麻烦,始终觉得很不爽,所以寻思着自己做一个轮子。想起旋转画廊的效果不是和横向滚动列表非常相似吗?那么是否可以利用RecycleView实现呢?
RecyclerView是google官方在support.v7中提供的一个控件,是ListView和GridView的升级版。该控件具有高度灵活、高度解耦的特性,并且还提供了添加、删除、移动的动画支持,分分钟让你作出漂亮的列表、九宫格、瀑布流。相信使用过该控件的人必定爱不释手。
先来看下如何简单的使用RecyclerView
RecyclerView listView = (RecyclerView)findViewById(R.id.lsit);
listView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
listView.setAdapter(new Adapter());复制代码
就是这么简单:
- 设置LayoutManager
- 设置Adapter(继承RecyclerView.Adapter)
其中,LayoutManager用于指定布局管理器,官方已经提供了几个布局管理器,可以满足大部分需求:
- LinearLayoutManger:提供了竖向和横向线性布局(可实现ListView功能)
- GridLayoutManager:表格布局(可实现GridView功能)
- StaggeredGridLayoutManager:瀑布流布局
Adapter的定义与ListView的Adapter用法类似。
重点来看LayoutManage。
LinearLayoutManager与其他几个布局管理器都是继承了该类,从而实现了对每个Item的布局。那么我们也可以通过自定义LayoutManager来实现旋转画廊的效果。
看下要实现的效果:
二、自定义LayoutManager
首先,我们来看看,自定义LayoutManager是什么样的流程:
- 计算每个Item的位置,并对Item布局。重写onLayoutChildren()方法
处理滑动事件(包括横向和竖向滚动、滑动结束、滑动到指定位置等)
i.横向滚动:重写scrollHorizontallyBy()方法
ii.竖向滚动:重写scrollVerticallyBy()方法
iii.滑动结束:重写onScrollStateChanged()方法
iiii.指定滚动位置:重写scrollToPosition()和smoothScrollToPosition()方法
- 重用和回收Item
- 重设Adapter 重写onAdapterChanged()方法
接下来,就来实现这个流程
第一步,定义CoverFlowLayoutManager继承RecyclerView.LayoutManager
public class CoverFlowLayoutManger extends RecyclerView.LayoutManager {@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);}
}复制代码
继承LayoutManager后,会强制要求必须实现generateDefaultLayoutParams()方法,提供默认的Item布局参数,设置为Wrap_Content,由Item自己决定。
第二步,计算Item的位置和布局,并根据显示区域回收出界的Item
i.计算Item位置
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {//如果没有item,直接返回//跳过preLayout,preLayout主要用于支持动画if (getItemCount() <= 0 || state.isPreLayout()) {mOffsetAll = 0;return;}mAllItemFrames.clear(); //mAllItemFrame存储了所有Item的位置信息mHasAttachedItems.clear(); //mHasAttachedItems存储了Item是否已经被添加到控件中//得到子view的宽和高,这里的item的宽高都是一样的,所以只需要进行一次测量View scrap = recycler.getViewForPosition(0);addView(scrap);measureChildWithMargins(scrap, 0, 0);//计算测量布局的宽高mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);//计算第一个Item X轴的起始位置坐标,这里第一个Item居中显示mStartX = Math.round((getHorizontalSpace() - mDecoratedChildWidth) * 1.0f / 2);//计算第一个Item Y轴的启始位置坐标,这里为控件竖直方向居中mStartY = Math.round((getVerticalSpace() - mDecoratedChildHeight) *1.0f / 2);float offset = mStartX; //item X轴方向的位置坐标for (int i = 0; i < getItemCount(); i++) { //存储所有item具体位置Rect frame = mAllItemFrames.get(i);if (frame == null) {frame = new Rect();}frame.set(Math.round(offset), mStartY, Math.round(offset + mDecoratedChildWidth), mStartY + mDecoratedChildHeight);mAllItemFrames.put(i, frame); //保存位置信息mHasAttachedItems.put(i, false);//计算Item X方向的位置,即上一个Item的X位置+Item的间距offset = offset + getIntervalDistance();}detachAndScrapAttachedViews(recycler);layoutItems(recycler, state, SCROLL_RIGHT); //布局ItemmRecycle = recycler; //保存回收器mState = state; //保存状态
}复制代码
以上,我们为Item的布局做了准备,计算了Item的宽高,以及首个Item的起始位置,并根据设置的Item间,计算每个Item的位置,并保存了下来。
接下来,来看看layoutItems()方法做了什么。
ii.布局和回收Item
private void layoutItems(RecyclerView.Recycler recycler,RecyclerView.State state, int scrollDirection) {if (state.isPreLayout()) return;Rect displayFrame = new Rect(mOffsetAll, 0, mOffsetAll + getHorizontalSpace(), getVerticalSpace()); //获取当前显示的区域//回收或者更新已经显示的Itemfor (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);int position = getPosition(child);if (!Rect.intersects(displayFrame, mAllItemFrames.get(position))) {//Item没有在显示区域,就说明需要回收removeAndRecycleView(child, recycler); //回收滑出屏幕的ViewmHasAttachedItems.put(position, false);} else { //Item还在显示区域内,更新滑动后Item的位置layoutItem(child, mAllItemFrames.get(position)); //更新Item位置mHasAttachedItems.put(position, true);}}for (int i = 0; i < getItemCount(); i++) {if (Rect.intersects(displayFrame, mAllItemFrames.get(i)) &&!mHasAttachedItems.get(i)) { //加载可见范围内,并且还没有显示的ItemView scrap = recycler.getViewForPosition(i);measureChildWithMargins(scrap, 0, 0);if (scrollDirection == SCROLL_LEFT || mIsFlatFlow) {//向左滚动,新增的Item需要添加在最前面addView(scrap, 0);} else { //向右滚动,新增的item要添加在最后面addView(scrap);}layoutItem(scrap, mAllItemFrames.get(i)); //将这个Item布局出来mHasAttachedItems.put(i, true);}}
}private void layoutItem(View child, Rect frame) {layoutDecorated(child,frame.left - mOffsetAll,frame.top,frame.right - mOffsetAll,frame.bottom);child.setScaleX(computeScale(frame.left - mOffsetAll)); //缩放child.setScaleY(computeScale(frame.left - mOffsetAll)); //缩放
}复制代码
第一个方法:在layoutItems()中
mOffsetAll记录了当前控件滑动的总偏移量,一开始mOffsetAll为0。
在第一个for循环中,先判断已经显示的Item是否已经超出了显示范围,如果是,则回收改Item,否则更新Item的位置。
在第二个for循环中,遍历了所有的Item,然后判断Item是否在当前显示的范围内,如果是,将Item添加到控件中,并根据Item的位置信息进行布局。
第二个方法:在layoutItem()中
调用了父类方法layoutDecorated对Item进行布局,其中mOffsetAll为整个旋转控件的滑动偏移量。
布局好后,对根据Item的位置对Item进行缩放,中间最大,距离中间越远,Item越小。
第三步,处理滑动事件
i. 处理横向滚动事件
由于旋转画廊只需横向滚动,所以这里只处理横向滚动事件复制代码
@Override
public boolean canScrollHorizontally() {return true;
}@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,RecyclerView.State state) {if (mAnimation != null && mAnimation.isRunning()) mAnimation.cancel();int travel = dx;if (dx + mOffsetAll < 0) {travel = -mOffsetAll;} else if (dx + mOffsetAll > getMaxOffset()){travel = (int) (getMaxOffset() - mOffsetAll);}mOffsetAll += travel; //累计偏移量layoutItems(recycler, state, dx > 0 ? SCROLL_RIGHT : SCROLL_LEFT);return travel;
}复制代码
首先,需要告诉RecyclerView,我们需要接收横向滚动事件。
当用户滑动控件时,会回调scrollHorizontallyBy()方法对Item进行重新布局。
我们先忽略第一句代码,mAnimation用于处理滑动停止后Item的居中显示。
然后,我们判断了滑动距离dx,加上之前已经滚动的总偏移量mOffsetAll,是否超出所有Item可以滑动的总距离(总距离= Item个数 * Item间隔),对滑动距离进行边界处理,并将实际滚动的距离累加到mOffsetAll中。
当dx>0时,控件向右滚动,即<--;当dx<0时,控件向左滚动,即-->复制代码
接着,调用先前已经写好的布局方法layoutItems(),对Item进行重新布局。
最后,返回实际滑动的距离。
ii.处理滑动结束事件,将Item居中显示
@Override
public void onScrollStateChanged(int state) {super.onScrollStateChanged(state);switch (state){case RecyclerView.SCROLL_STATE_IDLE://滚动停止时fixOffsetWhenFinishScroll();break;case RecyclerView.SCROLL_STATE_DRAGGING://拖拽滚动时break;case RecyclerView.SCROLL_STATE_SETTLING://动画滚动时break;}
}private void fixOffsetWhenFinishScroll() {//计算滚动了多少个Itemint scrollN = (int) (mOffsetAll * 1.0f / getIntervalDistance()); //计算scrollN位置的Item超出控件中间位置的距离float moreDx = (mOffsetAll % getIntervalDistance());if (moreDx > (getIntervalDistance() * 0.5)) { //如果大于半个Item间距,则下一个Item居中scrollN ++;}//计算最终的滚动距离int finalOffset = (int) (scrollN * getIntervalDistance());//启动居中显示动画startScroll(mOffsetAll, finalOffset);//计算当前居中的Item的位置mSelectPosition = Math.round (finalOffset * 1.0f / getIntervalDistance());
}复制代码
通过onScrollStateChanged()方法,可以监听到控件的滚动状态,这里我们只需处理滑动停止事件。
在fixOffsetWhenFinishScroll()中,getIntervalDistance()方法用于获取Item的间距。
根据滚动的总距离除以Item的间距计算出总共滚动了多少个Item,然后启动居中显示动画。
private void startScroll(int from, int to) {if (mAnimation != null && mAnimation.isRunning()) {mAnimation.cancel();}final int direction = from < to ? SCROLL_RIGHT : SCROLL_LEFT;mAnimation = ValueAnimator.ofFloat(from, to);mAnimation.setDuration(500);mAnimation.setInterpolator(new DecelerateInterpolator());mAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mOffsetAll = Math.round((float) animation.getAnimatedValue());layoutItems(mRecycle, mState, direction);}});
}复制代码
动画很简单,从滑动停止的位置,不断刷新Item布局,直到滚动到最终位置。
iii.处理指定位置滚动事件
@Override
public void scrollToPosition(int position) {if (position < 0 || position > getItemCount() - 1) return;mOffsetAll = calculateOffsetForPosition(position);if (mRecycle == null || mState == null) {//如果RecyclerView还没初始化完,先记录下要滚动的位置mSelectPosition = position;} else {layoutItems(mRecycle, mState, position > mSelectPosition ? SCROLL_RIGHT : SCROLL_LEFT);}
}@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {if (position < 0 || position > getItemCount() - 1) return;int finalOffset = calculateOffsetForPosition(position);if (mRecycle == null || mState == null) {//如果RecyclerView还没初始化完,先记录下要滚动的位置mSelectPosition = position;} else {startScroll(mOffsetAll, finalOffset);}
}复制代码
scrollToPosition()用于不带动画的Item直接跳转
smoothScrollToPosition()用于带动画Item滑动
也很简单,计算要跳转Item的所在位置需要滚动的距离,如果不需要动画,则直接对Item进行布局,否则启动滑动动画。
第四,处理重新设置Adapter
当重新调用RecyclerView的setAdapter时,需要对LayoutManager的所有状态进行重置
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {removeAllViews();mRecycle = null;mState = null;mOffsetAll = 0;mSelectPosition = 0;mLastSelectPosition = 0;mHasAttachedItems.clear();mAllItemFrames.clear();
}复制代码
清空所有的Item,已经所有存放的位置信息和状态。
最后RecyclerView会重新调用onLayoutChildren()进行布局。
以上,就是自定义LayoutManager的流程,但是,为了实现旋转画廊的功能,只自定义了LayoutManager是不够的。旋转画廊中,每个Item是有重叠部分的,因此会有Item绘制顺序的问题,如果不对Item的绘制顺序进行调整,将出现中间Item被旁边Item遮挡的问题。
为了解决这个问题,需要重写RecyclerView的getChildDrawingOrder()方法,对Item的绘制顺序进行调整。
三、重写RecyclerView
这里简单看下如何如何改变Item的绘制顺序,具体可以查看源码复制代码
public class RecyclerCoverFlow extends RecyclerView {public RecyclerCoverFlow(Context context) {super(context);init();}public RecyclerCoverFlow(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init();}public RecyclerCoverFlow(Context context, @Nullable AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);init();}private void init() {......setChildrenDrawingOrderEnabled(true); //开启重新排序......}@Overrideprotected int getChildDrawingOrder(int childCount, int i) {//计算正在显示的所有Item的中间位置int center = getCoverFlowLayout().getCenterPosition()- getCoverFlowLayout().getFirstVisiblePosition();if (center < 0) center = 0;else if (center > childCount) center = childCount;int order;if (i == center) {order = childCount - 1;} else if (i > center) {order = center + childCount - 1 - i;} else {order = i;}return order;}
}复制代码
首先,需要调用setChildrenDrawingOrderEnabled(true); 开启重新排序功能。
接着,在getChildDrawingOrder()中,childCount为当前已经显示的Item数量,i为item的位置。
旋转画廊中,中间位置的优先级是最高的,两边item随着递减。因此,在这里,我们通过以上定义的LayoutManager计算了当前显示的Item的中间位置,然后对Item的绘制进行了重新排序。
最后将计算出来的顺序优先级返回给RecyclerView进行绘制。
总结
以上,通过旋转画廊控件,我们过了一遍自定义LayoutManager的流程。当然RecyclerView的强大远远不至于此,结合LayoutManager的横竖滚动事件还可以做出更多有趣的效果。
最后,奉上源码。
转载于:https://juejin.im/post/59c3416a6fb9a00a562e8526
由旋转画廊,看自定义RecyclerView.LayoutManager相关推荐
- android自定义起止时间的时间刻度尺,Android中自定义RecyclerView如何实现不固定刻度的刻度尺...
Android中自定义RecyclerView如何实现不固定刻度的刻度尺 发布时间:2020-07-17 16:50:28 来源:亿速云 阅读:116 作者:小猪 这篇文章主要讲解了Android中自 ...
- coldfusion_在ColdFusion中建立旋转画廊
coldfusion They came, they bought- but did they come back? An ecommerce site's most important visito ...
- 从自定义TagLayout看自定义布局的一般步骤[手动加精]
从自定义TagLayout看自定义布局的一般步骤[手动加精] 我们常用的布局有LinearLayout,FrameLayout,RelativeLayout,大多数情况下都能满足我们的需求,但是也有很 ...
- 自定义RecyclerView.ItemDecoration,实现RecyclerView的分割线效果
[转] 原文 自定义RecyclerView.ItemDecoration,实现RecyclerView的分割线效果 字数1598 阅读302 评论2 喜欢23 1.背景 RecyclerView ...
- QGraphicsItem图元旋转缩放和自定义图元(三)
系列文章目录 QGraphicsItem图元的简单使用(一) QGraphicsItem图元拖动绘制(二) 文章目录 系列文章目录 前言 一.缩放和旋转 二.自定义图元 总结 前言 接上一章,图元绘制 ...
- 安卓作业----慕课移动应用开发作业13之使用自定义RecyclerView.ItemDecoration实现列表悬浮顶部效果
此博客通过RecyclerView.TextView等进行界面布局,使用自定义RecyclerView.Adapter.RecyclerViewAdapter.ViewHolder以及自定义Recyc ...
- 自定义RecyclerView支持快速滚动
问题描述: RecyclerView自带快速滚动无法控制滚动条的长度唯一,也就是说随着item的增多,滚动条的长度会越变越小. 解决问题: 通过自定义RecyclerView来实现滚动条的长度不会因为 ...
- android contextmenu 自定义,RecyclerView+ContextMenu实现菜单项
前言 最近自己写了一个问卷调查的APP,想要实现对RecyclerView里面列表进行移动或删除的功能,常规的方法会使用PopupWindow.AlertDialog或是DialogFragment等 ...
- 【Android】从无到有:手把手一步步教你自定义RecyclerView手势监听
转载请注明出处,原文链接:https://blog.csdn.net/u013642500/article/details/80488425 [前言] 1.关于如何构建并使用RecyclerView, ...
最新文章
- 动态规划-最优二叉查找树
- 函数式编程语言python-10分钟学会python函数式编程
- hazelcast入门教程_Hazelcast入门指南第4部分
- 12345组成三个不重复数java,求大神帮忙!五子棋!只能识别按顺序识别!例如 12345 不能...
- 新车入手,美利达公爵500
- php strtofloat,Delphi6函数大全(3)
- 如何自学成为设计师_不会自学,你永远只能是个三流设计师
- 【视频+PPT】2021年李宏毅版40节机器学习课程已更新完毕,推荐收藏!
- macbookair有没有touchbar_没有Touch Bar 全新13英寸MacBook Pro初体验
- 看教程学虚幻四——粒子特效之魔法阵
- 物联网NB-IoT之电信物联网开放平台对接流程浅析
- 零基础通过直播小程序组件实现电商带货
- ixgbe网卡驱动 Ⅳ----收发包流程详解
- Matlab多元非线性函数拟合
- 2021年中国股票市场成交情况、政策调整与股票市场异常波动及政策建议分析[图]
- 计算机科学领域专业,计算机科学与技术专业主要包括哪些领域?
- 原型设计模式—解决随机乱序出试卷(试题顺序、选项顺序随机打乱)
- 如何建立自己的技术壁垒
- Python打开Excel超链接
- captcha实现验证码功能