自定義LayoutManager

本文轉(zhuǎn)自RecyclerView系列之三自定義LayoutManager

LayoutManager主要用于布局其中的item,在LayoutManager中能夠?qū)γ總€(gè)item的大小、位置進(jìn)行更改,將它放到指定的位置上,在很多優(yōu)秀的效果中,都是通過自定義LayoutManager實(shí)現(xiàn)的。


20181121103142295.gif

下面先來看下如何自定義LinearLayoutManager。

1、初始化展示界面

1.1、自定義CustomLinearLayoutManager

public class CustomLinearLayoutManager extends LayoutManager {
    @Override
    public LayoutParams generateDefaultLayoutParams() {
        return null;
    }
}

繼承LayoutManager會(huì)強(qiáng)制讓我們實(shí)現(xiàn)generateDefaultLayoutParams()方法。該方法主要用于設(shè)置RecyclerView中item的LayoutParams。

當(dāng)在onCreateViewHolder方法中通過LayoutInflater.inflate(resource, root,attachToRoot)加載布局時(shí)root傳入為null時(shí)item的LayoutParams為null,此時(shí)generateDefaultLayoutParams()生成的LayoutParams就派上用場了。

一般來說沒有什么特殊的情況下,讓子item自己決定寬高,故設(shè)置為wrap_content。

public class CustomLayoutManager extends LayoutManager {
    @Override
    public LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
                RecyclerView.LayoutParams.WRAP_CONTENT);
    }
}

這樣自定義LayoutManager就完成了嗎,運(yùn)行一下你會(huì)發(fā)現(xiàn),啥也不顯示。這又是為啥呢?前面我們就說了LayoutManager主要負(fù)責(zé)item的布局,而上面代碼中并未對item布局進(jìn)行布局。

1.2、onLayoutManager

在LayoutManager中,item的布局是在onLayoutManager中完成的。

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //定義豎直方向的偏移量
    int offsetY = 0;
    for (int i = 0; i < getItemCount(); i++) {
        View view = recycler.getViewForPosition(i);
        addView(view);
        measureChildWithMargins(view, 0, 0);
        int width = getDecoratedMeasuredWidth(view);
        int height = getDecoratedMeasuredHeight(view);
        layoutDecorated(view, 0, offsetY, width, offsetY + height);
        offsetY += height;
    }
}

上面方法中主要做了兩件事:

  • 1、將所有的item添加進(jìn)來
//從緩存中取出合適的View,如果緩存中沒有則通過onCreateViewHolder創(chuàng)建
 View view = recycler.getViewForPosition(i);
   //添加View
 addView(view);
  • 2、對所有item進(jìn)行布局
//添加View后,進(jìn)行View測量
measureChildWithMargins(view, 0, 0);
//getDecoratedMeasuredWidth得到的是item+decoration總寬度
int width = getDecoratedMeasuredWidth(view);
//getDecoratedMeasuredHeight得到的是item+decoration總高度
int height = getDecoratedMeasuredHeight(view);
//布局item
layoutDecorated(view, 0, offsetY, width, offsetY + height);

再次運(yùn)行就會(huì)發(fā)現(xiàn)可以顯示界面了。

2、滾動(dòng)邏輯

2.1、添加滾動(dòng)效果

但是現(xiàn)在還不能滾動(dòng),如果要實(shí)現(xiàn)滾動(dòng),需要修改兩個(gè)地方。

@Override
public boolean canScrollVertically() {
    return true;
}

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 平移容器內(nèi)的item
    offsetChildrenVertical(-dy);
    return dy;
}
  • 1、重寫canScrollVertically()返回true,表示豎直滾動(dòng),如果想實(shí)現(xiàn)水平滾動(dòng)則需要重寫canScrollHorizontally()并返回true。
  • 2、方法scrollVerticallyBy()中接收參數(shù)dy表示每次豎直滾動(dòng)的距離。當(dāng)手指從下向上滑動(dòng)時(shí):dy>0,手指從上向下滑動(dòng)時(shí):dy<0。當(dāng)手指向上滑動(dòng)時(shí),RecyclerView中item應(yīng)該也跟著手指向上移動(dòng),所以在調(diào)用offsetChildrenVertical(-dy)方法實(shí)現(xiàn)item的平移時(shí),應(yīng)傳入-dy才合適。
    這樣就完成了item的滾動(dòng),運(yùn)行后發(fā)現(xiàn)有兩個(gè)問題,當(dāng)滾動(dòng)到頂部和底部時(shí),依然可以滾動(dòng),這顯然是不對的,下面就看下如何進(jìn)行邊界的限制。

2.2、邊界判斷

現(xiàn)在我們就面臨兩個(gè)問題:如何判斷滑動(dòng)到了頂部和底部。

2.2.1、判斷到頂部

將滑動(dòng)過程中所有dy累加,如果小于0,表示已經(jīng)滑動(dòng)到了頂部,此時(shí)不讓它移動(dòng)即可。

private int mSumDy = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    int travel = dy;
    //如果滑動(dòng)到最頂部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    }
    mSumDy += travel;
    // 平移容器內(nèi)的item
    offsetChildrenVertical(-travel);
    return travel;
}

上面代碼中,通過mSumDy累加所有滑動(dòng)過的距離,如果當(dāng)前滑動(dòng)距離小于0,就讓它移動(dòng)到y(tǒng)=0的位置,mSumDy是之前移動(dòng)過的距離,所以計(jì)算方法為:

travel+mSumDy=0,即travel = -mSumDy;

然后調(diào)用offsetChildrenVertical()將修正后的travel傳入。

2.2.2、判斷到底部

當(dāng)向上滑動(dòng),在恰好滑動(dòng)到底部時(shí),滑動(dòng)距離=所有item的高度和-RecyclerView的高度。當(dāng)滑動(dòng)的距離大于所有item的高度和-RecyclerView的高度時(shí)就說明已經(jīng)滑動(dòng)到了底部。

根據(jù)上面的分析,首先我們要得到所有item的高度和,在onLayoutChildren中我們會(huì)對每一個(gè)item進(jìn)行測量并布局,在這里進(jìn)行累加就可以算出總高度。

private int mTotalHeight = 0;
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //定義豎直方向的偏移量
    int offsetY = 0;
    for (int i = 0; i < getItemCount(); i++) {
        View view = recycler.getViewForPosition(i);
        addView(view);
        measureChildWithMargins(view, 0, 0);
        int width = getDecoratedMeasuredWidth(view);
        int height = getDecoratedMeasuredHeight(view);
        layoutDecorated(view, 0, offsetY, width, offsetY + height);
        offsetY += height;
    }
    //如果所有子View的高度和沒有填滿RecyclerView的高度,
    // 則將高度設(shè)置為RecyclerView的高度
    mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
private int getVerticalSpace() {
    return getHeight() - getPaddingBottom() - getPaddingTop();
}

getVerticalSpace()函數(shù)可以得到RecyclerView用于顯示item的真實(shí)高度。而相比上面的onLayoutChildren,這里只添加了一句代碼:mTotalHeight = Math.max(offsetY, getVerticalSpace());這里只所以取最offsetY和getVerticalSpace()的最大值是因?yàn)椋琽ffsetY是所有item的總高度,而當(dāng)item填不滿RecyclerView時(shí),offsetY應(yīng)該是比RecyclerView的真正高度小的,而此時(shí)的真正的高度應(yīng)該是RecyclerView本身所設(shè)置的高度。
接下來就是在scrollVerticallyBy中判斷到底并處理了:

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    int travel = dy;
    //如果滑動(dòng)到最頂部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        travel = mTotalHeight - getVerticalSpace() - mSumDy;
    }

    mSumDy += travel;
    // 平移容器內(nèi)的item
    offsetChildrenVertical(-travel);
    return dy;
}

當(dāng)滑動(dòng)偏移量大于 mTotalHeight - getVerticalSpace()時(shí)說明已經(jīng)滑到了底部,所以此時(shí)需要對將要滑動(dòng)的dy進(jìn)行修正,使得滿足

travel+mSumDy=mTotalHeight - getVerticalSpace();
即travel = mTotalHeight - getVerticalSpace() - mSumDy;

這樣就完成了底部邊界的問題的修復(fù)。下面貼出完整代碼:

public class CustomLayoutManager extends LayoutManager {
    private int mSumDy = 0;
    private int mTotalHeight = 0;

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

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //定義豎直方向的偏移量
        int offsetY = 0;
        for (int i = 0; i < getItemCount(); i++) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            int width = getDecoratedMeasuredWidth(view);
            int height = getDecoratedMeasuredHeight(view);
            layoutDecorated(view, 0, offsetY, width, offsetY + height);
            offsetY += height;
        }

        //如果所有子View的高度和沒有填滿RecyclerView的高度,
        // 則將高度設(shè)置為RecyclerView的高度
        mTotalHeight = Math.max(offsetY, getVerticalSpace());
    }

    private int getVerticalSpace() {
        return getHeight() - getPaddingBottom() - getPaddingTop();
    }

    @Override
    public boolean canScrollVertically() {
        return true;
    }
    
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int travel = dy;
        //如果滑動(dòng)到最頂部
        if (mSumDy + dy < 0) {
            travel = -mSumDy;
        } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        //如果滑動(dòng)到最底部
            travel = mTotalHeight - getVerticalSpace() - mSumDy;
        }

        mSumDy += travel;
        // 平移容器內(nèi)的item
        offsetChildrenVertical(-travel);
        return dy;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,115評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,234評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,621評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,822評(píng)論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,380評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,128評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,319評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,548評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評(píng)論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,048評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,285評(píng)論 2 376

推薦閱讀更多精彩內(nèi)容