RecyclerView进阶之层叠列表(上)

前言

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

gxxx.gif

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

ver.gif

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

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

   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;
    }

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

  1. 手指在屏幕上滑动,系统告诉我们一个滑动的距离
  2. 我们根据这个距离判断我们列表内部各个view的实际变化,然后和onLayoutChildren()一样重新布局就行
  3. 返回告诉系统我们滑动了多少,如果返回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上看下实现吧!

Github地址

hor.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341