上周五写了篇仿夸克浏览器底部工具栏,相信看过的同窗还有印象吧。在文末我抛出了一个问题,夸克浏览器底部工具栏只是单层层叠的ViewGroup,如何实现相似Android系统通知栏的多级层叠列表呢? java
recyclerView
+自定义
layoutManager
,因此周末又把自定义
layoutManager
狠补了一遍。终于大体实现了这个效果(固然细节有待优化( ̄. ̄))。老样子,先来看看效果吧:
layoutManager
实现的,因此只要一行代码就能把普通的RecyclerView替换成这种层叠列表:
mRecyclerView.setLayoutManager(new OverFlyingLayoutManager());
复制代码
好了废话很少说,直接来分析下怎么实现吧。如下的主要内容就是帮你从学会到熟悉自定义layoutManager
。git
先简单说下自定义layoutManager
的步骤吧,其实不少文章都讲过,适合没接触的同窗:github
generateDefaultLayoutParams()
方法,生成本身所定义扩展的LayoutParams
。onLayoutChildren()
中实现初始列表中各个itemView
的位置scrollVerticallyBy()
和scrollHorizontallyBy()
中处理横向和纵向滚动,还有view的回收复用。我的理解就是:layoutManager
就至关于自定义ViewGroup
中把onMeasure()
、onlayout()
,scrollTo()
等方法独立出来,单独交给它来作。实际表现也是相似:onLayoutChildren()
做用就是测量放置itemView
。浏览器
咱们先实现本身的布局参数:bash
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
复制代码
也就是不实现,自带的RecyclerView.LayoutParams
继承自ViewGroup.MarginLayoutParams
,已经够用了。经过查看源码,最终这个方法返回的布局参数对象会设置给:ide
holder.itemView.setLayoutParams(rvLayoutParams);
复制代码
而后实现onLayoutChildren()
,在里面要把全部itemView
没滑动前自身应该在的位置都记录并放置一遍: 定义两个集合:工具
// 用于保存item的位置信息
private SparseArray<Rect> allItemRects = new SparseArray<>();
// 用于保存item是否处于可见状态的信息
private SparseBooleanArray itemStates = new SparseBooleanArray();
复制代码
把全部View虚拟地放置一遍,记录下每一个view的位置信息,由于此时并无把View真正到recyclerview中,也是不可见的:布局
private void calculateChildrenSiteVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 先把全部的View先从RecyclerView中detach掉,而后标记为"Scrap"状态,表示这些View处于可被重用状态(非显示中)。
detachAndScrapAttachedViews(recycler);
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
// 测量View的尺寸。
measureChildWithMargins(view, 0, 0);
//去除ItemDecoration部分
calculateItemDecorationsForChild(view, new Rect());
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
Rect mTmpRect = allItemRects.get(i);
if (mTmpRect == null) {
mTmpRect = new Rect();
}
mTmpRect.set(0, totalHeight, width, totalHeight + height);
totalHeight += height;
// 保存ItemView的位置信息
allItemRects.put(i, mTmpRect);
// 因为以前调用过detachAndScrapAttachedViews(recycler),因此此时item都是不可见的
itemStates.put(i, false);
}
addAndLayoutViewVertical(recycler, state, 0);
}
复制代码
而后咱们开始真正地添加View到RecyclerView中。为何不在记录位置的时候添加呢?由于后添加的view若是和前面添加的view重叠,那么后添加的view会覆盖前者,和咱们想要实现的层叠的效果是相反的,因此须要正向记录位置信息,而后根据位置信息反向添加View:post
private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
int displayHeight = getWidth() - getPaddingLeft() - getPaddingRight();//计算recyclerView能够放置view的高度
//反向添加
for (int i = getItemCount() - 1; i >= 0; i--) {
// 遍历Recycler中保存的View取出来
View view = recycler.getViewForPosition(i);
//由于刚刚进行了detach操做,因此如今能够从新添加
addView(view);
//测量view的尺寸
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view); // 计算view实际大小,包括了ItemDecorator中设置的偏移量。
int height = getDecoratedMeasuredHeight(view);
//调用这个方法可以调整ItemView的大小,以除去ItemDecorator距离。
calculateItemDecorationsForChild(view, new Rect());
Rect mTmpRect = allItemRects.get(i);//取出咱们以前记录的位置信息
if (mTmpRect.bottom > displayHeight) {
//排到底了,后面统一置底
layoutDecoratedWithMargins(view, 0, displayHeight - height, width, displayHeight);
} else {
//按原位置放置
layoutDecoratedWithMargins(view, 0, mTmpRect.top, width, mTmpRect.bottom);
}
Log.e(TAG, "itemCount = " + getChildCount());
}
复制代码
这样一来,编译运行,界面上已经能看到列表了,就是它还不能滚动,只能停留在顶部。优化
先设置容许纵向滚动:
@Override
public boolean canScrollVertically() {
// 返回true表示能够纵向滑动
return orientation == OrientationHelper.VERTICAL;
}
复制代码
处理滚动原理其实很简单:
onLayoutChildren()
同样从新布局就行@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//列表向下滚动dy为正,列表向上滚动dy为负,这点与Android坐标系保持一致。
//dy是系统告诉咱们手指滑动的距离,咱们根据这个距离来处理列表实际要滑动的距离
int tempDy = dy;
//最多滑到总距离减去列表距离的位置,便可滑动的总距离是列表内容多余的距离
if (verticalScrollOffset <= totalHeight - getVerticalSpace()) {
//将竖直方向的偏移量+dy
verticalScrollOffset += dy;
}
if (verticalScrollOffset > totalHeight - getVerticalSpace()) {
verticalScrollOffset = totalHeight - getVerticalSpace();
tempDy = 0;//滑到底部了,就返回0,说明到边界了
} else if (verticalScrollOffset < 0) {
verticalScrollOffset = 0;
tempDy = 0;//滑到顶部了,就返回0,说明到边界了
}
//从新布局位置、显示View
addAndLayoutViewVertical(recycler, state, verticalScrollOffset);
return tempDy;
}
复制代码
上面说了,滚动其实就是根据滑动距离从新布局的过程,和onLayoutChildren()
中的初始化布局没什么两样。咱们扩展布局方法,传入偏移量,这样onLayoutChildren()
调用时只要传0就好了:
private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state, int offset) {
int displayHeight = getVerticalSpace();
for (int i = getItemCount() - 1; i >= 0; i--) {
// 遍历Recycler中保存的View取出来
View view = recycler.getViewForPosition(i);
addView(view); // 由于刚刚进行了detach操做,因此如今能够从新添加
measureChildWithMargins(view, 0, 0); // 通知测量view的margin值
int width = getDecoratedMeasuredWidth(view); // 计算view实际大小,包括了ItemDecorator中设置的偏移量。
int height = getDecoratedMeasuredHeight(view);
Rect mTmpRect = allItemRects.get(i);
//调用这个方法可以调整ItemView的大小,以除去ItemDecorator。
calculateItemDecorationsForChild(view, new Rect());
int bottomOffset = mTmpRect.bottom - offset;
int topOffset = mTmpRect.top - offset;
if (bottomOffset > displayHeight) {//滑到底了
layoutDecoratedWithMargins(view, 0, displayHeight - height, width, displayHeight);
} else {
if (topOffset <= 0 ) {//滑到顶了
layoutDecoratedWithMargins(view, 0, 0, width, height);
} else {//中间位置
layoutDecoratedWithMargins(view, 0, topOffset, width, bottomOffset);
}
}
Log.e(TAG, "itemCount = " + getChildCount());
}
复制代码
好了,这样就能滚动了。
由于自定义layoutManager
内容比较多,因此我分红了上下篇来说。到这里基础效果实现了,可是这个RecyclerView尚未实现回收复用(参看addAndLayoutViewVertical
末尾打印),还有边缘的层叠嵌套动画和视觉处理也都留到下篇说了。看了上面的内容,实现横向滚动也是很简单的,感兴趣的本身去github上看下实现吧!