初衷

工作四年了,能力水平并没有跟工作年限挂上钩,至今依旧是一个搞开发的小僧。由于公司是做物联网(车载、POS)的,项目UI并不像互联网公司那样花里胡哨的,所以自定义View一直在项目中体现的并不是很多,但是自定义View是一个Android工程师必备的一项基础技能,所以平常也会自己进行一些简单的练习。突发奇想做了一个时钟,感觉挺好玩的,网上也有很多这样的博客,这里也只是做一个小小的分享,能力不足水平有限仅供参考。

效果图

android自定义时间起始选择器 android自定义时钟_自定义时钟

android自定义时间起始选择器 android自定义时钟_android自定义时间起始选择器_02

流程

自定义View的流程很简单,无非就是一下这么几步:

  • 构建属性:attrs.xml
  • 构造方法中获取属性参数信息
  • 重写onDraw()方法:绘制View
  • 重写onMeasure()方法:确定View尺寸
  • 动画:通过动画可以让View更加的炫酷

工具

下述内容是做的总结可以忽略跳过,直接进入正文内容
Paint
顾明思议就是画笔的一次,既然是画笔那么功能自然是不少的,上菜。。

  • color/setARGB:设置画笔颜色
  • alpha:设置透明度
  • antiAlias:设置是否抗锯齿
  • style:画笔风格,Paint.Style.FILL(填充)、Paint.Style.STROKE(描边)、Paint.Style.FILL_AND_STROKE(填充并且描边)
  • strokeWidth:描边的宽度
  • strokeCap:线条末端样式,Paint.Cap.BUTT(默认)、Paint.Cap.ROUND(末端增加圆角)、Paint.Cap.SQUARE(末端增加矩形)
  • strokeJoin:拐角风格, MITER(尖角 ) 、 ROUND(圆角)、BEVEL(折角)
  • strokeMiter:对于strokeJoin的一个补充,补偿
  • setShader:颜色渲染器,LinearGradient( 线性渐变)、RadialGradient (辐射渐变)、SweepGradient (扫描渐变)、BitmapShader(bitmap着色)、ComposeShader( 混合着色器)
  • colorFilter:颜色过滤器,LightingColorFilter(模拟简单的光照效果)、PorterDuffColorFilter(合成)、ColorMatrixColorFilter(使用ColorMatrix 颜色处理)
  • filterBitmap:是否使用双线性过滤
  • pathEffect:图形轮廓效果,CornerPathEffect(把所有拐角变成圆角)、DiscretePathEffect(把线条进行随机的偏离)、DashPathEffect(虚线)、PathDashPathEffect(使用path绘制想要的效果)、SumPathEffectComposePathEffect(组合效果)
  • setShadowLayer(float radius, float dx, float dy, int shadowColor):在绘制内容下面加一层阴影
  • getTextpath(..):获取绘制的path
  • textSizetextAligntextSkewXtextScaleX:依次是文字大小、位置、缩放、错切
  • …等等

Canvas
有了画笔,也要有画布

  • drawRect(float left, float top, float right, float bottom, Paint paint)drawRect(RectF rect, Paint paint) :绘制矩形
  • drawRoundRect(float left, float top, float right, float bottom, float rx(圆角x大小), float ry(圆角x大小), Paint paint) drawRoundRect( RectF rect, float rx, float ry, Paint paint):绘制有圆角的矩形
    rx:表示圆角x大小
    ry:表示圆角y大小
  • drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint):绘制弧形
    startAngle:表示起始角度
    sweepAngle:表示(扫描的角度
    useCenter:表示是否闭合
  • drawCircle(float cx, float cy, float radius, Paint paint)绘制圆形,(x轴圆心坐标,y轴圆心坐标,半径)
  • drawPoint(float x, float y, Paint paint):绘制点
    -drawPoints(float[] pts, int offset, int count, Paint paint)drawPoints(float[] pts, Paint paint):绘制多个点
    offset:表示跳过数组的前几个数再开始记坐标
    count:表示一共要绘制几个点
  • drawOval(float left, float top, float right, float bottom, Paint paint) :绘制椭圆
  • drawLine(float startX, float startY, float stopX, float stopY, Paint paint): 绘制线
  • drawLines(float[] pts, int offset, int count, Paint paint)drawLines(float[] pts, Paint paint): 批量画线
  • drawPath(Path path, Paint paint) :绘制自定义图形

绘制的辅助

  • 裁切
  • clipRect(int left, int top, int right, int bottom)
  • clipPath(Path path) :裁切
  • 几何变换 (canvas.save()开始 canvas.restore()结束)

2.1 Canvas 的二维变换

  • Canvas.translate(float dx, float dy) :平移
  • Canvas.scale(float sx, float sy, float px, float py):缩放
    sx、sy:X/Y轴缩放倍数
    px、py:缩放轴心
  • skew(float sx, float sy):错切
    sx、sy:X/Y轴方向的错切系数
    .
    2.2 Matrix
  • pre/postTranslate/Rotate/Scale/Skew():效果与Canvas一样
val matrix1 = Matrix()

canvas.save();
matrix1.reset();
matrix1.postScale(1.5f, 1.5f, point.x + bitmapWidth / 2, point.y + bitmapHeight / 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmap, point.x, point.y, paint);
canvas.restore();
  • Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount): 用点对点映射的方式设置变换
    .
    2.3 Camera 三维变换
  • Camera.translate(float x, float y, float z):移动
  • Camera.rotate(float x)Camera.rotate(float y)Camera.rotate(float x, float y, float z): 三维旋转
  • Camera.setLocation(x, y, z):设置虚拟相机的位置

正文

步骤:

  1. 定义属性
  2. 绘制表环
  3. 绘制大刻度
  4. 绘制小刻度
  5. 绘制文字
  6. 绘制时针
  7. 绘制分针
  8. 绘制秒针
  9. 转起来
  10. 初始化以及动画
  11. 退出程序,停止动画
  12. 使用

属性attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ClockView">
        <!--表环颜色-->
        <attr name="clock_ring_color" format="color"/>
        <!--时钟颜色-->
        <attr name="clock_hour_color" format="color"/>
        <!--分钟颜色-->
        <attr name="clock_min_color" format="color"/>
        <!--秒钟颜色-->
        <attr name="clock_second_color" format="color"/>
        <!--大刻度颜色-->
        <attr name="clock_big_scale_color" format="color"/>
        <!--小刻度颜色-->
        <attr name="clock_small_scale_color" format="color"/>
        <!--文字颜色-->
        <attr name="clock_text_color" format="color"/>
        <!--表盘颜色是否充满(表环的颜色)-->
        <attr name="clock_bg_fill" format="boolean"/>
    </declare-styleable>
</resources>

绘制表环

private fun drawCircle(canvas: Canvas) {
        canvas.drawCircle(centerX, centerY, radius, circlePaint)
    }

绘制刻度以及文字

private fun drawScale(canvas: Canvas) {
        canvas.save()
        //顺时针旋转30° 让12点在正顶部
        canvas.rotate(360 / 12f, centerX, centerY)
        //总共60个刻度
        val angle = 360 / 60f
        for (i in 0 until 60) {
            //绘制大刻度以及文字
            if (i % 5 == 0) {
                canvas.drawLine(
                    phoneWidth / 2f,
                    //间隔的位置坐标减去stroke的一半+圆的stroke(因为会超出)
                    space - bigScaleStroke / 2 + stroke,
                    phoneWidth / 2f,
                    space + bigScaleLen,
                    bigScalePaint
                )
                val textWidth = textPaint.measureText(itemHour[i / 5].toString())
                canvas.drawText(
                    itemHour[i / 5].toString(),
                    centerX - textWidth / 2,
                    space + bigScaleLen + bigScaleStroke + textSize,
                    textPaint
                )
            } else {
                //绘制小刻度
                canvas.drawLine(
                    phoneWidth / 2f,
                    space - smallScaleStroke / 2 + stroke,
                    phoneWidth / 2f,
                    space + smallScaleLen,
                    smallScalePaint
                )
            }
            canvas.rotate(angle, centerX, centerY)
        }
        canvas.restore()
    }

绘制时针

private fun drawHour(canvas: Canvas) {
        canvas.save()
        canvas.rotate(hourDegrees, centerX, centerY)
        //radius-100:时针长度,时针默认8点即旋转-30° ,因此Y默认左边为radius-100
        canvas.drawLine(centerX, centerY, radius - 100, centerY, hourPaint)
        canvas.restore()
    }

绘制分针

private fun drawMin(canvas: Canvas) {
        canvas.save()
        canvas.rotate(minDegrees, centerX, centerY)
        //radius-180:分针长度,默认0°即对准12点, ,因此Y默认左边为radius-180
        canvas.drawLine(centerX, centerY, centerX, radius - 180, minPaint)
        canvas.restore()
    }

绘制秒针

private fun drwSecond(canvas: Canvas) {
        canvas.save()
        canvas.rotate(secondDegrees, centerX, centerY)
        //radius-秒针:时针长度,默认0°即对准12点,因此Y默认左边为radius-200
        canvas.drawLine(centerX, centerY, centerX, radius - 200, secondPaint)
        canvas.restore()
    }

由于各个时针、分钟、秒针旋转角度都是变量因此,只需要控制好这几个变量的变化即可,使用Handler进行控制指针动画

//时区使用北京时区,默认8点,时针偏转到-30°
    var hourDegrees = -30f
    lateinit var animatorHandler: Handler
    private fun startAnimator() {
        animatorHandler = Handler()
        val runnable = object : Runnable {
            override fun run() {
                //秒针
                //大于等于360说明过了一分钟
                if (secondDegrees >= 360) {
                    secondDegrees = 360 / 60f
                    //1分钟 时针走了0.5°
                    if (hourDegrees >= 360) {
                        hourDegrees = 0f
                    } else {
                        hourDegrees += 0.5f
                    }
                } else {
                    secondDegrees += 360 / 60f
                }
                //分钟:秒针转过1圈
                if (minDegrees >= 360) {
                    minDegrees = 360 / 60 / 60f
                } else {
                    minDegrees += 360 / 60 / 60f
                }
                animatorHandler.postDelayed(this, 1000)
            }
        }
        animatorHandler.post(runnable)
    }

初始化时动画

fun setCurrentTime(hour: Int, min: Int, second: Int) {
        var currentHour = hour
        if (currentHour > 12) {
            currentHour -= 12
        }
        hourDegrees = (9 - hour) * (-30).toFloat()
        minDegrees = min * 6f
        secondDegrees = second * 6f

        val secondAnimator = ValueAnimator.ofFloat(0f, secondDegrees)
        secondAnimator.addUpdateListener {
            secondDegrees = it.animatedValue as Float
        }
        val minAnimator = ValueAnimator.ofFloat(0f, minDegrees)
        minAnimator.addUpdateListener {
            minDegrees = it.animatedValue as Float
        }
        val hourAnimator = ValueAnimator.ofFloat(-30f, hourDegrees)
        hourAnimator.addUpdateListener {
            hourDegrees = it.animatedValue as Float
        }
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(secondAnimator, minAnimator, hourAnimator)
        animatorSet.duration = 1000
        animatorSet.start()
    }

停止动画

fun stop() {
        animatorHandler.removeCallbacksAndMessages(null)
    }

使用

<com.wxx.view.advance.clockdial.ClockView
        android:id="@+id/clockView"
        app:clock_ring_color="@color/colorPrimary"
        app:clock_big_scale_color="@color/colorPrimary"
        app:clock_small_scale_color="@color/colorPrimary"
        app:clock_hour_color="@color/colorPrimaryDark"
        app:clock_min_color="@android:color/holo_green_light"
        app:clock_second_color="@color/colorAccent"
        app:clock_text_color="@android:color/holo_blue_bright"
        app:clock_bg_fill="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
class ClockMainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.clock_view)

        val calendar = Calendar.getInstance()
        val hour = calendar.get(Calendar.HOUR + 1)
        val min = calendar.get(Calendar.MINUTE)
        val second = calendar.get(Calendar.SECOND)
        clockView.setCurrentTime(hour,min,second)
    }

    override fun onDestroy() {
        super.onDestroy()
        clockView.stop()
    }
}

完整代码

package com.wxx.view.advance.clockdial

import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.Handler
import android.util.AttributeSet
import android.view.View
import com.wxx.view.R

/**
 * @author :wuxinxi on 2019/12/12 .
 * @packages :com.wxx.view.advance.clockdial .
 * TODO:时钟
 * 1. 外圆
 * 2. 大刻度
 * 3. 小刻度
 * 4. 时针
 * 5. 分针
 * 6. 秒针
 */
class ClockView : View {

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet!!, 0)
    constructor(context: Context, attributeSet: AttributeSet, defStyle: Int) : super(
        context,
        attributeSet,
        defStyle
    ) {
        init(context, attributeSet)
    }

    val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    val bigScalePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    val smallScalePaint = Paint(Paint.ANTI_ALIAS_FLAG)
    val hourPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    val minPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    val secondPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    val textPaint = Paint(Paint.ANTI_ALIAS_FLAG)

    var circleColor = Color.RED
    var scaleColor = Color.BLACK
    var hourColor = Color.GREEN
    var minColor = Color.YELLOW
    var secondColor = Color.BLUE
    var textColor = Color.BLACK

    val stroke = 10f
    val bigScaleStroke = 7f
    val bigScaleLen = 40f
    val smallScaleStroke = 4f
    val smallScaleLen = 20f
    val hourStroke = 25f
    val minStroke = 15f
    val secondStroke = 8f

    val textSize = 50f

    var phoneWidth = 0
    var viewHeight = 0
    var radius = 0f
    var space = 100
    var centerX = 0f
    var centerY = 0f

    //背景是否全充满
    var bgFill = false

    private val itemHour = Array(12) { i -> i + 1 }

    private fun init(context: Context, attributeSet: AttributeSet) {
        val typeArray = context.obtainStyledAttributes(attributeSet, R.styleable.ClockView)
        circlePaint.color = typeArray.getColor(R.styleable.ClockView_clock_ring_color, circleColor)
        circlePaint.style = if (typeArray.getBoolean(
                R.styleable.ClockView_clock_bg_fill,
                bgFill
            )
        ) Paint.Style.FILL else Paint.Style.STROKE
        bigScalePaint.color = typeArray.getColor(R.styleable.ClockView_clock_big_scale_color, scaleColor)
        smallScalePaint.color = typeArray.getColor(R.styleable.ClockView_clock_small_scale_color, scaleColor)
        hourPaint.color = typeArray.getColor(R.styleable.ClockView_clock_hour_color, hourColor)
        minPaint.color = typeArray.getColor(R.styleable.ClockView_clock_min_color, minColor)
        secondPaint.color = typeArray.getColor(R.styleable.ClockView_clock_second_color, secondColor)
        textPaint.color = typeArray.getColor(R.styleable.ClockView_clock_text_color, textColor)

        typeArray.recycle()

        circlePaint.strokeWidth = stroke

        bigScalePaint.style = Paint.Style.STROKE
        bigScalePaint.strokeWidth = bigScaleStroke

        smallScalePaint.style = Paint.Style.STROKE
        smallScalePaint.strokeWidth = smallScaleStroke

        hourPaint.style = Paint.Style.STROKE
        hourPaint.strokeWidth = hourStroke
        hourPaint.strokeCap = Paint.Cap.ROUND

        minPaint.style = Paint.Style.STROKE
        minPaint.strokeWidth = minStroke
        minPaint.strokeCap = Paint.Cap.ROUND

        secondPaint.style = Paint.Style.STROKE
        secondPaint.strokeWidth = secondStroke
        secondPaint.strokeCap = Paint.Cap.ROUND

        textPaint.textSize = textSize


        val displayMetrics = context.resources.displayMetrics
        phoneWidth = displayMetrics.widthPixels

        radius = phoneWidth / 2f - space
        centerX = phoneWidth / 2f
        centerY = radius + space

        viewHeight =(centerY*2).toInt()

        startAnimator()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawCircle(canvas)
        drawScale(canvas)
        drawHour(canvas)
        drawMin(canvas)
        drwSecond(canvas)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(phoneWidth, viewHeight)

    }

    /**
     * 绘制表环
     */
    private fun drawCircle(canvas: Canvas) {
        canvas.drawCircle(centerX, centerY, radius, circlePaint)
    }

    /**
     * 绘制刻度
     */
    private fun drawScale(canvas: Canvas) {
        canvas.save()
        //顺时针旋转30° 让12点在正顶部
        canvas.rotate(360 / 12f, centerX, centerY)
        val angle = 360 / 60f
        for (i in 0 until 60) {
            if (i % 5 == 0) {
                canvas.drawLine(
                    phoneWidth / 2f,
                    //间隔的位置坐标减去stroke的一半+圆的stroke(因为会超出)
                    space - bigScaleStroke / 2 + stroke,
                    phoneWidth / 2f,
                    space + bigScaleLen,
                    bigScalePaint
                )
                val textWidth = textPaint.measureText(itemHour[i / 5].toString())
                canvas.drawText(
                    itemHour[i / 5].toString(),
                    centerX - textWidth / 2,
                    space + bigScaleLen + bigScaleStroke + textSize,
                    textPaint
                )
            } else {
                canvas.drawLine(
                    phoneWidth / 2f,
                    space - smallScaleStroke / 2 + stroke,
                    phoneWidth / 2f,
                    space + smallScaleLen,
                    smallScalePaint
                )
            }
            canvas.rotate(angle, centerX, centerY)
        }
        canvas.restore()
    }

    /**
     * 绘制秒针
     */
    private fun drwSecond(canvas: Canvas) {
        canvas.save()
        canvas.rotate(secondDegrees, centerX, centerY)
        canvas.drawLine(centerX, centerY, centerX, radius - 200, secondPaint)
        canvas.restore()
    }

    /**
     * 绘制分针
     */
    private fun drawMin(canvas: Canvas) {
        canvas.save()
        canvas.rotate(minDegrees, centerX, centerY)
        canvas.drawLine(centerX, centerY, centerX, radius - 180, minPaint)
        canvas.restore()
    }

    /**
     * 绘制时针
     */
    private fun drawHour(canvas: Canvas) {
        canvas.save()
        canvas.rotate(hourDegrees, centerX, centerY)
        canvas.drawLine(centerX, centerY, radius - 100, centerY, hourPaint)
        canvas.restore()
    }

    var secondDegrees = 0f
        set(value) {
            field = value
            invalidate()
        }

    var minDegrees = 0f

    //时区使用北京时区,默认8点,时针偏转到-30°
    var hourDegrees = -30f
    lateinit var animatorHandler: Handler
    private fun startAnimator() {
        animatorHandler = Handler()
        val runnable = object : Runnable {
            override fun run() {
                //秒针
                //大于等于360说明过了一分钟
                if (secondDegrees >= 360) {
                    secondDegrees = 360 / 60f
                    //1分钟 时针走了0.5°
                    if (hourDegrees >= 360) {
                        hourDegrees = 0f
                    } else {
                        hourDegrees += 0.5f
                    }
                } else {
                    secondDegrees += 360 / 60f
                }
                //分钟:秒针转过1圈
                if (minDegrees >= 360) {
                    minDegrees = 360 / 60 / 60f
                } else {
                    minDegrees += 360 / 60 / 60f
                }

                animatorHandler.postDelayed(this, 1000)
            }
        }
        animatorHandler.post(runnable)
    }

    /**
     * 设置当前时间
     */
    fun setCurrentTime(hour: Int, min: Int, second: Int) {
        var currentHour = hour
        if (currentHour > 12) {
            currentHour -= 12
        }
        hourDegrees = (9 - hour) * (-30).toFloat()
        minDegrees = min * 6f
        secondDegrees = second * 6f

        val secondAnimator = ValueAnimator.ofFloat(0f, secondDegrees)
        secondAnimator.addUpdateListener {
            secondDegrees = it.animatedValue as Float
        }
        val minAnimator = ValueAnimator.ofFloat(0f, minDegrees)
        minAnimator.addUpdateListener {
            minDegrees = it.animatedValue as Float
        }
        val hourAnimator = ValueAnimator.ofFloat(-30f, hourDegrees)
        hourAnimator.addUpdateListener {
            hourDegrees = it.animatedValue as Float
        }
        val animatorSet = AnimatorSet()
        animatorSet.playTogether(secondAnimator, minAnimator, hourAnimator)
        animatorSet.duration = 1000
        animatorSet.start()
    }

    fun stop() {
        animatorHandler.removeCallbacksAndMessages(null)
    }

}

项目地址

点我开车