學(xué)習(xí)資料:
- 張旭童同學(xué)的使用ItemDecoration為RecyclerView打造帶懸停頭部的分組列表
- Piasy大神的深入理解 RecyclerView 系列之一:ItemDecoration
Piasy大神的每篇博客質(zhì)量都很高,強烈推薦
網(wǎng)上有很多關(guān)于RecyclerView
學(xué)習(xí)博客,之前看了幾篇,但基本側(cè)重點都是RecyclerView.Adapter
。關(guān)于RecyclerView
的側(cè)滑刪除,之前有過簡單學(xué)習(xí)ItemTouchHleper實現(xiàn)RecyclerView側(cè)滑刪除,但對RecyclerView
了解遠(yuǎn)遠(yuǎn)不夠。除了Adapter
外,RecyclerView
還有很多其他強大的地方需要學(xué)習(xí)
天才木木同學(xué)收集整理的的Android開發(fā)之一些好用的RecyclerView輪子非常好
學(xué)習(xí)計劃:
- 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è)置四邊邊距,以及上下左右繪制分割線。當(dāng)然功能不止這些
ItemDecoration
一個有6個抽象方法,有3個還廢棄了,也就剩下3個需要學(xué)習(xí)
- 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條字符串?dāng)?shù)據(jù) <p>
直接使用RecyclerView
展示50條純字符串?dāng)?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
背景色導(dǎo)致整個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);//獲取當(dāng)前要進行布局的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));
運行后效果
由于是入門學(xué)習(xí),暫時也只是針對對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
當(dāng)前的狀態(tài),例如滑動目標(biāo)的Position
或者子控件的焦點。State
對象也可以對任意的數(shù)據(jù)通過資源id
進行保存或者識別
在State
中有3個用于標(biāo)記當(dāng)前所處步驟的常量值:
-
STEP_START
:布局開始 -
STEP_LAYOUT
:布局中 -
STEP_ANIMATIONS
:處于動畫中
RecyclerView
的工作流程肯定也會是measure,layout,draw
。3個值在RecyclerView
的onMeasure()
有使用,感覺是用來標(biāo)識RecyclerView
在測量過程中所處于的不同時機。目前并不清楚具體的影響,RecyclerView
工作流程需要以后再進行深入學(xué)習(xí)
方法 | 作用 |
---|---|
getItemCount() |
得到整個RecyclerView 中,目前的item 的數(shù)量 |
isMeasuring() |
是否正在測量 |
isPreLayout() |
是否準(zhǔn)備進行布局 |
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)可見的滑動目標(biāo)在Adapter 的索引值,滑動目標(biāo)由SmoothScroller 來指定 |
hasTargetScrollPosition() |
判斷是否已經(jīng)滑動到目標(biāo) |
willRunPredictiveAnimations() |
判斷是否進行預(yù)測模式的動畫在布局過程中 |
willRunSimpleAnimations() |
判斷是否進行簡單模式的動畫在布局過程中 |
getItemCount()
并不是完全等于getAdapter.getItemCount()
,在源碼的注釋中,關(guān)于postion
的計算,建議使用State.getItemCount()
而非立即直接通過Adapter
State
有些方法和屬性涉及到其他的類,有些涉及RecyclerView
的工作過程,目前我的學(xué)習(xí)程度也不是很了解,暫時并不打算繼續(xù)深挖學(xué)習(xí)下去,總覺得理解有錯誤,知道的同學(xué)請指出
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);//獲取當(dāng)前要進行布局的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
當(dāng)間距小于分割線的寬度時,分割線繪制的厚度會保持與間距一樣
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 坐標(biāo)
final float x = parent.getWidth() / 2;
////圓心 y 坐標(biāo)
final float y = 100;
//半徑
final float radius = 100;
//漸變著色器 坐標(biāo)隨意設(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>
在繪制底部分割線的時候,遇到一個問題:
當(dāng)快速滑動時,底部會閃動,造成體驗不好,如果分割線比較窄,不是很明顯,分割線寬的時候就很明顯
已解決 ,原因分析在下面
2.1 補充,問題修復(fù) <p>
問題原因:
問題出在drawHorizontalLines()
方法中final int itemCount = parent.getChildCount()-1
這行代碼,之所以減一考慮的是為了使最后一個item
下,不用再繪制分割線。
RecyclerView.getChildCount()
方法的返回值并不是recyclerView
的Adapter
中所有的item
的數(shù)量,而是當(dāng)前屏幕中出現(xiàn)在RecyclerView
中item
的數(shù)量,一個item
只要露出一點點,就算出現(xiàn),就會被包含在內(nèi)。
-1
就會導(dǎo)致RecycelrView
統(tǒng)計已經(jīng)出現(xiàn)的item
時的數(shù)量少一個,就會導(dǎo)致滑動過程中,屏幕中最后一個item
的底部分割線不進行繪制,造成閃屏
解決辦法:
不減1,就OK,修改為:
final int itemCount = parent.getChildCount();
注意:
ViewGroup
的getChildCount()
方法的返回值itemCount
便是 getChildAt(int index)
這個方法index
的區(qū)間上限 ,[0,itemCount)
。例如:
當(dāng)前屏幕顯示的是
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
,可以學(xué)習(xí)下代碼,有一些不錯的細(xì)節(jié)優(yōu)化
以上信息從drakeet 博客得知,果然關(guān)注大神,能夠多了解信息
3. 最后 <p>
作為一個青銅5
的選手,也是熱愛LOL
的,也有著一顆王者
心,可RNG,EDG
全輸了,止步8強,郁悶
本人很菜,有錯誤請指出
一個完整的練習(xí):TitleItemDecoration
慕課有一個不錯的視屏不一樣的RecyclerView優(yōu)雅實現(xiàn)復(fù)雜列表布局
共勉 :)