先上效果图:
知道自定义view很强大,能实现很多酷炫的UI效果,一直都没上手玩过,项目中遇到需要自定义的需求几乎都是搜一搜别人已经完善的特别好的来用
这两天看到别人博客里有一个类似图2的时钟,就想着这个该怎么实现,于是就写了个小demo。
首先我们自定义一个Clock继承View,这里我添加了几个简单的自定义属性(这里如要加什么属性可按照自己的需求来添加):
在res->values下新建attrs.xml
<declare-styleable name="Clock">
<attr name="scaleColor" format="color" />
<attr name="circleColor" format="color" />
<attr name="defScaleColor" format="color" />
<attr name="hourHandColor" format="color" />
<attr name="minuteHandColor" format="color" />
<attr name="secondHandColor" format="color" />
<attr name="centerIcon" format="reference" />
</declare-styleable>
在初始化的时候进行赋值:
var ta: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.Clock)
scaleColor = ta.getColor(R.styleable.Clock_scaleColor, Color.parseColor("#666666"))
circleColor = ta.getColor(R.styleable.Clock_circleColor, Color.parseColor("#666666"))
defScaleColor = ta.getColor(R.styleable.Clock_defScaleColor, Color.parseColor("#666666"))
hourHandColor = ta.getColor(R.styleable.Clock_hourHandColor, Color.parseColor("#666666"))
minuteHandColor =
ta.getColor(R.styleable.Clock_minuteHandColor, Color.parseColor("#666666"))
secondHandColor =
ta.getColor(R.styleable.Clock_secondHandColor, Color.parseColor("#fc0101ff"))
这里的centerIcon是图一中心的小图标,我这里是通过获取图标的ResourceId的方式来绘制的,下面是设置图标大小的相关代码,初始化完成之后要记得ta.recycle():
//设置中心图标的大小
var centerIconResourceId = ta.getResourceId(R.styleable.Clock_centerIcon, R.mipmap.star)
centerIcon = BitmapFactory.decodeResource(context.resources, centerIconResourceId)
var width = centerIcon.width
var height = centerIcon.height
var newWidth = 100
var newHeight = 100
val scaleWidth = newWidth.toFloat() / width
val scaleHeight = newHeight.toFloat() / height
//获取想要缩放的matrix
val matrix = Matrix()
matrix.postScale(scaleWidth, scaleHeight)
centerIcon = Bitmap.createBitmap(centerIcon, 0, 0, width, height, matrix, true)
ta.recycle()
属性准备好了之后然后是准备画笔:
/*刻度画笔*/
private var scalePaint = Paint()
/*时针画笔*/
private var hourHandPaint = Paint()
/*分针画笔*/
private var minuteHandPaint = Paint()
/*秒针画笔*/
private var secondHandPaint = Paint()
/*中心图标*/
private var centerIconPaint = Paint()
/*圆周画笔*/
private var circlePaint = Paint()
/*路径画笔*/
private var pathPaint = Paint()
在刚刚初始化属性的代码中将画笔也进行初始化(这里画笔的宽度我是写死的,可以添加到自定义属性中由用户自由控制):
circlePaint.color = circleColor
circlePaint.isAntiAlias = true
circlePaint.style = Paint.Style.STROKE
circlePaint.strokeWidth = 4f
scalePaint.color = scaleColor
scalePaint.isAntiAlias = true
scalePaint.style = Paint.Style.STROKE
hourHandPaint.color = hourHandColor
hourHandPaint.strokeWidth = 10f
hourHandPaint.isAntiAlias = true
hourHandPaint.style = Paint.Style.FILL_AND_STROKE
minuteHandPaint.color = minuteHandColor
minuteHandPaint.isAntiAlias = true
minuteHandPaint.strokeWidth = 7f
minuteHandPaint.style = Paint.Style.FILL_AND_STROKE
secondHandPaint.color = secondHandColor
secondHandPaint.isAntiAlias = true
secondHandPaint.strokeWidth = 4f
secondHandPaint.style = Paint.Style.FILL_AND_STROKE
pathPaint.color = circleColor
pathPaint.isAntiAlias = true
pathPaint.strokeWidth = 4f
pathPaint.style = Paint.Style.FILL
接下来就是获取当前的时间 也同样在初始化方法中:
var calendar = Calendar.getInstance()
hours = calendar.get(Calendar.HOUR)
minutes = calendar.get(Calendar.MINUTE)
seconds = calendar.get(Calendar.SECOND)
//开始计时刷新时钟
start()
前期的准备工作弄好之后,开始来进行自定义view的几个核心步骤,onMeasure() ,onLayout(),onDraw()由于我们现在定义的不是ViewGroup,所以不需要实现onLayout()
首先通onMeasure()来确定控件的宽高,这里根据MeasureSpec的三种模式来得出,再通过 onSizeChanged()来设置大小及确定中心点的位置和时钟的半径:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var width: Int = getMeasureSize(true, widthMeasureSpec)
var height: Int = getMeasureSize(false, heightMeasureSpec)
setMeasuredDimension(width, height)
}
private fun getMeasureSize(isWidth: Boolean, measureSpec: Int): Int {
var result = 0
var specSize = MeasureSpec.getSize(measureSpec)
var specMode = MeasureSpec.getMode(measureSpec)
when (specMode) {
MeasureSpec.AT_MOST -> {
result = if (isWidth) {
specSize.coerceAtMost(mWidth)
} else {
specSize.coerceAtMost(mheight)
}
}
MeasureSpec.EXACTLY -> {
result = specSize
}
MeasureSpec.UNSPECIFIED -> {
if (isWidth) {
result = suggestedMinimumWidth
} else {
result = suggestedMinimumHeight
}
}
}
return result
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mWidth = w
mheight = h
mCenterX = w / 2.0f
mCenterY = h / 2.0f
mClockRadius = (h / 2.0f) * 0.9f
}
确定好控件大小之后,就开始绘制了,绘制都在onDraw()中进行,
图一所展示的时钟绘制如下:
在画刻度和指针时用到的canvas.rotate(r)方法,我理解的是将画布旋转多少度,绘制的时候就都可以看做在横轴坐标0的位置进行绘制。
每次绘制完了之后复原画布
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//这里是将画布的起始点移到画布中心,中心点的坐标就变成(0,0)
canvas?.translate(mCenterX, mCenterY)
drawCircle(canvas)
drawScale(canvas)
drawHour(canvas)
drawminute(canvas)
drawSeconds(canvas)
drawCenterIcon(canvas)
}
/*画秒针*/
private fun drawSeconds(canvas: Canvas?) {
//分针一分钟会增加6度
Log.i("cx----canvasSeconds", "" + seconds)
canvas?.save()
canvas?.rotate(seconds * 6f)
canvas?.drawLine(0f, 10f, 0f, -mClockRadius + 40f, secondHandPaint)
canvas?.restore()
}
/*画分针*/
private fun drawminute(canvas: Canvas?) {
//分针一分钟会增加6度
canvas?.save()
canvas?.rotate(minutes * 6f)
canvas?.drawLine(0f, 10f, 0f, -mClockRadius + 80f, minuteHandPaint)
canvas?.restore()
}
/*画时针*/
private fun drawHour(canvas: Canvas?) {
//时针一小时为30度,分针一分钟时针会增加0.5度
canvas?.save()
canvas?.rotate(hours * 30f + minutes * 0.5f)
canvas?.drawLine(0f, 10f, 0f, -mClockRadius + 120f, hourHandPaint)
canvas?.restore()
}
/*画表盘中心图标*/
private fun drawCenterIcon(canvas: Canvas?) {
canvas?.save()
//这里的-50是上面bitmap款的一半,因为中心点是起始点,所以画图标的其实坐标是-50,-50
canvas?.drawBitmap(centerIcon, -50f, -50f, centerIconPaint)
canvas?.restore()
}
/*画刻度*/
private fun drawScale(canvas: Canvas?) {
for (i in 0 until 60) {
if (i % 5 == 0) {
//特殊刻度
scalePaint.setColor(scaleColor)
scalePaint.strokeWidth = 6f
canvas?.drawLine(0f, -mClockRadius + 4f, 0f, -mClockRadius + 30f, scalePaint)
} else {
//一般刻度
scalePaint.setColor(defScaleColor)
scalePaint.strokeWidth = 3f
canvas?.drawLine(0f, -mClockRadius + 4f, 0f, -mClockRadius + 20f, scalePaint)
}
//每画一个刻度将画布旋转6度(一分钟的一格)
canvas?.rotate(6f)
}
}
/*画时钟的外圆*/
private fun drawCircle(canvas: Canvas?) {
canvas?.drawCircle(0f, 0f, mClockRadius, circlePaint)
}
图二所展示的时钟绘制如下:同样在 onDraw()方法中进行
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//这里是将画布的起始点移到画布中心,中心点的坐标就变成(0,0)
canvas?.translate(mCenterX, mCenterY)
drawPath(canvas)
}
/*画出三个指针组成的三角形*/
private fun drawPath(canvas: Canvas?) {
canvas?.save()
var path = Path()
//计算角度,这里减90是起始位置按12点算的话,就是-90°
var hoursAngle = hours * 30f + minutes * 0.5f - 90
var minutesAngle = minutes * 6f - 90
var secondsAngle = seconds * 6f - 90
//根据角度算出时针分针秒针的坐标点
var xHours =
(mClockRadius - 120f) * kotlin.math.cos(hoursAngle * 3.14 / 180)
var yHours =
(mClockRadius - 120f) * kotlin.math.sin(hoursAngle * 3.14 / 180)
var xMinutes =
(mClockRadius - 80f) * kotlin.math.cos(minutesAngle * 3.14 / 180)
var yMinutes =
(mClockRadius - 80f) * kotlin.math.sin(minutesAngle * 3.14 / 180)
var xSeconds =
(mClockRadius - 40f) * kotlin.math.cos(secondsAngle * 3.14 / 180)
var ySeconds =
(mClockRadius - 40f) * kotlin.math.sin(secondsAngle * 3.14 / 180)
//将坐标点连起来画出三角形
path.moveTo(xHours.toFloat(), yHours.toFloat())
path.lineTo(xMinutes.toFloat(), yMinutes.toFloat())
path.lineTo(xSeconds.toFloat(), ySeconds.toFloat())
path.close()
canvas?.drawPath(path, pathPaint)
canvas?.restore()
}
指针刻度都画完了,现在就让时钟动起来,非常简单,定义一个计时器,在初始化中开始计时即可,计时中的postInvalidate()是用于刷新布局:
/**
* 定时器
*/
private val mTimer = Timer()
private val task: TimerTask = object : TimerTask() {
override fun run() {
var calendar = Calendar.getInstance()
hours = calendar.get(Calendar.HOUR)
minutes = calendar.get(Calendar.MINUTE)
seconds = calendar.get(Calendar.SECOND)
postInvalidate()
}
}
/**
* 开启定时器
*/
fun start() {
mTimer.schedule(task, 0, 1000)
}
这里完成了时钟的自定义,可以在自定义属性中增加自己需要的属性做扩展,起始完成之后也没有想象中那么复杂,这里做的记录也是为了让自己从头梳理一遍这些步骤,加深印象。