Android -- [SelfView] 自定义圆盘指针时钟

Android -- [SelfView] 自定义圆盘指针时钟

复制代码
ps:
	简约圆盘指针时钟,颜色可调、自由搭配;支持阿拉伯数字、罗马数字刻度显示;
效果图
使用:
xml 复制代码
<!-- 自定义属性参考 attrs.xml 文件 -->
<com.nepalese.harinetest.player.VirgoCircleClock
            android:id="@+id/circleclock"
            android:layout_width="300dp"
            android:layout_height="300dp"
            app:paddingFrame="10dp"
            app:strokeSize="5dp"
            app:offsetMark="-1dp"
            app:offsetText="-1dp"
            app:rSmall="5px"
            app:rBig="8px"
            app:needBg="true"
            app:frameColor="@color/colorBlack30"
            app:bgColor="@color/colorEye"
            app:markColor1="@color/black"
            app:markColor2="@color/colorWhite"
            app:textColor="@color/black"
            app:txtSize="18sp"
            app:displayType="type_num"/>
java 复制代码
private VirgoCircleClock circleClock;

//使用
circleClock = findViewById(R.id.circleclock);
circleClock.startPlay();
 
//注销
if (circleClock != null) {
   circleClock.releaseView();
}
码源:
1. attrs.xml
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<declare-styleable name="VirgoCircleClock">
        <!--是否绘制背景 默认透明-->
        <attr name="needBg" format="boolean" />
        <!--背景颜色 默认白色-->
        <attr name="bgColor" format="color|reference"/>
        <!--边框颜色 默认黑色-->
        <attr name="frameColor" format="color|reference"/>
        <!--小刻度颜色-->
        <attr name="markColor1" format="color|reference"/>
        <!--大刻度颜色-->
        <attr name="markColor2" format="color|reference"/>
        <!--文字颜色 & 时针、分针颜色-->
        <attr name="textColor" format="color|reference"/>
        <!--秒针颜色-->
        <attr name="secondColor" format="color|reference"/>
        <!--大、小刻度点半径-->
        <attr name="rBig" format="dimension|reference"/>
        <attr name="rSmall" format="dimension|reference"/>
        <attr name="offsetMark" format="dimension|reference"/>
        <attr name="offsetText" format="dimension|reference"/>
        <!--数字类型-->
        <attr name="displayType" format="integer">
            <enum name="type_num" value="1"/>
            <enum name="type_roma" value="2"/>
        </attr>
        <!--内缩间距-->
        <attr name="paddingFrame" format="dimension|reference"/>
        <!--文字大小-->
        <attr name="txtSize" format="dimension|reference"/>
        <!--边框厚度-->
        <attr name="strokeSize" format="dimension|reference"/>
    </declare-styleable>
</resources>
2. VirgoCircleClock.java
java 复制代码
package com.nepalese.harinetest.player;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import androidx.annotation.Nullable;

import com.nepalese.harinetest.R;

import java.util.Calendar;

public class VirgoCircleClock extends View {
    private static final String TAG = "VirgoCircleClock";

    private static final int DEF_RADIUS_BIG = 3;
    private static final int DEF_RADIUS_SMALL = 2;
    private static final int DEF_OFF_MARK = 2;//刻度与边框间距
    private static final int DEF_OFF_TEXT = 3;//数字与边框间距
    private static final int TYPE_NUM = 1;//阿拉伯数字
    private static final int TYPE_ROMA = 2;//罗马数字
    private static final int DEF_PADDING = 15;//内缩间距
    private static final float DEF_SIZE_TEXT = 18f;//数字大小
    private static final float DEF_FRAME_STROKE = 5f;//边框厚度
    private static final float RATE_HOUR = 0.5f;//时针与半径比例
    private static final float RATE_HOUR_TAIL = 0.05f;//时针尾巴与半径比例
    private static final float RATE_HOUR_WIDTH = 70f;//时针尾巴与半径比
    private static final float RATE_MINUTE = 0.6f;//分针与半径比例
    private static final float RATE_MINUTE_TAIL = 0.08f;//分针尾巴与半径比例
    private static final float RATE_MINUTE_WIDTH = 120f;//分针尾巴与半径比
    private static final float RATE_SECOND = 0.7f;//秒针与半径比例
    private static final float RATE_SECOND_TAIL = 0.1f;//秒针尾巴与半径比例
    private static final float RATE_SECOND_WIDTH = 240f;//秒针宽度与半径比

    private Paint paintMain;//刻度+指针
    private Paint paintFrame;//外环边框
    private Calendar calendar;

    private boolean needBg;//是否绘制背景 默认透明
    private int bgColor;//背景颜色 默认白色
    private int frameColor;//边框颜色
    private int markColor1;//小刻度颜色
    private int markColor2;//大刻度颜色
    private int textColor;//文字颜色 & 时针、分针颜色
    private int secondColor;//秒针颜色
    private int radiusBig, radiusSmall;//大、小刻度点半径
    private int offMark, offText;
    private int markY, txtY;
    private int displayStyle;//数字类型
    private int padding;//内缩间距
    private int radius;//表盘半径
    private int hours, minutes, seconds;//当前时分秒
    private int centerX, centerY;//表盘中心坐标
    private float textSize;//文字大小
    private float strokeSize;//边框厚度

    public VirgoCircleClock(Context context) {
        this(context, null);
    }

    public VirgoCircleClock(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public VirgoCircleClock(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VirgoCircleClock);
        needBg = typedArray.getBoolean(R.styleable.VirgoCircleClock_needBg, false);
        bgColor = typedArray.getColor(R.styleable.VirgoCircleClock_bgColor, Color.WHITE);
        frameColor = typedArray.getColor(R.styleable.VirgoCircleClock_frameColor, Color.BLACK);
        markColor1 = typedArray.getColor(R.styleable.VirgoCircleClock_markColor1, Color.BLACK);
        markColor2 = typedArray.getColor(R.styleable.VirgoCircleClock_markColor2, Color.DKGRAY);
        textColor = typedArray.getColor(R.styleable.VirgoCircleClock_textColor, Color.BLACK);
        secondColor = typedArray.getColor(R.styleable.VirgoCircleClock_secondColor, Color.RED);

        radiusBig = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_rBig, DEF_RADIUS_BIG);
        radiusSmall = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_rSmall, DEF_RADIUS_SMALL);
        offMark = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_offsetMark, DEF_OFF_MARK);
        offText = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_offsetText, DEF_OFF_TEXT);
        displayStyle = typedArray.getInteger(R.styleable.VirgoCircleClock_displayType, TYPE_NUM);
        textSize = typedArray.getDimension(R.styleable.VirgoCircleClock_txtSize, DEF_SIZE_TEXT);
        strokeSize = typedArray.getDimension(R.styleable.VirgoCircleClock_strokeSize, DEF_FRAME_STROKE);
        padding = typedArray.getDimensionPixelSize(R.styleable.VirgoCircleClock_paddingFrame, DEF_PADDING);

        initData();
    }

    /**
     * 设置|更改布局时调用
     *
     * @param width  容器宽
     * @param height 容器高
     */
    public void initLayout(int width, int height) {
        Log.d(TAG, "initLayout: " + width + " - " + height);
        //取小
        //表盘直径
        int diameter = Math.min(width, height);

        //半径
        radius = (int) ((diameter - padding - strokeSize) / 2);

        //圆心
        centerX = diameter / 2;
        centerY = diameter / 2;

        markY = (int) (padding + strokeSize + offMark);
        txtY = markY + radiusBig * 2 + offText;
    }

    private void initData() {
        paintMain = new Paint();
        paintMain.setAntiAlias(true);
        paintMain.setStyle(Paint.Style.FILL);

        paintFrame = new Paint();
        paintFrame.setAntiAlias(true);
        paintFrame.setStyle(Paint.Style.STROKE);
        paintFrame.setStrokeWidth(strokeSize);

        calendar = Calendar.getInstance();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0 && h > 0) {
            initLayout(w, h);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (radius < 1) {
            return;
        }
        getTimes();

        //画表盘
        drawPlate(canvas);

        //画指针
        drawPointHour(canvas);
        drawPointMinutes(canvas);
        drawPointSeconds(canvas);
    }

    //刷新时间
    private void getTimes() {
        calendar.setTimeInMillis(System.currentTimeMillis());
        hours = calendar.get(Calendar.HOUR_OF_DAY);
        minutes = calendar.get(Calendar.MINUTE);
        seconds = calendar.get(Calendar.SECOND);
    }

    //画表盘
    private void drawPlate(Canvas canvas) {
        //背景
        if(needBg){
            paintMain.setColor(bgColor);
            canvas.drawCircle(centerX, centerY, radius, paintMain);
        }

        //边框
        paintFrame.setColor(frameColor);
        canvas.drawCircle(centerX, centerY, radius, paintFrame);

        canvas.save();
        //刻度
        for (int i = 0; i < 60; i++) {
            if (i % 5 == 0) {
                //大刻度
                paintMain.setColor(markColor1);
                canvas.drawCircle(centerX - radiusBig / 2f, markY, radiusBig, paintMain);
            } else {
                paintMain.setColor(markColor2);
                canvas.drawCircle(centerX - radiusSmall / 2f, markY, radiusSmall, paintMain);
            }
            canvas.rotate(6, centerX, centerY);
        }

        canvas.restore();

        paintMain.setColor(textColor);
        paintMain.setTextSize(textSize);
        //数字
        for (int i = 1; i <= 12; i++) {
            //计算每个数字所在位置的角度
            double radians = Math.toRadians(30 * i); //将角度转换为弧度,以便计算正弦值和余弦值
            String hourText;
            if (displayStyle == TYPE_ROMA) {
                hourText = getHoursGreece(i);
            } else {
                hourText = String.valueOf(i);
            }

            Rect rect = new Rect(); //获取数字的宽度和高度
            paintMain.getTextBounds(hourText, 0, hourText.length(), rect);
            int textWidth = rect.width();
            int textHeight = rect.height();
            canvas.drawText(hourText,
                    (float) (centerX + (radius - txtY) * Math.sin(radians) - textWidth / 2),
                    (float) (centerY - (radius - txtY) * Math.cos(radians) + textHeight / 2),
                    paintMain); //通过计算出来的坐标进行数字的绘制
        }
    }

    private void drawPointHour(Canvas canvas) {
        //画时针
        drawPoint(canvas, 360 / 12 * hours + (30 * minutes / 60), RATE_HOUR, RATE_HOUR_TAIL, RATE_HOUR_WIDTH);
    }

    private void drawPointMinutes(Canvas canvas) {
        //画分针
        drawPoint(canvas, 360 / 60 * minutes + (6 * seconds / 60), RATE_MINUTE, RATE_MINUTE_TAIL, RATE_MINUTE_WIDTH);
    }

    private void drawPointSeconds(Canvas canvas) {
        paintMain.setColor(secondColor);
        //画秒针
        drawPoint(canvas, 360 / 60 * seconds, RATE_SECOND, RATE_SECOND_TAIL, RATE_SECOND_WIDTH);
    }

    /**
     * 画指针
     *
     * @param canvas    画布
     * @param degree    指针走过角度
     * @param rateLen   正向长度与半径比
     * @param rateTail  尾部长度与半径比
     * @param rateWidth 宽度占半径比
     */
    private void drawPoint(Canvas canvas, int degree, float rateLen, float rateTail, float rateWidth) {
        //角度的计算由当前的小时占用的角度加上分针走过的百分比占用的角度之和
        double radians = Math.toRadians(degree);
        //时针的起点为圆的中点
        //通过三角函数计算时针终点的位置,时针最短,取长度的0.5倍
        int endX = (int) (centerX + radius * Math.cos(radians) * rateLen); //计算直线终点x坐标
        int endY = (int) (centerY + radius * Math.sin(radians) * rateLen); //计算直线终点y坐标
        canvas.save();
        paintMain.setStrokeWidth(radius / rateWidth);
        //初始角度是0,应该从12点钟开始算,所以要逆时针旋转90度
        canvas.rotate((-90), centerX, centerY); // 因为角度是从x轴为0度开始计算的,所以要逆时针旋转90度,将开始的角度调整到与y轴重合
        canvas.drawLine(centerX, centerY, endX, endY, paintMain); //根据起始坐标绘制时针
        radians = Math.toRadians(degree - 180); //时针旋转180度,绘制小尾巴
        endX = (int) (centerX + radius * Math.cos(radians) * rateTail);
        endY = (int) (centerY + radius * Math.sin(radians) * rateTail);
        canvas.drawLine(centerX, centerY, endX, endY, paintMain);
        canvas.restore();
    }

    private String getHoursGreece(int i) {
        switch (i) {
            case 1:
                return "I";
            case 2:
                return "II";
            case 3:
                return "III";
            case 4:
                return "IV";
            case 5:
                return "V";
            case 6:
                return "VI";
            case 7:
                return "VII";
            case 8:
                return "VIII";
            case 9:
                return "IX";
            case 10:
                return "X";
            case 11:
                return "XI";
            case 12:
            default:
                return "XII";
        }
    }

    //===================================================
    private final int MSG_UPDATE_TIME = 1;

    private final Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what == MSG_UPDATE_TIME) {
                invalidate();
                startTask();
            }
            return false;
        }
    });

    private void startTask() {
        handler.removeMessages(MSG_UPDATE_TIME);
        handler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, 1000L);
    }

    private void pauseTask() {
        removeMsg();
    }

    private void stopTask() {
        removeMsg();
    }

    private void removeMsg() {
        handler.removeMessages(MSG_UPDATE_TIME);
    }

    //==========================api========================================
    public void startPlay() {
        startTask();
    }

    public void pausePlay() {
        pauseTask();
    }

    public void continuePlay() {
        startTask();
    }

    public void releaseView() {
        stopTask();
    }
}
相关推荐
xiangpanf29 分钟前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx3 小时前
安卓线程相关
android
消失的旧时光-19434 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon5 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon5 小时前
VSYNC 信号完整流程2
android
dalancon5 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户69371750013846 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android6 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才7 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶8 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle