今天我们一起来实现一个带有缺口的自定义view进度条,带有心跳动画效果
看一下截图是什么效果
说到自定义view我们就需要实现onDraw() 方法, 去操作其携带的Canvas参数,通过drawXXX()系列方法来绘制出自己想要的布局样式。
怎样实现一个缺口向下同时带有心跳动画的进度条呢?
思路
1. 通过drawArc()方法绘制两个空心的扇型圆
第一个扇型是未加载的样式,第二个扇型是进度条样式
RectF rect = new RectF(left, top, right, bottom);
canvas.drawArc(rect, startPosition, endPosition, false, paint);
canvas.save(); // 保存一下。
//
paint.setColor(Color.parseColor(progressColor));
//paint.setDither(true);
if(!TextUtils.isEmpty(shadowColor))
paint.setShadowLayer(shadowSize,1,1,Color.parseColor(shadowColor));
canvas.drawArc(rect, startPosition, progress, false, paint);
canvas.save(); // 保存一下。
需要注意的是startPosition & endPosition 这两个参数是通过计算得来, 首先要了解canvas.drawArc这几个参数的意思:
第一个参数rect是确定位置,RectF的几个位置信息参数分别代表:
left= 最左边view视图边界到绘制画布最左边的距离;
top=最顶部view视图的边界到绘制画布最上边的距离;
right=最左边view视图的边界到绘制画布最右边的距离;
bottom=最顶部view视图的边界到绘制画布最底部的距离
第二个参数开始画扇形的角度, 这个角度是以3点钟方向为0角度,顺时针旋转360度再回到3点钟的方向。
第三个参数是结束扇形的角度。
第四个参数是否要画一条线与中心点相连,这个还是好理解的。
第五个参数就是画笔paint,专门用来设定一些画笔属性。
现在来计算缺口的位置,计算方式也很简单,假设我们的缺口大小是30度,并且缺口的位置是垂直向下的,我们需要先确定垂直向下的角度为90度,也就是6点钟方向(3点为起始位置,到6点方向为90度),这条线将我们需要展示缺口一分为二,左右两边各占15度角,那么开始扇形的位置的计算方式就是90.0f + (30/2)。 确定了开始画扇形的位置,就可以计算出结束扇形的角度位置了,缺口大小为30度,从105度的地方顺时针画360度就是一个完整的圆了, 当然我们不需要一个完整的圆, 那么我们用360度减去缺口大小,就是缺口结束的位置。
通过计算, 就可以提供下列方法,抛出设置缺口大小的方法。
/**
* 计算缺口位置
* @param gap
*/
private void setGapPosition(float gap){
startPosition = 90.0f + (gap/2);
endPosition = 360 - gap;
}
思路:通过改变圆的半径实现心跳动画
看onDraw()方法中,先确定圆的半径,这里(l+levelOffset)代表在动态计算圆的半径,要实现心跳动画,就必须要动态改变圆的半径。
float r = getMeasuredWidth() / (l+levelOffset)-5; //
(l+levelOffset)中的 “l” 是 圆的等级大小,最低等级是2,2是最大的圆,与布局一样大,等级越大,画的圆就越小:
// 圆的大小,默认是最小规格的
private int level = XXXXXX;
// XX = 控件 / 2 = 圆的半径
public static final int XX = 2;
// XXX = 控件 / 3 = 圆的半径
public static final int XXX = 3;
// XXXX = 控件 / 4 = 圆的半径
public static final int XXXX = 4;
// XXXXX = 控件 / 5 = 圆的半径
public static final int XXXXX = 5;
// XXXXXX = 控件 / 6 = 圆的半径
public static final int XXXXXX = 6;
// 修改画笔的宽度,进度条的粗细设置
levelOffset的计算方式:
/**
* 计算偏移量/ 动态控制圆的大小
*/
public void reconOffset(){
if(isStarAnimation){
if(levelOffset <= 0f || isAdd){
// 增加
isAdd = true;
levelOffset +=0.02f;
if(levelOffset>=0.3)isAdd=false;
}else if(levelOffset>=0f && !isAdd){
//减小
isAdd = false;
levelOffset -=0.02f;
}
}else{
levelOffset = 0;
}
}
以上就介绍了实现的思路与计算方式,关于reconOffset()方法,可以自己尝试修改一下数值, 跑一下,看是什么效果。
完整代码如下:
package com.gongjiebin.drawtest;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import java.text.DecimalFormat;
/**
* @author gongjiebin
*/
public class JJumpProgress extends View {
public JJumpProgress(Context context) {
super(context);
initView();
}
public JJumpProgress(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public JJumpProgress(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
public void initView(){
setGapPosition(this.gap);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 得到view的宽度模式
* 取值- MeasureSpec.UNSPECIFIED view的大小没有限制,可以是任意大小
* 取值- MeasureSpec.EXACTLY 当前的尺寸大小就是view的大小 相当于match_parent 如果是具体的值也是这个
* 取值- MeasureSpec.AT_MOST View能取的尺寸大小不能超过当前值大小 相当于wrap_content
*/
// int widthSize = getSize(100, widthMeasureSpec);
int heightSize = getSize(200, heightMeasureSpec);
int widthSize = getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec);
int heightSize = getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec);
// int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// Log.i("GJB", "widthSize=" + widthSize + " heightSize=" + heightSize);
// 填充大小
setMeasuredDimension(widthSize, heightSize);
}
/**
* 通过默认值和widthMeasureSpec/heightMeasureSpec获取控件的最终宽度
*
* @param defSize 默认值
*/
public int getSize(int defSize, int measureSpec) {
/**
* 得到控件大小。 确定模式
*/
int widthModel = MeasureSpec.getMode(measureSpec);
int size = defSize;
switch (widthModel) {
case MeasureSpec.UNSPECIFIED: //撑满布局。
// 没有指定大小。则使用默认大小
size = defSize;
break;
case MeasureSpec.EXACTLY:
size = MeasureSpec.getSize(measureSpec);
break;
case MeasureSpec.AT_MOST:// 内容包裹
// 指定大小了,返回指定的大小-固定值了也取当前view 的大小
size = MeasureSpec.getSize(measureSpec);
}
return size;
}
/**
* 进度条
*/
private float progress = 0f;
// 缺口结束位置
private float endPosition;
// 缺口开始位置
private float startPosition;
// 进度文字
private String progressTxt = "";
// 偏移量
private float offset = 5f;
// 缺口 。设置圆缺口的大小,默认30度
private float gap = 30;
// 进度条的颜色 #000000 - 十六进制表达
private String progressColor;
// 进度条的颜色 #000000 - 背景圆的颜色
private String bg_color;
// 阴影的颜色
private String shadowColor;
// 阴影的大小
private float shadowSize = 5;
// 字体颜色 -- 默认黑色的。
private String textColor = "#000000";
// 默认显示百分比 / false 不显示
private boolean isShowPercentage;
// 字体大小
private float textSize;
// 圆的大小,默认是最小规格的
private int level = XXXXXX;
// 等级动画偏移量
private float levelOffset=0;
// XX = 控件 / 2 = 圆的半径
public static final int XX = 2;
// XXX = 控件 / 3 = 圆的半径
public static final int XXX = 3;
// XXXX = 控件 / 4 = 圆的半径
public static final int XXXX = 4;
// XXXXX = 控件 / 5 = 圆的半径
public static final int XXXXX = 5;
// XXXXXX = 控件 / 6 = 圆的半径
public static final int XXXXXX = 6;
// 修改画笔的宽度,进度条的粗细设置
private float strokeWidth=10;
public void setStrokeWidth(float strokeWidth) {
this.strokeWidth = strokeWidth;
}
private boolean isStarAnimation;
public boolean isStarAnimation() {
return isStarAnimation;
}
public void setStarAnimation(boolean starAnimation) {
isStarAnimation = starAnimation;
}
public void setLevel(int level) {
this.level = level;
}
public void setProgressTxt(String progressTxt) {
this.progressTxt = progressTxt;
}
public int getLevel() {
return level;
}
public void setTextSize(int textSize) {
this.textSize = textSize;
}
public void setShowPercentage(boolean percentage) {
this.isShowPercentage = percentage;
}
public void setTextColor(String textColor) {
this.textColor = textColor;
}
public void setShadowSize(float shadowSize) {
this.shadowSize = shadowSize;
}
public void setShadowColor(String shadowColor) {
this.shadowColor = shadowColor;
}
public void setBg_color(String bg_color) {
this.bg_color = bg_color;
}
public void setProgressColor(String progressColor) {
// 关闭硬件加速
setLayerType(LAYER_TYPE_SOFTWARE, null);
this.progressColor = progressColor;
}
public void setGap(float gap) {
this.gap = gap;
setGapPosition(this.gap);
}
/**
* 切换圆的大小
* @param level
*/
public void changeLevel(int level){
this.setLevel(level);
// 切换的时候不要动画
this.setStarAnimation(false);
// 刷新Ui
invalidate();
}
/**
* 计算缺口位置
* @param gap
*/
private void setGapPosition(float gap){
startPosition = 90.0f + (gap/2);
endPosition = 360 - gap;
}
/**
* 设置每次的偏移量。。越小动画执行的越慢, 越大动画执行越快
* @param offset
*/
public void setOffset(float offset) {
this.offset = offset;
}
/**
* 设置进度
*
* @param progress 取值 : 0-100
*/
public void setProgress(final float progress) {
// 计算当前的值
this.progress = endPosition / 100 * progress;
//刷新view
invalidate();
}
/**
* 有动画效果的执行
*
* @param progress 需要到达的进度条位置
*/
public void animationSetProgress(float progress) {
// 换算成view能够认识的数值
float jd = progress * (endPosition / 100);
if(jd == 0){
setProgress(jd);
}else{
animationStart(jd, this.offset,progress);
}
}
/**
* @param progress 需要到达的进度条位置
*/
public void animationSetProgress(float progress,boolean isStarAnimation) {
this.setStarAnimation(isStarAnimation);
this.animationSetProgress(progress);
}
/**
* 更改文字
* @param offset
*/
public void setTextCenter(float progress,float offset,float thasProgress){
if(!isShowPercentage)return;
//Log.i("TAG",isShowPercentage+"");
float b = (offset / progress * thasProgress);
DecimalFormat df = new DecimalFormat("#.00");
String t = df.format(b);
progressTxt = t+"%";
}
/**
* @param progress 总进度
* @param offset 偏移量 每次偏移的角度
*/
private void animationStart(final float progress, float offset,float thasProgress) {
// 得到百分比
float tPro = (offset / progress * thasProgress);
if(offset == progress){
this.progress = offset;
setTextCenter(progress,offset,thasProgress);
invalidate();
isStarAnimation = false;
if(onChangeListener!=null)onChangeListener.onProgress(tPro);
return;
}
// 换算百分比。
if(offset >= progress){
// 修改参数
offset = progress;
this.progress = offset;
setTextCenter(progress,offset,thasProgress);
}else{
this.progress = offset;
setTextCenter(progress,offset,thasProgress);
//("GJB","offset" + offset +"progress =" +progress);
offset+=this.offset;
}
//
if(onChangeListener!=null)onChangeListener.onProgress(tPro);
invalidate();
Message message = new Message();
float[] sets = new float[]{progress,offset,thasProgress};
message.obj = sets;
handler.sendMessage(message);
}
Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
float set[] = (float[]) msg.obj;
animationStart(set[0],set[1],set[2]);
return false;
}
});
public boolean isAdd = true;
/**
* 计算偏移量/ 控制圆的大小
*/
public void reconOffset(){
if(isStarAnimation){
if(levelOffset <= 0f || isAdd){
// 增加
isAdd = true;
levelOffset +=0.02f;
if(levelOffset>=0.3)isAdd=false;
}else if(levelOffset>=0f && !isAdd){
//减小
isAdd = false;
levelOffset -=0.02f;
}
}else{
//不需要动画
levelOffset = 0;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float l = Float.valueOf(level);
// 计算半径的偏移大小
this.reconOffset();
// 计算圆的半径
float r = getMeasuredWidth() / (l+levelOffset)-5; //
// Log.i("GJB","r = "+r + " levelOffset" + levelOffset);
// x坐标 中间位置
int c_x = getLeft() + (getMeasuredWidth() / 2);
// y坐标 中间位置
int c_y = getTop() + (getMeasuredHeight() / 2);
// 创建画笔
Paint paint = new Paint();
// 画笔的颜色 -
paint.setColor(Color.parseColor(bg_color));
// **当绘图样式为的时候,setStrokeJoin这个方法用于指定线条连接拐角样式: 相当于圆角
//paint.setStrokeJoin(Paint.Join.ROUND);
// 画笔的形状 Paint.Cap.Round 圆 ,Cap.SQUARE 方
paint.setStrokeCap(Paint.Cap.ROUND);
//画笔样式。 Paint.Style.STROKE为一条线。Paint.Style.FILL是从起点开始。一直到终点为止,形成一扇形的绘制区。Paint.Style.FILL_AND_STROKE 为扇形区再加上一个圈
paint.setStyle(Paint.Style.STROKE); // 空心
paint.setStrokeWidth(strokeWidth);
paint.setAntiAlias(true); // 设置锯齿效果 取值true 代表
// 画一个红色的圆在空心的中间位置
float left = c_x - r;
float top = c_y - r;
float right = c_x + r;
float bottom = c_y + r;
// RectF - left计算的是从左边边界到扇形最左边的位置,
// RectF -right是计算的是从左边边界到扇形最右边的位置
// RectF -top 是计算的是从顶部边界到扇形最上边的位置
// RectF -bottom 是计算的是从顶部边界到扇形最下边的位置
RectF rect = new RectF(left, top, right, bottom);
canvas.drawArc(rect, startPosition, endPosition, false, paint);
canvas.save(); // 保存一下。
//
paint.setColor(Color.parseColor(progressColor));
//paint.setDither(true);
if(!TextUtils.isEmpty(shadowColor))
paint.setShadowLayer(shadowSize,1,1,Color.parseColor(shadowColor));
canvas.drawArc(rect, startPosition, progress, false, paint);
canvas.save(); // 保存一下。
if(!TextUtils.isEmpty(progressTxt)){
// 在圆心画一个文字。
Paint textPaint = new Paint();
// 画笔的颜色
textPaint.setColor(Color.parseColor(textColor));
textPaint.setAntiAlias(true); // 设置锯齿效果 取值true 代表
// 计算textView的字体大小随着布局的变化而变化
float thasSize = 0;
if(textSize == 0){
thasSize = r / 6;
}else{
thasSize = textSize;
}
// Log.i("TAG",progressTxt+" progressTxt");
textPaint.setTextSize(sp2px(thasSize));
// 字体加粗
textPaint.setFakeBoldText(true);
// 测量一下文字的宽度
float textSize = textPaint.measureText(progressTxt); // 测量文字的大小
float center = c_x - (textSize/2);
/**
* 其中 y 表示文字的基线(baseline )所在的坐标,说白了就是我们小学写字用的那种带有横线的本子(一般都是按照一条基线来写字是吧?),
* 用于规范你写的字是否成一条直线,否则很多人写着写着就往上飘了。而 x 坐标就是文字绘制的起始水平坐标,但是每个文字本身两侧都有一定的间隙,
* 故实际文字的位置会比 x 的位置再偏右侧一些。
*/
canvas.drawText(progressTxt,center,c_y,textPaint);
canvas.save();
}
}
private int sp2px(float spValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getResources().getDisplayMetrics());
}
private OnChangeListener onChangeListener;
public void setOnChangeListener(OnChangeListener onChangeListener) {
this.onChangeListener = onChangeListener;
}
public interface OnChangeListener{
/**
* 当前进度回调
* @param progress
*/
void onProgress(float progress);
}
}
MainActivity完整代码如下(使用方法)
package com.gongjiebin.drawtest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private JJumpProgress jumpProgress;
private Button btn_pull;
private Button btn_level;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewGroup rootView = (ViewGroup) LayoutInflater.from(getBaseContext()).inflate(R.layout.activity_main, null);
setContentView(rootView);
btn_pull = rootView.findViewById(R.id.btn_pull);
btn_level = rootView.findViewById(R.id.btn_level);
jumpProgress = rootView.findViewById(R.id.dv_jd);
jumpProgress.setGap(30f);// 设置缺口的大小。
jumpProgress.setOffset(5f); // 数值越大,动画效果越快
jumpProgress.setBg_color("#E9E9E9");
jumpProgress.setProgressColor("#FFDF2C32");
jumpProgress.setShadowColor("#FFDF2C32");
jumpProgress.setShadowSize(8);
jumpProgress.setShowPercentage(true);
// dv_jd.setTextSize(8);
jumpProgress.setTextColor("#FFDF2C32");
// dv_jd.setStrokeWidth(20);
btn_pull.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
jumpProgress.animationSetProgress(100f, true);
}
});
btn_level.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int level = jumpProgress.getLevel();
if (level == JJumpProgress.XX) {
level = JJumpProgress.XXX;
} else if (jumpProgress.getLevel() == JJumpProgress.XXX) {
level = JJumpProgress.XXXX;
} else if (jumpProgress.getLevel() == JJumpProgress.XXXX) {
level = JJumpProgress.XXXXX;
} else if (jumpProgress.getLevel() == JJumpProgress.XXXXX) {
level = JJumpProgress.XXXXXX;
} else if (jumpProgress.getLevel() == JJumpProgress.XXXXXX) {
level = 10;
} else if (jumpProgress.getLevel() == 10) {
level = 15;
} else if (jumpProgress.getLevel() == 15) {
level = JJumpProgress.XX;
}
// 切换进度条圆的大小
jumpProgress.changeLevel(level);
}
});
initListener();
}
public void initListener() {
jumpProgress.setOnChangeListener(new JJumpProgress.OnChangeListener() {
@Override
public void onProgress(float progress) {
// 在这里可以显示你想显示的文本, 如果你已经开启了默认打开百分比请先关闭。setShowPercentage(false)
// float price = progress / 100 * countPrice;
// Log.i("TAG", "text=" + price + "progress =" + progress);
// DecimalFormat df = new DecimalFormat("#.0");
// String t = df.format(price);
// dv_jd.setProgressTxt("¥" + t);
}
});
}
@Override
protected void onResume() {
super.onResume();
// 完成百分之90。
jumpProgress.animationSetProgress(90f, true);
}
@Override
protected void onPause() {
super.onPause();
}
}
XML中使用:
<com.gongjiebin.drawtest.JJumpProgress
android:id="@+id/dv_jd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
需要注意的是, 在使用时,可以看看代码中的注释,还是写的比较清楚的。 比如一些参数的设置,找到对应的set方法就可以啦。
//进度
private float progress = 0f;
// 缺口结束位置
private float endPosition;
// 缺口开始位置
private float startPosition;
// 进度文字
private String progressTxt = "";
// 偏移量
private float offset = 5f;
// 缺口 。设置圆缺口的大小,默认30度
private float gap = 30;
// 进度条的颜色 #000000 - 十六进制表达
private String progressColor;
// 背景圆的颜色 #000000 - 十六进制表达
private String bg_color;
// 阴影的颜色 #000000 - 十六进制表达
private String shadowColor;
// 阴影的大小
private float shadowSize = 5;
// 字体颜色 -- 默认黑色的。
private String textColor = "#000000";
// 默认显示百分比 / false 不显示
private boolean isShowPercentage;
// 字体大小
private float textSize;
// 圆的大小,默认是最小规格的
private int level = XXXXXX;
// 修改画笔的宽度,进度条的粗细设置
private float strokeWidth=10;
中间的文字默认按照百分比显示, 如果想显示其它文字,需要按照下列方式进行设置。
jumpProgress.setShowPercentage(false);
动态改变文字的监听方法
jumpProgress.setOnChangeListener(new JJumpProgress.OnChangeListener() {
@Override
public void onProgress(float progress) {
// 在这里可以显示你想显示的文本, 如果你已经开启了默认打开百分比请先关闭。setShowPercentage(false)
float price = progress / 100 * countPrice;
Log.i("TAG", "text=" + price + "progress =" + progress);
DecimalFormat df = new DecimalFormat("#.0");
String t = df.format(price);
jumpProgress.setProgressTxt("¥" + t);
}
});
说明
关于textSize的设置, 如果设置了这个参数,那字体的动画效果就会消失了。不设置会根据进度条的大小变化而变化。 这个view的动画效果适合已经知道百分比的情况下使用(类似查看今天的步数,展示统计数据类), 当需要实时更新数据的进度条并不适合这个动画,或者说效果不是很好,但我们可以调用setProgress完成更新,只是不会出现心跳动画了。
/**
* 设置进度
*
* @param progress 取值 : 0-100
*/
public void setProgress(final float progress) {
// 计算当前的值
this.progress = endPosition / 100 * progress;
//刷新view
invalidate();
}