把握生命里的每一分鐘,全力以赴我們心中的夢,不經歷風雨 怎么見彩虹,沒有人能隨隨便便成功 -----《真心英雄》
在第一篇中已經講過,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;
}
}