欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-05 18:11:34
view仿华为圆形加载进度条效果图 实现思路 可以看出该view可分为三个部分来实现 最外围的圆,该部分需要区分进度圆和底部的刻度圆,进度部分的刻度需要和底色刻度...

view仿华为圆形加载进度条效果图

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

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