android 层叠view,RecyclerView进阶之层叠列表(上)

本文介绍了一种自定义RecyclerView LayoutManager的方法,通过RecyclerView和自定义LayoutManager实现了类似Android通知栏的多级层叠效果。作者详细解析了步骤,包括创建扩展LayoutParams、布局初始化、滚动处理等,展示了从理论到实践的全过程。
摘要由CSDN通过智能技术生成

前言

上周五写了篇仿夸克浏览器底部工具栏,相信看过的同学还有印象吧。在文末我抛出了一个问题,夸克浏览器底部工具栏只是单层层叠的ViewGroup,如何实现类似Android系统通知栏的多级层叠列表呢?

4772ae37e78c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

gxxx.gif

不过当时仅仅有了初步的思路:recyclerView+自定义layoutManager,所以周末又把自定义layoutManager狠补了一遍。终于大致实现了这个效果(当然细节有待优化( ̄. ̄))。老样子,先来看看效果吧:

4772ae37e78c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

ver.gif

实际使用时可能不需要顶部层叠,所以还有单边效果,看起来更自然些:

4772ae37e78c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

single.gif

怎么样,乍一看是不是非常形(神)似呢?以上的效果都是自定义layoutManager实现的,所以只要一行代码就能把普通的RecyclerView替换成这种层叠列表:

mRecyclerView.setLayoutManager(new OverFlyingLayoutManager());

好了废话不多说,直接来分析下怎么实现吧。以下的主要内容就是帮你从学会到熟悉自定义layoutManager。

概述

先简单说下自定义layoutManager的步骤吧,其实很多文章都讲过,适合没接触的同学:

实现generateDefaultLayoutParams()方法,生成自己所定义扩展的LayoutParams。

在onLayoutChildren()中实现初始列表中各个itemView的位置

在scrollVerticallyBy()和scrollHorizontallyBy()中处理横向和纵向滚动,还有view的回收复用。

个人理解就是:layoutManager就相当于自定义ViewGroup中把onMeasure()、onlayout(),scrollTo()等方法独立出来,单独交给它来做。实际表现也是类似:onLayoutChildren()作用就是测量放置itemView。

初始化列表

我们先实现自己的布局参数:

@Override

public RecyclerView.LayoutParams generateDefaultLayoutParams() {

return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

}

也就是不实现,自带的RecyclerView.LayoutParams继承自ViewGroup.MarginLayoutParams,已经够用了。通过查看源码,最终这个方法返回的布局参数对象会设置给:

holder.itemView.setLayoutParams(rvLayoutParams);

然后实现onLayoutChildren(),在里面要把所有itemView没滑动前自身应该在的位置都记录并放置一遍:

定义两个集合:

// 用于保存item的位置信息

private SparseArray 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:

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;

}

处理滚动原理其实很简单:

手指在屏幕上滑动,系统告诉我们一个滑动的距离

我们根据这个距离判断我们列表内部各个view的实际变化,然后和onLayoutChildren()一样重新布局就行

返回告诉系统我们滑动了多少,如果返回0,就说明滑到边界了,就会有一个边缘的波纹效果。

@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上看下实现吧!

4772ae37e78c?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

hor.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值