温馨提示:这篇文章已超过440天没有更新,请注意相关的内容是否还可用!
摘要:,,本文介绍了如何在Android中自定义View,实现圆形进度条。通过自定义View,可以灵活控制进度条的样式、颜色、大小等属性,以满足不同需求。圆形进度条作为一种常见的UI元素,可以用于展示任务进度、加载状态等。本文详细阐述了自定义View的实现过程,包括绘制圆形进度条的关键步骤和注意事项。
目录
- 前言
- 1.基础
- 2.绘制底层圆形
- 3.绘制上层弧形
- 4.绘制文本
- 5.添加动画
- 6.调整位置、宽高
- 附代码
- CircleProgressBar.java
- 布局文件
- Activity 调用
- 7.自定义属性 attr
- 7.1 创建 res/values/attrs.xml
- 7.2 使用 TypedArray 获取 attrs
- 7.3 在 xml 中初始化 attr
- 7.4 效果
- 附代码V2
- CircleProgressBar.java
- 布局文件
- Activity 调用
- attrs
- 8.进度条按照进度变色
- 参考资料:
前言
很多场景下都用到这种进度条,有的还带动画效果,
今天我也来写一个。
写之前先拆解下它的组成:
- 底层圆形
- 上层弧形
- 中间文字
那我们要做的就是:
- 绘制底层圆形;
- 在同位置绘制上层弧形,但颜色不同;
- 在中心点绘制文本,显示进度。
按照这个目标,学习下自定义View的流程。
(本文用到 Canvas 相关的知识点,不熟悉的可以参考文末参考资料。)
1.基础
新建一个类,继承 View ,重写构造函数,如,
package com.test.luodemo.customerview; import android.content.Context; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; public class CircleProgressBar extends View { public CircleProgressBar(Context context) { super(context); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
在 xml 中使用,LinearLayout 加了背景颜色,方便看出所在位置。
此时运行,是没效果的,因为这个View还没有绘制,啥也没有。
2.绘制底层圆形
初始化3个图形的画笔 ,底层圆形和上层弧形的画笔宽度一致、颜色不一致,方便区分
重写 onDraw(Canvas canvas) 方法,用 canvas.drawCircle 绘制底层圆形,
package com.test.luodemo.customerview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; public class CircleProgressBar extends View { private Paint paintCircleBottom = new Paint(); private Paint paintArcTop = new Paint(); private Paint paintText = new Paint(); public CircleProgressBar(Context context) { super(context); init(); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ //初始化文本的画笔 paintText.setFlags(Paint.ANTI_ALIAS_FLAG); paintText.setColor(Color.BLACK); paintText.setTextAlign(Paint.Align.CENTER); paintText.setTextSize(80f); //初始化底层圆形的画笔 paintCircleBottom.setFlags(Paint.ANTI_ALIAS_FLAG); paintCircleBottom.setColor(Color.LTGRAY); paintCircleBottom.setStrokeWidth(10f); paintCircleBottom.setStrokeCap(Paint.Cap.ROUND); paintCircleBottom.setStyle(Paint.Style.STROKE); //初始化弧形的画笔 paintArcTop.setFlags(Paint.ANTI_ALIAS_FLAG); paintArcTop.setColor(Color.MAGENTA); paintArcTop.setStrokeWidth(10f); paintArcTop.setStrokeCap(Paint.Cap.ROUND); paintArcTop.setStyle(Paint.Style.STROKE); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制底层圆形 canvas.drawCircle(300, 300, 200, paintCircleBottom); } }
效果,
3.绘制上层弧形
在之前的基础上绘制上层弧形,弧形的中心和圆心一致。
用 canvas.drawArc 绘制弧形。这里直接指定绘制的角度是 90° ,后续会动态指定。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制底层圆形 canvas.drawCircle( 300, 300, 200, paintCircleBottom); //绘制上层弧形,从顶部开始,顺时针走90° _angle = 90; canvas.drawArc(100,100,500,500,270, _angle,false, paintArcTop); }
效果,
4.绘制文本
用 canvas.drawText 绘制文本,
使用 DecimalFormat 格式化输入,保留小数点后两位,如果小数点后两位都是0则不显示小数点后两位。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制底层圆形 canvas.drawCircle(300, 300, 200, paintCircleBottom); //绘制上层弧形,从顶部开始,顺时针走90° _angle = 90; canvas.drawArc(100,100,500,500,270, _angle,false, paintArcTop); //绘制文本 DecimalFormat dt = new DecimalFormat("0.##"); canvas.drawText(dt.format(100 * _angle/360)+"%", 300 , 300, paintText); }
效果,
可以看到,文本虽然居中,但是文本是显示在中心线上,
期望结果是文本的水平中心线和圆心重合,改为,
//绘制文本,文字中心和圆心保持一致 Paint.FontMetrics fontMetrics = paintText.getFontMetrics(); float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom; float baseline= 300 + distance; canvas.drawText(dt.format(100 * _angle/360)+"%", 300, baseline, paintText);//文字中心和圆心一致
效果,符合预期。
5.添加动画
创建一个设置进度的接口,供外部调用。
使用 ValueAnimator ,监听动画过程,然后逐渐刷新角度值。使用 AccelerateInterpolator 插值器,动画速度开始慢、逐渐加速。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制底层圆形 canvas.drawCircle(300, 300, 200, paintCircleBottom); //绘制上层弧形,从顶部开始,顺时针走90° canvas.drawArc(100,100,500,500,270, _angle,false, paintArcTop); //绘制文本,文字中心和圆心保持一致 DecimalFormat dt = new DecimalFormat("0.##"); Paint.FontMetrics fontMetrics = paintText.getFontMetrics(); float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom; float baseline= 300 + distance; canvas.drawText(dt.format(100 * _angle/360)+"%", 300, baseline, paintText);//文字中心和圆心一致 } /** * 设置进度,展现动画 * */ public void setProgress(int progress){ ValueAnimator animator = ValueAnimator.ofFloat(0,100f); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float cur = (float) animation.getAnimatedValue(); _angle = cur/100 * 360 * progress/100; invalidate(); //刷新 View } }); animator.setDuration(3000); animator.setInterpolator(new AccelerateInterpolator()); animator.start(); }
注意要去掉 3.绘制上层弧形 中固定90°的逻辑。
外部调用,
CircleProgressBar mCircleProgressBar1 = (CircleProgressBar) findViewById(R.id.circle_progress_bar1); mCircleProgressBar1.setProgress((int) (100 * Math.random()));
随机生成一个 0.0 - 0.1 的数值,乘以 100 设置为进度。
效果,
可以看到动画效果, 虽然 git 丢帧了 ~ 。
6.调整位置、宽高
前文我是设定了 View 宽高都是 300dp ,并且绘制图形是随意指定的坐标。
实际开发时,不可能用这些值,所以要优化下绘制的逻辑。
实际使用时,可能宽度高度一样,宽度大于高度 ,宽度小于高度,
采用这个逻辑:
- 取宽度、高度的最小值,作为圆的直径,除以 2 得到半径。
- 对角线交汇点作为圆心。
简言之,以对角线为圆心画最大内切圆。
重写 onMeasure 方法,重绘 View 的宽高,这部分参考《Android 开发艺术探索》,
private int DEFAULT_WIDTH = 100;//默认宽度 private int DEFAULT_HEIGHT = 100;//默认宽度 private int DEFAULT_RADIUS = 50;//默认半径 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } else if (widthMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, heightMeasureSpec); } else if (heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthMeasureSpec, DEFAULT_HEIGHT); } }
修改 onDraw 绘制逻辑 ,
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 圆心坐标是(centerX,centerY) int centerX = getWidth()/2; int centerY = getHeight()/2; //确定半径 float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth(); //绘制底层圆形 canvas.drawCircle(centerX, centerY, radius, paintCircleBottom); //绘制上层弧形,从顶部开始,顺时针走 _angle canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop); //绘制文本,文字中心和圆心保持一致 Paint.FontMetrics fontMetrics = paintText.getFontMetrics(); float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom; float baseline= centerY + distance; canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致 }
分别写了 3 个布局,布局依次是 宽度等于高度 、宽度大宇高度、宽度小于高度,效果,
至此,基本是一个还可以的版本了。
附代码
贴下当前代码,
CircleProgressBar.java
package com.test.luodemo.customerview; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.AccelerateInterpolator; import androidx.annotation.Nullable; import java.text.DecimalFormat; public class CircleProgressBar extends View { private Paint paintCircleBottom = new Paint(); private Paint paintArcTop = new Paint(); private Paint paintText = new Paint(); private int DEFAULT_WIDTH = 100;//默认宽度 private int DEFAULT_HEIGHT = 100;//默认宽度 private int DEFAULT_RADIUS = 50;//默认半径 private float _angle;//弧形的角度 public CircleProgressBar(Context context) { super(context); init(); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ //初始化文本的画笔 paintText.setFlags(Paint.ANTI_ALIAS_FLAG); paintText.setColor(Color.BLACK); paintText.setTextAlign(Paint.Align.CENTER); paintText.setTextSize(80f); //初始化底层圆形的画笔 paintCircleBottom.setFlags(Paint.ANTI_ALIAS_FLAG); paintCircleBottom.setColor(Color.LTGRAY); paintCircleBottom.setStrokeWidth(10f); paintCircleBottom.setStrokeCap(Paint.Cap.ROUND); paintCircleBottom.setStyle(Paint.Style.STROKE); //初始化弧形的画笔 paintArcTop.setFlags(Paint.ANTI_ALIAS_FLAG); paintArcTop.setColor(Color.MAGENTA); paintArcTop.setStrokeWidth(10f); paintArcTop.setStrokeCap(Paint.Cap.ROUND); paintArcTop.setStyle(Paint.Style.STROKE); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } else if (widthMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, heightMeasureSpec); } else if (heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthMeasureSpec, DEFAULT_HEIGHT); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 圆心坐标是(centerX,centerY) int centerX = getWidth()/2; int centerY = getHeight()/2; //确定半径 float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth(); //绘制底层圆形 canvas.drawCircle(centerX, centerY, radius, paintCircleBottom); //绘制上层弧形,从顶部开始,顺时针走90° canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop); //绘制文本,文字中心和圆心保持一致 DecimalFormat dt = new DecimalFormat("0.##"); Paint.FontMetrics fontMetrics = paintText.getFontMetrics(); float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom; float baseline= centerY + distance; canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致 } /** * 设置进度,展现动画 * */ public void setProgress(int progress){ ValueAnimator animator = ValueAnimator.ofFloat(0,100f); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float cur = (float) animation.getAnimatedValue(); _angle = cur/100 * 360 * progress/100; invalidate(); } }); animator.setDuration(3000); animator.setInterpolator(new AccelerateInterpolator()); animator.start(); } }
布局文件
Activity 调用
public class CircleProgressBarActivity extends AppCompatActivity { private CircleProgressBar mCircleProgressBar1 , mCircleProgressBar2 , mCircleProgressBar3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_circle_progress_bar); Objects.requireNonNull(getSupportActionBar()).setTitle("CircleProgressBarActivity"); mCircleProgressBar1 = (CircleProgressBar) findViewById(R.id.circle_progress_bar1); mCircleProgressBar2 = (CircleProgressBar) findViewById(R.id.circle_progress_bar2); mCircleProgressBar3 = (CircleProgressBar) findViewById(R.id.circle_progress_bar3); } public void onCPBButtonClick(View view) { switch (view.getId()) { case R.id.button_cpb1: mCircleProgressBar1.setProgress((int) (100 * Math.random())); break; case R.id.button_cpb2: mCircleProgressBar2.setProgress((int) (100 * Math.random())); break; case R.id.button_cpb3: mCircleProgressBar3.setProgress((int) (100 * Math.random())); break; case R.id.button_cpb_all: mCircleProgressBar1.setProgress((int) (100 * Math.random())); mCircleProgressBar2.setProgress((int) (100 * Math.random())); mCircleProgressBar3.setProgress((int) (100 * Math.random())); break; default: break; } } }
7.自定义属性 attr
需求是不停的,会有这些需求:可指定画笔(宽度、颜色等)、可指定动画时长等。
这些可以通过在自定义的View中创建 Java 接口来设置,但我要学自定义View,就要用 attr 。
7.1 创建 res/values/attrs.xml
如果已有就不用创建,直接用就行了。
写入如下内容,
中 CircleProgressBar 就是自定义 View 的名字,要保持一致。
不一致AS会报黄,
By convention, the custom view (CircleProgressBar) and the declare-styleable (CircleProgressBar111) should have the same name (various editor features rely on this convention)
是 CircleProgressBar 的属性,可指定类型
类型 说明 boolean 布尔类型,true 或 false color 颜色值,如 @android:color/white dimension dp 值,如 20dp enum 枚举 flags 位或运算,如 app:cus_view_gravity=“top|right” fraction 百分比,如 30% float float 型 integer int 型 reference 引用资源,如 @drawable/pic string 字符串 7.2 使用 TypedArray 获取 attrs
在构造函数中,通过 TypedArray 获取自定义的属性。基本逻辑就是有设置 attr 就用设置的值,没有就用默认值。
使用后一定要调用 TypedArray.recycle();
public CircleProgressBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar); textColor = typedArray.getColor(R.styleable.CircleProgressBar_textColor, Color.BLACK); textSize = typedArray.getFloat(R.styleable.CircleProgressBar_textSize, 80f); circleColor = typedArray.getColor(R.styleable.CircleProgressBar_circleColor, Color.LTGRAY); circleWidth = typedArray.getFloat(R.styleable.CircleProgressBar_circleWidth, 10f); arcColor = typedArray.getColor(R.styleable.CircleProgressBar_arcColor, Color.MAGENTA); arcWidth = typedArray.getFloat(R.styleable.CircleProgressBar_arcWidth, 10f); progress = typedArray.getInt(R.styleable.CircleProgressBar_initProgress, 0); typedArray.recycle(); init(); }
有两个带 AttributeSet 参数的构造函数,
- public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {}
- public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {}
为什么用前面这个? 因为我们是在 xml 中定义的 CircleProgressBar 。参考源码说明,
/** * Constructor that is called when inflating a view from XML. This is called * when a view is being constructed from an XML file, supplying attributes * that were specified in the XML file. This version uses a default style of * 0, so the only attribute values applied are those in the Context's Theme * and the given AttributeSet. * *
* The method onFinishInflate() will be called after all children have been * added. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @see #View(Context, AttributeSet, int) */ public View(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } /** * Perform inflation from XML and apply a class-specific base style from a * theme attribute. This constructor of View allows subclasses to use their * own base style when they are inflating. For example, a Button class's * constructor would call this version of the super class constructor and * supply R.attr.buttonStyle for defStyleAttr; this * allows the theme's button style to modify all of the base view attributes * (in particular its background) as well as the Button class's attributes. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. * @param attrs The attributes of the XML tag that is inflating the view. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. * @see #View(Context, AttributeSet) */ public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); }
7.3 在 xml 中初始化 attr
xml 关键代码如下,
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".customerview.CircleProgressBarActivity" app:circleColor="@android:color/white" app:circleWidth="30" app:arcColor="@color/my_red" app:arcWidth="15" app:textColor="@android:color/holo_orange_dark" app:initProgress="30" /
注释2处就是初始化 attr ,以 app: 开头是对应注释1处。
7.4 效果
左一是自定义 attr 的效果,左二、左三是没有自定义 attr 的效果。
差异有:底层圆形的颜色、画笔大小;上层弧形的颜色、画笔大小、开始的角度;中间文字的颜色。
说明自定义 attr 起效了。
附代码V2
CircleProgressBar.java
package com.test.luodemo.customerview; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.AccelerateInterpolator; import androidx.annotation.Nullable; import com.test.luodemo.R; import java.text.DecimalFormat; public class CircleProgressBar extends View { private Paint paintCircleBottom = new Paint(); private Paint paintArcTop = new Paint(); private Paint paintText = new Paint(); private int DEFAULT_WIDTH = 100;//默认宽度 private int DEFAULT_HEIGHT = 100;//默认宽度 private int DEFAULT_RADIUS = 50;//默认半径 private float _angle;//弧形的角度 /***************************** attr *******************************/ int textColor; float textSize; int circleColor ; int arcColor; float circleWidth; float arcWidth; int progress; /***************************** attr *******************************/ public CircleProgressBar(Context context) { super(context); init(); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar); textColor = typedArray.getColor(R.styleable.CircleProgressBar_textColor, Color.BLACK); textSize = typedArray.getFloat(R.styleable.CircleProgressBar_textSize, 80f); circleColor = typedArray.getColor(R.styleable.CircleProgressBar_circleColor, Color.LTGRAY); circleWidth = typedArray.getFloat(R.styleable.CircleProgressBar_circleWidth, 10f); arcColor = typedArray.getColor(R.styleable.CircleProgressBar_arcColor, Color.MAGENTA); arcWidth = typedArray.getFloat(R.styleable.CircleProgressBar_arcWidth, 10f); progress = typedArray.getInt(R.styleable.CircleProgressBar_initProgress, 0); typedArray.recycle(); init(); } public CircleProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ //初始化文本的画笔 paintText.setFlags(Paint.ANTI_ALIAS_FLAG); paintText.setStyle(Paint.Style.FILL); paintText.setColor(textColor);//设置自定义属性值 paintText.setTextAlign(Paint.Align.CENTER); paintText.setTextSize(textSize); //初始化底层圆形的画笔 paintCircleBottom.setFlags(Paint.ANTI_ALIAS_FLAG); paintCircleBottom.setStrokeCap(Paint.Cap.ROUND); paintCircleBottom.setStyle(Paint.Style.STROKE); paintCircleBottom.setColor(circleColor);//设置自定义属性值 paintCircleBottom.setStrokeWidth(circleWidth);//设置自定义属性值 //初始化弧形的画笔 paintArcTop.setFlags(Paint.ANTI_ALIAS_FLAG); paintArcTop.setStrokeCap(Paint.Cap.ROUND); paintArcTop.setStyle(Paint.Style.STROKE); paintArcTop.setColor(arcColor);//设置自定义属性值 paintArcTop.setStrokeWidth(arcWidth);//设置自定义属性值 _angle = progress; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, DEFAULT_HEIGHT); } else if (widthMode == MeasureSpec.AT_MOST) { setMeasuredDimension(DEFAULT_WIDTH, heightMeasureSpec); } else if (heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthMeasureSpec, DEFAULT_HEIGHT); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 圆心坐标是(centerX,centerY) int centerX = getWidth()/2; int centerY = getHeight()/2; //确定半径 float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth(); //绘制底层圆形 canvas.drawCircle(centerX, centerY, radius, paintCircleBottom); //绘制上层弧形,从顶部开始,顺时针走90° canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop); //绘制文本,文字中心和圆心保持一致 DecimalFormat dt = new DecimalFormat("0.##"); Paint.FontMetrics fontMetrics = paintText.getFontMetrics(); float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom; float baseline= centerY + distance; canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致 } /** * 设置进度,展现动画 * */ public void setProgress(int progress){ ValueAnimator animator = ValueAnimator.ofFloat(0,100f); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float cur = (float) animation.getAnimatedValue(); _angle = cur/100 * 360 * progress/100; invalidate(); } }); animator.setDuration(3000); animator.setInterpolator(new AccelerateInterpolator()); animator.start(); } }
布局文件
Activity 调用
和之前一样。
attrs
8.进度条按照进度变色
继续整,进度条按照的进度,从红色渐变为黄色再渐变为绿色。
渐变就考虑用 Shader ,它专注于颜色渐变。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 圆心坐标是(centerX,centerY) int centerX = getWidth()/2; int centerY = getHeight()/2; //确定半径 float radius = Math.min(centerX, centerY) - paintCircleBottom.getStrokeWidth(); //绘制底层圆形 canvas.drawCircle(centerX, centerY, radius, paintCircleBottom); //注释1 canvas.save();//保存画布状态 canvas.rotate(-90, centerX, centerY);//以 (centerX, centerY) 为中心,逆时针旋转 90° //注释2 SweepGradient sweepGradient = new SweepGradient(centerX,centerY,new int[]{Color.RED,Color.YELLOW, Color.GREEN},null); paintArcTop.setShader(sweepGradient);// paintArcTop.setStrokeCap(Paint.Cap.BUTT);//设置画笔边缘为切面,默认是半圆状 //绘制上层弧形,从顶部开始,顺时针走90° // canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,270, _angle,false, paintArcTop); canvas.drawArc(centerX - radius,centerY-radius,centerX + radius,centerY + radius,0, _angle,false, paintArcTop);//注释3 canvas.restore();//注释4 //绘制文本,文字中心和圆心保持一致 DecimalFormat dt = new DecimalFormat("0.##"); Paint.FontMetrics fontMetrics = paintText.getFontMetrics(); float distance =(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom; float baseline= centerY + distance; canvas.drawText(dt.format(100 * _angle/360)+"%", centerX, baseline, paintText);//文字中心和圆心一致 }
注释1处:保存画布状态,已绘制的图形正常显示;以 (centerX, centerY) 为中心,逆时针旋转 90° ,此操作修改了画布的坐标,修改后 x轴和 y 轴的情况是这样的,
注释2处:创建 SweepGradient (梯度渐变,也称之为扫描式渐变,其效果有点类似雷达的扫描效果,从 3点钟方向顺时针旋转回到 3 点钟方向),画笔设置 Shader 为 SweepGradient ,设置 Shader 后画笔颜色就失效了,以 Shader 为准;画笔边缘设置为切边状(默认是半圆状),不设置的话颜色交汇处会有重叠。
注释3处:因为画布坐标系变了,所以 drawArc() 方法中的 startAngle 参数变了。
注释4处:还原对画布的修改,本例就是还原旋转画布的操作,坐标系恢复为原始状态。
效果如图,gif 失真看颜色不清晰了 =.=|
参考资料:
Android属性动画深入分析:让你成为动画牛人_singwhatiwanna的博客-CSDN博客
Android Canvas的使用_南国樗里疾的博客-CSDN博客
Android Canvas的drawText()和文字居中方案 - 简书
自定义控件其实很简单1/3_AigeStudio的博客-CSDN博客
关于Android Paint.Cap枚举和Paint.Join枚举的使用_5hand的博客-CSDN博客
还没有评论,来说两句吧...