效果图
- 可以通过旋转view或者直接拖动来控制进度。
- 根据旋转角度,音量的移动速度会改变
- 根据旋转角度,归位时候的速度会改变
实现流程
- 重写
onMeasure()
,使得高度为外部矩形的高度+padding。 - 重写
onDraw()
,绘制两个矩形和一个球。 - 重写
onTouchEvent()
判断是点击小球移动还是旋转控件移动,并且判断点击是控件左半部分还是右半部分,在手指抬起时,执行归为动画。 - 设置音量改变接口供外部使用。
使用
记得要在外层Linearlayout中要添加clipChildred=false
。。
代码
public class ProgressView extends View {
private static final String TAG = "HappyVoiceView";
/**
* 外层矩形框画笔
*/
private Paint outRectPaint;
/**
* 内层矩形框画笔
*/
private Paint innerRectPaint;
/**
* 音量控制球画笔
*/
private Paint ballPaint;
/**
* 外层,内层,球的矩形范围
*/
private RectF rectF1, rectF2, ballRect;
/**
* 外层矩形高度
*/
private int outRectHeight = 50;
/**
* 内层矩形框画笔
*/
private int innerRectHeight = 20;
/**
* 小球的半径
*/
private int circleRadius = 15;
/**
* 内层小球可移动范围
*/
private int length = 0;
/**
* 是否是旋转控件
*/
private boolean doRoate = false;
/**
* 竖直方向偏移
*/
private float downY;
/**
* 角度
*/
private int degress = 0;
/**
* 手指抬起归位
*/
private ValueAnimator valueAnimator;
/**
* 小球当前位置
*/
private int ballCurrentLength = 0;
/**
* 从左面旋转控件
*/
private boolean isTouchLeft = false;
/**
* 从右面面旋转控件
*/
private boolean isTouchBall = false;
/**
* 小球移动最终速度
*/
private int speed = 1;
/**
* 小球最小速度
*/
private int minSpeed = 2;
/**
* 音量改变监听
*/
private OnVoiceUpdateLinstener voiceUpdateLinstener;
public void setVoiceUpdateLinstener(OnVoiceUpdateLinstener voiceUpdateLinstener) {
this.voiceUpdateLinstener = voiceUpdateLinstener;
}
public ProgressView(Context context) {
super(context);
init();
}
public ProgressView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(widthSize, outRectHeight + getPaddingTop() + getPaddingBottom());
}
private void init() {
outRectPaint = initPaint();
outRectPaint.setColor(Color.CYAN);
innerRectPaint = initPaint();
innerRectPaint.setColor(Color.YELLOW);
ballPaint = initPaint();
ballPaint.setColor(Color.RED);
ballRect = new RectF();
}
private void initValueAnimator() {
valueAnimator = ValueAnimator.ofInt(degress, 0);
valueAnimator.setDuration(Math.abs(degress)/10*100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
degress = (int) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.start();
}
private Paint initPaint() {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); //抗锯齿
paint.setDither(true); //防抖动
paint.setColor(Color.CYAN);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeCap(Paint.Cap.SQUARE);
return paint;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
dealBallLength();
rectF1 = new RectF((-getMeasuredWidth() / 2) + getPaddingLeft(), -outRectHeight / 2, (getMeasuredWidth() / 2) - getPaddingLeft(), outRectHeight / 2);
rectF2 = new RectF((-getMeasuredWidth() / 2) + getPaddingLeft() + getPaddingLeft(), -innerRectHeight / 2, (getMeasuredWidth() / 2) - getPaddingLeft() - getPaddingLeft(), innerRectHeight / 2);
length = (int) rectF2.width();
canvas.rotate(degress);
canvas.drawRoundRect(rectF1, 10, 10, outRectPaint);
canvas.drawRoundRect(rectF2, 20, 20, innerRectPaint);
canvas.drawCircle((rectF2.left + ballCurrentLength) + circleRadius / 2, rectF2.centerY(), circleRadius, ballPaint);
ballRect.left = (rectF2.left + ballCurrentLength) + circleRadius / 2 - circleRadius;
ballRect.right = (rectF2.left + ballCurrentLength) + circleRadius / 2 + circleRadius;
ballRect.top = rectF2.centerY() - circleRadius;
ballRect.bottom = rectF2.centerY() + circleRadius;
if (voiceUpdateLinstener != null) {
voiceUpdateLinstener.onVoiceChanged((int) ((float) ballCurrentLength / length * 100));
}
}
private int dealBallLength() {
speed = Math.abs(degress) / 3 + minSpeed;
if (degress > 0 && ballCurrentLength < length) {
speed = ballCurrentLength + speed > length ? (length - ballCurrentLength) : speed;
ballCurrentLength += speed;
invalidate();
} else if (degress < 0 && ballCurrentLength > 0) {
speed = ballCurrentLength - speed < 0 ? ballCurrentLength : speed;
ballCurrentLength -= speed;
invalidate();
}
return ballCurrentLength;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float y = event.getY();
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (valueAnimator != null && valueAnimator.isRunning()) {
return false;
}
if (ballRect.contains(event.getX() - getMeasuredWidth() / 2, y - getMeasuredHeight() / 2)) {
isTouchBall = true;
break;
}
if (rectF1.contains(event.getX() - getMeasuredWidth() / 2, y - getMeasuredHeight() / 2)) { //平移过坐标系
doRoate = true;
downY = (int) event.getY();
if (event.getX() - getMeasuredWidth() / 2 <= 0) {
isTouchLeft = true;
} else {
isTouchLeft = false;
}
}
break;
case MotionEvent.ACTION_MOVE:
if (isTouchBall) {
ballCurrentLength = (int) (x - getPaddingLeft() - getPaddingLeft());
if (ballCurrentLength < 0) {
ballCurrentLength = 0;
} else if (ballCurrentLength > length) {
ballCurrentLength = length;
}
invalidate();
break;
}
x = (float) Math.atan((y - downY) / rectF1.right);
degress = (int) Math.toDegrees(x);
degress = isTouchLeft ? -degress : degress;
invalidate();
break;
case MotionEvent.ACTION_UP:
if (doRoate) {
initValueAnimator();
}
doRoate = false;
isTouchBall = false;
break;
}
return true;
}
public interface OnVoiceUpdateLinstener {
void onVoiceChanged(int voice);
}
}