时间:2019 年 01 月 14 日 ~ 2019 年 01 月 18 日
一、自定义水平进度条
效果截图:
public class LineProgress extends View {
private Paint mOriginalPaint; // 进度条中未拖动部分的画笔
private Paint mProgressPaint; // 进度条中已拖动部分的画笔
private Paint mCircleDragPaint; // 拖动圆画笔
private Paint mProgressTipPaint; // 进度条进度提示文字画笔
private int mMaxProgress = 100; // 进度条最大进度
private int mCurProgress; // 进度条已拖动进度值
private int mProgressDefaultColor = Color.parseColor("#F0F0F0"); // 进度条中未拖动部分的颜色
private int mProgressDragColor = Color.parseColor("#0DE6C2"); // 进度条中已拖动部分的颜色
private int mCircleRadius = dp2px(12); // 拖动圆半径
private int mCircleDragColor = Color.RED; // 拖动圆颜色
private int mWidth; // 进度条宽度
private int mHeight; // 进度条高度
private int mPaddingLeft; // 进度条左 Padding
private int mPaddingRight; // 进度条右 Padding
private float ratio; // 当前进度和最大进度的比值
private int mTouchX; // 手指在进度条上点击的横坐标
private int circle_x; // 拖动圆的圆心横坐标
private boolean isTouch; // 当前是否在触摸进度条
public LineProgress(Context context) {
this(context, null);
}
public LineProgress(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LineProgress(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineProgress);
mProgressDefaultColor = ta.getColor(R.styleable.LineProgress_progress_default_color, mProgressDefaultColor);
mProgressDragColor = ta.getColor(R.styleable.LineProgress_progress_drag_color, mProgressDragColor);
mCircleDragColor = ta.getColor(R.styleable.LineProgress_progress_circle_drag_color, mCircleDragColor);
mMaxProgress = ta.getInt(R.styleable.LineProgress_progress_max_value, mMaxProgress);
ta.recycle();
// 初始化画笔
initPaint();
}
/**
* 初始化画笔
*/
private void initPaint() {
// 初始化进度条背景画笔
mOriginalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mOriginalPaint.setColor(mProgressDefaultColor);
mOriginalPaint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充且描边
mOriginalPaint.setStrokeCap(Paint.Cap.ROUND); // 圆头
mOriginalPaint.setStrokeWidth(dp2px(3));
// 初始化进度条画笔
mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mProgressPaint.setColor(mProgressDragColor);
mProgressPaint.setStyle(Paint.Style.FILL_AND_STROKE); // 填充且描边
mProgressPaint.setStrokeCap(Paint.Cap.ROUND); // 圆头
mProgressPaint.setStrokeWidth(dp2px(3));
// 拖动圆点画笔
mCircleDragPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCircleDragPaint.setColor(mCircleDragColor);
mCircleDragPaint.setStyle(Paint.Style.FILL);
// 进度条进度提示画笔
mProgressTipPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mProgressTipPaint.setColor(mCircleDragColor);
mProgressTipPaint.setTextSize(sp2px(12));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int wm = MeasureSpec.getMode(widthMeasureSpec);
int ws = MeasureSpec.getSize(widthMeasureSpec);
// 高度模式
int mode = MeasureSpec.getMode(heightMeasureSpec);
// 高度大小
int size = MeasureSpec.getSize(heightMeasureSpec);
int hs;
if (mode == MeasureSpec.EXACTLY) {
hs = size;
} else { // 没有指定具体值
Rect bounds = new Rect();
mProgressTipPaint.getTextBounds(mCurProgress + "", 0, (mCurProgress + "").length(), bounds);
int dy = (bounds.bottom - bounds.top) / 2 - bounds.bottom;
// 高度设置为能放下进度文字提示即可,根据文字大小去准确计算高度
hs = mCircleRadius * 2 + bounds.height() * 2 + dy;
}
if (wm == MeasureSpec.AT_MOST) {
ws = 200;
}
setMeasuredDimension(ws, hs);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 获取宽高
mWidth = getWidth();
mHeight = getHeight();
// 获取左右 Padding 值
mPaddingLeft = getPaddingLeft();
mPaddingRight = getPaddingRight();
// 修正拖动点圆心和左右 Padding 的大小关系
if (mPaddingLeft != mCircleRadius) {
mPaddingLeft = mCircleRadius;
}
if (mPaddingRight != mCircleRadius) {
mPaddingRight = mCircleRadius;
}
}
@Override
protected void onDraw(Canvas canvas) {
if (null != mOnProgressListener) {
mOnProgressListener.onDrag(mCurProgress);
}
// 画背景线
canvas.drawLine(mPaddingLeft, mHeight - mCircleRadius, mWidth - mPaddingRight, mHeight - mCircleRadius, mOriginalPaint);
// 画进度条(当 ratio 为 0 时,代表初始状态,即为拖动,此时拖动颜色结束点的 X 坐标应该向右移动 mPaddingLeft 的距离)
int progress_x = (int) (mWidth * ratio);
if (mWidth - progress_x < mCircleRadius) {
progress_x = mWidth - mPaddingRight;
}
canvas.drawLine(mPaddingLeft, mHeight - mCircleRadius, progress_x, mHeight - mCircleRadius, mProgressPaint);
if (isTouch) {
if (mTouchX < mCircleRadius) {
circle_x = mCircleRadius;
} else if (mWidth - mTouchX < mCircleRadius) {
circle_x = mWidth - mCircleRadius;
} else {
circle_x = (int) (mWidth * ratio);
}
} else {
int circle_dx = 0;
if (ratio == 0) {
circle_dx = mPaddingLeft;
}
circle_x = (int) (mWidth * ratio) + circle_dx;
}
// 画拖动圆
canvas.drawCircle(circle_x, mHeight - mCircleRadius, mCircleRadius, mCircleDragPaint);
// 获取进度提示文字
String progressStr = mCurProgress + "";
Rect bounds = new Rect();
mProgressTipPaint.getTextBounds(progressStr, 0, progressStr.length(), bounds);
// 获取进度提示文字宽度
float progressStrWidth = bounds.width();
// 获取进度提示文字起始位置横坐标
float strX = getProgressStrX(progressStrWidth);
int dy = (bounds.bottom - bounds.top) / 2 - bounds.bottom;
// 画进度提示文字
canvas.drawText(progressStr, strX, mHeight - 2 * mCircleRadius - bounds.height() - dy, mProgressTipPaint);
}
private float getProgressStrX(float progressStrWidth) {
// 获取进度提示文字起始位置(效果设置为提示文字关于拖动圆的圆心横坐标左右对称)
float progressStrX = circle_x - progressStrWidth / 2;
// 进度提示文字横坐标 < 进度提示文字宽度 / 2
if (progressStrX < progressStrWidth / 2) {
// 进度提示文字横坐标:拖动圆半径 /2
progressStrX = mCircleRadius / 2;
} else if (mWidth - circle_x < progressStrWidth) { // 进度条宽度 - 拖动圆的圆心横坐标 < 进度提示文字宽度
// 进度提示文字横坐标:进度条宽度 -文字宽度 - 拖动圆半径 / 2
progressStrX = mWidth - progressStrWidth - mCircleRadius / 2;
}
return progressStrX;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int touchX = (int) event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// 设置正在触摸
isTouch = true;
// 避免触摸越界
if (touchX < 0) {
touchX = 0;
} else if (touchX > mWidth) {
touchX = mWidth;
}
// 上次的触摸点 mTouchX 和当前触摸点 touchX 不是同一点(避免触摸点重复,导致界面多次绘制)
if (mTouchX != touchX) {
mTouchX = touchX;
// 根据手指触摸横坐标设置进度值
updateTouchProgress();
invalidate();
}
break;
case MotionEvent.ACTION_UP:
isTouch = false;
break;
}
return true;
}
/**
* 根据手指触摸横坐标设置进度值
*/
private void updateTouchProgress() {
mCurProgress = (int) (mTouchX * 1f / mWidth * mMaxProgress + 0.5f);
ratio = mTouchX * 1f / mWidth;
invalidate();
}
/**
* 设置进度
*/
public void setProgress(int progress) {
if (progress < 0 || progress > mMaxProgress) {
return;
}
mCurProgress = progress;
// 这里必须使用 1f 转为小数,否则 progress < mMaxProgress 时,progress / mMaxProgress 始终为 0
ratio = mCurProgress * 1f / mMaxProgress;
postInvalidate();
}
/**
* 获取最大进度
*/
public int getMaxProgress() {
return mMaxProgress;
}
private OnProgressListener mOnProgressListener;
/**
* 设置进度回调监听
*/
public void setOnProgressListener(OnProgressListener onProgressListener) {
this.mOnProgressListener = onProgressListener;
}
public interface OnProgressListener {
// 进度回调
void onDrag(int progress);
}
/**
* dp 转 px
*/
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
/**
* sp 转 px
*/
private float sp2px(int sp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
}
attrs:
<declare-styleable name="LineProgress">
<!--进度条中未拖动部分的颜色-->
<attr name="progress_default_color" format="color" />
<!--进度条中已拖动部分的颜色-->
<attr name="progress_drag_color" format="color" />
<!--拖动圆颜色-->
<attr name="progress_circle_drag_color" format="color" />
<!--最大进度-->
<attr name="progress_max_value" format="integer" />
</declare-styleable>
二、自定义圆环拖动进度条
效果截图:
发现的 BUG:在布局文件中设置宽或高为 match_parent
、wrap_content
时,拖动会有问题
public class RingSeekBar extends View {
private static final int DEFAULT_RING_WIDTH = 30; // 默认宽度 dp
private static final int DEFAULT_ROTATE_ANGLE = -90; // 默认旋转角度设置为 -90°(即拖动按钮初始显示在 CircleSeekBar 最顶部)
private static final int DEFAULT_BORDER_WIDTH = 0; // 默认描边宽度
private static final int DEFAULT_BORDER_COLOR = 0xffffffff; // 默认描边颜色
private static final int DEFAULT_THUMB_RADIUS = 15; // 默认拖动按钮半径 dp
private static final int DEFAULT_THUMB_WIDTH = 2; // 默认拖动按钮描边宽度 dp
private static final int DEFAULT_THUMB_COLOR = 0xffffffff; // 默认拖动按钮颜色
private static final int DEFAULT_EDGE_LENGTH = 260; // 默认宽高
private static final int DEFAULT_PROGRESS_COLOR = 0xffD46526; // 默认进度条颜
private static final int DEFAULT_MAX_VALUE = 100; // 默认最大进度值
private static final int DEFAULT_MIN_VALUE = 0; // 默认最小进度值
private static final int DEFAULT_TEXT_SIZE = 12; // 默认文字大小
private static final int DEFAULT_TEXT_COLOR = Color.BLACK; // 默认文字颜色
private static final int DEFAULT_THUMB_CIRCLE = 0; // 拖动按钮为实心圆
private static final int DEFAULT_THUMB_RING = 1; // 拖动按钮为圆环
private int[] mRingColors; // CircleSeekBar 颜色
private float mRingWidth; // CircleSeekBar 宽度
private float mRotateAngle; // 当前旋转的角度(有新的旋转角度后就把当前值赋给 mLastAngle,避免旋转角度连续不变,导致界面重复绘制,即多次调用)
private float mLastAngle; // 最后一次保存的旋转角度
private final int mBorderWidth; // 描边宽度
private final int mBorderColor; // 描边颜色
private final int mThumbRadius; // 拖动按钮半径
private final int mThumbBorderWidth; // 拖动按钮描边宽度
private final int mThumbStyle; // 拖动按钮样式
private final int mThumbColor; // 拖动按钮颜色
private final int mProgressColor; // 进度条颜色
private Paint mRingPaint; // CircleSeekBar 画笔
private Paint mProgressPaint; // 进度画笔
private Paint mThumbPaint; // 拖动按钮画笔
private Paint mBorderPaint; // 描边画笔
private Paint mTextPaint; // 中间文字画笔
private int minValidateTouchRingRadius; // 最小有效点击半径
private int maxValidateTouchRingRadius; // 最大有效点击半径
private int mMaxValue; // 最大值
private int mMinValue; // 最小值
/**
* @see OnProgressChangeListener#onProgressChanged(RingSeekBar seekBar, int progress, boolean isUser)
*/
private int mCurProgress; // 当前进度(有新的进度后就把当前值赋给 mLastProgress,避免经由旋转角度转换而来的进度值连续不变,导致重复回调进度监听)
private int mLastProgress; // 最后一次保存的进度值
private final int mTextSize; // 中间大小
private final int mTextColor; // 中间文字颜色
private boolean mTextBold = false; // 中间文字是否加粗,默认不加粗
private float centerX; // 中心坐标 X
private float centerY; // 中心坐标 Y
private float radius; // 半径
private boolean downOnArc; // 手指触摸点是否在圆弧上(大于 minValidateTouchArcRadius,小于 maxValidateTouchArcRadius 为 true,否则为 false)
int dy; // 中间文字居中时,文字需要向下移动的偏移量
/******************************三个构造函数*****************************/
public RingSeekBar(Context context) {
this(context, null);
}
public RingSeekBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RingSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 获取自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RingSeekBar);
mRingColors = getArcColors(context, ta);
mRingWidth = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_width, dp2px(DEFAULT_RING_WIDTH));
mRotateAngle = ta.getInt(R.styleable.RingSeekBar_ring_rotate_angle, 0);
mBorderWidth = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_border_width, dp2px(DEFAULT_BORDER_WIDTH));
mBorderColor = ta.getColor(R.styleable.RingSeekBar_ring_border_color, DEFAULT_BORDER_COLOR);
mProgressColor = ta.getColor(R.styleable.RingSeekBar_ring_progress_color, DEFAULT_PROGRESS_COLOR);
mThumbStyle = ta.getInt(R.styleable.RingSeekBar_ring_thumb_style, DEFAULT_THUMB_CIRCLE);
mThumbColor = ta.getColor(R.styleable.RingSeekBar_ring_thumb_color, DEFAULT_THUMB_COLOR);
mThumbRadius = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_thumb_radius, dp2px(DEFAULT_THUMB_RADIUS));
mThumbBorderWidth = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_thumb_border_width, dp2px(DEFAULT_THUMB_WIDTH));
mMaxValue = ta.getInt(R.styleable.RingSeekBar_ring_max_value, DEFAULT_MAX_VALUE);
mMinValue = ta.getInt(R.styleable.RingSeekBar_ring_min_value, DEFAULT_MIN_VALUE);
updateMaxAndMinValue(); // 校正最大进度值、最小进度值
mTextSize = ta.getDimensionPixelSize(R.styleable.RingSeekBar_ring_text_size, DEFAULT_TEXT_SIZE);
mTextColor = ta.getColor(R.styleable.RingSeekBar_ring_text_color, DEFAULT_TEXT_COLOR);
mTextBold = ta.getBoolean(R.styleable.RingSeekBar_ring_text_bold, mTextBold);
ta.recycle();
// 初始化画笔
initPaint();
}
/**
* 初始化画笔
*/
private void initPaint() {
// ArcSeekBar 画笔
mRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRingPaint.setStrokeWidth(mRingWidth);
mRingPaint.setStyle(Paint.Style.STROKE);
mRingPaint.setStrokeCap(Paint.Cap.ROUND);
// 拖动按钮画笔
mThumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mThumbPaint.setStrokeWidth(mThumbBorderWidth);
mThumbPaint.setColor(mThumbColor);
if (mThumbStyle == DEFAULT_THUMB_CIRCLE) {
mThumbPaint.setStyle(Paint.Style.FILL);
} else if (mThumbStyle == DEFAULT_THUMB_RING) {
mThumbPaint.setStyle(Paint.Style.STROKE);
}
// 进度画笔
mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mProgressPaint.setStrokeWidth(mRingWidth);
mProgressPaint.setColor(mProgressColor);
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
// 描边画笔
mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBorderPaint.setStrokeWidth(mBorderWidth);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStyle(Paint.Style.STROKE);
// 中间文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(sp2px(mTextSize));
mTextPaint.setColor(mTextColor);
mTextPaint.setFakeBoldText(mTextBold);
}
/**
* 测量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int ws = MeasureSpec.getSize(widthMeasureSpec); //取出宽度的确切数值
int wm = MeasureSpec.getMode(widthMeasureSpec); //取出宽度的测量模式
int hs = MeasureSpec.getSize(heightMeasureSpec); //取出高度的确切数值
int hm = MeasureSpec.getMode(heightMeasureSpec); //取出高度的测量模式
if (wm == MeasureSpec.UNSPECIFIED) {
ws = dp2px(DEFAULT_EDGE_LENGTH);
} else if (wm == MeasureSpec.AT_MOST) {
ws = Math.min(dp2px(DEFAULT_EDGE_LENGTH), ws);
}
if (hm == MeasureSpec.UNSPECIFIED) {
hs = dp2px(DEFAULT_EDGE_LENGTH);
} else if (hm == MeasureSpec.AT_MOST) {
hs = Math.min(dp2px(DEFAULT_EDGE_LENGTH), hs);
}
setMeasuredDimension(ws, hs);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
centerX = w / 2;
centerY = h / 2;
radius = Math.min(centerX, centerY);
minValidateTouchRingRadius = (int) (radius * 0.7f);
maxValidateTouchRingRadius = (int) radius;
resetShaderColor();
}
/**
* 绘制
*/
@Override
protected void onDraw(Canvas canvas) {
// 画外圆
canvas.drawCircle(centerX, centerY, radius - mRingWidth / 2 - mBorderWidth - mThumbBorderWidth / 2, mRingPaint);
canvas.drawCircle(centerX, centerY, radius - mBorderWidth / 2 - mThumbBorderWidth / 2, mBorderPaint);
if (mRotateAngle >= 0) {
// 画进度
RectF oval = new RectF(centerX - (radius - mRingWidth / 2) + mBorderWidth + mThumbBorderWidth / 2, centerY - (radius - mRingWidth / 2) + mBorderWidth + mThumbBorderWidth / 2, centerX + (radius - mRingWidth / 2) - mBorderWidth - mThumbBorderWidth / 2, centerY + (radius - mRingWidth / 2) - mBorderWidth - mThumbBorderWidth / 2);
canvas.drawArc(oval, -90, mRotateAngle, false, mProgressPaint);
// 画点
PointF startPoint = calcArcEndPointXY(centerX, centerY, radius - mRingWidth / 2 - mBorderWidth - mThumbBorderWidth / 2, mRotateAngle, DEFAULT_ROTATE_ANGLE);
canvas.drawCircle(startPoint.x, startPoint.y, mRingWidth / 2, mThumbPaint);
}
String curProgressStr = mCurProgress + mMinValue + "";
float curProgressStrWidth = mTextPaint.measureText(curProgressStr);
if (dy == 0) {
Rect bounds = new Rect();
mTextPaint.getTextBounds(curProgressStr, 0, curProgressStr.length(), bounds);
dy = (bounds.bottom - bounds.top) / 2 - bounds.bottom;
}
canvas.drawText(curProgressStr, centerX - curProgressStrWidth / 2, centerY + dy, mTextPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
int x = (int) event.getX();
int y = (int) event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (isTouchArc(x, y)) {
downOnArc = true;
updateArc(x, y);
if (null != mOnProgressChangeListener) {
mOnProgressChangeListener.onStartTrackingTouch(this);
mOnProgressChangeListener.onProgressChanged(this, mCurProgress, true);
}
}
break;
case MotionEvent.ACTION_MOVE:
if (downOnArc) {
updateArc(x, y);
if (mLastAngle != mRotateAngle) {
if (null != mOnProgressChangeListener) {
if (mLastProgress != mCurProgress) {
mOnProgressChangeListener.onProgressChanged(this, mCurProgress, true);
mLastProgress = mCurProgress;
}
}
mLastAngle = mRotateAngle;
}
}
break;
case MotionEvent.ACTION_UP:
downOnArc = false;
if (null != mOnProgressChangeListener) {
mOnProgressChangeListener.onStopTrackingTouch(this);
}
invalidate();
break;
}
return true;
}
/**
* 校正最大进度值、最小进度值
*/
private void updateMaxAndMinValue() {
// 最大进度值小于等于最小进度值
if (mMaxValue <= mMinValue) {
// 最大进度值赋值为默认最大进度值 100
mMaxValue = DEFAULT_MAX_VALUE;
// 最小进度值赋值为默认最小进度值 0
mMinValue = DEFAULT_MIN_VALUE;
}
}
/**
* 根据点 (x, y) 的位置,更新中间文字进度
*/
private void updateArc(int x, int y) {
if (Math.abs(mRotateAngle - getAngle(x, y)) < 180) {
mRotateAngle = (int) getAngle(x, y);
}
if (mRotateAngle != mLastAngle) {
updateText(mRotateAngle);
invalidate();
}
}
/**
* 计算指定位置 (px, py) 与内容区域中心点(圆点)的夹角
*/
private float getAngle(float px, float py) {
float angle = (float) ((Math.atan2(py - centerY, px - centerY)) * 180 / 3.14f);
if (angle < DEFAULT_ROTATE_ANGLE) {
angle += 360;
}
return angle - DEFAULT_ROTATE_ANGLE + 0.5f;
}
/**
* 判断手指触摸点 (x, y) 是否按在圆环上(大于 minValidateTouchArcRadius,小于 maxValidateTouchArcRadius 为 true,否则为 false)
*/
private boolean isTouchArc(int x, int y) {
double d = getTouchRadius(x, y);
boolean flag = false;
if (d >= minValidateTouchRingRadius && d <= maxValidateTouchRingRadius) {
flag = true;
}
return flag;
}
/**
* 计算手指触摸点 (x, y) 到内容区域中心点(圆点)的距离
*/
private double getTouchRadius(int x, int y) {
int cx = (int) (x - centerX);
int cy = (int) (y - centerY);
double v = Math.hypot(cx, cy);
return v;
}
/**
* 依圆心坐标、半径、扇形角度,计算出扇形终射线与圆弧交叉点的 XY 坐标
*
* @param cirX 圆 centerX
* @param cirY 圆 centerY
* @param radius 圆半径
* @param cirAngle 当前滑过的弧对应的角度
* @param originalAngle 起点弧角度
* @return 扇形终射线与圆弧交叉点的 XY 坐标
*/
public static PointF calcArcEndPointXY(float cirX, float cirY, float radius, float cirAngle, float originalAngle) {
float posX;
float posY;
cirAngle = (originalAngle + cirAngle) % 360;
//将角度转换为弧度
float arcAngle = (float) (Math.PI * cirAngle / 180.0);
if (cirAngle < 90) {
posX = cirX + (float) (Math.cos(arcAngle)) * radius;
posY = cirY + (float) (Math.sin(arcAngle)) * radius;
} else if (cirAngle == 90) {
posX = cirX;
posY = cirY + radius;
} else if (cirAngle > 90 && cirAngle < 180) {
arcAngle = (float) (Math.PI * (180 - cirAngle) / 180.0);
posX = cirX - (float) (Math.cos(arcAngle)) * radius;
posY = cirY + (float) (Math.sin(arcAngle)) * radius;
} else if (cirAngle == 180) {
posX = cirX - radius;
posY = cirY;
} else if (cirAngle > 180 && cirAngle < 270) {
arcAngle = (float) (Math.PI * (cirAngle - 180) / 180.0);
posX = cirX - (float) (Math.cos(arcAngle)) * radius;
posY = cirY - (float) (Math.sin(arcAngle)) * radius;
} else if (cirAngle == 270) {
posX = cirX;
posY = cirY - radius;
} else {
arcAngle = (float) (Math.PI * (360 - cirAngle) / 180.0);
posX = cirX + (float) (Math.cos(arcAngle)) * radius;
posY = cirY - (float) (Math.sin(arcAngle)) * radius;
}
return new PointF(posX, posY);
}
/*****************************设置圆环背景渐变*****************************/
/**
* 获取 ArcSeekBar 颜色数组
*/
private int[] getArcColors(Context context, TypedArray ta) {
int[] ret;
int resId = ta.getResourceId(R.styleable.RingSeekBar_ring_colors, 0);
if (0 == resId) {
resId = R.array.arc_colors_default;
}
ret = getColorsByArrayResId(context, resId);
return ret;
}
/**
* 根据 resId 获取颜色数组
*/
private int[] getColorsByArrayResId(Context context, int resId) {
int[] ret;
TypedArray colorArray = context.getResources().obtainTypedArray(resId);
ret = new int[colorArray.length()];
for (int i = 0; i < colorArray.length(); i++) {
ret[i] = colorArray.getColor(i, 0);
}
colorArray.recycle();
return ret;
}
/**
* 重置 Shader 颜色
*/
private void resetShaderColor() {
// 计算渐变数组
float startPos = (mRotateAngle / 2) / 360;
float stopPos = (360 - (mRotateAngle / 2)) / 360;
int len = mRingColors.length - 1;
float distance = (stopPos - startPos) / len;
float pos[] = new float[mRingColors.length];
for (int i = 0; i < mRingColors.length; i++) {
pos[i] = startPos + (distance * i);
}
SweepGradient gradient = new SweepGradient(centerX, centerY, mRingColors, pos);
mRingPaint.setShader(gradient);
}
/******************************外部修改*****************************/
private void updateText(float curAngle) {
mCurProgress = (int) ((curAngle * (mMaxValue - mMinValue)) / 360);
}
public void setProgress(int progress) {
this.mCurProgress = progress;
this.mRotateAngle = mCurProgress * 360 / (mMaxValue - mMinValue);
if (null != mOnProgressChangeListener && mLastProgress != mCurProgress) {
mOnProgressChangeListener.onProgressChanged(this, mCurProgress, false);
mLastProgress = mCurProgress;
}
postInvalidate();
}
public RingSeekBar setMaxValue(int maxValue) {
this.mMaxValue = maxValue;
updateMaxAndMinValue();
return this;
}
public RingSeekBar setMinValue(int minValue) {
this.mMinValue = minValue;
updateMaxAndMinValue();
return this;
}
/******************************设置进度回调*****************************/
private OnProgressChangeListener mOnProgressChangeListener;
public void setOnProgressChangeListener(OnProgressChangeListener onProgressChangeListener) {
this.mOnProgressChangeListener = onProgressChangeListener;
}
public interface OnProgressChangeListener {
/**
* 进度发生变化
*
* @param seekBar 圆环拖动进度条
* @param progress 当前进度值
* @param isUser 是否由用户操作
*/
void onProgressChanged(RingSeekBar seekBar, int progress, boolean isUser);
/**
* 用户开始拖动
*/
void onStartTrackingTouch(RingSeekBar seekBar);
/**
* 用户结束拖动
*/
void onStopTrackingTouch(RingSeekBar seekBar);
}
/*****************************单位转换*****************************/
/**
* sp 转 px
*/
private float sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
/**
* dp 转 px
*/
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
}
attrs:
<declare-styleable name="RingSeekBar">
<!--圆弧宽度-->
<attr name="ring_width" format="dimension" />
<!--圆弧旋转角度-->
<attr name="ring_rotate_angle" format="integer" />
<!--描边宽度-->
<attr name="ring_border_width" format="dimension" />
<!--圆弧渐变色-->
<attr name="ring_colors" format="reference" />
<!--描边颜色-->
<attr name="ring_border_color" format="color" />
<!--拖动进度颜色-->
<attr name="ring_progress_color" format="color" />
<!--拖动按钮样式-->
<attr name="ring_thumb_style" format="enum">
<enum name="CIRCLE" value="0" />
<enum name="RING" value="1" />
</attr>
<!--拖动按钮描边颜色-->
<attr name="ring_thumb_color" format="color" />
<!--拖动按钮半径-->
<attr name="ring_thumb_radius" format="dimension" />
<!--拖动按钮描边宽度-->
<attr name="ring_thumb_border_width" format="dimension" />
<!--进度最大值-->
<attr name="ring_max_value" format="integer" />
<!--进度最小值-->
<attr name="ring_min_value" format="integer" />
<!--进度文字大小-->
<attr name="ring_text_size" format="dimension" />
<!--进度文字颜色-->
<attr name="ring_text_color" format="color" />
<!--进度文字是否加粗-->
<attr name="ring_text_bold" format="boolean" />
</declare-styleable>
colors:
<array name="arc_colors_default">
<item>#1a2a6c</item>
<item>#b21f1f</item>
<item>#fdbb2d</item>
</array>
三、时分秒时间格式显示控件
效果截图:
发现的 BUG:在电视上显示时,会导致应用不响应遥控器的方向键
public class TimeSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private static final long DRAW_INTERVAL = 30; // 最大帧数 (1000 / 30)
private SurfaceHolder mHolder; // 里面保存了一个对 Surface 对象的引用
private Canvas mCanvas; // 绘图的画布
private boolean mIsCanDrawing; // 控制绘画线程的标识
private Paint mPaint; // 画笔
private int mTextSize = 12; // 文字大小
private int w; // 宽
private int h; // 高
private int dy; // 文字在高度上居中显示时,需要向下偏移的距离
private final int SHOW_FULL_TIME = 0;
private final int SHOW_NO_WEEK = 1;
private final int SHOW_ONE_DAY = 2;
private int mTimeStyle = SHOW_FULL_TIME;
private final int GRAVITY_LEFT = 100;
private final int GRAVITY_RIGHT = 101;
private int mGravity = GRAVITY_LEFT;
private int mTextColor = Color.BLACK;
public TimeSurfaceView(Context context) {
this(context, null);
}
public TimeSurfaceView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TimeSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TimeSurfaceView);
mTextSize = ta.getDimensionPixelSize(R.styleable.TimeSurfaceView_time_text_size, sp2px(mTextSize));
mTextColor = ta.getColor(R.styleable.TimeSurfaceView_time_text_color, mTextColor);
mTimeStyle = ta.getInt(R.styleable.TimeSurfaceView_time_style, mTimeStyle);
mGravity = ta.getInt(R.styleable.TimeSurfaceView_time_gravity, mGravity);
ta.recycle();
init();
}
public void setTextSize(int textSize) {
this.mTextSize = sp2px(textSize);
}
/**
* sp 转 px
*/
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
/**
* 初始化
*/
private void init() {
// 获取 SurfaceHolder 对象
mHolder = getHolder();
// 注册 SurfaceHolder 的回调方法
mHolder.addCallback(this);
// 设置画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(mTextColor);
mPaint.setFakeBoldText(true);
mPaint.setTextSize(mTextSize);
// 数字 0~9 的宽度不一致
// 如果显示的初始时间含有比较窄的数字(如:1),当显示比较宽的数字(如:4)时就会导致,如果时间居右显示时,会显示不全
// 当控件位于 LinearLayout 中,当控件宽度大于实际内容宽度时,且假设实际内容居右显示,则需要时间文本中各数字取最大宽度,左侧宽度为控件宽度减去时间文本最大宽度(各数字取最大宽度)
String maxWidthTime = "4444年02月24日 星期五 24:44:44";
Rect bounds = new Rect();
mPaint.getTextBounds(maxWidthTime, 0, maxWidthTime.length(), bounds);
w = bounds.width();
h = bounds.height();
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
dy = (int) ((fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom);
setZOrderOnTop(true);
mHolder.setFormat(PixelFormat.TRANSLUCENT);
}
/**
* 格式化系统时间
*/
private String getTime() {
// 创建 Calendar 对象,用于获取系统时间
Calendar calendar = Calendar.getInstance();
String ymd;
String week;
String sfm;
switch (mTimeStyle) {
case SHOW_FULL_TIME: // 格式如:2019年01月18日 星期五 11:48:23
ymd = getYMD(calendar); // 获取年月日
week = getWeek(calendar); // 获取星期
sfm = getSFM(calendar); // 获取时分秒
return ymd + " " + week + " " + sfm;
case SHOW_NO_WEEK: // 格式如:2019年01月18日 11:48:23
ymd = getYMD(calendar); // 获取年月日
sfm = getSFM(calendar); // 获取时分秒
return ymd + " " + sfm;
case SHOW_ONE_DAY: // 格式如:11:48:23
sfm = getSFM(calendar); // 获取时分秒
return sfm;
}
return "";
}
/*****************************获取系统时间*****************************/
/**
* 获取时分秒
*/
private String getSFM(Calendar calendar) {
int hour_num = calendar.get(Calendar.HOUR_OF_DAY);
String hour = String.valueOf(hour_num < 10 ? "0" + hour_num : hour_num);
int minute_num = calendar.get(Calendar.MINUTE);
String minute = String.valueOf(minute_num < 10 ? "0" + minute_num : minute_num);
int second_num = calendar.get(Calendar.SECOND);
String second = String.valueOf(second_num < 10 ? "0" + second_num : second_num);
return hour + ":" + minute + ":" + second;
}
/**
* 获取星期
*/
private String getWeek(Calendar calendar) {
String week = String.valueOf(calendar.get(Calendar.DAY_OF_WEEK));
switch (week) {
case "1":
week = "日";
break;
case "2":
week = "一";
break;
case "3":
week = "二";
break;
case "4":
week = "三";
break;
case "5":
week = "四";
break;
case "6":
week = "五";
break;
case "7":
week = "六";
break;
}
return "星期" + week;
}
/**
* 获取年月日
*/
private String getYMD(Calendar calendar) {
String year = String.valueOf(calendar.get(Calendar.YEAR));
int month_num = calendar.get(Calendar.MONTH) + 1;
String month = String.valueOf(month_num < 10 ? "0" + month_num : month_num);
int day_num = calendar.get(Calendar.DAY_OF_MONTH);
String day = String.valueOf(day_num < 10 ? "0" + day_num : day_num);
return year + "年" + month + "月" + day + "日";
}
/*****************************子线程中绘制时间*****************************/
/**
* 绘制时间的线程
*/
private class TimeThread extends Thread {
@Override
public void run() {
do {
long tickTime = System.currentTimeMillis();
// 绘制时间
drawTime(getTime());
long deltaTime = System.currentTimeMillis() - tickTime;
// 绘制消耗的时间小于系统单个绘制帧的绘制时间
if (deltaTime < DRAW_INTERVAL) {
SystemClock.sleep(DRAW_INTERVAL - deltaTime);
}
} while (mIsCanDrawing);
}
}
/**
* 绘制时间
*/
private void drawTime(String time) {
// 绘制标识为 true,可以绘制,如果为 false,则线程停止
if (mIsCanDrawing) {
try {
synchronized (mHolder) {
// 通过 SurfaceHolder#lockCanvans() 方法获取 Canvas 绘图对象
mCanvas = mHolder.lockCanvas();
if (null != mCanvas) {
/**
* 设置背景透明步骤:
* 1. 在构造方法中调用
* setZOrderOnTop(true);
* mHolder.setFormat(PixelFormat.TRANSLUCENT);
* 2. 绘制前,通过 drawColor() 方法进行清屏操作
* mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
*/
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// 绘制文字
if (wm == MeasureSpec.AT_MOST) {
mCanvas.drawText(time, 0, h / 2 + dy, mPaint);
} else {
// 居右显示时,当前绘制的 x 起始坐标为 getWidth() - w
// 因为 0~9 间各数字宽度不一致,所以为了保证不出现显示不全的情况,时间文本中各数字应取最宽的数字,x 才能最小,最宽的时间文本才能显示完全
if (mGravity == GRAVITY_RIGHT) {
mCanvas.drawText(time, getWidth() - w, h / 2 + dy, mPaint);
} else {
mCanvas.drawText(time, 0, h / 2 + dy, mPaint);
}
}
}
}
} finally {
if (null != mCanvas) {
// 通过 SurfaceHolder#unlockCanvasAndPost() 方法对画布内容进行提交
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
int wm;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int ws = MeasureSpec.getSize(widthMeasureSpec);
wm = MeasureSpec.getMode(widthMeasureSpec);
if (wm == MeasureSpec.AT_MOST) {
ws = w;
}
int hs = MeasureSpec.getSize(heightMeasureSpec);
int hm = MeasureSpec.getMode(heightMeasureSpec);
if (hm == MeasureSpec.AT_MOST) {
hs = h;
}
setMeasuredDimension(ws, hs);
}
/******************************SurfaceView 回调*****************************/
@Override
public void surfaceCreated(SurfaceHolder holder) {
// 控制绘画线程的标识置为 true,可以开始绘制
mIsCanDrawing = true;
// 开启子线程,进行绘制
new TimeThread().start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// 控制绘画线程的标识置为 false,停止绘制
mIsCanDrawing = false;
}
}
attrs:
<declare-styleable name="TimeSurfaceView">
<!--文字大小-->
<attr name="time_text_size" format="dimension" />
<!--文字颜色-->
<attr name="time_text_color" format="color" />
<!--时间显示格式-->
<attr name="time_style" format="enum">
<enum name="SHOW_FULL_TIME" value="0" />
<enum name="SHOW_NO_WEEK" value="1" />
<enum name="SHOW_ONE_DAY" value="2" />
</attr>
<!--实际显示内容对应的 GRAVITY-->
<attr name="time_gravity" format="enum">
<enum name="left" value="100" />
<enum name="right" value="101" />
</attr>
</declare-styleable>