Android自定义View仿华为圆形加载进度条
view仿华为圆形加载进度条效果图
实现思路
可以看出该view可分为三个部分来实现
最外围的圆,该部分需要区分进度圆和底部的刻度圆,进度部分的刻度需要和底色刻度区分开来
中间显示的文字进度,需要让文字在view中居中显示
旋转的小圆点,小圆点需要模拟小球下落运动时的加速度效果,开始下落的时候慢,到最底部时最快,上来时速度再逐渐减慢
具体实现
先具体细分讲解,博客最后面给出全部源码
(1)首先为view创建自定义的xml属性
在工程的values目录下新建attrs.xml文件
<resources> <!-- 仿华为圆形加载进度条 --> <declare-styleable name="circleloading"> <attr name="indexcolor" format="color"/> <attr name="basecolor" format="color"/> <attr name="dotcolor" format="color"/> <attr name="textsize" format="dimension"/> <attr name="textcolor" format="color"/> </declare-styleable> </resources>
各个属性的作用:
indexcolor:进度圆的颜色
basecolor:刻度圆底色
dotcolor:小圆点颜色
textsize:文字大小
textcolor:文字颜色
(2)新建circleloadingview类继承view类,重写它的三个构造方法,获取用户设置的属性,同时指定默认值
public circleloadingview(context context) { this(context, null); } public circleloadingview(context context, attributeset attrs) { this(context, attrs, 0); } public circleloadingview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); // 获取用户配置属性 typedarray tya = context.obtainstyledattributes(attrs, r.styleable.circleloading); basecolor = tya.getcolor(r.styleable.circleloading_basecolor, color.ltgray); indexcolor = tya.getcolor(r.styleable.circleloading_indexcolor, color.blue); textcolor = tya.getcolor(r.styleable.circleloading_textcolor, color.blue); dotcolor = tya.getcolor(r.styleable.circleloading_dotcolor, color.red); textsize = tya.getdimensionpixelsize(r.styleable.circleloading_textsize, 36); tya.recycle(); initui(); }
我们从view绘制的第一步开始
(3)测量onmeasure,首先需要测量出view的宽和高,并指定view在wrap_content时的最小范围,对于view绘制流程还不熟悉的同学,可以先去了解下具体的绘制流程
浅谈android view绘制三大流程探索及常见问题
重写onmeasure方法,其中我们要考虑当view的宽高被指定为wrap_content时的情况,如果我们不对wrap_content的情况进行处理,那么当使用者指定view的宽高为wrap_content时将无法正常显示出view
@override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); int mywidthspecmode = measurespec.getmode(widthmeasurespec); int mywidthspecsize = measurespec.getsize(widthmeasurespec); int myheightspecmode = measurespec.getmode(heightmeasurespec); int myheightspecsize = measurespec.getsize(heightmeasurespec); // 获取宽 if (mywidthspecmode == measurespec.exactly) { // match_parent/精确值 mwidth = mywidthspecsize; } else { // wrap_content mwidth = densityutil.dip2px(mcontext, 120); } // 获取高 if (myheightspecmode == measurespec.exactly) { // match_parent/精确值 mheight = myheightspecsize; } else { // wrap_content mheight = densityutil.dip2px(mcontext, 120); } // 设置该view的宽高 setmeasureddimension(mwidth, mheight); }
measurespec的状态分为三种exactly、at_most、unspecified,这里只要单独指定非精确值exactly之外的情况就好了。
本文中使用到的densityutil类,是为了将dp转换为px来使用,以便适配不同的屏幕显示效果
public static int dip2px(context context, float dpvalue) { final float scale = context.getresources().getdisplaymetrics().density; return (int) (dpvalue * scale + 0.5f); }
(4)重写ondraw,绘制需要显示的内容
因为做的是单纯的view而不是viewgroup,内部没有子控件需要确定位置,所以可直接跳过onlayout方法,直接开始对view进行绘制
分为三个部分绘制,绘制刻度圆,绘制文字值,绘制旋转小圆点
@override protected void ondraw(canvas canvas) { drawarcscale(canvas); drawtextvalue(canvas); drawrotatedot(canvas); }
绘制刻度圆
先画一个小竖线,通过canvas.rotate()方法每次旋转3.6度(总共360度,用100/360=3.6)得到一个刻度为100的圆,然后通过progress参数,得到要显示的进度数,并把小于progress的刻度变成进度圆的颜色
/** * 画刻度 */ private void drawarcscale(canvas canvas) { canvas.save(); for (int i = 0; i < 100; i++) { if (progress > i) { mscalepaint.setcolor(indexcolor); } else { mscalepaint.setcolor(basecolor); } canvas.drawline(mwidth / 2, 0, mheight / 2, densityutil.dip2px(mcontext, 10), mscalepaint); // 旋转的度数 = 100 / 360 canvas.rotate(3.6f, mwidth / 2, mheight / 2); } canvas.restore(); }
绘制中间文字
文字绘制的坐标是以文字的左下角开始绘制的,所以需要先通过把文字装载到一个矩形rect,通过画笔的gettextbounds方法取得字符串的长度和宽度,通过动态计算,来使文字居中显示
/** * 画内部数值 */ private void drawtextvalue(canvas canvas) { canvas.save(); string showvalue = string.valueof(progress); rect textbound = new rect(); mtextpaint.gettextbounds(showvalue, 0, showvalue.length(), textbound); // 获取文字的矩形范围 float textwidth = textbound.right - textbound.left; // 获得文字宽 float textheight = textbound.bottom - textbound.top; // 获得文字高 canvas.drawtext(showvalue, mwidth / 2 - textwidth / 2, mheight / 2 + textheight / 2, mtextpaint); canvas.restore(); }
绘制旋转小圆点
这个小圆点就是简单的绘制一个填充的圆形就好
/** * 画旋转小圆点 */ private void drawrotatedot(final canvas canvas) { canvas.save(); canvas.rotate(mdotprogress * 3.6f, mwidth / 2, mheight / 2); canvas.drawcircle(mwidth / 2, densityutil.dip2px(mcontext, 10) + densityutil.dip2px(mcontext, 5), densityutil.dip2px(mcontext, 3), mdotpaint); canvas.restore(); }
让它自己动起来可以通过两种方式,一种是开一个线程,在线程中改变mdotprogress的数值,并通过postinvalidate方法跨线程刷新view的显示效果
new thread() { @override public void run() { while (true) { mdotprogress++; if (mdotprogress == 100) { mdotprogress = 0; } postinvalidate(); try { thread.sleep(50); } catch (interruptedexception e) { e.printstacktrace(); } } } }.start();
开线程的方式不推荐使用,这是没必要的开销,而且线程不好控制,要实现让小圆点在运行过程中开始和结束时慢,运动到中间时加快这种效果不好实现,所以最好的方式是使用属性动画,需要让小圆点动起来时,调用以下方法就好了
/** * 启动小圆点旋转动画 */ public void startdotanimator() { animator = valueanimator.offloat(0, 100); animator.setduration(1500); animator.setrepeatcount(valueanimator.infinite); animator.setrepeatmode(valueanimator.restart); animator.setinterpolator(new acceleratedecelerateinterpolator()); animator.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator animation) { // 设置小圆点的进度,并通知界面重绘 mdotprogress = (float) animation.getanimatedvalue(); invalidate(); } }); animator.start(); }
在属性动画中可以通过setinterpolator方法指定不同的插值器,这里要模拟小球掉下来的重力效果,所以需要使用acceleratedecelerateinterpolator插值类,该类的效果就是在动画开始时和结束时变慢,中间加快
(5)设置当前进度值
对外提供一个方法,用来更新当前圆的进度
/** * 设置进度 */ public void setprogress(int progress) { this.progress = progress; invalidate(); }
通过外部调用setprogress方法就可以跟更新当前圆的进度了
源码
/** * 仿华为圆形加载进度条 * created by zhuwentao on 2017-08-19. */ public class circleloadingview extends view { private context mcontext; // 刻度画笔 private paint mscalepaint; // 小原点画笔 private paint mdotpaint; // 文字画笔 private paint mtextpaint; // 当前进度 private int progress = 0; /** * 小圆点的当前进度 */ public float mdotprogress; // view宽 private int mwidth; // view高 private int mheight; private int indexcolor; private int basecolor; private int dotcolor; private int textsize; private int textcolor; private valueanimator animator; public circleloadingview(context context) { this(context, null); } public circleloadingview(context context, attributeset attrs) { this(context, attrs, 0); } public circleloadingview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); // 获取用户配置属性 typedarray tya = context.obtainstyledattributes(attrs, r.styleable.circleloading); basecolor = tya.getcolor(r.styleable.circleloading_basecolor, color.ltgray); indexcolor = tya.getcolor(r.styleable.circleloading_indexcolor, color.blue); textcolor = tya.getcolor(r.styleable.circleloading_textcolor, color.blue); dotcolor = tya.getcolor(r.styleable.circleloading_dotcolor, color.red); textsize = tya.getdimensionpixelsize(r.styleable.circleloading_textsize, 36); tya.recycle(); initui(); } private void initui() { mcontext = getcontext(); // 刻度画笔 mscalepaint = new paint(); mscalepaint.setantialias(true); mscalepaint.setstrokewidth(densityutil.dip2px(mcontext, 1)); mscalepaint.setstrokecap(paint.cap.round); mscalepaint.setcolor(basecolor); mscalepaint.setstyle(paint.style.stroke); // 小圆点画笔 mdotpaint = new paint(); mdotpaint.setantialias(true); mdotpaint.setcolor(dotcolor); mdotpaint.setstrokewidth(densityutil.dip2px(mcontext, 1)); mdotpaint.setstyle(paint.style.fill); // 文字画笔 mtextpaint = new paint(); mtextpaint.setantialias(true); mtextpaint.setcolor(textcolor); mtextpaint.settextsize(textsize); mtextpaint.setstrokewidth(densityutil.dip2px(mcontext, 1)); mtextpaint.setstyle(paint.style.fill); } @override protected void ondraw(canvas canvas) { drawarcscale(canvas); drawtextvalue(canvas); drawrotatedot(canvas); } /** * 画刻度 */ private void drawarcscale(canvas canvas) { canvas.save(); for (int i = 0; i < 100; i++) { if (progress > i) { mscalepaint.setcolor(indexcolor); } else { mscalepaint.setcolor(basecolor); } canvas.drawline(mwidth / 2, 0, mheight / 2, densityutil.dip2px(mcontext, 10), mscalepaint); // 旋转的度数 = 100 / 360 canvas.rotate(3.6f, mwidth / 2, mheight / 2); } canvas.restore(); } /** * 画内部数值 */ private void drawtextvalue(canvas canvas) { canvas.save(); string showvalue = string.valueof(progress); rect textbound = new rect(); mtextpaint.gettextbounds(showvalue, 0, showvalue.length(), textbound); // 获取文字的矩形范围 float textwidth = textbound.right - textbound.left; // 获得文字宽 float textheight = textbound.bottom - textbound.top; // 获得文字高 canvas.drawtext(showvalue, mwidth / 2 - textwidth / 2, mheight / 2 + textheight / 2, mtextpaint); canvas.restore(); } /** * 画旋转小圆点 */ private void drawrotatedot(final canvas canvas) { canvas.save(); canvas.rotate(mdotprogress * 3.6f, mwidth / 2, mheight / 2); canvas.drawcircle(mwidth / 2, densityutil.dip2px(mcontext, 10) + densityutil.dip2px(mcontext, 5), densityutil.dip2px(mcontext, 3), mdotpaint); canvas.restore(); } /** * 启动小圆点旋转动画 */ public void startdotanimator() { animator = valueanimator.offloat(0, 100); animator.setduration(1500); animator.setrepeatcount(valueanimator.infinite); animator.setrepeatmode(valueanimator.restart); animator.setinterpolator(new acceleratedecelerateinterpolator()); animator.addupdatelistener(new valueanimator.animatorupdatelistener() { @override public void onanimationupdate(valueanimator animation) { // 设置小圆点的进度,并通知界面重绘 mdotprogress = (float) animation.getanimatedvalue(); invalidate(); } }); animator.start(); } /** * 设置进度 */ public void setprogress(int progress) { this.progress = progress; invalidate(); } @override protected void onmeasure(int widthmeasurespec, int heightmeasurespec) { super.onmeasure(widthmeasurespec, heightmeasurespec); int mywidthspecmode = measurespec.getmode(widthmeasurespec); int mywidthspecsize = measurespec.getsize(widthmeasurespec); int myheightspecmode = measurespec.getmode(heightmeasurespec); int myheightspecsize = measurespec.getsize(heightmeasurespec); // 获取宽 if (mywidthspecmode == measurespec.exactly) { // match_parent/精确值 mwidth = mywidthspecsize; } else { // wrap_content mwidth = densityutil.dip2px(mcontext, 120); } // 获取高 if (myheightspecmode == measurespec.exactly) { // match_parent/精确值 mheight = myheightspecsize; } else { // wrap_content mheight = densityutil.dip2px(mcontext, 120); } // 设置该view的宽高 setmeasureddimension(mwidth, mheight); } }
总结
在的ondraw方法中需要避免频繁的new对象,所以把一些如初始化画笔paint的方法放到了最前面的构造方法中进行。
在分多个模块绘制时,应该使用canvas.save()和canvas.restore()的组合,来避免不同模块绘制时的相互干扰,在这两个方法中绘制相当于ps中的图层概念,上一个图层进行的修改不会影响到下一个图层的显示效果。
在需要显示动画效果的地方使用属性动画来处理,可自定义的效果强,在系统提供的插值器类不够用的情况下,我么还可通过继承animation类,重写它的applytransformation方法来处理各种复杂的动画效果。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。