仿MIUI计时器+圆形SeekBar+钟表表盘UI 三位一体式自定义View

废话不多说,先上效果图,然后整体代码,再细细说来—-

访MIUI计时器样式

表盘样式

/**
* 提供类似小米计时器、表盘两种外观的控件
* 使用方法:先定义容器布局,再通过CircularTimePicker(Context context, int srcId,ViewType viewType)方法构造对象
* viewType 为该控件的外观(计时器式和表盘式)
* int srcId 是拖动手柄的图片资源ID
* 然后用容器布局的addView方法把该对象添加进去
* 之后setListener/setMaxProgerss
*
* @author Wjk
*
*/
public class CircularTimePicker extends View {

private Paint paint_arc_bg;
private Paint paint_arc;
private Paint paint_text;
private RectF innerRectf;
private Paint paint_grey;
private RectF outterRectf;
/** 基准标尺 */
private float common_size;
/** 内环左部 */
private float innerLeft;
private float innerTop;
private float innerRight;
private float innerBottom;
/** 外环左部 */
private float outterLeft;
private float outterTop;
private float outterRight;
private float outterBottom;

/** 字体大小 */
private float textSize;

/** 画字体时的X坐标 */
private float textX;
private float textY;

/** 圆心坐标 */
private float cX;
private float cY;
/**外圆半径*/
private float circleRadius;
/**内圆半径*/
private float innerCircleRadius;

/**整个总布局长宽度 */
private int totalSize;
/**控件类型,默认是时间器类型 */
private ViewType viewType = ViewType.TimePiker;
DashPathEffect  effect = new DashPathEffect(new float[] { 4, 4}, 0);


private int arcBgColor = Color.parseColor("#40f16c50");
private int arcColor = Color.parseColor("#fff16c50");
private int outerCircleColor = Color.parseColor("#d4d4d4");
private Bitmap bitmap;

/** 进度变更的监听对象 */
private OnSeekChangeListener mListener;

public OnSeekChangeListener getmListener() {
    return mListener;
}

public void setSeekChangeListener(OnSeekChangeListener mListener) {
    this.mListener = mListener;
}

/**
 * 构造器
 * @param context
 * @param srcId 手柄图片资源ID
 * @param viewType 外观类型
 */
public CircularTimePicker(Context context, int srcId,ViewType viewType) {
    super(context);
    this.viewType = viewType;
    bitmap = BitmapFactory.decodeResource(context.getResources(), srcId);
    init();
}

private void init(){
    innerRectf = new RectF();
    outterRectf = new RectF();

    paint_arc_bg = new Paint();
    paint_arc_bg.setColor(arcBgColor);
    paint_arc_bg.setAntiAlias(true);
    paint_arc_bg.setFlags(Paint.ANTI_ALIAS_FLAG);
    paint_arc_bg.setStyle(Style.STROKE);

    paint_arc = new Paint();
    paint_arc.setColor(arcColor);
    paint_arc.setAntiAlias(true);
    paint_arc.setFlags(Paint.ANTI_ALIAS_FLAG);
    paint_arc.setStyle(Style.STROKE);


    paint_arc_bg.setPathEffect(effect);
    paint_arc.setPathEffect(effect);

    paint_grey = new Paint();
    paint_grey.setColor(outerCircleColor);
    paint_grey.setAntiAlias(true);
    paint_grey.setStyle(Style.STROKE);

    paint_text = new Paint();
    paint_text.setAntiAlias(true);
    paint_text.setTextAlign(Align.CENTER);
    paint_text.setStyle(Style.STROKE);
    paint_text.setColor(arcColor);
}

/**手柄中心的X坐标 */
private double curBitmapX;
/**手柄中心的Y坐标 */
private double curBitmapY;
/**手柄左上角的X轴坐标 */
private double dx;
/**手柄左上角的Y轴坐标 */
private double dy;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    //int width = getWidth(); 
    //int height = getHeight();
    int size = (width > height) ? height : width; 
    /** 整个总布局宽度 */
    totalSize = size;
    common_size = totalSize/18f;
    /** 内环左部 */
    innerLeft = common_size * 3f;
    innerTop = common_size * 3f;
    innerRight = common_size * 15f;
    innerBottom = common_size * 15f;

    arcStrokeWidth = common_size * 1f;
    paint_arc.setStrokeWidth(arcStrokeWidth);

    switch (viewType) {
    case Clock:
        /** 字体大小 */
        textSize = common_size * 1.2f;
        innerCircleRadius = (innerBottom - innerTop) / 2f;
        /**外环左部 */
        outterLeft = innerLeft-bitmap.getWidth()/2-arcStrokeWidth/2-10;
        outterTop = innerTop-bitmap.getHeight()/2-arcStrokeWidth/2-10;
        outterRight = innerRight+bitmap.getWidth()+arcStrokeWidth/2/2+10;
        outterBottom = innerBottom+bitmap.getHeight()/2+arcStrokeWidth/2+10;

        /** 画字体时的X坐标 */
        textX = (innerRight - innerLeft) / 2f + innerLeft;
        textY = (innerBottom - innerTop) / 2f + innerTop - textSize;

        break;
    case TimePiker:
        /** 字体大小 */
        textSize = common_size * 2f;
        paint_arc_bg.setStrokeWidth(arcStrokeWidth);
        /** 外环左部 */
        outterLeft = innerLeft-bitmap.getWidth()/2-arcStrokeWidth/2-5;
        outterTop = innerTop-bitmap.getHeight()/2-arcStrokeWidth/2-5;
        outterRight = innerRight+bitmap.getWidth()/2+arcStrokeWidth/2+5;
        outterBottom = innerBottom+bitmap.getHeight()/2+arcStrokeWidth/2+5;

        /** 画字体时的X坐标 */
        textX = (innerRight - innerLeft) / 2f + innerLeft;
        textY = (innerBottom - innerTop) / 2f + innerTop + textSize
                / 2f;
        break;
    }

    innerRectf.set(innerLeft, innerTop, innerRight, innerBottom);
    outterRectf.set(outterLeft, outterTop, outterRight, outterBottom);

    paint_grey.setStrokeWidth(3.0f);
    paint_text.setTextSize(textSize);
    paint_text.setStrokeWidth(4);

    /** 圆心坐标 */
    cX = (innerRight - innerLeft) / 2f + innerLeft;
    cY = (innerBottom - innerTop) / 2f + innerTop;
    circleRadius = (outterBottom - outterTop) / 2f;

    curBitmapX = cX; // 12点方向
    curBitmapY = cY - circleRadius;//  12点方向
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawDial(canvas);
    String text = getTextToDraw();
    canvas.drawText(text, textX, textY, paint_text);
    // 画外环
    canvas.drawCircle(cX, cY, circleRadius, paint_grey);
    dx = getXFromAngle();
    dy = getYFromAngle();
    drawMarkerAtProgress(canvas);
}

private String getTextToDraw() {
    String text = "";
    switch (viewType) {
    case Clock:
        int minutes = (int) (currentAngle*2);
        int hours = minutes/60;
        int minute = minutes%60;
        text = hours+"小时"+minute+"分钟";
        break;
    case TimePiker:
        if(this.progress<10){
            text = "00:0"+this.progress;
        }else{
            text = "00:"+this.progress;
        }
        break;
    }
    return text;
}

private void drawMarkerAtProgress(Canvas canvas) {
    canvas.drawBitmap(bitmap, (float)dx, (float)dy, null);
}

/**
 * 画短线、表盘刻度
 * 
 * @param canvas
 */
public void drawDial(Canvas canvas) {
    switch (viewType) {
    case Clock:
        DashPathEffect  effect2 = new DashPathEffect(new float[] { 3, (float) (Math.PI*innerCircleRadius/6.0-3)}, 3);
        paint_arc.setPathEffect(effect2);
        canvas.drawCircle(cX, cY, 8, paint_text);
        canvas.drawCircle(cX, cY, innerCircleRadius+arcStrokeWidth/2, paint_text);
        canvas.drawCircle(cX, cY, innerCircleRadius, paint_arc);
        canvas.drawText("3", cX+innerCircleRadius-arcStrokeWidth/2-textSize/2, cY+textSize/3, paint_text);
        canvas.drawText("6", cX, cY+innerCircleRadius-arcStrokeWidth/2-textSize/3, paint_text);
        canvas.drawText("9", cX-innerCircleRadius+arcStrokeWidth/2+textSize/2, cY+textSize/2, paint_text);
        canvas.drawText("12", cX, cY-innerCircleRadius+arcStrokeWidth/2+textSize, paint_text);
        //画表针
        float x1 = (float) (cX + (innerCircleRadius - arcStrokeWidth * 2)* Math.sin(currentAngle * (Math.PI / 180)));
        float y1 = (float) (cY - (innerCircleRadius - arcStrokeWidth * 2)* Math.cos(currentAngle * (Math.PI / 180)));
        canvas.drawLine(cX, cY, x1, y1, paint_text);
        break;

    case TimePiker:
        paint_arc.setPathEffect(effect);
        canvas.drawArc(innerRectf, 270f+currentAngle, 360f-currentAngle, false, paint_arc_bg);
        canvas.drawArc(innerRectf, 270f, currentAngle, false, paint_arc);
        break;
    }
}

/**当前指针的角度(0----360)*/
private float currentAngle = 0;
/** 此标志用于表示setAngle方法被调用还是setProgress被调用了 */
private boolean CALLED_FROM_ANGLE = false;
private int maxProgress =60 ;
private int progress;
private int progressPercent;
private float arcStrokeWidth;

@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {

    float x = event.getX();
    float y = event.getY();
    boolean up = false;
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        break;
    case MotionEvent.ACTION_MOVE:
        moveArm(x, y, up);
        break;
    case MotionEvent.ACTION_UP:
        up = true;
        mListener.onProgressChange(this, this.getProgress());
        break;
    }
    return true;
}

private void moveArm(float x, float y, boolean up) {
    curBitmapX = cX + circleRadius* Math.cos(Math.atan2(x - cX, cY - y) - (Math.PI / 2d));
    curBitmapY = cY + circleRadius* Math.sin(Math.atan2(x - cX, cY - y) - (Math.PI / 2d));
    float degrees = (float) ((float) ((Math.toDegrees(Math.atan2(
            x - cX, cY - y)) + 360.0)) % 360.0);
    if (degrees < 0) {
        degrees += 2 * Math.PI;
    }
    invalidate();
    setAngle(degrees);
}

private void setAngle(float angle) {
    currentAngle = angle;
    float donePercent =  this.currentAngle / 360 * 100;
    float progress = this.currentAngle / 360 * getMaxProgress();
    setProgressPercent(Math.round(donePercent));
    CALLED_FROM_ANGLE = true;
    setProgress(Math.round(progress));

}

public int getMaxProgress() {
    return maxProgress;
}

public void setMaxProgress(int maxProgress) {
    this.maxProgress = maxProgress;
}

public int getProgress() {
    return progress;
}

public void setProgress(int progress) {
    if (this.progress != progress) {
        this.progress = progress;
        if (!CALLED_FROM_ANGLE) {
            int newPercent = (this.progress * 100) / this.maxProgress;
            int newAngle = (this.progress * 360) / this.maxProgress;
            this.setAngle(newAngle);
            this.setProgressPercent(newPercent);
        }
        CALLED_FROM_ANGLE = false;
    }
}

public int getProgressPercent() {
    return progressPercent;
}

public void setProgressPercent(int progressPercent) {
    this.progressPercent = progressPercent;
}

/**
 * 获取手柄左上角的X轴坐标
 * @return
 */
public double getXFromAngle() {
    int size = bitmap.getWidth();
    double x = curBitmapX - (size / 2d);
    return x;
}

/**
 * 获取手柄左一角的Y轴坐标
 * @return
 */
public double getYFromAngle() {
    int size = bitmap.getHeight();
    double y = curBitmapY - (size / 2d);
    return y;
}

/**
 * 进度改变的监听类,用于接收当前的进度
 * @author wjk
 *
 */
public interface OnSeekChangeListener {
    public void onProgressChange(CircularTimePicker view, int newProgress);
}

public static enum ViewType {
    Clock,
    TimePiker
};

}

项目中有这样的需求:用圆形的SeekBar做一个时间选择器,可以用在选择提醒时间的页面上。要求最好有两种模式,由调用 者传入类型参数,根据类型绘制相对应的View

接下来说思路:总体都是利用canvas.drawCircle和canvas.drawArc方法在拖动手柄的过程中不断的重绘。
先说MIUI样式—-内环是Arc,外环是Circle 。
1、在手指拖动手柄移动的过程中,由当前触摸点的坐标和圆心坐标这两个点算出移动的角度,用三角函数实现,具体怎么算,高中数学都学过。
2、再用得到的角度和圆的半径算出来手柄应该在圆上“待”在点,把手柄用drawBitmap画在这里就行了
3、角度有了,画内环的Arc自然也不成问题,关键点在drawArc时所用的Paint,这paint还真是能画好多意想不出来的东西,比如这个效果 DashPathEffect effect = new DashPathEffect(new float[] { 4, 4}, 0); 把effect设置到paint中再画Arc就能出现蚂蚁线的效果了,其中两个参数new float[] { 4, 4}的意思是:画4像素,然后留4像素的空白,再画4像素,再留4像素空白…… ;0的意思是上去就开画,不犹豫,不等待,不迟疑,当然如果设置成int n>0的数,那就是先跳过n个像素再开始画。

再说表盘样式—确切的说这里有三个环,内环Arc,中间环和外环是Circle
1、画数字(3、6、9、12),圆心有了,半径有了,这四个数字所在的坐标也不难求,画上去再根据字体大小微调就行,画字体(drawText)时的坐标点就是文字的底边中点。
canvas.drawText(“3”, cX+innerCircleRadius-arcStrokeWidth/2-textSize/2, cY+textSize/3, paint_text);
canvas.drawText(“6”, cX, cY+innerCircleRadius-arcStrokeWidth/2-textSize/3, paint_text);
canvas.drawText(“9”, cX-innerCircleRadius+arcStrokeWidth/2+textSize/2, cY+textSize/2, paint_text);
canvas.drawText(“12”, cX, cY-innerCircleRadius+arcStrokeWidth/2+textSize, paint_text);
2、画表盘刻度,也是用的画蚂蚁线的思路(也可以用drawPath的方法,定义DashPathEffect 时,设置参数为new float[] { n,周长/12-n},n是刻度的粗细度。
在代码中的体现就是:
DashPathEffect effect2 = new DashPathEffect(new float[] { n, (float) (Math.PI*innerCircleRadius/6.0-n)}, n);
paint_arc.setPathEffect(effect2);

3、画表针 ,我这里是直接画的直线,其实最好是让美工弄个表针的图片画上去。表针尾部的坐标就是圆心,再确定表针头部的坐标就可以画直线了
//画表针
float x1 = (float) (cX + (innerCircleRadius - arcStrokeWidth * 2)* Math.sin(currentAngle * (Math.PI / 180)));
float y1 = (float) (cY - (innerCircleRadius - arcStrokeWidth * 2)* Math.cos(currentAngle * (Math.PI / 180)));
canvas.drawLine(cX, cY, x1, y1, paint_text);
最后:画的过程都是在获取当前角度currentAngle的前提下不断invalidate开画的。

参考资料:https://github.com/RaghavSood/AndroidCircularSeekBar

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值