github上有很多弧形或者圆形的ProgressBar和SeekBar。前几天无意中发现一个弧形的ProgressBar觉得挺不错的,就下载来看看源码。ColorArcProgressBar
地址是这个:https://github.com/Shinelw/ColorArcProgressBar
运行他的Demo的时候发现几个问题,并且有些问题在issue里面也有人提出了,但是作者一直没有回复,我把问题修复之后加上了Seek的功能,让它能当做SeekBar用。
原作者的Demo效果图
存在的问题:
- 不能通过XML配置控件的大小,源码里面写死了,写成占屏幕的百分比。
- 有几个属性的颜色值设置无效。比如默认的弧形背景色
- 放大缩小控件之后对应的字体没有相应的调整大小
需要考虑的问题点:
我们从最下面带刻度带提示的效果图入手
####我们呢先分析原作者的代码:
package com.shinelw.colorarcprogressbar;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;
/**
* colorful arc progress bar
* Created by shinelw on 12/4/15.
*/
public class ColorArcProgressBar extends View{
private int mWidth;
private int mHeight;
//直径
private int diameter = 500;
private RectF bgRect;
//圆心
private float centerX;
private float centerY;
private Paint allArcPaint;
private Paint progressPaint;
private Paint vTextPaint;
private Paint hintPaint;
private Paint degreePaint;
private Paint curSpeedPaint;
private float startAngle = 135;
private float sweepAngle = 270;
private float currentAngle = 0;
private float lastAngle;
private int[] colors = {Color.GREEN, Color.YELLOW, Color.RED, Color.RED};
private ValueAnimator progressAnimator;
private float maxValues = 60;
private float curValues = 0;
private int bgArcWidth = dipToPx(2);
private int progressWidth = dipToPx(10);
private int textSize = dipToPx(80);
private int hintSize = dipToPx(22);
private int curSpeedSize = dipToPx(13);
private int aniSpeed = 1000;
private int longdegree = dipToPx(13);
private int shortdegree = dipToPx(5);
private final int DEGREE_PROGRESS_DISTANCE = dipToPx(8);
private String hintColor = "#676767";
private String longDegreeColor = "#d2d2d2";
private String shortDegreeColor = "#adadad";
private String bgArcColor = "#111111";
private boolean isShowCurrentSpeed = true;
private String hintString = "Km/h";
// sweepAngle / maxValues 的值
private float k;
public ColorArcProgressBar(Context context) {
super(context);
initView();
}
public ColorArcProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public ColorArcProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 2 * longdegree + progressWidth + diameter + 2 * DEGREE_PROGRESS_DISTANCE;
int height= 2 * longdegree + progressWidth + diameter + 2 * DEGREE_PROGRESS_DISTANCE;
setMeasuredDimension(width, height);
}
private void initView() {
diameter = 3 * getScreenWidth() / 5;
//弧形的矩阵区域
bgRect = new RectF();
bgRect.top = longdegree + progressWidth/2 + DEGREE_PROGRESS_DISTANCE;
bgRect.left = longdegree + progressWidth/2 + DEGREE_PROGRESS_DISTANCE;
bgRect.right = diameter + (longdegree + progressWidth/2 + DEGREE_PROGRESS_DISTANCE);
bgRect.bottom = diameter + (longdegree + progressWidth/2 + DEGREE_PROGRESS_DISTANCE);
//圆心
centerX = (2 * longdegree + progressWidth + diameter + 2 * DEGREE_PROGRESS_DISTANCE)/2;
centerY = (2 * longdegree + progressWidth + diameter + 2 * DEGREE_PROGRESS_DISTANCE)/2;
//外部刻度线
degreePaint = new Paint();
degreePaint.setColor(Color.parseColor(longDegreeColor));
//整个弧形
allArcPaint = new Paint();
allArcPaint.setAntiAlias(true);
allArcPaint.setStyle(Paint.Style.STROKE);
allArcPaint.setStrokeWidth(bgArcWidth);
allArcPaint.setColor(Color.parseColor(bgArcColor));
allArcPaint.setStrokeCap(Paint.Cap.ROUND);
//当前进度的弧形
progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeCap(Paint.Cap.ROUND);
progressPaint.setStrokeWidth(progressWidth);
progressPaint.setColor(Color.GREEN);
//当前速度显示文字
vTextPaint = new Paint();
vTextPaint.setTextSize(textSize);
vTextPaint.setColor(Color.WHITE);
vTextPaint.setTextAlign(Paint.Align.CENTER);
//显示“km/h”文字
hintPaint = new Paint();
hintPaint.setTextSize(hintSize);
hintPaint.setColor(Color.parseColor(hintColor));
hintPaint.setTextAlign(Paint.Align.CENTER);
//显示“km/h”文字
curSpeedPaint = new Paint();
curSpeedPaint.setTextSize(curSpeedSize);
curSpeedPaint.setColor(Color.parseColor(hintColor));
curSpeedPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
//抗锯齿
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG));
//画刻度线
for (int i = 0; i < 40; i++) {
if (i > 15 && i < 25) {
canvas.rotate(9, centerX, centerY);
continue;
}
if (i%5 == 0) {
degreePaint.setStrokeWidth(dipToPx(2));
degreePaint.setColor(Color.parseColor(longDegreeColor));
canvas.drawLine(centerX, centerY - diameter/2 - progressWidth/2 - DEGREE_PROGRESS_DISTANCE, centerX, centerY - diameter/2 - progressWidth/2 - DEGREE_PROGRESS_DISTANCE - longdegree, degreePaint);
}else {
degreePaint.setStrokeWidth(dipToPx(1.4f));
degreePaint.setColor(Color.parseColor(shortDegreeColor));
canvas.drawLine(centerX, centerY - diameter/2 - progressWidth/2 - DEGREE_PROGRESS_DISTANCE - (longdegree - shortdegree)/2, centerX, centerY - diameter/2 - progressWidth/2 - DEGREE_PROGRESS_DISTANCE - (longdegree - shortdegree)/2 - shortdegree, degreePaint);
}
canvas.rotate(9, centerX, centerY);
}
//整个弧
canvas.drawArc(bgRect,startAngle, sweepAngle, false, allArcPaint);
//设置渐变色
SweepGradient sweepGradient = new SweepGradient(centerX, centerY, colors, null);
Matrix matrix = new Matrix();
matrix.setRotate(130, centerX, centerY);
sweepGradient.setLocalMatrix(matrix);
progressPaint.setShader(sweepGradient);
//当前进度
canvas.drawArc(bgRect, startAngle, currentAngle, false, progressPaint);
if (isShowCurrentSpeed) {
canvas.drawText(String.format("%.1f",curValues) , centerX, centerY + textSize / 3, vTextPaint);
}
canvas.drawText(hintString,centerX, centerY + 2*textSize/3, hintPaint);
canvas.drawText("CURRENT SPEED",centerX, centerY - 2*textSize/3, curSpeedPaint);
invalidate();
}
/**
* 设置最大值
* @param maxValues
*/
public void setMaxValues(float maxValues) {
this.maxValues = maxValues;
k = sweepAngle/maxValues;
}
/**
* 设置当前值
* @param currentValues
*/
public void setCurrentValues(float currentValues) {
if (currentValues > maxValues) {
currentValues = maxValues;
}
if (currentValues < 0) {
currentValues = 0;
}
this.curValues = currentValues;
lastAngle = currentAngle;
setAnimation(lastAngle, currentValues * k, aniSpeed);
}
/**
* 设置整个圆弧宽度
* @param bgArcWidth
*/
public void setBgArcWidth(int bgArcWidth) {
this.bgArcWidth = bgArcWidth;
}
/**
* 设置进度宽度
* @param progressWidth
*/
public void setProgressWidth(int progressWidth) {
this.progressWidth = progressWidth;
}
/**
* 设置速度文字大小
* @param textSize
*/
public void setTextSize(int textSize) {
this.textSize = textSize;
}
/**
* 设置单位文字大小
* @param hintSize
*/
public void setHintSize(int hintSize) {
this.hintSize = hintSize;
}
/**
* 设置单位文字
* @param hintString
*/
public void setUnit(String hintString) {
this.hintString = hintString;
invalidate();
}
/**
* 设置直径大小
* @param diameter
*/
public void setDiameter(int diameter) {
this.diameter = dipToPx(diameter);
}
public void setAniSpeed() {
}
/**
* 为进度设置动画
* @param last
* @param current
*/
private void setAnimation(float last, float current, int length) {
progressAnimator = ValueAnimator.ofFloat(last, current);
progressAnimator.setDuration(length);
progressAnimator.setTarget(currentAngle);
progressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentAngle= (float) animation.getAnimatedValue();
}
});
progressAnimator.start();
}
/**
* dip 转换成px
* @param dip
* @return
*/
private int dipToPx(float dip) {
float density = getContext().getResources().getDisplayMetrics().density;
return (int)(dip * density + 0.5f * (dip >= 0 ? 1 : -1));
}
/**
* 得到屏幕宽度
* @return
*/
private int getScreenWidth() {
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.widthPixels;
}
public void setIsShowCurrentSpeed(boolean isShowCurrentSpeed) {
this.isShowCurrentSpeed = isShowCurrentSpeed;
}
/**
* 初始加载页面时设置加载动画
*/
public void setDefaultWithAnimator() {
setAnimation(sweepAngle, currentAngle, 2000);
}
}
从以上代码可以看出:
在initView 的时候设置了弧形的直径diameter = 3 * getScreenWidth() / 5;也就是弧形的直径是根据屏幕的大小定的。
我们再看OnMeasure方法@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = 2 * longdegree + progressWidth + diameter + 2 * DEGREE_PROGRESS_DISTANCE; int height= 2 * longdegree + progressWidth + diameter + 2 * DEGREE_PROGRESS_DISTANCE; setMeasuredDimension(width, height); }
所以我们知道:不管你怎么设置控件的宽高,都没有效果。这是第一个问题
设置某些颜色值无效的问题:
颜色值的设置是通常作用于画笔上,我们先看看几个画笔:private Paint allArcPaint;//背景弧形的画笔(圆弧) private Paint progressPaint;//进度画笔(当前速度) private Paint vTextPaint;//内容文字画笔(80) private Paint hintPaint;//单位文字画笔(km/h) private Paint degreePaint;//刻度画笔(刻度条) private Paint curSpeedPaint;//标题文字画笔(当前速度)
这些画笔的颜色值在initView的时候初始化,但是颜色值并没有在initConfig的时候取出属性值并且配置,所以这是导致设置颜色无效的原因
注意:给画笔设置的颜色值必须带有透明度,不然会自动带上全透明效果。看不到任何信息
- 调整控件大小的时候圆弧内的文字没有做出大小和位置的调整
在源代码中,initView的时候就已经设置TextSize了,而initView是在实例化的时候调用的,这时候控件的大小都还没有确定,所以需要把和大小相关的值都放到OnSizeChange中,或者在OnMeasure中获取控件的实际大小之后再初始化这些值
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
Log.v("ColorArcProgressBar", "onSizeChanged: mWidth:" + mWidth + " mHeight:" + mHeight);
diameter = (int) (Math.min(mWidth, mHeight) - 2 * (longDegree + DEGREE_PROGRESS_DISTANCE + progressWidth / 2));
Log.v("ColorArcProgressBar", "onSizeChanged: diameter:" + diameter);
//弧形的矩阵区域
bgRect = new RectF();
bgRect.top = longDegree + DEGREE_PROGRESS_DISTANCE + progressWidth / 2;
bgRect.left = longDegree + DEGREE_PROGRESS_DISTANCE + progressWidth / 2;
bgRect.right = diameter + (longDegree + progressWidth / 2 + DEGREE_PROGRESS_DISTANCE);
bgRect.bottom = diameter + (longDegree + progressWidth / 2 + DEGREE_PROGRESS_DISTANCE);
Log.v("ColorArcProgressBar", "initView: " + diameter);
//圆心
centerX = (2 * (longDegree + DEGREE_PROGRESS_DISTANCE + progressWidth / 2) + diameter) / 2;
centerY = (2 * (longDegree + DEGREE_PROGRESS_DISTANCE + progressWidth / 2) + diameter) / 2;
sweepGradient = new SweepGradient(centerX, centerY, colors, null);
mTouchInvalidateRadius = Math.max(mWidth, mHeight) / 2 - longDegree - DEGREE_PROGRESS_DISTANCE - progressWidth * 2;
if(isAutoTextSize)
{
textSize = (float) (diameter * 0.3);
hintSize = (float) (diameter * 0.1);
curSpeedSize = (float) (diameter * 0.1);
vTextPaint.setTextSize(textSize);
hintPaint.setTextSize(hintSize);
curSpeedPaint.setTextSize(curSpeedSize);
}
}
通过在OnSizeChange中初始化这些动态的尺寸,就解决了文字缩放的问题。
增加拖动功能
1. 定义Seek接口
public interface OnSeekArcChangeListener
{
void onProgressChanged(ColorArcProgressBar seekArc, int progress, boolean fromUser);
void onStartTrackingTouch(ColorArcProgressBar seekArc);
void onStopTrackingTouch(ColorArcProgressBar seekArc);
}
2.重写onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event)
{
if(seekEnable)
{
this.getParent().requestDisallowInterceptTouchEvent(true);//一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
onStartTrackingTouch();
updateOnTouch(event);
break;
case MotionEvent.ACTION_MOVE:
updateOnTouch(event);
break;
case MotionEvent.ACTION_UP:
onStopTrackingTouch();
setPressed(false);
this.getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_CANCEL:
onStopTrackingTouch();
setPressed(false);
this.getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return true;
}
return false;
}
3.处理触摸事件
private void updateOnTouch(MotionEvent event)
{
boolean validateTouch = validateTouch(event.getX(), event.getY());//滑动的位置是否合法
if(!validateTouch)
{
return;
}
setPressed(true);
double mTouchAngle = getTouchDegrees(event.getX(), event.getY());
int progress = angleToProgress(mTouchAngle);
Log.v("ColorArcProgressBar", "updateOnTouch: " + progress);
onProgressRefresh(progress, true);
}
/**
* 判断触摸是否有效
*
* @param xPos 触摸点x坐标
* @param yPos 触摸点y坐标
* @return is validate touch
*/
private boolean validateTouch(float xPos, float yPos)
{
boolean validate = false;
float x = xPos - centerX;//触摸点X坐标与圆心X坐标的距离
float y = yPos - centerY;//触摸点Y坐标与圆心Y坐标的距离
float touchRadius = (float) Math.sqrt(((x * x) + (y * y)));//触摸半径
//toDegrees是弧度转换成角度
double angle = Math.toDegrees(Math.atan2(y, x) + (Math.PI / 2) - Math.toRadians(225));
if(angle < 0)
{
angle = 360 + angle;
}
// mTouchInvalidateRadius = Math.max(mWidth, mHeight) / 2 - longDegree - DEGREE_PROGRESS_DISTANCE - progressWidth * 2;这个有效触摸半径在OnSizeChange方法计算,我们让可以触摸的范围是从控件的最边缘到弧形的再往内靠一个弧形宽度的距离,有效增大触摸的范围
//我们的弧形的弧度是270°,适当增加了最大值的触摸角度,增加触摸的灵敏度
if(touchRadius > mTouchInvalidateRadius && (angle >= 0 && angle <= 280))//其实角度小于270就够了,但是弧度换成角度是不精确的,所以需要适当放大范围,不然有时候滑动不到最大值
{
validate = true;
}
Log.v("ColorArcProgressBar", "validateTouch: " + angle);
return validate;
}
private double getTouchDegrees(float xPos, float yPos)
{
float x = xPos - centerX;
float y = yPos - centerY;
// Math.toDegrees convert to arc Angle
//Math.atan2(y, x)以弧度为单位计算并返回点 y /x 的角度,该角度从圆的 x 轴(0 点在其上,0 表示圆心)沿逆时针方向测量。返回值介于正 pi 和负 pi 之间。
//触摸点与圆心的夹角- Math.toRadians(225)是因为我们希望0°从圆弧的起点开始,默认角度从穿过圆心的X轴开始
double angle = Math.toDegrees(Math.atan2(y, x) + (Math.PI / 2) - Math.toRadians(225));
if(angle < 0)
{
angle = 360 + angle;
}
Log.v("ColorArcProgressBar", "getTouchDegrees: " + angle);
// angle -= mStartAngle;
return angle;
}
private int angleToProgress(double angle)
{
int progress = (int) Math.round(valuePerDegree() * angle);
progress = (progress < 0) ? 0 : progress;
progress = (progress > maxValues) ? (int) maxValues : progress;
return progress;
}
private float valuePerDegree()
{
return maxValues / sweepAngle;
}
private void onProgressRefresh(int progress, boolean fromUser)
{
updateProgress(progress, fromUser);
}
private void updateProgress(int progress, boolean fromUser)
{
currentValues = progress;
if(listener != null)
{
listener.onProgressChanged(this, progress, fromUser);
}
currentAngle = (float) progress / maxValues * sweepAngle;//计算划过当前的角度
lastAngle = currentAngle;
invalidate();
}
最终效果图: