Android RecyclerView自定義LayoutManager

把握生命里的每一分鐘,全力以赴我們心中的夢,不經歷風雨 怎么見彩虹,沒有人能隨隨便便成功 -----《真心英雄》

在第一篇中已經講過,LayoutManager主要用于布局其中的Item,在LayoutManager中能夠對每個Item的大小,位置進行更改,將它放在我們想要的位置,在很多優秀的效果中,都是通過自定義LayoutManager來實現的,比如:



可以看到效果非常棒,通過這一節的學習,大家也就理解了自定義LayoutManager的方法,然后再理解這些控件的代碼就不再難了。

在這節中,我們先自己制作一個LinearLayoutManager,來看下如何自定義LayoutManager,下節中,我們會通過自定義LayoutManager來制作第一個滾輪翻頁的效果。

自定義CustomLayoutManager

先生成一個類CustomLayoutManager,派生自LayoutManager:

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

當我們派生自LayoutManager時,會強制讓我們生成一個方法generateDefaultLayoutParams。這個方法就是RecyclerView Item的布局參數,換種說法,就是RecyclerView 子 item 的 LayoutParameters,若是想修改子Item的布局參數(比如:寬/高/margin/padding等等),那么可以在該方法內進行設置。一般來說,沒什么特殊需求的話,則可以直接讓子item自己決定自己的寬高即可(wrap_content)。

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

如果這時候,我們把上節demo中LinearLayoutManager替換下:

public class LinearActivity extends AppCompatActivity {
    private ArrayList<String> mDatas = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_linear);

        …………
        RecyclerView mRecyclerView = (RecyclerView) findViewById(R.id.linear_recycler_view);
        
        mRecyclerView.setLayoutManager(new CustomLayoutManager());

        RecyclerAdapter adapter = new RecyclerAdapter(this, mDatas);
        mRecyclerView.setAdapter(adapter);
    }
    …………
}

運行一下,發現頁面完全空白:


我們說過所有的Item的布局都是在LayoutManager中處理的,很明顯,我們目前在CustomLayoutManager中并沒有布局任何的Item。當然沒有Item出現了。

onLayoutChildren()

在LayoutManager中,所有Item的布局都是在onLayoutChildren()函數中處理的,所以我們在CustomLayoutItem中添加onLayoutChildren()函數:

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

在這個函數中,我主要做了兩個事:第一:把所有的item所對應的view加進來:

for (int i = 0; i < getItemCount(); i++) {
    View view = recycler.getViewForPosition(i);
    addView(view);
    …………
}

第二:把所有的Item擺放在它應在的位置:

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

measureChildWithMargins(view, 0, 0);函數測量這個View,并且通過getDecoratedMeasuredWidth(view)得到測量出來的寬度,需要注意的是通過getDecoratedMeasuredWidth(view)得到的是item+decoration的總寬度。如果你只想得到view的測量寬度,通過View.getMeasuredWidth()就可以得到了。

然后通過layoutDecorated()函數將每個item擺放在對應的位置,每個Item的左右位置都是相同的,從左側x=0開始擺放,只是y的點需要計算。所以這里有一個變量offsetY,用以累加當前Item之前所有item的高度。從而計算出當前item的位置。這個部分難度不大,就不再細講了。

在此之后,我們再運行程序,會發現,現在item顯示出來了:


添加滾動效果

但是,現在還不能滑動,如果我們要給它添加上滑動,需要修改兩個地方:

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

@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 平移容器內的item
    offsetChildrenVertical(-dy);
    return dy;
}

我們通過在canScrollVertically()中return true;使LayoutManager具有垂直滾動的功能。然后scrollVerticallyBy中接收每次滾動的距離dy。如果你想使LayoutManager具有橫向滾動的功能,可以通過在canScrollHorizontally()中return true;

這里需要注意的是,在scrollVerticallyBy中,dy表示手指在屏幕上每次滑動的位移。

  • 當手指由下往上滑時,dy>0
  • 當手指由上往下滑時,dy<0

當手指向上滑動時,我們需要讓所有子Item向上移動,向上移動明顯是需要減去dy的。所以,大家經過測試也可以發現,讓容器內的item移動-dy距離,才符合生活習慣。在LayoutManager中,我們可以通過public void offsetChildrenVertical(int dy)函數來移動RecycerView中的所有item。

現在我們再運行一下:


這里雖然實現了滾動,但是Item到頂之后,仍然可以滾動,這明顯是不對的,我們需要在滾動時添加判斷,如果到頂了或者到底了就不讓它滾動了。

判斷到頂

判斷到頂相對比較容易,我們只需要把所有的dy相加,如果小于0,就表示已經到頂了。就不讓它再移動就行,代碼如下:

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

在這段代碼中,通過變量mSumDy 保存所有移動過的dy,如果當前移動的距離<0,那么就不再累加dy,直接讓它移動到y=0的位置,因為之前已經移動的距離是mSumdy;
所以計算方法為:
travel+mSumdy = 0;
=> travel = -mSumdy
所以要將它移到y=0的位置,需要移動的距離為-mSumdy.效果如下圖所示:


從效果圖中可以看到,現在在到頂時,就不會再移動了。下面再來看看到底的問題。

判斷到底

判斷到底的方法,其實就是我們需要知道所有item的總高度,用總高度減去最后一屏的高度,就是到底的時的偏移值,如果大于這個偏移值就說明超過底部了。

所以,我們首先需要得到所有item的總高度,我們知道在onLayoutChildren中會測量所有的item并且對每一個item布局,所以我們只需要在onLayoutChildren中將所有item的高度相加就可以得到所有Item的總高度了。

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的高度,
    // 則將高度設置為RecyclerView的高度
    mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
private int getVerticalSpace() {
    return getHeight() - getPaddingBottom() - getPaddingTop();
}

getVerticalSpace()函數可以得到RecyclerView用于顯示Item的真實高度。而相比上面的onLayoutChildren,這里只添加了一句代碼:mTotalHeight = Math.max(offsetY, getVerticalSpace());這里只所以取最offsetY和getVerticalSpace()的最大值是因為,offsetY是所有item的總高度,而當item填不滿RecyclerView時,offsetY應該是比RecyclerView的真正高度小的,而此時的真正的高度應該是RecyclerView本身所設置的高度。

接下來就是在scrollVerticallyBy中判斷到底并處理了:

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

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

mSumDy + dy > mTotalHeight - getVerticalSpace()中:
mSumDy + dy 表示當前的移動距離,mTotalHeight - getVerticalSpace()表示當滑動到底時滾動的總距離;

當滑動到底時,此次的移動距離要怎么算呢?
算法如下:
travel + mSumDy = mTotalHeight - getVerticalSpace();
即此將將要移動的距離加上之前的總移動距離,應該是到底的距離。
=> travel = mTotalHeight - getVerticalSpace() - mSumDy;

現在再運行一下代碼,可以看到,這時候的垂直滑動列表就完成了:


從列表中可以看出,現在到頂和到底可以繼續滑動的問題就都解決了。下面貼出完整的CustomLayoutManager代碼,供大家參考:

package com.example.myrecyclerview;

import android.util.Log;
import android.view.View;

import androidx.recyclerview.widget.RecyclerView;

public class CustomLayoutManager extends RecyclerView.LayoutManager {

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

    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的高度,
        // 則將高度設置為RecyclerView的高度
        mTotalHeight = Math.max(offsetY, getVerticalSpace());
    }

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


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

    private int mSumDy = 0;

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

項目地址 https://github.com/githubwwj/MyRecyclerView

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