当前位置: 移动技术网 > 移动技术>移动开发>Android > Android自定义View仿华为圆形加载进度条

Android自定义View仿华为圆形加载进度条

2019年07月24日  | 移动技术网移动技术  | 我要评论

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方法来处理各种复杂的动画效果。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网