自定義控件三部曲視圖篇(六)——RecyclerView系列之三自定義LayoutManager

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


系列文章: Android自定義控件三部曲文章索引: http://blog.csdn.net/harvic880925/article/details/50995268


在第一篇中已經講過,LayoutManager主要用於佈局其中的Item,在LayoutManager中能夠對每個Item的大小,位置進行更改,將它放在我們想要的位置,在很多優秀的效果中,都是通過自定義LayoutManager來實現的,比如:
在這裏插入圖片描述 在這裏插入圖片描述
在這裏插入圖片描述
可以看到這些效果都非常棒,通過這一節的學習,大家也就理解了自定義LayoutManager的方法,然後再理解這些控件的代碼就不再難了。

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

一、初始化展示界面

1.1 自定義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出現了。

1.2 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顯示出來了:
在這裏插入圖片描述

二、添加滾動效果

2.1 添加滾動效果

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

@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到頂和到底之後,仍然可以滾動,這明顯是不對的,我們需要在滾動時添加判斷,如果到底了或者到底了就不讓它滾動了.

2.2 添加異常判斷

(1)、判斷到頂
判斷到頂相對比較容易,我們只需要把所有的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.效果如下圖所示:
在這裏插入圖片描述
從效果圖中可以看到,現在在到頂時,就不會再移動了。下面再來看看到底的問題。

(2)、判斷到底
判斷到底的方法,其實就是我們需要知道所有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代碼,供大家參考:

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的高度,
        // 則將高度設置爲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;
        //如果滑動到最頂部
        if (mSumDy + dy < 0) {
            travel = -mSumDy;
        } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        //如果滑動到最底部
            travel = mTotalHeight - getVerticalSpace() - mSumDy;
        }

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

源碼在文章底部給出

如果本文有幫到你,記得加關注哦
CSDN源碼現在不能零分下載了,必須強制最低一分,我設置爲了最低分,如果沒分的同學,可以從github上下載。
源碼地址:https://download.csdn.net/download/harvic880925/10833634
github代碼地址:https://github.com/harvic/harvic_blg_share 位於RecylcerView(三)
轉載請標明出處,https://blog.csdn.net/harvic880925/article/details/84789602 謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章