Android 自定义View—清爽小巧灵活的多节点进度条
程序员文章站
2022-03-30 09:51:29
前言 最近项目有一个节点进度条的小需求,完成后,想分享出来希望可以帮到有需要的同学。 真机效果图 自定义View完整代码 开箱即用~,注释已经炒鸡详细了 注意点 1. 控件的节点总个数是与传入的节点底部标题列表中元素个数控制(相同)的,简而言之就是传入的标题列表中有多少个标题,节点就会绘制多少个 2 ......
前言
最近项目有一个节点进度条的小需求,完成后,想分享出来希望可以帮到有需要的同学。
真机效果图
自定义view完整代码
开箱即用~,注释已经炒鸡详细了
/** * @description: 节点进度条 * @author: dmingo * @date: 2020/4/15 */ public class pointprocessbar extends view { /** * 未选中时的连线画笔 */ private paint mlinepaint; /** * 选中时的连线画笔 */ private paint mlineselectedpaint; /** * 未选中时的文字画笔 */ private paint mtextpaint; /** * 选中时的文字画笔 */ private paint mtextselpaint; /** * 未选中时的实心圆画笔 */ private paint mcirclepaint; /** * 选中时的内部实心圆画笔 */ private paint mcircleselpaint; /** * 选中时的边框圆画笔 */ private paint mcirclestrokeselpaint; /** * 未选中时的线,节点圆的颜色 */ private int mcolorunselected = color.parsecolor("#1ca8b0d9"); /** * 选中时的颜色 */ private int mcolorselected = color.parsecolor("#61a4e4"); /** * 未选中的文字颜色 */ private int mcolortextunselected = color.parsecolor("#5c030f09"); /** * 绘制的节点个数,由底部节点标题数量控制 */ int circlecount ; /** * 连线的高度 */ float mlineheight = 7f; //圆的直径 float mcircleheight = 50f; float mcircleselstroke = 8f; float mcirclefillradius = 15f; //文字大小 float mtextsize = 35f; //文字离顶部的距离 float mmargintop = 40f; /** * 首个圆向中心偏移的距离 */ float marginleft = 30f; /** * 最后一个圆向中心偏移的距离 */ float marginright = marginleft; /** * 每个节点相隔的距离 */ float dividewidth; int defaultheight; /** * 节点底部的文字列表 */ list<string> textlist = new arraylist<>(); /** * 文字同宽高的矩形,用来测量文字 */ list<rect> mbounds; /** * 存储每个圆心在同一直线上的节点圆的 x 坐标值 */ list<float> circlelinejunctions = new arraylist<>(); /** * 选中项集合 */ set<integer> selectedindexset = new hashset<>(); public pointprocessbar(context context) { super(context); } public pointprocessbar(context context, @nullable attributeset attrs) { super(context, attrs); initpaint(); } public pointprocessbar(context context, @nullable attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); } public pointprocessbar(context context, @nullable attributeset attrs, int defstyleattr, int defstyleres) { super(context, attrs, defstyleattr, defstyleres); } /** * 初始化画笔属性 */ private void initpaint(){ mlinepaint = new paint(); mlineselectedpaint = new paint(); mcirclepaint = new paint(); mtextpaint = new paint(); mcirclestrokeselpaint = new paint(); mtextselpaint=new paint(); mcircleselpaint = new paint(); mlinepaint.setcolor(mcolordef); //设置填充 mlinepaint.setstyle(paint.style.fill); //笔宽像素 mlinepaint.setstrokewidth(mlineheight); //锯齿不显示 mlinepaint.setantialias(true); mlineselectedpaint.setcolor(mcolorselected); mlineselectedpaint.setstyle(paint.style.fill); mlineselectedpaint.setstrokewidth(mlineheight); mlineselectedpaint.setantialias(true); mcirclepaint.setcolor(mcolordef); //设置填充 mcirclepaint.setstyle(paint.style.fill); //笔宽像素 mcirclepaint.setstrokewidth(1); //锯齿不显示 mcirclepaint.setantialias(true); //选中时外框空心圆圈画笔 mcirclestrokeselpaint.setcolor(mcolorselected); mcirclestrokeselpaint.setstyle(paint.style.stroke); mcirclestrokeselpaint.setstrokewidth(mcircleselstroke); mcirclestrokeselpaint.setantialias(true); //选中时的内部填充圆画笔 mcircleselpaint.setstyle(paint.style.fill); mcircleselpaint.setstrokewidth(1); mcircleselpaint.setantialias(true); mcircleselpaint.setcolor(mcolorselected); //普通状态的文本 画笔 mtextpaint.settextsize(mtextsize); mtextpaint.setcolor(mcolortextdef); mtextpaint.setantialias(true); mtextpaint.settextalign(paint.align.center); //选中后的文本画笔 mtextselpaint.settextsize(mtextsize); mtextselpaint.setcolor(mcolorselected); mtextselpaint.setantialias(true); mtextselpaint.settextalign(paint.align.center); } /** * 测量文字的长宽,将文字视为rect矩形 */ private void measuretext(){ mbounds = new arraylist<>(); for(string name : textlist){ rect mbound = new rect(); mtextpaint.gettextbounds(name, 0, name.length(), mbound); mbounds.add(mbound); } } /** * 测量view的高度 */ private void measureheight(){ if (mbounds!=null && mbounds.size()!=0) { defaultheight = (int) (mcircleheight + mmargintop + mcircleselstroke + mbounds.get(0).height()/2); } else { defaultheight = (int) (mcircleheight + mmargintop+mcircleselstroke); } } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { int widthspecmode = measurespec.getmode(widthmeasurespec); int widthspecsize = measurespec.getsize(widthmeasurespec); int heightspecmode = measurespec.getmode(heightmeasurespec); int heightspecsize = measurespec.getsize(heightmeasurespec); //宽高都设置为wrap_content if(widthspecmode == measurespec.at_most && heightspecmode == measurespec.at_most){ //宽设置为wrap_content setmeasureddimension(widthspecsize,defaultheight); }else if(widthspecmode == measurespec.at_most){ setmeasureddimension(widthspecsize,heightspecsize); }else if(heightspecmode == measurespec.at_most){ //高设置为wrap_content setmeasureddimension(widthspecsize, defaultheight); }else{ //宽高都设置为match_parent或具体的dp值 setmeasureddimension(widthspecsize, heightspecsize); } } @override protected void ondraw(canvas canvas) { //若未设置节点标题或者选中项的列表,则取消绘制 if (textlist == null || textlist.isempty() || selectedindexset == null || selectedindexset.isempty() || mbounds == null || mbounds.isempty()) { return; } //画灰色圆圈的个数 circlecount=textlist.size(); //每个圆相隔的距离(重要),可以通过这个调节节点间距 dividewidth = (getwidth() - mcircleheight ) / (circlecount - 1); //绘制文字和圆形 for (int i=0; i < circlecount ;i++){ float cx; float cy; float textx; if (i==0){ //第一个节点,圆心需要向右偏移 cx = mcircleheight / 2 + i * dividewidth + marginleft; cy = mcircleheight / 2 + mcircleselstroke; textx = cx; circlelinejunctions.add(cx + mcircleheight / 2); }else if (i==textlist.size()-1){ //最后一个节点,圆心需要向左偏移 cx = mcircleheight / 2 + i * dividewidth - marginright; cy = mcircleheight / 2 + mcircleselstroke; textx = cx; circlelinejunctions.add(cx - mcircleheight / 2); }else { //中间部分的节点 cx = mcircleheight / 2 + i * dividewidth; cy = mcircleheight / 2+mcircleselstroke; textx = cx; circlelinejunctions.add(cx - mcircleheight / 2); circlelinejunctions.add(cx + mcircleheight / 2); } if (getselectedindexset().contains(i)){ //若当前位置节点被包含在选中项set中,判定此节点被选中 canvas.drawcircle(cx , cy, mcircleheight / 2, mcirclestrokeselpaint); canvas.drawcircle(cx, cy, mcirclefillradius, mcircleselpaint); canvas.drawtext(textlist.get(i), textx, (float) (mcircleheight + mmargintop +mcircleselstroke+mbounds.get(i).height()/2.0), mtextselpaint); }else { //若当前位置节点没有被包含在选中项set中,判定此节点没有被选中 canvas.drawcircle(cx , cy, mcircleheight / 2, mcirclepaint); canvas.drawtext(textlist.get(i), textx, (float) (mcircleheight + mmargintop +mcircleselstroke+mbounds.get(i).height()/2.0), mtextpaint); } } for(int i = 1 , j = 1 ; j <= circlelinejunctions.size() && ! circlelinejunctions.isempty() ; ++i , j=j+2){ if(getselectedindexset().contains(i)){ canvas.drawline(circlelinejunctions.get(j-1),mcircleheight/2+mcircleselstroke, circlelinejunctions.get(j) ,mcircleheight/2+mcircleselstroke,mlineselectedpaint); }else { canvas.drawline(circlelinejunctions.get(j-1),mcircleheight/2+mcircleselstroke, circlelinejunctions.get(j) ,mcircleheight/2+mcircleselstroke,mlinepaint); } } } /** * 供外部调用,显示控件 * @param titles 底部标题内容列表 * @param indexset 选中项set */ public void show(list<string> titles , set<integer> indexset){ if(titles != null && ! titles.isempty()){ this.textlist = titles; } if(indexset != null && ! indexset.isempty()){ this.selectedindexset = indexset; } measuretext(); measureheight(); //绘制 invalidate(); } /** * 更新底部节点标题内容 * @param textlist 节点标题内容列表 */ public void refreshtextlist(list<string> textlist) { this.textlist = textlist; measuretext(); measureheight(); invalidate(); } /** * 获取节点选中状态 * @return 节点选中状态列表 */ public set<integer> getselectedindexset() { return selectedindexset; } /** * 更新选中项 * @param set 选中项set */ public void refreshselectedindexset(set<integer> set) { this.selectedindexset = set; invalidate(); } }
注意点
- 控件的节点总个数是与传入的节点底部标题列表中元素个数控制(相同)的,简而言之就是传入的标题列表中有多少个标题,节点就会绘制多少个
- 控件通过show方法进行view的初始化和显示内容,传入节点标题列表和节点选中项集合,控制view的选中状态和显示的内容
- 控件初始化显示后,可以通过refreshtextlist(),refreshselectedindexset() 更新标题和选中项
- 具体不同的颜色,大小可以具体在view中调整
总结
可以看到效果不复杂,因此自定义view的代码行数不多,也很容易看懂,直接拿走代码即可在项目中食用啦。
由于不同项目设计稿会有不同,这里也仅仅给有需要的同学一个思路,可以改造具体实现代码~
谢谢阅读到这里的同学~欢迎与我讨论和交流
上一篇: vue组件实现v-model双向绑定
下一篇: mvc进阶项目(二)