Android——RecyclerView入門學(xué)習(xí)之ItemDecoration(一)

學(xué)習(xí)資料:

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í)計劃:


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è)置ItemDecorationLayoutManagerLineatLayoutManager


子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è)置了背景色,字體是白色

運行效果:

不設(shè)置ItemDecroation

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大部分時候考慮itemposition就可以,但GridLayoutManagerStaggeredGridLayoutManager需要考慮行和列,情況就比較復(fù)雜。


方法中有4個參數(shù)

  • Rect outRect:可以簡單理解為item四邊邊距奉封裝在這個對象中,用來設(shè)置Itempadding
  • 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個值在RecyclerViewonMeasure()有使用,感覺是用來標(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>

這個方法是在itemonDraw()方法之后進行回調(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()方法的返回值并不是recyclerViewAdapter中所有的item的數(shù)量,而是當(dāng)前屏幕中出現(xiàn)在RecyclerViewitem的數(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();

注意:
ViewGroupgetChildCount()方法的返回值itemCount便是 getChildAt(int index)這個方法index的區(qū)間上限 ,[0,itemCount)。例如:

position示例

當(dāng)前屏幕顯示的是25--到-->42parent.getChildCount()的返回結(jié)果itemCount便是18。凡是在屏幕上第一個出現(xiàn)的itemindex便是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ù)雜列表布局

共勉 :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容