初衷
工作四年了,能力水平并没有跟工作年限挂上钩,至今依旧是一个搞开发的小僧。由于公司是做物联网(车载、POS)的,项目UI并不像互联网公司那样花里胡哨的,所以自定义View一直在项目中体现的并不是很多,但是自定义View是一个Android工程师必备的一项基础技能,所以平常也会自己进行一些简单的练习。突发奇想做了一个时钟,感觉挺好玩的,网上也有很多这样的博客,这里也只是做一个小小的分享,能力不足水平有限仅供参考。
效果图
流程
自定义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绘制想要的效果)、SumPathEffect
、ComposePathEffect
(组合效果)setShadowLayer(float radius, float dx, float dy, int shadowColor)
:在绘制内容下面加一层阴影getTextpath(..)
:获取绘制的pathtextSize
、textAlign
、textSkewX
、textScaleX
:依次是文字大小、位置、缩放、错切- …等等
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)
:设置虚拟相机的位置
正文
步骤:
- 定义属性
- 绘制表环
- 绘制大刻度
- 绘制小刻度
- 绘制文字
- 绘制时针
- 绘制分针
- 绘制秒针
- 转起来
- 初始化以及动画
- 退出程序,停止动画
- 使用
属性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)
}
}