Android ItemDecoration 實現(xiàn)分組索引列表的示例代碼
本文介紹了Android ItemDecoration 實現(xiàn)分組索引列表的示例代碼,分享給大家。具體如下:
先來看看效果:
我們要實現(xiàn)的效果主要涉及三個部分:
- 分組 GroupHeader
- 分割線
- SideBar
前兩個部分涉及到一個ItemDecoration類,也是我們接下來的重點,該類是RecyclerView的一個抽象靜態(tài)內(nèi)部類,主要作用就是給RecyclerView的ItemView繪制額外的裝飾效果,例如給RecyclerView添加分割線。
使用ItemDecoration時需要繼承該類,根據(jù)需求可以重寫如下三個方法,其它的方法已經(jīng)deprecated了:
public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); } }
然后將其添加到RecyclerView中:
recyclerView.addItemDecoration(new GroupHeaderItemDecoration())
了解這個三個方法的作用,這樣才能更好的實現(xiàn)我們想要的功能:
1、getItemOffsets()
給指定的ItemView設置偏移量,具體怎么設置呢,咱們看圖說話:
圖中左邊的是原始RecyclerView列表,右邊是設置了ItemView偏移量的列表,其實相當于在ItemView外部添加了一個矩形區(qū)域
其中l(wèi)eft、top、right、bottom就是ItemView在四個方向的偏移量,對應的設置代碼如下:
outRect.set(left, top, right, bottom)
在我們的分組索引列表中,只需要對ItemView設置頂部的偏移量,其它三個偏移量為0即可。這樣就可以在ItemView頂部預留出一定高度的區(qū)域,如下圖:
2、onDraw()
在getItemOffsets()方法中,我們設置了偏移量,進而得到了對應的偏移區(qū)域,接下來在onDraw()中就可以給ItemView繪制裝飾效果了,所以我們在該方法中將分組索引列表中的GroupHeader的內(nèi)容繪制在ItemView頂部偏移區(qū)域里。也就是繪制前邊 gif 圖里的A、B、C... GroupHeader,雖然看起來像一個個獨立的ItemView,但并不是的哦!
注意該繪制操作會在ItemView的onDraw()前完成的!
3、onDrawOver()
該方法同樣也是用來繪制的,但是它在ItemDecoration的onDraw()方法和ItemView的onDraw()完成后才執(zhí)行。所以其繪制的內(nèi)容會遮擋在RecyclerView上,因此我們可以在該方法中繪制分組索引列表中懸浮的GroupHeader,也就是在列表頂部隨著列表滾動切換的GroupHeader。
一、分組GroupHeader
三個方法的作用已經(jīng)解釋完了,接下來就是代碼實現(xiàn)我們的效果了:
首先保證RecyclerView的數(shù)據(jù)源已經(jīng)按照某種規(guī)律進行了分組排序,具體什么規(guī)律你說了算,我們例子中按照數(shù)據(jù)源中指定字段的值的首字母升序排列,也就是常見通訊錄的排序方式。然后在每個data中保存需要在GroupHeader上顯示的內(nèi)容,可以使用tag字段,我們這里保存的是對應的首字母。這里沒必要將整個數(shù)據(jù)源設置到ItemDecoration里邊,所以我們只需要提取排序后數(shù)據(jù)源的tag保存到列表中,然后設置到ItemDecoration里邊,后邊的操作就依賴設置的數(shù)據(jù)源了,根據(jù)tag的異同來決定是否繪制GroupHeader等。
上邊已經(jīng)分析了,GroupHeader只在列表中每組數(shù)據(jù)對應的第一個ItemView頂部顯示,只需要對ItemView設置頂部的偏移量即可:
public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); RecyclerView.LayoutManager manager = parent.getLayoutManager(); //只處理線性垂直類型的列表 if ((manager instanceof LinearLayoutManager) && LinearLayoutManager.VERTICAL != ((LinearLayoutManager) manager).getOrientation()) { return; } int position = parent.getChildAdapterPosition(view); //ItemView的position==0 或者 當前ItemView的data的tag和上一個ItemView的不相等,則為當前ItemView設置top 偏移量 if (!Utils.listIsEmpty(tags) && (position == 0 || !tags.get(position).equals(tags.get(position - 1)))) { outRect.set(0, groupHeaderHeight, 0, 0); } } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); } }
其中tags就是我們設置到ItemDecoration的數(shù)據(jù)源,是一個String集合。groupHeaderHeight就是ItemView的頂部偏移量。
之后就是在ItemView的頂部偏移區(qū)域繪制GroupHeader了:
public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); for (int i = 0; i < parent.getChildCount(); i++) { View view = parent.getChildAt(i); int position = parent.getChildAdapterPosition(view); String tag = tags.get(position); //和getItemOffsets()里的條件判斷類似,開始繪制分組的GroupHeader if (!Utils.listIsEmpty(tags) && (position == 0 || !tag.equals(tags.get(position - 1)))) { drawGroupHeader(c, parent, view, tag); } } } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); } private void drawGroupHeader(Canvas c, RecyclerView parent, View view, String tag) { RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); int bottom = view.getTop() - params.topMargin; int top = bottom - groupHeaderHeight; c.drawRect(left, top, right, bottom, mPaint); int x = left + groupHeaderLeftPadding; int y = top + (groupHeaderHeight + Utils.getTextHeight(mTextPaint, tag)) / 2; c.drawText(tag, x, y, mTextPaint); } }
繪制GroupHeader就是Canvasc操作,先繪制一個矩形框,再繪制相應的文字,當然繪制圖片也是沒問題的,其中groupHeaderLeftPadding是個可配置字段,代表繪制的文字或圖片到列表左邊沿的距離,也可以理解為GroupHeader的左padding。
最后就是懸浮在頂部的GroupHeader繪制了:
public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); if (!show) { return; } //列表第一個可見的ItemView位置 int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition(); String tag = tags.get(position); View view = parent.findViewHolderForAdapterPosition(position).itemView; //當前ItemView的data的tag和下一個itemView的不相等,則代表將要重新繪制懸停的GroupHeader boolean flag = false; if (!Utils.listIsEmpty(tags) && (position + 1) < tags.size() && !tag.equals(tags.get(position + 1))) { //如果第一個可見ItemView的底部坐標小于groupHeaderHeight,則執(zhí)行Canvas向上位移操作 if (view.getBottom() <= groupHeaderHeight) { c.save(); flag = true; c.translate(0, view.getHeight() + view.getTop() - groupHeaderHeight); } } drawSuspensionGroupHeader(c, parent, tag); if (flag) { c.restore(); } } private void drawSuspensionGroupHeader(Canvas c, RecyclerView parent, String tag) { int left = parent.getPaddingLeft(); int right = parent.getWidth() - parent.getPaddingRight(); int bottom = groupHeaderHeight; int top = 0; c.drawRect(left, top, right, bottom, mPaint); int x = left + groupHeaderLeftPadding; int y = top + (groupHeaderHeight + Utils.getTextHeight(mTextPaint, tag)) / 2; c.drawText(tag, x, y, mTextPaint); } }
繪制操作和onDraw中的類似,gif 中有一個懸浮GroupHeader上移的動畫,就是通過Canvas位移來實現(xiàn)的,注意在Canvas位移的前后進行save()和restore()操作。
我們給GroupHeaderItemDecoration提供了設置GroupHeader左padding、高度、背景色、文字顏色、尺寸、以及是否顯示頂部懸浮GroupHeader的方法,方便使用。
關于繪制操作需要注意的是,GroupHeader所在的偏移區(qū)域和ItemView是相互獨立的,不要把GroupHeader當做ItemView的一部分哦。到這里GroupHeader的功能就實現(xiàn)了,只需要將GroupHeaderItemDecoration添加到RecyclerView即可。
至于如何通過layout或者View來實現(xiàn)GroupHeader,做過一些嘗試,效果都不理想,期待大家的好想法哦!
這里先用一個接口,對外提供自定義繪制GroupHeader的方法:
public interface OnDrawItemDecorationListener { /** * 繪制GroupHeader * @param c * @param paint 繪制GroupHeader區(qū)域的paint * @param textPaint 繪制文字的paint * @param params 共四個值left、top、right、bottom 代表GroupHeader所在區(qū)域的四個坐標值 * @param position 原始數(shù)據(jù)源中的position */ void onDrawGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position); /** * 繪制懸浮在列表頂部的GroupHeader */ void onDrawSuspensionGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position); }
二、分割線
現(xiàn)在RecyclerView還差一個分割線,當前最笨的辦法可以在ItemView的布局文件中設置,既然系統(tǒng)都提供了ItemDecoration,那用它來優(yōu)雅的實現(xiàn)為何不可呢,我們只需要給列表中每組數(shù)據(jù)除了最后一項數(shù)據(jù)對應的ItemView之外的添加分割線即可,也就是不給每組數(shù)據(jù)對應的最后一個ItemView添加分割線。很簡單,直接上核心代碼:
public class DivideItemDecoration extends RecyclerView.ItemDecoration { @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); RecyclerView.LayoutManager manager = parent.getLayoutManager(); //只處理線性垂直類型的列表 if ((manager instanceof LinearLayoutManager) && LinearLayoutManager.VERTICAL != ((LinearLayoutManager) manager).getOrientation()) { return; } int position = parent.getChildAdapterPosition(view); if (!Utils.listIsEmpty(tags) && (position + 1) < tags.size() && tags.get(position).equals(tags.get(position + 1))) { //當前ItemView的data的tag和下一個ItemView的不相等,則為當前ItemView設置bottom 偏移量 outRect.set(0, 0, 0, divideHeight); } } @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, state); for (int i = 0; i < parent.getChildCount(); i++) { View view = parent.getChildAt(i); int position = parent.getChildAdapterPosition(view); //和getItemOffsets()里的條件判斷類似 if (!Utils.listIsEmpty(tags) && (position + 1) < tags.size() && tags.get(position).equals(tags.get(position + 1))) { drawDivide(c, parent, view); } } } @Override public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDrawOver(c, parent, state); } private void drawDivide(Canvas c, RecyclerView parent, View view) { RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); int left = parent.getPaddingLeft(); int right = parent.getWidth(); int top = view.getBottom() + params.bottomMargin; int bottom = top + divideHeight; c.drawRect(left, top, right, bottom, mPaint); } }
三、SideBar
SideBar就是 gif 圖右邊的垂直字符條,是一個自定義View。手指觸摸選中一個字符,則列表會滾動到對應的分組頭部位置。實現(xiàn)起來也蠻簡單的,核心代碼如下:
public class SideBar extends View { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //重新計算SideBar寬高 if (heightMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.AT_MOST) { getMaxTextSize(); if (heightMode == MeasureSpec.AT_MOST) { heightSize = (maxHeight + 15) * indexArray.length; } if (widthMode == MeasureSpec.AT_MOST) { widthSize = maxWidth + 10; } } setMeasuredDimension(widthSize, heightSize); } @Override protected void onDraw(Canvas canvas) { for (int i = 0; i < indexArray.length; i++) { String index = indexArray[i]; float x = (mWidth - mTextPaint.measureText(index)) / 2; float y = mMarginTop + mHeight * i + (mHeight + Utils.getTextHeight(mTextPaint, index)) / 2; //繪制字符 canvas.drawText(index, x, y, mTextPaint); } } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: // 選中字符的下標 int pos = (int) ((event.getY() - mMarginTop) / mHeight); if (pos >= 0 && pos < indexArray.length) { setBackgroundColor(TOUCH_COLOR); if (onSideBarTouchListener != null) { for (int i = 0; i < tags.size(); i++) { if (indexArray[pos].equals(tags.get(i))) { onSideBarTouchListener.onTouch(indexArray[pos], i); break; } else { onSideBarTouchListener.onTouch(indexArray[pos], -1); } } } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: setBackgroundColor(UNTOUCH_COLOR); if (onSideBarTouchListener != null) { onSideBarTouchListener.onTouchEnd(); } break; } return true; } }
在onMeasure()方法里,如果SideBar的寬、高測量模式為MeasureSpec.AT_MOST則重新計算SideBar的寬、高。onDraw()方法則是遍歷索引數(shù)組,并繪制字符索引。在onTouchEvent()方法里,我們根據(jù)手指在SideBar上觸摸坐標點的y值,計算出觸摸的相應字符,以便在OnSideBarTouchListener接口進行后續(xù)操作,例如列表的跟隨滾動等等。
四、實例
前邊已經(jīng)完成了三大核心功能,最后來愉快的使用下吧:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list); SideBar sideBar = (SideBar) findViewById(R.id.side_bar); final TextView tip = (TextView) findViewById(R.id.tip); final List<ItemData> datas = new ArrayList<>(); ItemData data = new ItemData("北京"); datas.add(data); ItemData data1 = new ItemData("上海"); datas.add(data1); ItemData data2 = new ItemData("廣州"); datas.add(data2); . . . ItemData data34 = new ItemData("Hello China"); datas.add(data34); ItemData data35 = new ItemData("寧波"); datas.add(data35); SortHelper<ItemData> sortHelper = new SortHelper<ItemData>() { @Override public String sortField(ItemData data) { return data.getTitle(); } }; sortHelper.sortByLetter(datas);//將數(shù)據(jù)源按指定字段首字母排序 List<String> tags = sortHelper.getTags(datas);//提取已排序數(shù)據(jù)源的tag值 MyAdapter adapter = new MyAdapter(this, datas, false); final LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(layoutManager); //添加分割線 recyclerView.addItemDecoration(new DivideItemDecoration().setTags(tags)); //添加GroupHeader recyclerView.addItemDecoration(new GroupHeaderItemDecoration(this) .setTags(tags)//設置tag集合 .setGroupHeaderHeight(30)//設置GroupHeader高度 .setGroupHeaderLeftPadding(20));//設置GroupHeader 左padding recyclerView.setAdapter(adapter); sideBar.setOnSideBarTouchListener(tags, new OnSideBarTouchListener() { @Override public void onTouch(String text, int position) { tip.setVisibility(View.VISIBLE); tip.setText(text); if ("↑".equals(text)) { layoutManager.scrollToPositionWithOffset(0, 0); return; } //滾動列表到指定位置 if (position != -1) { layoutManager.scrollToPositionWithOffset(position, 0); } } @Override public void onTouchEnd() { tip.setVisibility(View.GONE); } }); } }
這也就是文章開頭的 gif 效果。如果需要自定義ItemView的繪制可以這樣寫:
recyclerView.addItemDecoration(new GroupHeaderItemDecoration(this) .setTags(tags) .setGroupHeaderHeight(30) .setGroupHeaderLeftPadding(20) .setOnDrawItemDecorationListener(new OnDrawItemDecorationListener() { @Override public void onDrawGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position) { c.drawRect(params[0], params[1], params[2], params[3], paint); int x = params[0] + Utils.dip2px(context, 20); int y = params[1] + (Utils.dip2px(context, 30) + Utils.getTextHeight(textPaint, tags.get(position))) / 2; Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, null); Bitmap icon1 = Bitmap.createScaledBitmap(icon, Utils.dip2px(context, 20), Utils.dip2px(context, 20), true); c.drawBitmap(icon1, x, params[1] + Utils.dip2px(context, 5), paint); c.drawText(tags.get(position), x + Utils.dip2px(context, 25), y, textPaint); } @Override public void onDrawSuspensionGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position) { c.drawRect(params[0], params[1], params[2], params[3], paint); int x = params[0] + Utils.dip2px(context, 20); int y = params[1] + (Utils.dip2px(context, 30) + Utils.getTextHeight(textPaint, tags.get(position))) / 2; Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, null); Bitmap icon1 = Bitmap.createScaledBitmap(icon, Utils.dip2px(context, 20), Utils.dip2px(context, 20), true); c.drawBitmap(icon1, x, params[1] + Utils.dip2px(context, 5), paint); c.drawText(tags.get(position), x + Utils.dip2px(context, 25), y, textPaint); } }) );
坐標計算有點復雜了......0_o......
看下效果:
當然不止于此,更多的效果等待著機智的你去創(chuàng)造。
更多代碼細節(jié)及用法可參考:https://github.com/Othershe/GroupIndexLib
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Android使用viewpager實現(xiàn)畫廊式效果
這篇文章主要為大家詳細介紹了Android使用viewpager實現(xiàn)畫廊式效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-08-08android實現(xiàn)搜索功能并將搜索結(jié)果保存到SQLite中(實例代碼)
這篇文章主要介紹了android實現(xiàn)搜索功能并將搜索結(jié)果保存到SQLite中,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04Android數(shù)據(jù)流之Channel和Flow實現(xiàn)原理和技巧詳解
在 Android 應用程序的開發(fā)中,處理異步數(shù)據(jù)流是一個常見的需求,為了更好地應對這些需求,Kotlin 協(xié)程引入了 Channel 和 Flow,它們提供了強大的工具來處理數(shù)據(jù)流,本文將深入探討 Channel 和 Flow 的內(nèi)部實現(xiàn)原理、高級使用技巧以及如何在 Android 開發(fā)中充分利用它們2023-11-11Android中自定義View實現(xiàn)圓環(huán)等待及相關的音量調(diào)節(jié)效果
這篇文章主要介紹了Android中自定義View實現(xiàn)圓環(huán)等待及相關的音量調(diào)節(jié)效果,邏輯非常簡單,或許繪圖方面更加繁瑣XD 需要的朋友可以參考下2016-04-04Android ListView滾動到底后自動加載數(shù)據(jù)
這篇文章主要為大家詳細介紹了Android之ListView滾動到底后自動加載數(shù)據(jù),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-09-09android實現(xiàn)可上下回彈的scrollview
這篇文章主要為大家詳細介紹了android實現(xiàn)可上下回彈的scrollview,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04