RecyclerView ItemDecoration 實現分組吸頂效果

本文實現的吸頂效果爲:

簡介

我們都知道 ListView 添加分割線可以通過在佈局文件中添加 android:divider 屬性即可,但是 RecyclerView 並沒有提供那樣的屬性。如若需要使用分割線,則需要使用其他的方式實現:

  1. 給 RecyclerView item 設置 margin : 首先將佈局文件中的 RecyclerView 背景設置成分割線的顏色(如黑色),itemView 的背景顏色設置成白色,然後在 onCreateViewHolder() 中爲 itemView 設置 top / bottom margin;

    <!--佈局文件-->
    <android.support.v7.widget.RecyclerView
           android:id="@+id/recycler_view"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:background="#000" />
    // RecyclerAdapter.java
    @Override
       public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
           View itemView = inflater.inflate(R.layout.item_recycler_view, parent, false);
           RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) itemView.getLayoutParams();
           layoutParams.bottomMargin = 50;  // 爲方便觀察,設置一個很大的間距
           itemView.setLayoutParams(layoutParams);
           return new ItemViewHolder(itemView);
       }

可以看到上面效果圖:這種實現方式第一個 item 或者 最後一個 item(如上圖 最後一個 item) 都會存在 分割線。同時也會增加不必要的背景設置,從而導致過度繪製。

. 2 自定義 ItemDecoration;

RecyclerView.ItemDecoration 的使用主要涉及到以下三個函數:

void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state)
void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state)

ItemDecoration 實現常規分割線

一般的,對於列表添加分割線,第一個 itemView 頂部和最後一個 itemView 底部是不需要分割線的,以下代碼實現這種效果。

繼承自 RecyclerView.ItemDecoration 類,在 onDraw() 中給 itemView 的頂部繪製一個矩形,第一個 itemView 沒有繪製(我們通過 getChildAdapterPosition() 方法獲取第 1 個 itemView 的索引,而不是直接使用 for 循環中的 i = 0,因爲 i=0 是表示當前屏幕中第一個可見的 itemView 的索引)

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final String TAG = "DividerItemDecoration";

    private Context context;

    private int dividerHeight;

    private int dividerPaddingLeft;

    private int dividerPaddingRight;

    private Paint paint;

    public DividerItemDecoration(Context context) {
        this.context = context;
        dividerHeight = dp2Px(20);
        dividerPaddingLeft = dp2Px(10);
        dividerPaddingRight = dp2Px(10);
        initPaint();
    }

    private void initPaint() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(context.getResources().getColor(R.color.colorAccent));
        paint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        // 不是第一個 item 才設置 top
        if (parent.getChildAdapterPosition(view) != 0) {
            outRect.top = dividerHeight;
        }
        Log.d(TAG, "getItemOffsets: left = " + outRect.left + ",top = " + outRect.top + ",right = " + outRect.right
                + ", bottom = " + outRect.bottom);


    }


    @Override
    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(canvas, parent, state);
        // 獲取當前屏幕可見 item 數量,而不是 RecyclerView 所有的 item 數量
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int childAdapterPosition = parent.getChildAdapterPosition(view);
            // 第一個 itemview 不需要繪製
            if (childAdapterPosition == 0) {
                continue;
            }
            // 由於分割線是繪製在每一個 itemview 的頂部,所以分割線矩形 rect.bottom = itemview.top,
            // rect.top = itemview.top - dividerHeight
            int bottom = view.getTop();
            int top = bottom - dividerHeight;
            int left = parent.getPaddingLeft();
            int right = parent.getPaddingRight();
            Log.d(TAG, "onDraw: top = " + top + ",bottom = " + bottom);
            // 考慮 divider 左右 padding
            canvas.drawRect(new Rect(left + dividerPaddingLeft, top,
                    view.getWidth() - right - dividerPaddingRight, bottom), paint);
        }
    }

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);

    }


    private int dp2Px(int dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics());
    }
}

吸頂效果分割線

(1) 觀察其他 App 中的列表吸頂效果,發現 第一個 itemView 的頂部也是有分割線的,因此,我們首先需要將 RecyclerView 的 第一個 itemView 改成含有 分割線的。

 @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        // 不是第一個 item 才設置 top
//        if (parent.getChildAdapterPosition(view) != 0) {
        outRect.top = dividerHeight;
//        }
        Log.d(TAG, "getItemOffsets: left = " + outRect.left + ",top = " + outRect.top + ",right = " + outRect.right
                + ", bottom = " + outRect.bottom);


    }


    @Override
    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(canvas, parent, state);
        // 獲取當前屏幕可見 item 數量,而不是 RecyclerView 所有的 item 數量
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int childAdapterPosition = parent.getChildAdapterPosition(view);
//            // 第一個 itemview 不需要繪製
//            if (childAdapterPosition == 0) {
//                continue;
//            }
            // 由於分割線是繪製在每一個 itemview 的頂部,所以分割線矩形 rect.bottom = itemview.top,
            // rect.top = itemview.top - dividerHeight
            int bottom = view.getTop();
            int top = bottom - dividerHeight;
            int left = parent.getPaddingLeft();
            int right = parent.getPaddingRight();
            Log.d(TAG, "onDraw: top = " + top + ",bottom = " + bottom);
            canvas.drawRect(left + dividerPaddingLeft, top,
                    view.getWidth() - right - dividerPaddingRight, bottom, paint);
        }
    }

(2) 在 RecyclerView 頂部添加一個固定的分割線,向上滾動時,當前屏幕第一個可見 itemView 的 bottom 開始小於分割線高度時,第二個 itemView 的分割線將第一個 itemView 分割線擠出屏幕,第二個 itemView 的分割線充當頂部固定的分割線。

@Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);
        View firstVisibleView = parent.getChildAt(0);
        int left = parent.getPaddingLeft();
        int right = firstVisibleView.getWidth() - parent.getPaddingRight();
        // 第一個itemview(firstVisibleView) 的 bottom 值小於分割線高度,分割線隨着 recyclerview 滾動,
        // 分割線top固定不變,bottom=firstVisibleView.bottom
        if (firstVisibleView.getBottom() <= dividerHeight) {
            canvas.drawRect(left, 0, right, firstVisibleView.getBottom(), topDividerPaint);
        } else {
            canvas.drawRect(left, 0, right, dividerHeight, topDividerPaint);
        }
    }

(3) 在分組分割線上繪製 分組文字。

完整代碼 DividerItemDecoration.java:

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final String TAG = "DividerItemDecoration";

    private Context context;

    private int groupDividerHeight;     // 分組分割線高度

    private int itemDividerHeight;     // 分組內item分割線高度

    private int dividerPaddingLeft;    // 分割線左間距

    private int dividerPaddingRight;   // 分割線右間距

    private Paint dividerPaint;     // 繪製分割線畫筆

    private Paint textPaint;        // 繪製文字畫筆

    private Paint topDividerPaint;


    public DividerItemDecoration(Context context, OnGroupListener listener) {
        this.context = context;
        this.listener = listener;
        groupDividerHeight = dp2Px(24);
        itemDividerHeight = dp2Px(1);
        initPaint();
    }

    private void initPaint() {
        dividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        dividerPaint.setColor(context.getResources().getColor(R.color.colorAccent));
        dividerPaint.setStyle(Paint.Style.FILL);


        topDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        topDividerPaint.setColor(Color.parseColor("#9924f715"));
        topDividerPaint.setStyle(Paint.Style.FILL);


        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(sp2Px(14));

    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
//        // 不是第一個 item 才設置 top
////        if (parent.getChildAdapterPosition(view) != 0) {
//        outRect.top = groupDividerHeight;
////        }
//        Log.d(TAG, "getItemOffsets: left = " + outRect.left + ",top = " + outRect.top + ",right = " + outRect.right
//                + ", bottom = " + outRect.bottom);

        int position = parent.getChildAdapterPosition(view);
        // 獲取組名
        String groupName = getGroupName(position);
        if (groupName == null) {
            return;
        }
        if (position == 0 || isGroupFirst(position)) {
            outRect.top = groupDividerHeight;
        } else {
            outRect.top = dp2Px(1);
        }
    }


    @Override
    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(canvas, parent, state);
        // getChildCount() 獲取的是當前屏幕可見 item 數量,而不是 RecyclerView 所有的 item 數量
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = parent.getChildAt(i);
            // 獲取當前itemview在adapter中的索引
            int childAdapterPosition = parent.getChildAdapterPosition(childView);
            /**
             * 由於分割線是繪製在每一個 itemview 的頂部,所以分割線矩形 rect.bottom = itemview.top,
             * rect.top = itemview.top - groupDividerHeight
             */
            int bottom = childView.getTop();
            int left = parent.getPaddingLeft();
            int right = parent.getPaddingRight();
            if (isGroupFirst(childAdapterPosition)) {   // 是分組第一個,則繪製分組分割線
                int top = bottom - groupDividerHeight;
                Log.d(TAG, "onDraw: top = " + top + ",bottom = " + bottom);
                // 繪製分組分割線矩形
                canvas.drawRect(left + dividerPaddingLeft, top,
                        childView.getWidth() - right - dividerPaddingRight, bottom, dividerPaint);
                // 繪製分組分割線中的文字
                float baseLine = (top + bottom) / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
                canvas.drawText(getGroupName(childAdapterPosition), left + dp2Px(10),
                        baseLine, textPaint);
            } else {    // 不是分組中第一個,則繪製常規分割線
                int top = bottom - dp2Px(1);
                canvas.drawRect(left + dividerPaddingLeft, top,
                        childView.getWidth() - right - dividerPaddingRight, bottom, dividerPaint);
            }
        }
    }

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(canvas, parent, state);
        View firstVisibleView = parent.getChildAt(0);
        int firstVisiblePosition = parent.getChildAdapterPosition(firstVisibleView);
        String groupName = getGroupName(firstVisiblePosition);
        int left = parent.getPaddingLeft();
        int right = firstVisibleView.getWidth() - parent.getPaddingRight();
        // 第一個itemview(firstVisibleView) 的 bottom 值小於分割線高度,分割線隨着 recyclerview 滾動,
        // 分割線top固定不變,bottom=firstVisibleView.bottom
        if (firstVisibleView.getBottom() <= groupDividerHeight && isGroupFirst(firstVisiblePosition + 1)) {
            canvas.drawRect(left, 0, right, firstVisibleView.getBottom(), dividerPaint);
            float baseLine = firstVisibleView.getBottom() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
            canvas.drawText(groupName, left + dp2Px(10),
                    baseLine, textPaint);
        } else {
            canvas.drawRect(left, 0, right, groupDividerHeight, dividerPaint);
            float baseLine = groupDividerHeight / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
            canvas.drawText(groupName, left + dp2Px(10), baseLine, textPaint);
        }


    }


    private OnGroupListener listener;

    static interface OnGroupListener {

        // 獲取分組中第一個文字
        String getGroupName(int position);
    }


    public String getGroupName(int position) {
        if (listener != null) {
            return listener.getGroupName(position);
        }
        return null;
    }


    /**
     * 是否是某組中第一個item
     *
     * @param position
     * @return
     */
    private boolean isGroupFirst(int position) {
        // 第一個 itemView 肯定是新的一個分組
        if (position == 0) {
            return true;
        } else {
            String preGroupName = getGroupName(position - 1);
            String groupName = getGroupName(position);
            return !TextUtils.equals(preGroupName, groupName);
        }
    }


    private int dp2Px(int dpValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics());
    }

    private int sp2Px(int spValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, context.getResources().getDisplayMetrics());
    }
}

Activity 中調用:

public class RecyclerViewActivity extends AppCompatActivity {

    private List<String> dataList = new ArrayList();

    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler_view);
        recyclerView = findViewById(R.id.recycler_view);
        initData();
        LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        recyclerView.setLayoutManager(layoutManager);

        RecyclerAdapter adapter = new RecyclerAdapter(this, dataList);
        recyclerView.setAdapter(adapter);

        recyclerView.addItemDecoration(new DividerItemDecoration(this, new DividerItemDecoration.OnGroupListener() {
            @Override
            public String getGroupName(int position) {
                return dataList.get(position).substring(0, 1);
            }
        }));

    }

    private void initData() {
        dataList.add("java");
        dataList.add("jdk");
        dataList.add("php");
        dataList.add("c++");
        dataList.add("linux");
        dataList.add("windows");
        dataList.add("macos");
        dataList.add("red hat");
        dataList.add("python");
        dataList.add("jvm");
        dataList.add("wechat");
        dataList.add("cellphone");
        dataList.add("iphone");
        dataList.add("mouse");
        dataList.add("huawei");
        dataList.add("xiaomi");
        dataList.add("meizu");
        dataList.add("mocrosoft");
        dataList.add("google");
        dataList.add("whatsapp");
        dataList.add("iMac");
        dataList.add("c#");
        dataList.add("iOS");
        dataList.add("water");
        dataList.add("xiaohongshu");
        dataList.add("jake");
        dataList.add("zuk");


        Collections.sort(dataList);

    }


    public static void start(Context context) {
        Intent intent = new Intent(context, RecyclerViewActivity.class);
        context.startActivity(intent);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章