前言:

如果你想读懂或者更好的理解本篇文章关于自定义圆环或圆弧的内容.请你务必提前阅读下Android自定义View之画圆环(手把手教你如何一步步画圆环).在这篇文章中,详细描述了最基本的自定义圆环的绘制流程以及操作步骤.请务必阅读,不然的话,理解本片文章比较吃力.(嘿嘿,不怕阁下笑话,当初我就是没学会走,就想着跑,后来发现跑偏了…于是从最基本最简单的开始学起).切记,切记,切记,一定要看啊,不然的话,有些基本知识,我是默认你已经知道了哦.

另外,为了保证完整性,下面我会将三种自定义带进度条圆环的整个代码贴出来.可能比较长,请保持耐心.如果你没有耐心,直接拿去用也是可以的.

声明:

本篇文章主要来源于Android 自定义 View 之圆形进度条总结 (大神写的很详细)
之所以要重新写一篇.主要给基于原因:

  • 原博文的排版我不是很感冒
  • 在对原博文做了细细研究之后,希望记录下这个过程,以便加深自身对自定义view之绘制圆环(或圆弧)的认识
  • 对原博文中一些容易产生疑问的地方做了更为详细注解,便于后来者的理解
无图无真相,上图好说话: (gif动态图,别着急)

工程结构:

工程包括主程序和其依赖的工程类库.其中三种自定义带进度圆环就是被封装在了工程类库中.

概述:

自定义带进度圆环思路主要可以分为以下几步:

1.自定义View属性
2.View 的测量
3.计算绘制 View 所需参数
4.圆弧的绘制及渐变的实现
5.文字的绘制
6.动画效果的实现
切记,接下来提到的三种自定义圆环都基本遵从上述步骤.

接下来,本人将会从三个方面对自定义圆形进度条做讲解:

  • 基本的圆形进度条
  • 带刻度的圆形进度条
  • 水波纹效果的圆形进度条

鉴于接下来自定义圆环的自定义view属性全都统一整合在res/value/attrxml文件中,那么索性统一介绍下.

自定义属性资源文件:attr.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><!-- 是否开启抗锯齿 --><attr name="antiAlias" format="boolean" /><!-- 圆弧起始角度,3点钟方向为0,顺时针递增,小于0或大于360进行取余 --><attr name="startAngle" format="float" /><!-- 圆弧度数 --><attr name="sweepAngle" format="float" /><!-- 设置动画时间 --><attr name="animTime" format="integer" /><!-- 绘制内容的数值 --><attr name="maxValue" format="float" /><attr name="value" format="float" /><!-- 绘制内容的单位 reference参考资源id--><attr name="unit" format="string|reference" /><attr name="unitSize" format="dimension" /><attr name="unitColor" format="color|reference" /><!-- 绘制内容相应的提示语 --><attr name="hint" format="string|reference" /><attr name="hintSize" format="dimension" /><attr name="hintColor" format="color|reference" /><!-- 精度,默认为0 --><attr name="precision" format="integer" /><attr name="valueSize" format="dimension" /><attr name="valueColor" format="color|reference" /><!-- 圆弧颜色,设置多个可实现渐变 --><attr name="arcColor1" format="color|reference" /><attr name="arcColor2" format="color|reference" /><attr name="arcColor3" format="color|reference" /><!-- 背景圆弧颜色,默认白色 --><attr name="bgArcColor" format="color|reference" /><!-- 圆弧宽度 --><attr name="arcWidth" format="dimension" /><!-- 圆弧颜色, --><attr name="arcColors" format="color|reference" /><!-- 文字的偏移量。相对于圆半径而言,默认三分之一 --><attr name="textOffsetPercentInRadius" format="float" /><!--********************基本圆形进度条***************--><declare-styleable name="CircleProgressBar"><attr name="antiAlias" /><attr name="startAngle" /><attr name="sweepAngle" /><attr name="animTime" /><attr name="maxValue" /><attr name="value" /><attr name="precision" /><attr name="valueSize" /><attr name="valueColor" /><attr name="textOffsetPercentInRadius" /><!-- 绘制内容相应的提示语 --><attr name="hint" /><attr name="hintSize" /><attr name="hintColor" /><!-- 绘制内容的单位 --><attr name="unit" /><attr name="unitSize" /><attr name="unitColor" /><!-- 圆弧宽度 --><attr name="arcWidth" /><attr name="arcColors" /><!-- 背景圆弧颜色 --><attr name="bgArcColor" /><!-- 背景圆弧宽度 --><attr name="bgArcWidth" format="dimension" /></declare-styleable><!--********************带刻度圆形进度条***************--><declare-styleable name="DialProgress"><attr name="antiAlias" /><attr name="startAngle" /><attr name="sweepAngle" /><attr name="animTime" /><attr name="maxValue" /><attr name="value" /><attr name="precision" /><attr name="valueSize" /><attr name="valueColor" /><attr name="textOffsetPercentInRadius" /><!-- 绘制内容的单位 --><attr name="unit" /><attr name="unitSize" /><attr name="unitColor" /><!-- 绘制内容相应的提示语 --><attr name="hint" /><attr name="hintSize" /><attr name="hintColor" /><!-- 圆弧的宽度 --><attr name="arcWidth" /><!-- 刻度的宽度 --><attr name="dialWidth" format="dimension|reference" /><!-- 刻度之间的间隔 --><attr name="dialIntervalDegree" format="integer" /><!-- 圆弧颜色, --><attr name="arcColors" /><!-- 背景圆弧线颜色 --><attr name="bgArcColor" /><!-- 刻度线颜色 --><attr name="dialColor" format="color|reference" /></declare-styleable><!--********************带水波纹圆形进度条***************--><declare-styleable name="WaveProgress"><!-- 是否开启抗锯齿 --><attr name="antiAlias" /><!-- 深色水波动画时间 --><attr name="darkWaveAnimTime" format="integer" /><!-- 浅色水波动画时间 --><attr name="lightWaveAnimTime" format="integer" /><!-- 最大值 --><attr name="maxValue" /><!-- 当前值 --><attr name="value" /><attr name="valueColor" /><attr name="valueSize" /><!-- 绘制内容相应的提示语 --><attr name="hint" /><attr name="hintSize" /><attr name="hintColor" /><!-- 圆环宽度 --><attr name="circleWidth" format="dimension" /><!-- 圆环颜色 --><attr name="circleColor" format="color|reference" /><!-- 背景圆环颜色 --><attr name="bgCircleColor" format="color|reference" /><!-- 锁定水波不随圆环进度改变,默认锁定在50%处 --><attr name="lockWave" format="boolean" /><!-- 水波数量 --><attr name="waveNum" format="integer" /><!-- 水波高度,峰值和谷值之和 --><attr name="waveHeight" format="dimension" /><!-- 深色水波颜色 --><attr name="darkWaveColor" format="color|reference" /><!-- 是否显示浅色水波 --><attr name="showLightWave" format="boolean" /><!-- 浅色水波颜色 --><attr name="lightWaveColor" format="color|reference" /><!-- 浅色水波的方向 --><attr name="lightWaveDirect" format="enum"><enum name="L2R" value="0" /><enum name="R2L" value="1" /></attr></declare-styleable>
</resources>

各种属性都做了详细的注解,相信你能看懂.

既然属性定义好了,那么就要设置属性.没错属性的设置实在布局文件中引用自定义控件的时候调用.
 
布局文件activity_main,xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/activity_main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.littlejie.app.MainActivity"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><Buttonandroid:id="@+id/btn_reset_all"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/resetStr" /><!--最基本的圆--><com.littlejie.circleprogress.CircleProgressandroid:id="@+id/circle_progress_bar1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"app:antiAlias="true"app:arcColors="@array/gradient_arc_color"app:arcWidth="@dimen/small"app:bgArcColor="@color/colorAccent"app:bgArcWidth="@dimen/small"app:hint="截止当前已走"app:hintSize="15sp"app:maxValue="10000"app:startAngle="135"app:sweepAngle="270"app:unit="步"app:unitSize="15sp"app:value="10000"app:valueSize="25sp" /><com.littlejie.circleprogress.CircleProgressandroid:id="@+id/circle_progress_bar2"android:layout_width="100dp"android:layout_height="200dp"android:layout_gravity="center_horizontal"app:antiAlias="true"app:arcWidth="@dimen/small"app:bgArcColor="@color/colorAccent"app:bgArcWidth="@dimen/small"app:hint="百分比"app:hintSize="@dimen/text_size_15"app:maxValue="100"app:startAngle="135"app:sweepAngle="270"app:textOffsetPercentInRadius="0.5"app:unit="%"app:unitSize="@dimen/text_size_15"app:value="75"app:valueSize="@dimen/text_size_20" /><com.littlejie.circleprogress.CircleProgressandroid:id="@+id/circle_progress_bar3"android:layout_width="200dp"android:layout_height="200dp"android:layout_gravity="center_horizontal"app:antiAlias="true"app:arcWidth="@dimen/small"app:bgArcColor="@android:color/darker_gray"app:bgArcWidth="@dimen/small"app:hint="当前进度"app:hintSize="@dimen/text_size_25"app:maxValue="100"app:startAngle="270"app:sweepAngle="360"app:unit="%"app:unitSize="@dimen/text_size_25"app:value="100"app:valueSize="@dimen/text_size_35" /><!--带刻度的圆环--><com.littlejie.circleprogress.DialProgressandroid:id="@+id/dial_progress_bar"android:layout_width="300dp"android:layout_height="300dp"android:layout_gravity="center_horizontal"android:padding="@dimen/medium"app:animTime="1000"app:arcColors="@array/gradient_arc_color"app:arcWidth="@dimen/large"app:dialIntervalDegree="3"app:dialWidth="2dp"app:hint="当前时速"app:hintSize="@dimen/text_size_25"app:maxValue="300"app:startAngle="135"app:sweepAngle="270"app:unit="km/h"app:unitSize="@dimen/text_size_25"app:value="300"app:valueSize="@dimen/text_size_35" /><!--带水波纹的圆--><com.littlejie.circleprogress.WaveProgressandroid:id="@+id/wave_progress_bar"android:layout_width="300dp"android:layout_height="300dp"android:layout_gravity="center_horizontal"app:darkWaveAnimTime="1000"app:darkWaveColor="@color/dark"app:lightWaveAnimTime="2000"app:lightWaveColor="@color/light"app:lightWaveDirect="R2L"app:lockWave="false"app:valueSize="@dimen/text_size_35"app:waveHeight="30dp"app:waveNum="1" /></LinearLayout>
</ScrollView>

OK,开始动手

基本的圆形进度条

效果图:

Ok,在介绍正文之前,顺便提下圆弧和圆环的区别.看图:

通俗点就是闭合的是圆环,非闭合的是圆弧.
介绍这个就是为了告诉大家,接下来的自定义圆弧和圆环进度条虽然显示效果不一样,但是画法是一样的.仅仅只要改变绘制圆环时的起始角度(mStartAngle)以及扫过的角度(mSweepAngle)即可造成圆弧和圆环的差异.仅此而已.

ok,贴出基本圆形进度条的实现类 CircleProgress.java.

package com.littlejie.circleprogress;import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;import com.littlejie.circleprogress.utils.Constant;
import com.littlejie.circleprogress.utils.MiscUtil;public class CircleProgress extends View {private static final String TAG = CircleProgress.class.getSimpleName();private Context mContext;//默认大小private int mDefaultSize;//是否开启抗锯齿private boolean antiAlias;//绘制提示private TextPaint mHintPaint;private CharSequence mHint;private int mHintColor;private float mHintSize;private float mHintOffset;//绘制单位private TextPaint mUnitPaint;private CharSequence mUnit;private int mUnitColor;private float mUnitSize;private float mUnitOffset;//绘制数值private TextPaint mValuePaint;private float mValue;private float mMaxValue;private float mValueOffset;private int mPrecision;private String mPrecisionFormat;private int mValueColor;private float mValueSize;//绘制圆弧private Paint mArcPaint;private float mArcWidth;private float mStartAngle, mSweepAngle;private RectF mRectF;//渐变的颜色是360度,如果只显示270,那么则会缺失部分颜色private SweepGradient mSweepGradient;private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};//当前进度,[0.0f,1.0f]private float mPercent;//动画时间private long mAnimTime;//属性动画private ValueAnimator mAnimator;//绘制背景圆弧private Paint mBgArcPaint;private int mBgArcColor;private float mBgArcWidth;//圆心坐标,半径private Point mCenterPoint;private float mRadius;private float mTextOffsetPercentInRadius;public CircleProgress(Context context, AttributeSet attrs) {super(context, attrs);init(context, attrs);}private void init(Context context, AttributeSet attrs) {mContext = context;mDefaultSize = MiscUtil.dipToPx(mContext, Constant.DEFAULT_SIZE);mAnimator = new ValueAnimator();mRectF = new RectF();//画矩形mCenterPoint = new Point();initAttrs(attrs);initPaint();setValue(mValue);}private void initAttrs(AttributeSet attrs) {TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);antiAlias = typedArray.getBoolean(R.styleable.CircleProgressBar_antiAlias, Constant.ANTI_ALIAS);mHint = typedArray.getString(R.styleable.CircleProgressBar_hint);mHintColor = typedArray.getColor(R.styleable.CircleProgressBar_hintColor, Color.BLACK);mHintSize = typedArray.getDimension(R.styleable.CircleProgressBar_hintSize, Constant.DEFAULT_HINT_SIZE);mValue = typedArray.getFloat(R.styleable.CircleProgressBar_value, Constant.DEFAULT_VALUE);//50mMaxValue = typedArray.getFloat(R.styleable.CircleProgressBar_maxValue, Constant.DEFAULT_MAX_VALUE);//100//内容数值精度格式mPrecision = typedArray.getInt(R.styleable.CircleProgressBar_precision, 0);mPrecisionFormat = MiscUtil.getPrecisionFormat(mPrecision);mValueColor = typedArray.getColor(R.styleable.CircleProgressBar_valueColor, Color.BLACK);mValueSize = typedArray.getDimension(R.styleable.CircleProgressBar_valueSize, Constant.DEFAULT_VALUE_SIZE);mUnit = typedArray.getString(R.styleable.CircleProgressBar_unit);mUnitColor = typedArray.getColor(R.styleable.CircleProgressBar_unitColor, Color.BLACK);mUnitSize = typedArray.getDimension(R.styleable.CircleProgressBar_unitSize, Constant.DEFAULT_UNIT_SIZE);mArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_arcWidth, Constant.DEFAULT_ARC_WIDTH);mStartAngle = typedArray.getFloat(R.styleable.CircleProgressBar_startAngle, Constant.DEFAULT_START_ANGLE);mSweepAngle = typedArray.getFloat(R.styleable.CircleProgressBar_sweepAngle, Constant.DEFAULT_SWEEP_ANGLE);mBgArcColor = typedArray.getColor(R.styleable.CircleProgressBar_bgArcColor, Color.WHITE);mBgArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_bgArcWidth, Constant.DEFAULT_ARC_WIDTH);//圆弧宽度(一般和背景圆弧宽度相等)mTextOffsetPercentInRadius = typedArray.getFloat(R.styleable.CircleProgressBar_textOffsetPercentInRadius, 0.33f);//        mPercent = typedArray.getFloat(R.styleable.CircleProgressBar_percent, 0);mAnimTime = typedArray.getInt(R.styleable.CircleProgressBar_animTime, Constant.DEFAULT_ANIM_TIME);int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0);//圆弧颜色Log.i(TAG, "initAttrs: gradientArcColors::"+gradientArcColors);if (gradientArcColors != 0) {try {int[] gradientColors = getResources().getIntArray(gradientArcColors);Log.i(TAG, "initAttrs: gradientColors.length::"+gradientColors.length);if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值int color = getResources().getColor(gradientArcColors);mGradientColors = new int[2];mGradientColors[0] = color;mGradientColors[1] = color;} else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色mGradientColors = new int[2];mGradientColors[0] = gradientColors[0];mGradientColors[1] = gradientColors[0];} else {mGradientColors = gradientColors;}} catch (Resources.NotFoundException e) {throw new Resources.NotFoundException("the give resource not found.");}}typedArray.recycle();}private void initPaint() {// hint画笔mHintPaint = new TextPaint();// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。mHintPaint.setAntiAlias(antiAlias);// 设置绘制文字大小mHintPaint.setTextSize(mHintSize);// 设置画笔颜色mHintPaint.setColor(mHintColor);// 从中间向两边绘制,不需要再次计算文字mHintPaint.setTextAlign(Paint.Align.CENTER);// value画笔mValuePaint = new TextPaint();mValuePaint.setAntiAlias(antiAlias);mValuePaint.setTextSize(mValueSize);mValuePaint.setColor(mValueColor);// 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等mValuePaint.setTypeface(Typeface.DEFAULT_BOLD);mValuePaint.setTextAlign(Paint.Align.CENTER);// unit画笔mUnitPaint = new TextPaint();mUnitPaint.setAntiAlias(antiAlias);mUnitPaint.setTextSize(mUnitSize);mUnitPaint.setColor(mUnitColor);mUnitPaint.setTextAlign(Paint.Align.CENTER);//  圆环画笔mArcPaint = new Paint();mArcPaint.setAntiAlias(antiAlias);// 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKEmArcPaint.setStyle(Paint.Style.STROKE);//画圆环必有,不然是扇弧// 设置画笔粗细mArcPaint.setStrokeWidth(mArcWidth);// 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式// Cap.ROUND,或方形样式 Cap.SQUAREmArcPaint.setStrokeCap(Paint.Cap.ROUND);//作用于圆环结尾// 绘制背景圆环mBgArcPaint = new Paint();mBgArcPaint.setAntiAlias(antiAlias);mBgArcPaint.setColor(mBgArcColor);mBgArcPaint.setStyle(Paint.Style.STROKE);mBgArcPaint.setStrokeWidth(mBgArcWidth);mBgArcPaint.setStrokeCap(Paint.Cap.ROUND);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize),MiscUtil.measure(heightMeasureSpec, mDefaultSize));}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);//求圆弧和背景圆弧的最大宽度float maxArcWidth = Math.max(mArcWidth, mBgArcWidth);//求最小值作为实际值int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth,h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth);Log.i(TAG, "onSizeChanged: mArcWidth::"+mArcWidth+"\n"+" mBgArcWidth::"+mBgArcWidth+"\n"+" maxArcWidth::"+maxArcWidth+"\n"+" minSize::"+minSize);//获取圆心,圆半径mRadius = minSize / 2;//获取圆的相关参数mCenterPoint.x = w / 2;mCenterPoint.y = h / 2;//绘制圆弧的边界(画圆弧(或圆环)先要画矩形)mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2;mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2;mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2;mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2;//计算文字绘制时的 baseline//由于文字的baseline、descent、ascent等属性只与textSize和typeface有关,所以此时可以直接计算//若value、hint、unit由同一个画笔绘制或者需要动态设置文字的大小,则需要在每次更新后再次计算mValueOffset = mCenterPoint.y + getBaselineOffsetFromY(mValuePaint);//value处在中间mHintOffset = mCenterPoint.y - mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mHintPaint);//hint向上偏移1/3mUnitOffset = mCenterPoint.y + mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mUnitPaint);//unit向下偏移1/3updateArcPaint();Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + w + ", " + h + ")"+ "圆心坐标 = " + mCenterPoint.toString()+ ";圆半径 = " + mRadius+ ";圆的外接矩形 = " + mRectF.toString());}/*** 根据paint获取text字体高度的y方向的中点* @param paint* @return*/private float getBaselineOffsetFromY(Paint paint) {return MiscUtil.measureTextHeight(paint) / 2;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawText(canvas);drawArc(canvas);}/*** 绘制内容文字** @param canvas*/private void drawText(Canvas canvas) {// 计算文字宽度,由于Paint已设置为居中绘制,故此处不需要重新计算// float textWidth = mValuePaint.measureText(mValue.toString());// float x = mCenterPoint.x - textWidth / 2;canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint);if (mHint != null) {canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint);}if (mUnit != null) {canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint);}}private void drawArc(Canvas canvas) {// 绘制背景圆弧// 从进度圆弧结束的地方开始重新绘制,优化性能canvas.save();//用于表示数值对应的圆弧的当前角度float currentAngle = mSweepAngle * mPercent;//顺时针旋转135度(将起点至于7.5点钟方向)canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);//从135度开始绘制Log.i(TAG, "drawArc: currentAngle::"+currentAngle+" mSweepAngle::"+mSweepAngle+" (mSweepAngle - currentAngle + 2)::"+(mSweepAngle - currentAngle + 2));canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle, false, mBgArcPaint);// 第一个参数 oval 为 RectF 类型,即圆弧显示区域// startAngle 和 sweepAngle  均为 float 类型,分别表示圆弧起始角度和圆弧度数// 3点钟方向为0度,顺时针递增// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360// useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形//因为画圆弧的画笔是圆头类型的,在起始地方0度偏左还会有一个半圆,但是我们又采用了渐变色渲染,所以圆头部分就变成了结束的颜色值canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);canvas.restore();}/*** 更新圆弧画笔*/private void updateArcPaint() {// 设置渐变mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);mArcPaint.setShader(mSweepGradient);}public boolean isAntiAlias() {return antiAlias;}public CharSequence getHint() {return mHint;}public void setHint(CharSequence hint) {mHint = hint;}public CharSequence getUnit() {return mUnit;}public void setUnit(CharSequence unit) {mUnit = unit;}public float getValue() {return mValue;}//**********************************用于点击设置随机值,用于进度显示*****************************/*** 设置当前值** @param value*/public void setValue(float value) {if (value > mMaxValue) {value = mMaxValue;}float start = mPercent;float end = value / mMaxValue;startAnimator(start, end, mAnimTime);}private void startAnimator(float start, float end, long animTime) {Log.i(TAG, "startAnimator: start::"+start+" end::"+end);mAnimator = ValueAnimator.ofFloat(start, end);//获取%区间 当前进度,[0.0f,1.0f]mAnimator.setDuration(animTime);mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mPercent = (float) animation.getAnimatedValue();mValue = mPercent * mMaxValue;Log.d(TAG, "onAnimationUpdate: percent = " + mPercent+ ";currentAngle = " + (mSweepAngle * mPercent)+ ";value = " + mValue);invalidate();}});mAnimator.start();}/*** 获取最大值** @return*/public float getMaxValue() {return mMaxValue;}/*** 设置最大值** @param maxValue*/public void setMaxValue(float maxValue) {mMaxValue = maxValue;}/*** 获取精度** @return*/public int getPrecision() {return mPrecision;}public void setPrecision(int precision) {mPrecision = precision;mPrecisionFormat = MiscUtil.getPrecisionFormat(precision);}public int[] getGradientColors() {return mGradientColors;}/*** 设置渐变** @param gradientColors*/public void setGradientColors(int[] gradientColors) {mGradientColors = gradientColors;updateArcPaint();}public long getAnimTime() {return mAnimTime;}public void setAnimTime(long animTime) {mAnimTime = animTime;}/*** 重置*/public void reset() {startAnimator(mPercent, 0.0f, 1000L);}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();//释放资源}
}

原博文中代码结构比较清晰了,我在其中又添加了注解.基本每行代码你都能读懂,应该不难理解.这里不在赘述.

注意点:

Num 1. 可以略微关注下(了解下即可,不影响展现效果),代码中在绘制彩色圆弧(注意不是圆环)时

 canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle, false, mBgArcPaint);// 第一个参数 oval 为 RectF 类型,即圆弧显示区域// startAngle 和 sweepAngle  均为 float 类型,分别表示圆弧起始角度和圆弧度数// 3点钟方向为0度,顺时针递增// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360// useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形//因为画圆弧的画笔是圆头类型的,在起始地方0度偏左还会有一个半圆,但是我们又采用了渐变色渲染,所以圆头部分就变成了结束的颜色值canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);

你可能发现了, canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);中的第二个参数是2,为什么会这样呢?正常情况下不应该是0么?

没错,之所以这样,是因为在构造圆环画笔的时候,我们为了好看,使用了

 mArcPaint.setStrokeCap(Paint.Cap.ROUND);//作用于圆环结尾

这样的话,因为画圆弧的画笔是圆头类型的,在起始地方0度偏左还会有一个半圆,但是我们又采用了渐变色渲染,所以圆头部分就变成了结束的颜色值.可能有点难懂,直接上图,看看如果第二个参数设置成"0"的话,会出现什么效果.如图:

看到没?左下角多了个"小红点"(小半圆).至于为什么设置成"2"能遮住小红点,我不太理解.如果有知道的小伙伴请告知!!!
但是这样做的话,会造成圆弧最下端左高又低(不仔细看看不出来哦),当然了一般要求不高的话,这样做是完全可以的.

那么有没有可以遮住小红点,又不会造成圆弧底部左高右低的方法呢?
办法到是有一个,那就是以小红点的中心为圆心,圆弧宽/2为半径画一个与起点颜色一致的圆即可.详情可参考:一步步做Android自定义圆环百分比控件,里面的解决方案可参考.但是不建议如此,因为没什么必要.(实在不行,你可以调节mSweepAngle角度也是可以的)
 
Num 2. 代码中在绘制圆环(或圆弧)渐变色的时候,原博文中是这样的.

 if (gradientArcColors != 0) {try {int[] gradientColors = getResources().getIntArray(gradientArcColors);Log.i(TAG, "initAttrs: gradientColors.length::"+gradientColors.length);if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值int color = getResources().getColor(gradientArcColors);mGradientColors = new int[2];mGradientColors[0] = color;mGradientColors[1] = color;} else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色mGradientColors = new int[2];mGradientColors[0] = gradientColors[0];mGradientColors[1] = gradientColors[0];} else {mGradientColors = gradientColors;}} catch (Resources.NotFoundException e) {throw new Resources.NotFoundException("the give resource not found.");}}

可是我实在想不明白:
1. 什么情况下能满足(gradientColors.length == 0)? 应该不存在的
2. 在if (gradientColors.length == 1)给渐变色赋值时,既然mGradientColors = new int[2];了
,那么 mGradientColors[1] = gradientColors[0];是什么鬼? mGradientColors[1] = gradientColors[1];才正常.相信作者是笔误吧.

因此.上述代码改成下面是完全可以的.

 if (gradientArcColors != 0) {try {int[] gradientColors = getResources().getIntArray(gradientArcColors);Log.i(TAG, "initAttrs: gradientColors.length::"+gradientColors.length);if (gradientColors.length == 1) {//如果渐变色为数组为0,则尝试以单色读取色值mGradientColors = new int[2];mGradientColors[0] = gradientColors[0];} else if (gradientColors.length == 2) {//如果渐变数组只有一种颜色,默认设为两种相同颜色mGradientColors = new int[2];mGradientColors[0] = gradientColors[0];mGradientColors[1] = gradientColors[1];} else {mGradientColors = new int[3];mGradientColors[0] = gradientColors[0];mGradientColors[1] = gradientColors[1];mGradientColors[2] = gradientColors[2];
//                    mGradientColors = gradientColors;}} catch (Resources.NotFoundException e) {throw new Resources.NotFoundException("the give resource not found.");}}

这里还有两个注意点:

1. 不要在 ValueAnimator.AnimatorUpdateListener 中输出 Log,特别是动画调用频繁的情况下,因为输出 Log 频繁会生成大量 String 对象造成内存抖动,当然也可以使用 StringBuilder 来优化。

2. 关于 invalidate() 和 postInvalidate() 两者最本质的前者只能在 UI 线程中使用,而后者可以在非 UI 线程中使用,其实 postInvalidate() 内部也是使用 Handler 实现的。

带刻度的圆形进度条

效果图:

友情提示:

DialProgress 与 CircleProgress 的实现极其相似.仅仅是**在基本圆环进度条的基础上添加了一个不断旋转的"小白色矩形(刻度中间的白色间隔)"**而已.因为两者之间其实就差了一个刻度,但考虑到扩展以及类职责的单一,所以将两者分开。这里主要讲一下刻度的绘制。刻度绘制主要使用 Canvas 类的 save()、rotate()和restore() 方法,当然你也可以使用 translate() 方法对坐标系进行平移,方便计算。

再次强调下,图中的彩色刻度不是我们画上去的,我们只是绘制了一个"白色刻度间隔",然后通过旋转canvas来实现"白色刻度间隔"的沿圆弧绘制.明白了吗,绘制的是刻度间隔,不是刻度(想想也会明白,刻度其实是一个个小梯形,绘制梯形不太现实).

上代码:DialProgress.java

package com.littlejie.circleprogress;import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;import com.littlejie.circleprogress.utils.Constant;
import com.littlejie.circleprogress.utils.MiscUtil;/*** 带有刻度的圆形进度条* Created by littlejie on 2017/2/26.*/public class DialProgress extends View {private static final String TAG = DialProgress.class.getSimpleName();private Context mContext;//圆心坐标private Point mCenterPoint;private float mRadius;private float mTextOffsetPercentInRadius;private boolean antiAlias;//绘制提示private TextPaint mHintPaint;private CharSequence mHint;private int mHintColor;private float mHintSize;private float mHintOffset;//绘制数值private Paint mValuePaint;private int mValueColor;private float mMaxValue;private float mValue;private float mValueSize;private float mValueOffset;private String mPrecisionFormat;//绘制单位private Paint mUnitPaint;private float mUnitSize;private int mUnitColor;private float mUnitOffset;private CharSequence mUnit;//前景圆弧private Paint mArcPaint;private float mArcWidth;//刻度之间的间隔private int mDialIntervalDegree;private float mStartAngle, mSweepAngle;private RectF mRectF;//渐变private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};//当前进度,[0.0f,1.0f]private float mPercent;//动画时间private long mAnimTime;//属性动画private ValueAnimator mAnimator;//背景圆弧private Paint mBgArcPaint;private int mBgArcColor;//刻度线颜色private Paint mDialPaint;private float mDialWidth;private int mDialColor;private int mDefaultSize;public DialProgress(Context context, AttributeSet attrs) {super(context, attrs);init(context, attrs);}private void init(Context context, AttributeSet attrs) {mContext = context;mDefaultSize = MiscUtil.dipToPx(context, Constant.DEFAULT_SIZE);mRectF = new RectF();mCenterPoint = new Point();initConfig(context, attrs);initPaint();setValue(mValue);}private void initConfig(Context context, AttributeSet attrs) {TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DialProgress);antiAlias = typedArray.getBoolean(R.styleable.DialProgress_antiAlias, true);mMaxValue = typedArray.getFloat(R.styleable.DialProgress_maxValue, Constant.DEFAULT_MAX_VALUE);mValue = typedArray.getFloat(R.styleable.DialProgress_value, Constant.DEFAULT_VALUE);mValueSize = typedArray.getDimension(R.styleable.DialProgress_valueSize, Constant.DEFAULT_VALUE_SIZE);mValueColor = typedArray.getColor(R.styleable.DialProgress_valueColor, Color.BLACK);mDialIntervalDegree = typedArray.getInt(R.styleable.DialProgress_dialIntervalDegree, 10);int precision = typedArray.getInt(R.styleable.DialProgress_precision, 0);mPrecisionFormat = MiscUtil.getPrecisionFormat(precision);mUnit = typedArray.getString(R.styleable.DialProgress_unit);mUnitColor = typedArray.getColor(R.styleable.DialProgress_unitColor, Color.BLACK);mUnitSize = typedArray.getDimension(R.styleable.DialProgress_unitSize, Constant.DEFAULT_UNIT_SIZE);mHint = typedArray.getString(R.styleable.DialProgress_hint);mHintColor = typedArray.getColor(R.styleable.DialProgress_hintColor, Color.BLACK);mHintSize = typedArray.getDimension(R.styleable.DialProgress_hintSize, Constant.DEFAULT_HINT_SIZE);mArcWidth = typedArray.getDimension(R.styleable.DialProgress_arcWidth, Constant.DEFAULT_ARC_WIDTH);mStartAngle = typedArray.getFloat(R.styleable.DialProgress_startAngle, Constant.DEFAULT_START_ANGLE);mSweepAngle = typedArray.getFloat(R.styleable.DialProgress_sweepAngle, Constant.DEFAULT_SWEEP_ANGLE);mAnimTime = typedArray.getInt(R.styleable.DialProgress_animTime, Constant.DEFAULT_ANIM_TIME);mBgArcColor = typedArray.getColor(R.styleable.DialProgress_bgArcColor, Color.GRAY);mDialWidth = typedArray.getDimension(R.styleable.DialProgress_dialWidth, 2);mDialColor = typedArray.getColor(R.styleable.DialProgress_dialColor, Color.WHITE);mTextOffsetPercentInRadius = typedArray.getFloat(R.styleable.DialProgress_textOffsetPercentInRadius, 0.33f);int gradientArcColors = typedArray.getResourceId(R.styleable.DialProgress_arcColors, 0);if (gradientArcColors != 0) {try {int[] gradientColors = getResources().getIntArray(gradientArcColors);if (gradientColors.length == 0) {int color = getResources().getColor(gradientArcColors);mGradientColors = new int[2];mGradientColors[0] = color;mGradientColors[1] = color;} else if (gradientColors.length == 1) {mGradientColors = new int[2];mGradientColors[0] = gradientColors[0];mGradientColors[1] = gradientColors[0];} else {mGradientColors = gradientColors;}} catch (Resources.NotFoundException e) {throw new Resources.NotFoundException("the give resource not found.");}}typedArray.recycle();}private void initPaint() {mHintPaint = new TextPaint();// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。mHintPaint.setAntiAlias(antiAlias);// 设置绘制文字大小mHintPaint.setTextSize(mHintSize);// 设置画笔颜色mHintPaint.setColor(mHintColor);// 从中间向两边绘制,不需要再次计算文字mHintPaint.setTextAlign(Paint.Align.CENTER);mValuePaint = new Paint();mValuePaint.setAntiAlias(antiAlias);mValuePaint.setTextSize(mValueSize);mValuePaint.setColor(mValueColor);mValuePaint.setTypeface(Typeface.DEFAULT_BOLD);mValuePaint.setTextAlign(Paint.Align.CENTER);mUnitPaint = new Paint();mUnitPaint.setAntiAlias(antiAlias);mUnitPaint.setTextSize(mUnitSize);mUnitPaint.setColor(mUnitColor);mUnitPaint.setTextAlign(Paint.Align.CENTER);mArcPaint = new Paint();mArcPaint.setAntiAlias(antiAlias);mArcPaint.setStyle(Paint.Style.STROKE);mArcPaint.setStrokeWidth(mArcWidth);mArcPaint.setStrokeCap(Paint.Cap.BUTT);mBgArcPaint = new Paint();mBgArcPaint.setAntiAlias(antiAlias);mBgArcPaint.setStyle(Paint.Style.STROKE);mBgArcPaint.setStrokeWidth(mArcWidth);mBgArcPaint.setStrokeCap(Paint.Cap.BUTT);mBgArcPaint.setColor(mBgArcColor);mDialPaint = new Paint();mDialPaint.setAntiAlias(antiAlias);mDialPaint.setColor(mDialColor);mDialPaint.setStrokeWidth(mDialWidth);}/*** 更新圆弧画笔*/private void updateArcPaint() {// 设置渐变// 渐变的颜色是360度,如果只显示270,那么则会缺失部分颜色SweepGradient sweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);mArcPaint.setShader(sweepGradient);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize),MiscUtil.measure(heightMeasureSpec, mDefaultSize));}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);int minSize = Math.min(getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - 2 * (int) mArcWidth,getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - 2 * (int) mArcWidth);mRadius = minSize / 2;mCenterPoint.x = getMeasuredWidth() / 2;mCenterPoint.y = getMeasuredHeight() / 2;//绘制圆弧的边界mRectF.left = mCenterPoint.x - mRadius - mArcWidth / 2;mRectF.top = mCenterPoint.y - mRadius - mArcWidth / 2;mRectF.right = mCenterPoint.x + mRadius + mArcWidth / 2;mRectF.bottom = mCenterPoint.y + mRadius + mArcWidth / 2;mValueOffset = mCenterPoint.y + getBaselineOffsetFromY(mValuePaint);mHintOffset = mCenterPoint.y - mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mHintPaint);mUnitOffset = mCenterPoint.y + mRadius * mTextOffsetPercentInRadius + getBaselineOffsetFromY(mUnitPaint);updateArcPaint();Log.d(TAG, "onMeasure: 控件大小 = " + "(" + getMeasuredWidth() + ", " + getMeasuredHeight() + ")"+ ";圆心坐标 = " + mCenterPoint.toString()+ ";圆半径 = " + mRadius+ ";圆的外接矩形 = " + mRectF.toString());}private float getBaselineOffsetFromY(Paint paint) {return MiscUtil.measureTextHeight(paint) / 2;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawArc(canvas);drawDial(canvas);drawText(canvas);}private void drawArc(Canvas canvas) {// 绘制背景圆弧// 从进度圆弧结束的地方开始重新绘制,优化性能float currentAngle = mSweepAngle * mPercent;canvas.save();canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle, false, mBgArcPaint);// 第一个参数 oval 为 RectF 类型,即圆弧显示区域// startAngle 和 sweepAngle  均为 float 类型,分别表示圆弧起始角度和圆弧度数// 3点钟方向为0度,顺时针递增// 如果 startAngle < 0 或者 > 360,则相当于 startAngle % 360// useCenter:如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形canvas.drawArc(mRectF, 0, currentAngle, false, mArcPaint);canvas.restore();}/*** 绘制刻度* @param canvas*/private void drawDial(Canvas canvas) {//获取分成多少个间隔int total = (int) (mSweepAngle / mDialIntervalDegree);canvas.save();canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);for (int i = 0; i <= total; i++) {//这一点可能比较难理解点:drawLine(...)从表面看画的是圆最右边的一条白线(白色小矩形),但是由于在drawArc()中已经将canvas顺时针旋转了135度,一次刻度间隔的白线也就从圆弧起点开始了canvas.drawLine(mCenterPoint.x + mRadius, mCenterPoint.y, mCenterPoint.x + mRadius + mArcWidth, mCenterPoint.y, mDialPaint);canvas.rotate(mDialIntervalDegree, mCenterPoint.x, mCenterPoint.y);}canvas.restore();}private void drawText(Canvas canvas) {canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint);if (mUnit != null) {canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint);}if (mHint != null) {canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint);}}public float getMaxValue() {return mMaxValue;}public void setMaxValue(float maxValue) {mMaxValue = maxValue;}/*** 设置当前值** @param value*/public void setValue(float value) {if (value > mMaxValue) {value = mMaxValue;}float start = mPercent;float end = value / mMaxValue;startAnimator(start, end, mAnimTime);}private void startAnimator(float start, float end, long animTime) {mAnimator = ValueAnimator.ofFloat(start, end);mAnimator.setDuration(animTime);mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mPercent = (float) animation.getAnimatedValue();mValue = mPercent * mMaxValue;if (BuildConfig.DEBUG) {Log.d(TAG, "onAnimationUpdate: percent = " + mPercent+ ";currentAngle = " + (mSweepAngle * mPercent)+ ";value = " + mValue);}invalidate();}});mAnimator.start();}public int[] getGradientColors() {return mGradientColors;}public void setGradientColors(int[] gradientColors) {mGradientColors = gradientColors;updateArcPaint();}public void reset() {startAnimator(mPercent, 0.0f, 1000L);}
}

要说的话,都已经在注解中了.
老样子,下面将其中你可能有疑问的地方拿出说明下:
(1). 代码中有这样一句话

  //这一点可能比较难理解点:drawLine(...)从表面看画的是圆最右边的一条白线(白色小矩形),但是由于在drawArc()中已经将canvas顺时针旋转了135度,一次刻度间隔的白线也就从圆弧起点开始了canvas.drawLine(mCenterPoint.x + mRadius, mCenterPoint.y, mCenterPoint.x + mRadius + mArcWidth, mCenterPoint.y, mDialPaint);

如果只看代码中的canvas.drawLine(…)绘制的"白色刻度间隔"应该是在圆弧最右侧.这个不难理解,因为在绘制圆弧时drawArc和SweepGradient这两个类的起始点0度不是在我们习惯的圆环最上面那个点,而是从圆环最右边那个点开始,如图(再贴一遍吧)

如果只看这行代码,确实是这样,但是别忘了,还有

 canvas.rotate(mDialIntervalDegree, mCenterPoint.x, mCenterPoint.y);

canvas.rotate(…)的作用就在于将"白色刻度间隔"顺时针旋转135度(这个角度是可调的,在xml中设置自定义控件的属性即可),也就是左下角圆弧起始处,这样一来,"白色刻度间隔"就会从起始处沿圆弧绘制了.

可以看下刻度间隔的绘制流程1----->2------->3------->4:
1. 2. 3. 4.
就是这样,明白了吧.

水波纹效果的圆形进度条 (适用于Android 19及以上)

效果图:

吐槽:

原博文作者是真牛逼,我花了好长时间,才基本弄明白.我也是照着"灵魂画手"的图,然后各种log和断点调试.才基本弄明白,但是对于其中对绘制水波曲线上的各种坐标计算的的算法到底是怎么总结出来的,抱歉,水平有限-------我只能验证其中的坐标算法正确.但是如何来的,我无能为力.

水波纹效果的进度条实现需要用到贝塞尔曲线,主要难点在于 绘制区域的计算波浪效果 的实现,其余的逻辑跟上述两种进度条相似。

这里使用了 Path 类,该类在 Android 2D 绘图中是非常重要的,Path 不仅能够绘制简单图形,也可以绘制这些比较复杂的图形。也可以对多个路径进行布尔操作,类似设置 Paint 的 setXfermode() ,具体使用可以参考这篇博客:安卓自定义View进阶-Path基本操作
 
先上代码:WaveProgress.java

package com.littlejie.circleprogress;import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.RectF;
import android.os.Build;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;import com.littlejie.circleprogress.utils.Constant;
import com.littlejie.circleprogress.utils.MiscUtil;/*** 水波进度条* Created by littlejie on 2017/2/26.*/public class WaveProgress extends View {private static final String TAG = WaveProgress.class.getSimpleName();//浅色波浪方向private static final int L2R = 0;private static final int R2L = 1;private int mDefaultSize;//圆心private Point mCenterPoint;//半径private float mRadius;//圆的外接矩形private RectF mRectF;//深色波浪移动距离private float mDarkWaveOffset;//浅色波浪移动距离private float mLightWaveOffset;//浅色波浪方向private boolean isR2L;//是否锁定波浪不随进度移动private boolean lockWave;//是否开启抗锯齿private boolean antiAlias;//最大值private float mMaxValue;//当前值private float mValue;//当前进度private float mPercent;//绘制提示private TextPaint mHintPaint;private CharSequence mHint;private int mHintColor;private float mHintSize;private Paint mPercentPaint;private float mValueSize;private int mValueColor;//圆环宽度private float mCircleWidth;//圆环private Paint mCirclePaint;//圆环颜色private int mCircleColor;//背景圆环颜色private int mBgCircleColor;//水波路径private Path mWaveLimitPath;private Path mWavePath;//水波高度private float mWaveHeight;//水波数量private int mWaveNum;//深色水波private Paint mWavePaint;//深色水波颜色private int mDarkWaveColor;//浅色水波颜色private int mLightWaveColor;//深色水波贝塞尔曲线上的起始点、控制点private Point[] mDarkPoints;//浅色水波贝塞尔曲线上的起始点、控制点private Point[] mLightPoints;//贝塞尔曲线点的总个数private int mAllPointCount;private int mHalfPointCount;private ValueAnimator mProgressAnimator;private long mDarkWaveAnimTime;private ValueAnimator mDarkWaveAnimator;private long mLightWaveAnimTime;private ValueAnimator mLightWaveAnimator;public WaveProgress(Context context, AttributeSet attrs) {super(context, attrs);init(context, attrs);}private void init(Context context, AttributeSet attrs) {mDefaultSize = MiscUtil.dipToPx(context, Constant.DEFAULT_SIZE);mRectF = new RectF();mCenterPoint = new Point();initAttrs(context, attrs);initPaint();initPath();}private void initAttrs(Context context, AttributeSet attrs) {TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveProgress);antiAlias = typedArray.getBoolean(R.styleable.WaveProgress_antiAlias, true);mDarkWaveAnimTime = typedArray.getInt(R.styleable.WaveProgress_darkWaveAnimTime, Constant.DEFAULT_ANIM_TIME);mLightWaveAnimTime = typedArray.getInt(R.styleable.WaveProgress_lightWaveAnimTime, Constant.DEFAULT_ANIM_TIME);mMaxValue = typedArray.getFloat(R.styleable.WaveProgress_maxValue, Constant.DEFAULT_MAX_VALUE);mValue = typedArray.getFloat(R.styleable.WaveProgress_value, Constant.DEFAULT_VALUE);mValueSize = typedArray.getDimension(R.styleable.WaveProgress_valueSize, Constant.DEFAULT_VALUE_SIZE);mValueColor = typedArray.getColor(R.styleable.WaveProgress_valueColor, Color.BLACK);mHint = typedArray.getString(R.styleable.WaveProgress_hint);mHintColor = typedArray.getColor(R.styleable.WaveProgress_hintColor, Color.BLACK);mHintSize = typedArray.getDimension(R.styleable.WaveProgress_hintSize, Constant.DEFAULT_HINT_SIZE);mCircleWidth = typedArray.getDimension(R.styleable.WaveProgress_circleWidth, Constant.DEFAULT_ARC_WIDTH);mCircleColor = typedArray.getColor(R.styleable.WaveProgress_circleColor, Color.GREEN);mBgCircleColor = typedArray.getColor(R.styleable.WaveProgress_bgCircleColor, Color.WHITE);mWaveHeight = typedArray.getDimension(R.styleable.WaveProgress_waveHeight, Constant.DEFAULT_WAVE_HEIGHT);mWaveNum = typedArray.getInt(R.styleable.WaveProgress_waveNum, 1);mDarkWaveColor = typedArray.getColor(R.styleable.WaveProgress_darkWaveColor,getResources().getColor(android.R.color.holo_blue_dark));mLightWaveColor = typedArray.getColor(R.styleable.WaveProgress_lightWaveColor,getResources().getColor(android.R.color.holo_green_light));isR2L = typedArray.getInt(R.styleable.WaveProgress_lightWaveDirect, R2L) == R2L;lockWave = typedArray.getBoolean(R.styleable.WaveProgress_lockWave, false);typedArray.recycle();}private void initPaint() {//todo hint画笔mHintPaint = new TextPaint();// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢。mHintPaint.setAntiAlias(antiAlias);// 设置绘制文字大小mHintPaint.setTextSize(mHintSize);// 设置画笔颜色mHintPaint.setColor(mHintColor);// 从中间向两边绘制,不需要再次计算文字mHintPaint.setTextAlign(Paint.Align.CENTER);//todo 圆环画笔mCirclePaint = new Paint();mCirclePaint.setAntiAlias(antiAlias);mCirclePaint.setStrokeWidth(mCircleWidth);mCirclePaint.setStyle(Paint.Style.STROKE);mCirclePaint.setStrokeCap(Paint.Cap.ROUND);//todo 波浪画笔mWavePaint = new Paint();mWavePaint.setAntiAlias(antiAlias);mWavePaint.setStyle(Paint.Style.FILL);//todo 数值画笔mPercentPaint = new Paint();mPercentPaint.setTextAlign(Paint.Align.CENTER);mPercentPaint.setAntiAlias(antiAlias);mPercentPaint.setColor(mValueColor);mPercentPaint.setTextSize(mValueSize);}private void initPath() {mWaveLimitPath = new Path();mWavePath = new Path();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);setMeasuredDimension(MiscUtil.measure(widthMeasureSpec, mDefaultSize),MiscUtil.measure(heightMeasureSpec, mDefaultSize));}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);int minSize = Math.min(getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - 2 * (int) mCircleWidth,getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - 2 * (int) mCircleWidth);mRadius = minSize / 2;mCenterPoint.x = getMeasuredWidth() / 2;mCenterPoint.y = getMeasuredHeight() / 2;//绘制圆弧的边界mRectF.left = mCenterPoint.x - mRadius - mCircleWidth / 2;mRectF.top = mCenterPoint.y - mRadius - mCircleWidth / 2;mRectF.right = mCenterPoint.x + mRadius + mCircleWidth / 2;mRectF.bottom = mCenterPoint.y + mRadius + mCircleWidth / 2;Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + getMeasuredWidth() + ", " + getMeasuredHeight() + ")"+ ";圆心坐标 = " + mCenterPoint.toString()+ ";圆半径 = " + mRadius+ ";圆的外接矩形 = " + mRectF.toString());initWavePoints();//开始动画setValue(mValue);startWaveAnimator();}private void initWavePoints() {//当前波浪宽度float waveWidth = (mRadius * 2) / mWaveNum;mAllPointCount = 8 * mWaveNum + 1;mHalfPointCount = mAllPointCount / 2;Log.i(TAG, "initWavePoints: mHalfPointCount::"+mHalfPointCount);mDarkPoints = getPoint(false, waveWidth);//从左向右mLightPoints = getPoint(isR2L, waveWidth);//从右向左}/*** 从左往右或者从右往左获取贝塞尔点** @return*/private Point[] getPoint(boolean isR2L, float waveWidth) {Point[] points = new Point[mAllPointCount];//9//第1个点特殊处理,即数组的中点//todo 待论证points[mHalfPointCount] = new Point((int) (mCenterPoint.x + (isR2L ? mRadius : -mRadius)), mCenterPoint.y);Log.i(TAG, "getPoint: points[mHalfPointCount]::"+points[mHalfPointCount]);//points[4]::Point(15, 450)//屏幕内的贝塞尔曲线点for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) {Log.i(TAG, "getPoint: i="+i+" (i / 4 - mWaveNum)::"+(i / 4 - mWaveNum)+" waveWidth::"+waveWidth+" final::"+(waveWidth * (i / 4 - mWaveNum)));//width为中点到原点沿x方向的offsetfloat width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum);//points[mHalfPointCount].x::15 waveWidth::870.0 width::15.0Log.i(TAG, "getPoint: points[mHalfPointCount].x::"+points[mHalfPointCount].x+" waveWidth::"+waveWidth+" width::"+width);points[i] = new Point((int) (waveWidth*1 / 4 + width), (int) (mCenterPoint.y - mWaveHeight));//points[5]::Point(232, 360)points[i + 1] = new Point((int) (waveWidth *2/ 4 + width), mCenterPoint.y);//points[6]::Point(450, 450)points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight));//points[7]::Point(667, 540)points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y);//points[8]::Point(885, 450)Log.i(TAG, "getPoint:"+"\n"+"points["+i+"]::"+points[i]+"\n"+"points["+(i+1)+"]::"+points[i+1]+"\n"+"points["+(i+2)+"]::"+points[i+2]+"\n"+"points["+(i+3)+"]::"+points[i+3]+"\n");}//屏幕外的贝塞尔曲线点for (int i = 0; i < mHalfPointCount; i++) {int reverse = mAllPointCount - i - 1;//8 7 6 5points[i] = new Point((isR2L ? 2 : 1) * points[mHalfPointCount].x - points[reverse].x,points[mHalfPointCount].y * 2 - points[reverse].y);//  getPoint: points[0]::Point(-870, 450)//    getPoint: points[1]::Point(-652, 360)//    getPoint: points[2]::Point(-435, 450)//    getPoint: points[3]::Point(-217, 540)Log.i(TAG, "getPoint: "+"points["+i+"]::"+points[i]+"\n");}//对从右向左的贝塞尔点数组反序,方便后续处理return isR2L ? MiscUtil.reverse(points) : points;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawCircle(canvas);drawLightWave(canvas);drawDarkWave(canvas);drawProgress(canvas);}/*** 绘制圆环** @param canvas*/private void drawCircle(Canvas canvas) {canvas.save();canvas.rotate(270, mCenterPoint.x, mCenterPoint.y);int currentAngle = (int) (360 * mPercent);//画背景圆环mCirclePaint.setColor(mBgCircleColor);canvas.drawArc(mRectF, currentAngle, 360 - currentAngle, false, mCirclePaint);//画圆环mCirclePaint.setColor(mCircleColor);canvas.drawArc(mRectF, 0, currentAngle, false, mCirclePaint);canvas.restore();}/*** 绘制深色波浪(贝塞尔曲线)** @param canvas*/private void drawDarkWave(Canvas canvas) {mWavePaint.setColor(mDarkWaveColor);drawWave(canvas, mWavePaint, mDarkPoints, mDarkWaveOffset);}/*** 绘制浅色波浪(贝塞尔曲线)** @param canvas*/private void drawLightWave(Canvas canvas) {mWavePaint.setColor(mLightWaveColor);//从右向左的水波位移应该被减去drawWave(canvas, mWavePaint, mLightPoints, isR2L ? -mLightWaveOffset : mLightWaveOffset);}@TargetApi(Build.VERSION_CODES.KITKAT)private void drawWave(Canvas canvas, Paint paint, Point[] points, float waveOffset) {mWaveLimitPath.reset();mWavePath.reset();float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent;//moveTo和lineTo绘制出水波区域矩形mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height);for (int i = 1; i < mAllPointCount; i += 2) {mWavePath.quadTo(points[i].x + waveOffset, points[i].y + height,points[i + 1].x + waveOffset, points[i + 1].y + height);}//mWavePath.lineTo(points[mAllPointCount - 1].x, points[mAllPointCount - 1].y + height);//不管如何移动,波浪与圆路径的交集底部永远固定,否则会造成上移的时候底部为空的情况mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius);mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius);mWavePath.close();mWaveLimitPath.addCircle(mCenterPoint.x, mCenterPoint.y, mRadius, Path.Direction.CW);//取该圆与波浪路径的交集,形成波浪在圆内的效果mWaveLimitPath.op(mWavePath, Path.Op.INTERSECT);canvas.drawPath(mWaveLimitPath, paint);}//前一次绘制时的进度private float mPrePercent;//当前进度值private String mPercentValue;private void drawProgress(Canvas canvas) {float y = mCenterPoint.y - (mPercentPaint.descent() + mPercentPaint.ascent()) / 2;if (BuildConfig.DEBUG) {Log.d(TAG, "mPercent = " + mPercent + "; mPrePercent = " + mPrePercent);}if (mPrePercent == 0.0f || Math.abs(mPercent - mPrePercent) >= 0.01f) {mPercentValue = String.format("%.0f%%", mPercent * 100);mPrePercent = mPercent;}canvas.drawText(mPercentValue, mCenterPoint.x, y, mPercentPaint);if (mHint != null) {float hy = mCenterPoint.y * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2;canvas.drawText(mHint.toString(), mCenterPoint.x, hy, mHintPaint);}}public float getMaxValue() {return mMaxValue;}public void setMaxValue(float maxValue) {mMaxValue = maxValue;}public float getValue() {return mValue;}/*** 设置当前值** @param value*/public void setValue(float value) {if (value > mMaxValue) {value = mMaxValue;}float start = mPercent;float end = value / mMaxValue;Log.d(TAG, "setValue, value = " + value + ";start = " + start + "; end = " + end);startAnimator(start, end, mDarkWaveAnimTime);}private void startAnimator(final float start, float end, long animTime) {Log.d(TAG, "startAnimator,value = " + mValue+ ";start = " + start + ";end = " + end + ";time = " + animTime);//当start=0且end=0时,不需要启动动画if (start == 0 && end == 0) {return;}mProgressAnimator = ValueAnimator.ofFloat(start, end);mProgressAnimator.setDuration(animTime);mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mPercent = (float) animation.getAnimatedValue();if (mPercent == 0.0f || mPercent == 1.0f) {stopWaveAnimator();} else {startWaveAnimator();}mValue = mPercent * mMaxValue;if (BuildConfig.DEBUG) {Log.d(TAG, "onAnimationUpdate: percent = " + mPercent+ ";value = " + mValue);}invalidate();}});mProgressAnimator.start();}private void startWaveAnimator() {startLightWaveAnimator();startDarkWaveAnimator();}private void stopWaveAnimator() {if (mDarkWaveAnimator != null && mDarkWaveAnimator.isRunning()) {mDarkWaveAnimator.cancel();mDarkWaveAnimator.removeAllUpdateListeners();mDarkWaveAnimator = null;}if (mLightWaveAnimator != null && mLightWaveAnimator.isRunning()) {mLightWaveAnimator.cancel();mLightWaveAnimator.removeAllUpdateListeners();mLightWaveAnimator = null;}}private void startLightWaveAnimator() {if (mLightWaveAnimator != null && mLightWaveAnimator.isRunning()) {return;}mLightWaveAnimator = ValueAnimator.ofFloat(0, 2 * mRadius);mLightWaveAnimator.setDuration(mLightWaveAnimTime);mLightWaveAnimator.setRepeatCount(ValueAnimator.INFINITE);mLightWaveAnimator.setInterpolator(new LinearInterpolator());mLightWaveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mLightWaveOffset = (float) animation.getAnimatedValue();postInvalidate();}});mLightWaveAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {mLightWaveOffset = 0;}@Overridepublic void onAnimationEnd(Animator animation) {}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});mLightWaveAnimator.start();}private void startDarkWaveAnimator() {if (mDarkWaveAnimator != null && mDarkWaveAnimator.isRunning()) {return;}mDarkWaveAnimator = ValueAnimator.ofFloat(0, 2 * mRadius);mDarkWaveAnimator.setDuration(mDarkWaveAnimTime);mDarkWaveAnimator.setRepeatCount(ValueAnimator.INFINITE);mDarkWaveAnimator.setInterpolator(new LinearInterpolator());mDarkWaveAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mDarkWaveOffset = (float) animation.getAnimatedValue();postInvalidate();}});mDarkWaveAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {mDarkWaveOffset = 0;}@Overridepublic void onAnimationEnd(Animator animation) {}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});mDarkWaveAnimator.start();}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();stopWaveAnimator();if (mProgressAnimator != null && mProgressAnimator.isRunning()) {mProgressAnimator.cancel();mProgressAnimator.removeAllUpdateListeners();mProgressAnimator = null;}}
}

上示意图:

图中黑色的圆为我们要绘制的进度条圆,黑色的曲线为初始状态的的波浪,该波浪使用贝塞尔曲线绘制,其中奇数的点为贝塞尔曲线的起始点,偶数的点为贝塞尔曲线的控制点。例如:1——>2——>3就为一条贝塞尔曲线,1 是起点,2 是控制点,3 是终点。从图中可以看到波浪在园内圆外各一个(1—>5 和 5->9),通过对波浪在 x 轴上做平移,即图中蓝色实线,来实现波浪的动态效果,所以一个波浪的完整动画效果需要有两个波浪来实现。同理,通过控制 y 轴的偏移量,即图中蓝色虚线,可以实现波浪随进度的上涨下降。
 
贝塞尔曲线上起始点和控制点的计算如下:

    /*** 计算贝塞尔曲线上的起始点和控制点* @param waveWidth 一个完整波浪的宽度*/private Point[] getPoint(float waveWidth) {Point[] points = new Point[mAllPointCount];//第1个点特殊处理,即数组的中心points[mHalfPointCount] = new Point((int) (mCenterPoint.x - mRadius), mCenterPoint.y);//屏幕内的贝塞尔曲线点for (int i = mHalfPointCount + 1; i < mAllPointCount; i += 4) {float width = points[mHalfPointCount].x + waveWidth * (i / 4 - mWaveNum);points[i] = new Point((int) (waveWidth / 4 + width), (int) (mCenterPoint.y - mWaveHeight));points[i + 1] = new Point((int) (waveWidth / 2 + width), mCenterPoint.y);points[i + 2] = new Point((int) (waveWidth * 3 / 4 + width), (int) (mCenterPoint.y + mWaveHeight));points[i + 3] = new Point((int) (waveWidth + width), mCenterPoint.y);}//屏幕外的贝塞尔曲线点for (int i = 0; i < mHalfPointCount; i++) {int reverse = mAllPointCount - i - 1;points[i] = new Point(points[mHalfPointCount].x - points[reverse].x,points[mHalfPointCount].y * 2 - points[reverse].y);}return points;}

以上,我们已经获取到绘制贝塞尔曲线所需的路径点。接下来,我们就需要来计算出绘制区域,即使用 Path 类。

紫色区域为贝塞尔曲线需要绘制的整体区域。

红色区域为上图紫色区域与圆的交集,也就是波浪要显示的区域
代码如下:

/该方法必须在 Android 19以上的版本才能使用(Path.op())@TargetApi(Build.VERSION_CODES.KITKAT)private void drawWave(Canvas canvas, Paint paint, Point[] points, float waveOffset) {mWaveLimitPath.reset();mWavePath.reset();//lockWave 用于判断波浪是否随进度条上涨下降float height = lockWave ? 0 : mRadius - 2 * mRadius * mPercent;//moveTo和lineTo绘制出水波区域矩形mWavePath.moveTo(points[0].x + waveOffset, points[0].y + height);for (int i = 1; i < mAllPointCount; i += 2) {mWavePath.quadTo(points[i].x + waveOffset, points[i].y + height,points[i + 1].x + waveOffset, points[i + 1].y + height);}mWavePath.lineTo(points[mAllPointCount - 1].x, points[mAllPointCount - 1].y + height);//不管如何移动,波浪与圆路径的交集底部永远固定,否则会造成上移的时候底部为空的情况mWavePath.lineTo(points[mAllPointCount - 1].x, mCenterPoint.y + mRadius);mWavePath.lineTo(points[0].x, mCenterPoint.y + mRadius);mWavePath.close();mWaveLimitPath.addCircle(mCenterPoint.x, mCenterPoint.y, mRadius, Path.Direction.CW);//取该圆与波浪路径的交集,形成波浪在圆内的效果mWaveLimitPath.op(mWavePath, Path.Op.INTERSECT);canvas.drawPath(mWaveLimitPath, paint);

对获取坐标点的代码进行优化之后则是WaveProgress.java代码中贴出的

**
* 从左往右或者从右往左获取贝塞尔点
* @return
*/
private Point[] getPoint(boolean isR2L, float waveWidth) {..........
.........
........return isR2L ? MiscUtil.reverse(points) : points;
}

建议:

建议大家分析代码时先画从左向右的深色水波(暂时将浅色水波逻辑注释掉),然后再画从右向左的浅色水波.
这样比较好分析.下图中,我将绘制深色水波时,计算处的贝塞尔曲线的各个点的坐标都标出来了.(浅色水波的分析方法一样)要注意,自定义view的坐标原点实在控件所处的父布局的左上角.

好了,至此,自定义带进度的圆环介绍完毕.
 
 
参考博文:Android 自定义 View 之圆形进度条总结

原博文代码已全部上传至 Git ,欢迎大家 Star 和 Fork,传送门:CircleProgress。
https://github.com/MyLifeMyTravel/CircleProgress

如果你想看在下添加了更多注解之后的源码,也可以移步下载:
https://download.csdn.net/download/zhangqunshuai/10492766

Android自定义View之画圆环(进阶篇:圆形进度条)相关推荐

  1. Android自定义View之画圆环(手把手教你如何一步步画圆环)

    关于自定义View: 好了,吐槽时间到.自定义view是Android开发知识体系中的重点,也是难点.好多小伙伴(也包括我)之前对自定义view也是似懂非懂.那种感觉老难受了.因此作为社会主义好青年, ...

  2. Android 自定义View实现环形带刻度颜色渐变的进度条

    上次写了一篇Android 自定义View实现环形带刻度的进度条,这篇文章就简单了,只是在原来的基础上加一个颜色渐变. 按照惯例,我们先来看看效果图 一.概述 1.相比于上篇文章,这里我们的颜色渐变主 ...

  3. 自定义view(二) Path绘画详解 圆形进度条

    目录 简介 基础api 圆形进度条 总结 简介 view的绘制可以由无数个形状组成,在canvas基础图形绘制中,我们已经把api提供好的基本图形讲过了.Path之所以单独一章出来是因为path可以由 ...

  4. android 自定义view 加载图片,Android自定义View基础开发之图片加载进度条

    学会了Paint,Canvas的基本用法之后,我们就可以动手开始实践了,先写个简单的图片加载进度条看看. 按照惯例,先看效果图,再决定要不要往下看: 既然看到这里了,应该是想了解这个图片加载进度条了, ...

  5. android插件数字,Android自定义控件实现带文本与数字的圆形进度条

    本文实例为大家分享了Android实现圆形进度条的具体代码,供大家参考,具体内容如下 实现的效果图如下所示: 第一步:绘制下方有缺口的空心圆,称为外围大弧吧 anvas.clipRect(0, 0, ...

  6. 微信小程序进度条组件自定义数字_微信小程序之圆形进度条(自定义组件)

    前言 昨天在微信小程序实现了圆形进度条,今天想把这个圆形进度条做成一个组件,方便以后直接拿来用. 根据官方文档自定义组件一步一步来 创建自定义组遇新是直朋能到件 第一步创建项遇新是直朋能到分览目结构 ...

  7. Android 自定义View实现画背景和前景(ViewGroup篇)

    2019独角兽企业重金招聘Python工程师标准>>> 在定义ListView的Selector时候,有个drawSelectorOnTop的属性,如果drawSelectorOnT ...

  8. android自定义壁纸制作,Android 自定义View实现画背景和前景(ViewGroup篇)

    在定义ListView的Selector时候,有个drawSelectorOnTop的属性,如果drawSelectorOnTop为true的话,Selector的效果是画在List Item的上面( ...

  9. Android自定义View如此简单 实现点击动画+进度刷新的提交/下载按钮(填坑面试题)

    SubmitButton 背景 实现思路 继承View 面试题:构造方法如何选择 自定义属性 面试题:styleable.AttributeSet.TypedArray的关系 测量宽高 面试题:UNS ...

最新文章

  1. 工具用途_机械加工中研磨加工刀具(砂轮)﹑治工具及其用途
  2. 麦司机博客项目技术选型-Java后端
  3. JavaWeb之Servlet学习-----实现文件动态下载功能 手写servlet 手动构建web程序
  4. 当面试官问我————Java是值传递还是引用传递?
  5. python教程龟叔_Python新手入门
  6. php方法参数,关于PHP方法参数的那一些事
  7. 专利挖掘和撰写(京东技术资质申请和创造专利挖掘)
  8. 用null_blk工具来实现模拟分区块设备
  9. 汉信码在iOS客户端中的应用和遇到的坑
  10. 体验部署ThinkAdmin
  11. CSS-margin外边距
  12. mysql添加中国省份城市sql语句
  13. CentOS 安装与配置
  14. mmo手游地图同步总结
  15. 5年开发经验的阿里巴巴Java程序员分享从业心得总结,帮助还在迷茫的朋友
  16. 安卓系统能运行 linux,重磅!安卓系统竟能运行PC软件,实测效果令人惊在当场!...
  17. winfomlabel 从右边_炒菜时,用左边的燃气灶还是右边的燃气灶?燃气师傅提醒,别弄错...
  18. 计算机毕业设计springboot社区志愿者管理系统的设计与实现【前后端分离·新项目】
  19. 套接字技术java_java网络编程之套接字TCP
  20. 智慧路灯地方标准:“江苏省城市照明智慧灯杆建设指南”发布实施

热门文章

  1. 微信公众号多媒体文件的处理
  2. 汽车云算力“竞速”,个性化进阶成新风向
  3. 交易接口 TradeX-M.lic
  4. android 仿阅读,发布一个练笔的 Android 阅读器,轻微仿91 Android 阅读器【后续将提供源码】...
  5. kvm与openvz等不同的虚拟化技术有什么区别
  6. python【第一篇】基础
  7. java并查集判断是否是连通图_并查集-判断图的连通
  8. com.ibm.mq.MQException: MQJE001: 完成代码为“2”,原因为“2495”。 no mqjbnd64 in java.library.path
  9. 调用MQ发生错误, MQJE001: 完成代码为“2”,原因为“2495”
  10. javaweb记账本系统