學習資料:
Piasy大神的每篇博客質(zhì)量都很高,強烈推薦
網(wǎng)上有很多關(guān)于RecyclerView學習博客,之前看了幾篇,但基本側(cè)重點都是RecyclerView.Adapter。關(guān)于RecyclerView的側(cè)滑刪除,之前有過簡單學習ItemTouchHleper實現(xiàn)RecyclerView側(cè)滑刪除,但對RecyclerView了解遠遠不夠。除了Adapter外,RecyclerView還有很多其他強大的地方需要學習
天才木木同學收集整理的的Android開發(fā)之一些好用的RecyclerView輪子非常好
學習計劃:
- ItemDecoration
- LayoutManager
- RecyclerView.Adapter
- DiffUtil
- SimpleOnItemTouchListener
- SmoothScroller
- ItemAnimator
1. ItemDecoration 條目裝飾<p>
是一個抽象類,顧名思義,就是用來裝飾RecyclerView的子item的,通過名字就可以知道,功能并不僅僅是添加間距繪制分割線,是用來裝飾item的。源碼中的描述:
An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.
基本的功能是可以用來給RecyclerView的子item設(shè)置四邊邊距,以及上下左右繪制分割線。當然功能不止這些
ItemDecoration一個有6個抽象方法,有3個還廢棄了,也就剩下3個需要學習
- getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) 設(shè)置四邊邊距
- onDraw(Canvas c, RecyclerView parent, State state) 繪制裝飾
- onDrawOver(Canvas c, RecyclerView parent, State state) 繪制蒙層
1.1 使用RecyclerView展示50條字符串數(shù)據(jù) <p>
直接使用RecyclerView展示50條純字符串數(shù)據(jù),代碼:
public class MainActivity extends AppCompatActivity {
private RecyclerView rv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
rv = (RecyclerView) findViewById(R.id.rv_main_activity);
//設(shè)置布局管理器
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
rv.setLayoutManager(manager);
//設(shè)置ItemDecoration
//適配器
RecyclerViewAdapter adapter = new RecyclerViewAdapter(rv, R.layout.item_layout);
rv.setAdapter(adapter);
//添加數(shù)據(jù)
addData(adapter);
}
/**
* 添加數(shù)據(jù)
*/
private void addData(RecyclerViewAdapter adapter) {
List<String> listData = new ArrayList<>();
for (int i = 0; i < 50; i++) {
listData.add("英勇青銅5---->"+i);
}
adapter.setData(listData);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (null != rv) {
rv.setAdapter(null);
}
}
}
代碼中沒有為RecyclerView設(shè)置ItemDecoration,LayoutManager為LineatLayoutManager
子item布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tv_item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:textSize="20sp" />
</LinearLayout>
布局也特別簡單,給TextView設(shè)置了背景色,字體是白色
運行效果:

item間就沒有間距,也沒有任何的分割線,TextView背景色導致整個RecyclerView看起來都設(shè)置了背景色
下面為每個item底部添加間距
1.2 getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) 設(shè)置四邊偏移量
自定義一個RVItemDecoration繼承ItemDecroation,重寫getItemOffsets()
代碼:
public class RVItemDecoration extends RecyclerView.ItemDecoration {
private static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL;//水平方向
private static final int VERTICAL = LinearLayoutManager.VERTICAL;//垂直方向
private int orientation;//方向
private final int decoration;//邊距大小 px
public RVItemDecoration(@LinearLayoutCompat.OrientationMode int orientation int orientation, int decoration) {
this.orientation = orientation;
this.decoration = decoration;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
final int lastPosition = state.getItemCount() - 1;//整個RecyclerView最后一個item的position
final int current = parent.getChildLayoutPosition(view);//獲取當前要進行布局的item的position
Log.e("0000", "0000---->" + current);
Log.e("0000", "0000state.getItemCount()---->" + state.getItemCount());
Log.e("0000", "0000getTargetScrollPosition---->" + state.getTargetScrollPosition());
Log.e("0000", "0000state---->" + state.toString());
if (current == -1) return;//holder出現(xiàn)異常時,可能為-1
if (layoutManager instanceof LinearLayoutManager && !(layoutManager instanceof GridLayoutManager)) {//LinearLayoutManager
if (orientation == LinearLayoutManager.VERTICAL) {//垂直
outRect.set(0, 0, 0, decoration);
if (current == lastPosition) {//判斷是否為最后一個item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0, 0, decoration);
}
} else {//水平
if (current == lastPosition) {//判斷是否為最后一個item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0,decoration, 0);
}
}
}
}
}
在Acivity中,初始化RecyclerView的時候使用:
//設(shè)置ItemDecoration
rv.addItemDecoration(new RVItemDecoration(LinearLayoutManager.VERTICAL,30));
運行后效果

由于是入門學習,暫時也只是針對對LinearLayoutManager做了一點簡單處理,最后1個item不再添加底部間距。實際開發(fā)的時候考慮的就要比這復(fù)雜的多。LinearLayoutManager大部分時候考慮item的position就可以,但GridLayoutManager和StaggeredGridLayoutManager需要考慮行和列,情況就比較復(fù)雜。
方法中有4個參數(shù)
- Rect outRect:可以簡單理解為
item四邊邊距奉封裝在這個對象中,用來設(shè)置Item的padding - View view: childView,就是item,可以理解為item的根
View,并不是item中的控件 - RecyclerView parent:就是
RecyclerView自身 - RecyclerView.State state : RecyclerView的狀態(tài),但并不包含滑動狀態(tài)
1.2.1 RecyclerView.State <p>
這個類是RecyclerView的一個靜態(tài)內(nèi)部類,源碼中的解釋:
Contains useful information about the current RecyclerView state like target scroll position or view focus. State object can also keep arbitrary data, identified by resource ids.
個人理解:
這個State封裝著RecyclerView當前的狀態(tài),例如滑動目標的Position或者子控件的焦點。State對象也可以對任意的數(shù)據(jù)通過資源id進行保存或者識別
在State中有3個用于標記當前所處步驟的常量值:
-
STEP_START:布局開始 -
STEP_LAYOUT:布局中 -
STEP_ANIMATIONS:處于動畫中
RecyclerView的工作流程肯定也會是measure,layout,draw。3個值在RecyclerView的onMeasure()有使用,感覺是用來標識RecyclerView在測量過程中所處于的不同時機。目前并不清楚具體的影響,RecyclerView工作流程需要以后再進行深入學習
| 方法 | 作用 |
|---|---|
getItemCount() |
得到整個RecyclerView中,目前的item的數(shù)量 |
isMeasuring() |
是否正在測量 |
isPreLayout() |
是否準備進行布局 |
get(int resourceId) |
根據(jù)資源id獲取item中的控件,建議使用R.id.*
|
put(int resourceId, Object data) |
添加一個指定id映射的資源對象,建議使用R.id.*來避免沖突 |
remove(int resourceId) |
根據(jù)使用R.id.*指定id來刪除存入的控件對象 |
getTargetScrollPosition() |
返回已經(jīng)可見的滑動目標在Adapter的索引值,滑動目標由SmoothScroller來指定 |
hasTargetScrollPosition() |
判斷是否已經(jīng)滑動到目標 |
willRunPredictiveAnimations() |
判斷是否進行預(yù)測模式的動畫在布局過程中 |
willRunSimpleAnimations() |
判斷是否進行簡單模式的動畫在布局過程中 |
getItemCount()并不是完全等于getAdapter.getItemCount(),在源碼的注釋中,關(guān)于postion的計算,建議使用State.getItemCount()而非立即直接通過Adapter
State有些方法和屬性涉及到其他的類,有些涉及RecyclerView的工作過程,目前我的學習程度也不是很了解,暫時并不打算繼續(xù)深挖學習下去,總覺得理解有錯誤,知道的同學請指出
1.3 onDraw(Canvas c, RecyclerView parent, State state)繪制裝飾 <p>
這個用于繪制divider,繪制在item的下一層,也就是說item會蓋在divider所在層的上面
使用重寫了onDrawer()方法和onDrawOver()的ItemDecoration后,對RecyclerView在繪制item時有些影響,主要是由于繪制順序:
mItemDecoration.onDraw()-->item.onDraw()--->mItemDecoration.onDrawOver()
onDraw()方法可以為divier設(shè)置繪制范圍,并且繪制范圍可以超出在 getItemOffsets 中設(shè)置的范圍,但由于是在item下面一層進行繪制,會存在overdraw
簡單使用,完整代碼
public class RVItemDecoration extends RecyclerView.ItemDecoration {
private final int orientation;//方向
private final int decoration;//邊距大小 px
private final int lineSize ;//分割線厚度
private final ColorDrawable mDivider;
public RVItemDecoration(@LinearLayoutCompat.OrientationMode int orientation, int decoration, @ColorInt int color, int lineSize) {
mDivider = new ColorDrawable(color);
this.orientation = orientation;
this.decoration = decoration;
this.lineSize = lineSize;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
final int lastPosition = state.getItemCount() -1;//整個RecyclerView最后一個item的position
final int current = parent.getChildLayoutPosition(view);//獲取當前要進行布局的item的position
if (current == -1) return;
if (layoutManager instanceof LinearLayoutManager && !(layoutManager instanceof GridLayoutManager)) {//LinearLayoutManager
if (orientation == LinearLayoutManager.VERTICAL) {//垂直
if (current == lastPosition) {//判斷是否為最后一個item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0, 0, decoration);
}
} else {//水平
if (current == lastPosition) {//判斷是否為最后一個item
outRect.set(0, 0, 0, 0);
} else {
outRect.set(0, 0, decoration, 0);
}
}
}
}
/**
* 繪制裝飾
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
if (orientation == LinearLayoutManager.VERTICAL) {//垂直
drawHorizontalLines(c, parent);
} else {//水平
drawVerticalLines(c, parent);
}
}
/**
* 繪制垂直布局 水平分割線
*/
private void drawHorizontalLines(Canvas c, RecyclerView parent) {
// final int itemCount = parent.getChildCount()-1;//出現(xiàn)問題的地方 下面有解釋
final int itemCount = parent.getChildCount();
Log.e("item","---->"+itemCount);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
for (int i = 0; i < itemCount; i++) {
final View child = parent.getChildAt(i);
if (child == null) return;
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top +lineSize;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
/**
* 繪制水平布局 豎直的分割線
*/
private void drawVerticalLines(Canvas c, RecyclerView parent) {
final int itemCount = parent.getChildCount();
final int top = parent.getPaddingTop();
for (int i = 0; i < itemCount; i++) {
final View child = parent.getChildAt(i);
if (child == null) return;
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int bottom = child.getHeight() - parent.getPaddingBottom();
final int left = child.getRight() + params.rightMargin;
final int right = left +lineSize;
if (mDivider == null) return;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
運行后的效果:

同樣這里也只是考慮了最簡單的
LinerLayoutManager一種情況。使用這個方法時,注意繪制范圍,盡量避免overdraw
當間距小于分割線的寬度時,分割線繪制的厚度會保持與間距一樣
1.3 onDrawOver(Canvas c, RecyclerView parent, State state) 繪制蒙層<p>
這個方法是在item的onDraw()方法之后進行回調(diào),也就繪制在了最上層
簡單使用,繪制一個顏色紅黃漸變的圓
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
//畫筆
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//圓心 x 坐標
final float x = parent.getWidth() / 2;
////圓心 y 坐標
final float y = 100;
//半徑
final float radius = 100;
//漸變著色器 坐標隨意設(shè)置的
final LinearGradient shader = new LinearGradient(x-50, 0, x+100, 200, Color.RED, Color.YELLOW, Shader.TileMode.REPEAT);
paint.setShader(shader);
//繪制圓
c.drawCircle(x, y, radius, paint);
}

只要手指在RecyclerView上進行滑動,onDrawOver()方法就會被回調(diào)。但onDrawOver()每回調(diào)一次,會將上次的繪制清除,只有最后一次的繪制會被保留。也就是說繪制的蒙層在屏幕只會有一個
2. 遇到的問題<p>
在繪制底部分割線的時候,遇到一個問題:

當快速滑動時,底部會閃動,造成體驗不好,如果分割線比較窄,不是很明顯,分割線寬的時候就很明顯
已解決 ,原因分析在下面
2.1 補充,問題修復(fù) <p>
問題原因:
問題出在drawHorizontalLines()方法中final int itemCount = parent.getChildCount()-1這行代碼,之所以減一考慮的是為了使最后一個item下,不用再繪制分割線。
RecyclerView.getChildCount()方法的返回值并不是recyclerView的Adapter中所有的item的數(shù)量,而是當前屏幕中出現(xiàn)在RecyclerView中item的數(shù)量,一個item只要露出一點點,就算出現(xiàn),就會被包含在內(nèi)。
-1就會導致RecycelrView統(tǒng)計已經(jīng)出現(xiàn)的item時的數(shù)量少一個,就會導致滑動過程中,屏幕中最后一個item的底部分割線不進行繪制,造成閃屏
解決辦法:
不減1,就OK,修改為:
final int itemCount = parent.getChildCount();
注意:
ViewGroup的getChildCount()方法的返回值itemCount便是 getChildAt(int index)這個方法index的區(qū)間上限 ,[0,itemCount)。例如:

當前屏幕顯示的是
25--到-->42,parent.getChildCount()的返回結(jié)果itemCount便是18。凡是在屏幕上第一個出現(xiàn)的item的index便是0,哪怕只是漏出一點點。在parent.getChildAt(int index)中,index的取值范圍便是0<= index < 18
2016.10.17 13:48
3.0 補充 官方推出DividerItemDecoration <p>
2016.10.20
Android support libraries更新了25.0.0,新增了BottomNavigationView,并增加了一個官方版的DividerItemDecoration,可以學習下代碼,有一些不錯的細節(jié)優(yōu)化
以上信息從drakeet 博客得知,果然關(guān)注大神,能夠多了解信息
3. 最后 <p>
作為一個青銅5的選手,也是熱愛LOL的,也有著一顆王者心,可RNG,EDG全輸了,止步8強,郁悶
本人很菜,有錯誤請指出
一個完整的練習:TitleItemDecoration
慕課有一個不錯的視屏不一樣的RecyclerView優(yōu)雅實現(xiàn)復(fù)雜列表布局
共勉 :)