RecyclerView進(jìn)階之層疊列表(上)

前言

上周五寫了篇仿夸克瀏覽器底部工具欄,相信看過的同學(xué)還有印象吧。在文末我拋出了一個(gè)問題,夸克瀏覽器底部工具欄只是單層層疊的ViewGroup佛呻,如何實(shí)現(xiàn)類似Android系統(tǒng)通知欄的多級層疊列表呢?

gxxx.gif

不過當(dāng)時(shí)僅僅有了初步的思路:recyclerView+自定義layoutManager,所以周末又把自定義layoutManager狠補(bǔ)了一遍祟印。終于大致實(shí)現(xiàn)了這個(gè)效果(當(dāng)然細(xì)節(jié)有待優(yōu)化( ̄. ̄))。老樣子粟害,先來看看效果吧:

ver.gif

實(shí)際使用時(shí)可能不需要頂部層疊蕴忆,所以還有單邊效果,看起來更自然些:

single.gif

怎么樣悲幅,乍一看是不是非常形(神)似呢套鹅?以上的效果都是自定義layoutManager實(shí)現(xiàn)的驻襟,所以只要一行代碼就能把普通的RecyclerView替換成這種層疊列表:

mRecyclerView.setLayoutManager(new OverFlyingLayoutManager());

好了廢話不多說,直接來分析下怎么實(shí)現(xiàn)吧芋哭。以下的主要內(nèi)容就是幫你從學(xué)會(huì)到熟悉自定義layoutManager

概述

先簡單說下自定義layoutManager的步驟吧郁副,其實(shí)很多文章都講過减牺,適合沒接觸的同學(xué):

  • 實(shí)現(xiàn)generateDefaultLayoutParams()方法,生成自己所定義擴(kuò)展的LayoutParams存谎。
  • onLayoutChildren()中實(shí)現(xiàn)初始列表中各個(gè)itemView的位置
  • scrollVerticallyBy()scrollHorizontallyBy()中處理橫向和縱向滾動(dòng)拔疚,還有view的回收復(fù)用。

個(gè)人理解就是:layoutManager就相當(dāng)于自定義ViewGroup中把onMeasure()既荚、onlayout()稚失,scrollTo()等方法獨(dú)立出來,單獨(dú)交給它來做恰聘。實(shí)際表現(xiàn)也是類似:onLayoutChildren()作用就是測量放置itemView句各。

初始化列表

我們先實(shí)現(xiàn)自己的布局參數(shù):

  @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    }

也就是不實(shí)現(xiàn),自帶的RecyclerView.LayoutParams繼承自ViewGroup.MarginLayoutParams晴叨,已經(jīng)夠用了凿宾。通過查看源碼,最終這個(gè)方法返回的布局參數(shù)對象會(huì)設(shè)置給:

holder.itemView.setLayoutParams(rvLayoutParams);

然后實(shí)現(xiàn)onLayoutChildren(),在里面要把所有itemView沒滑動(dòng)前自身應(yīng)該在的位置都記錄并放置一遍:
定義兩個(gè)集合:

  // 用于保存item的位置信息
    private SparseArray<Rect> allItemRects = new SparseArray<>();
    // 用于保存item是否處于可見狀態(tài)的信息
    private SparseBooleanArray itemStates = new SparseBooleanArray();

把所有View虛擬地放置一遍兼蕊,記錄下每個(gè)view的位置信息初厚,因?yàn)榇藭r(shí)并沒有把View真正到recyclerview中,也是不可見的:

   private void calculateChildrenSiteVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 先把所有的View先從RecyclerView中detach掉孙技,然后標(biāo)記為"Scrap"狀態(tài)产禾,表示這些View處于可被重用狀態(tài)(非顯示中)。
     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);
            // 由于之前調(diào)用過detachAndScrapAttachedViews(recycler)亚情,所以此時(shí)item都是不可見的
            itemStates.put(i, false);
        }

        addAndLayoutViewVertical(recycler, state, 0);
    }

然后我們開始真正地添加View到RecyclerView中。為什么不在記錄位置的時(shí)候添加呢蕾久?因?yàn)楹筇砑拥膙iew如果和前面添加的view重疊势似,那么后添加的view會(huì)覆蓋前者,和我們想要實(shí)現(xiàn)的層疊的效果是相反的,所以需要正向記錄位置信息僧著,然后根據(jù)位置信息反向添加View:

   private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
        int displayHeight = getWidth() - getPaddingLeft() - getPaddingRight();//計(jì)算recyclerView可以放置view的高度
        //反向添加
        for (int i = getItemCount() - 1; i >= 0; i--) {
            // 遍歷Recycler中保存的View取出來
            View view = recycler.getViewForPosition(i);
            //因?yàn)閯倓傔M(jìn)行了detach操作履因,所以現(xiàn)在可以重新添加
            addView(view); 
            //測量view的尺寸
            measureChildWithMargins(view, 0, 0); 
            int width = getDecoratedMeasuredWidth(view); // 計(jì)算view實(shí)際大小,包括了ItemDecorator中設(shè)置的偏移量盹愚。
            int height = getDecoratedMeasuredHeight(view);
            //調(diào)用這個(gè)方法能夠調(diào)整ItemView的大小栅迄,以除去ItemDecorator距離。
            calculateItemDecorationsForChild(view, new Rect());
             Rect mTmpRect = allItemRects.get(i);//取出我們之前記錄的位置信息
            if (mTmpRect.bottom > displayHeight) {
                //排到底了,后面統(tǒng)一置底
                layoutDecoratedWithMargins(view, 0, displayHeight - height, width, displayHeight);
            } else {
                //按原位置放置
                 layoutDecoratedWithMargins(view, 0,  mTmpRect.top, width, mTmpRect.bottom);
            }
        Log.e(TAG, "itemCount = " + getChildCount());
    }

這樣一來皆怕,編譯運(yùn)行毅舆,界面上已經(jīng)能看到列表了西篓,就是它還不能滾動(dòng),只能停留在頂部憋活。

處理滾動(dòng)

先設(shè)置允許縱向滾動(dòng):

 @Override
    public boolean canScrollVertically() {
        // 返回true表示可以縱向滑動(dòng)
        return orientation == OrientationHelper.VERTICAL;
    }

處理滾動(dòng)原理其實(shí)很簡單:

  1. 手指在屏幕上滑動(dòng)岂津,系統(tǒng)告訴我們一個(gè)滑動(dòng)的距離
  2. 我們根據(jù)這個(gè)距離判斷我們列表內(nèi)部各個(gè)view的實(shí)際變化,然后和onLayoutChildren()一樣重新布局就行
  3. 返回告訴系統(tǒng)我們滑動(dòng)了多少悦即,如果返回0吮成,就說明滑到邊界了,就會(huì)有一個(gè)邊緣的波紋效果辜梳。
 @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //列表向下滾動(dòng)dy為正粱甫,列表向上滾動(dòng)dy為負(fù),這點(diǎn)與Android坐標(biāo)系保持一致作瞄。
        //dy是系統(tǒng)告訴我們手指滑動(dòng)的距離茶宵,我們根據(jù)這個(gè)距離來處理列表實(shí)際要滑動(dòng)的距離
        int tempDy = dy;
        //最多滑到總距離減去列表距離的位置,即可滑動(dòng)的總距離是列表內(nèi)容多余的距離
        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;
    }

上面說了,滾動(dòng)其實(shí)就是根據(jù)滑動(dòng)距離重新布局的過程宵喂,和onLayoutChildren()中的初始化布局沒什么兩樣糠赦。我們擴(kuò)展布局方法,傳入偏移量锅棕,這樣onLayoutChildren()調(diào)用時(shí)只要傳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); // 因?yàn)閯倓傔M(jìn)行了detach操作拙泽,所以現(xiàn)在可以重新添加
            measureChildWithMargins(view, 0, 0); // 通知測量view的margin值
            int width = getDecoratedMeasuredWidth(view); // 計(jì)算view實(shí)際大小,包括了ItemDecorator中設(shè)置的偏移量裸燎。
            int height = getDecoratedMeasuredHeight(view);

            Rect mTmpRect = allItemRects.get(i);
            //調(diào)用這個(gè)方法能夠調(diào)整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());
    }

好了德绿,這樣就能滾動(dòng)了荷荤。

小結(jié)

因?yàn)樽远xlayoutManager內(nèi)容比較多,所以我分成了上下篇來講移稳。到這里基礎(chǔ)效果實(shí)現(xiàn)了蕴纳,但是這個(gè)RecyclerView還沒有實(shí)現(xiàn)回收復(fù)用(參看addAndLayoutViewVertical末尾打印)个粱,還有邊緣的層疊嵌套動(dòng)畫和視覺處理也都留到下篇說了古毛。看了上面的內(nèi)容,實(shí)現(xiàn)橫向滾動(dòng)也是很簡單的稻薇,感興趣的自己去github上看下實(shí)現(xiàn)吧嫂冻!

Github地址

hor.gif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市塞椎,隨后出現(xiàn)的幾起案子桨仿,更是在濱河造成了極大的恐慌,老刑警劉巖案狠,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹬敲,死亡現(xiàn)場離奇詭異,居然都是意外死亡莺戒,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門急波,熙熙樓的掌柜王于貴愁眉苦臉地迎上來从铲,“玉大人,你說我怎么就攤上這事澄暮∶危” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵泣懊,是天一觀的道長伸辟。 經(jīng)常有香客問我,道長馍刮,這世上最難降的妖魔是什么信夫? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮卡啰,結(jié)果婚禮上静稻,老公的妹妹穿的比我還像新娘。我一直安慰自己匈辱,他們只是感情好振湾,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著亡脸,像睡著了一般押搪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上浅碾,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天大州,我揣著相機(jī)與錄音,去河邊找鬼垂谢。 笑死摧茴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的埂陆。 我是一名探鬼主播苛白,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼娃豹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了购裙?” 一聲冷哼從身側(cè)響起懂版,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躏率,沒想到半個(gè)月后躯畴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薇芝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年蓬抄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片夯到。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嚷缭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耍贾,到底是詐尸還是另有隱情阅爽,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布荐开,位于F島的核電站付翁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏晃听。R本人自食惡果不足惜百侧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望能扒。 院中可真熱鬧移层,春花似錦、人聲如沸赫粥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽越平。三九已至频蛔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秦叛,已是汗流浹背晦溪。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挣跋,地道東北人三圆。 一個(gè)月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舟肉。 傳聞我的和親對象是個(gè)殘疾皇子修噪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348