Android自定义View实现时钟功能

最近在练习自定义view, 想起之前面试的时候笔试有道题是写出自定义一个时钟的关键代码. 今天就来实现一下. 步骤依然是先分析, 再上代码.

实现效果

View分析

时钟主要分为五个部分:

1、中心点: 圆心位置

2、圆盘: 以中心点为圆心,drawCircle画个圆

3、刻度:

paint有个aip, setPathEffect可以根据path画特效, 那么刻度就可以根据圆的path画一个矩形path的特效, 并且这个api只会画特效, 不会画出圆.

/** * shape: 特效的path, 这里传一个矩形 * advance: 两个特效path之间的间距, 即两个矩形的left间距 * phase: 特效起始位置的偏移 * style: 原始path拐弯的时候特效path的转换方式,这里用ROTATE跟着旋转即可 */ PathDashPathEffect(Path shape, float advance, float phase,                               Style style)

刻度又分两种, 粗一点刻度: 3、6、9、12, 和细一点的刻度. 两种特效又可以用SumPathEffect合起来画

SumPathEffect(PathEffect first, PathEffect second) 

4、时分秒指针

时分秒指针都是一个圆角矩形, 先把他们的位置计算出来, 然后旋转圆心去绘制不同角度的指针.

5、动画效果

TimerTask每隔一秒计算时间, 根据时间去换算当前时分秒指针的角度, 动态变量只有三个角度.

实现源码 // // Created by skylar on 2022/4/19. // class ClockView : View {     private var mTimer: Timer? = null     private val mCirclePaint: Paint = Paint()     private val mPointerPaint: Paint = Paint()     private val mTextPaint: Paint = Paint()     private val mCirclePath: Path = Path()     private val mHourPath: Path = Path()     private val mMinutePath: Path = Path()     private val mSecondPath: Path = Path()     private lateinit var mPathMeasure: PathMeasure     private lateinit var mSumPathEffect: SumPathEffect     private var mViewWidth = 0     private var mViewHeight = 0     private var mCircleWidth = 6f     private var mRadius = 0f     private var mRectRadius = 20f     private var mHoursDegree = 0f     private var mMinutesDegree = 0f     private var mSecondsDegree = 0f     private var mCurrentTimeInSecond = 0L     constructor(context: Context) : super(context)     constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)     constructor(         context: Context, attrs: AttributeSet?,         defStyleAttr: Int     ) : super(context, attrs, defStyleAttr)     init {         mCirclePaint.color = Color.BLACK         mCirclePaint.isAntiAlias = true         mCirclePaint.style = Paint.Style.STROKE         mCirclePaint.strokeWidth = mCircleWidth         mPointerPaint.color = Color.BLACK         mPointerPaint.isAntiAlias = true         mPointerPaint.style = Paint.Style.FILL         mTextPaint.color = Color.BLACK         mTextPaint.isAntiAlias = true         mTextPaint.style = Paint.Style.FILL         mTextPaint.textSize = 40f     }     override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {         super.onSizeChanged(w, h, oldw, oldh)         mViewWidth = measuredWidth - paddingLeft - paddingRight         mViewHeight = measuredHeight - paddingTop - paddingBottom         mRadius = mViewWidth / 2 - mCircleWidth         mCirclePath.addCircle(0f, 0f, mRadius, Path.Direction.CW)         mPathMeasure = PathMeasure(mCirclePath, false)         val minutesShapePath = Path()         val quarterShapePath = Path()         minutesShapePath.addRect(0f, 0f, mRadius * 0.01f, mRadius * 0.06f, Path.Direction.CW)         quarterShapePath.addRect(0f, 0f, mRadius * 0.02f, mRadius * 0.06f, Path.Direction.CW)         val minutesDashPathEffect = PathDashPathEffect(             minutesShapePath,             mPathMeasure.length / 60,             0f,             PathDashPathEffect.Style.ROTATE         )         val quarterDashPathEffect = PathDashPathEffect(             quarterShapePath,             mPathMeasure.length / 12,             0f,             PathDashPathEffect.Style.ROTATE         )         mSumPathEffect = SumPathEffect(minutesDashPathEffect, quarterDashPathEffect)         val hourPointerHeight = mRadius * 0.5f         val hourPointerWidth = mRadius * 0.07f         val hourRect = RectF(             -hourPointerWidth / 2,             -hourPointerHeight * 0.7f,             hourPointerWidth / 2,             hourPointerHeight * 0.3f         )         mHourPath.addRoundRect(hourRect, mRectRadius, mRectRadius, Path.Direction.CW)         val minutePointerHeight = mRadius * 0.7f         val minutePointerWidth = mRadius * 0.05f         val minuteRect = RectF(             -minutePointerWidth / 2,             -minutePointerHeight * 0.8f,             minutePointerWidth / 2,             minutePointerHeight * 0.2f         )         mMinutePath.addRoundRect(minuteRect, mRectRadius, mRectRadius, Path.Direction.CW)         val secondPointerHeight = mRadius * 0.9f         val secondPointerWidth = mRadius * 0.03f         val secondRect = RectF(             -secondPointerWidth / 2,             -secondPointerHeight * 0.8f,             secondPointerWidth / 2,             secondPointerHeight * 0.2f         )         mSecondPath.addRoundRect(secondRect, mRectRadius, mRectRadius, Path.Direction.CW)         startAnimator()     }     override fun onDraw(canvas: Canvas?) {         super.onDraw(canvas)         if (canvas == null) {             return         }         canvas.translate((mViewWidth / 2).toFloat(), (mViewHeight / 2).toFloat())         //画圆盘         mCirclePaint.pathEffect = null         canvas.drawPath(mCirclePath, mCirclePaint)         //画刻度         mCirclePaint.pathEffect = mSumPathEffect         canvas.drawPath(mCirclePath, mCirclePaint)         //时分秒指针         mPointerPaint.color = Color.BLACK         canvas.save()         canvas.rotate(mHoursDegree)         canvas.drawPath(mHourPath, mPointerPaint)         canvas.restore()         canvas.save()         canvas.rotate(mMinutesDegree)         canvas.drawPath(mMinutePath, mPointerPaint)         canvas.restore()         canvas.save()         canvas.rotate(mSecondsDegree)         canvas.drawPath(mSecondPath, mPointerPaint)         canvas.restore()         //画中心点         mPointerPaint.color = Color.WHITE         canvas.drawCircle(0f, 0f, mRadius * 0.02f, mPointerPaint)     }     private fun startAnimator() {         val cal = Calendar.getInstance()         val hour = cal.get(Calendar.HOUR)  //小时         val minute = cal.get(Calendar.MINUTE)  //分         val second = cal.get(Calendar.SECOND)  //秒         mCurrentTimeInSecond = (hour * 60 * 60 + minute * 60 + second).toLong()         if (mTimer == null) {             mTimer = Timer()         } else {             mTimer?.cancel()             mTimerTask.cancel()         }         mTimer?.schedule(mTimerTask, 0, 1000)     }     private var mTimerTask: TimerTask = object : TimerTask() {         override fun run() {             mCurrentTimeInSecond++             computeDegree()             invalidate()         }     }     //12小时 00:00:00 ~ 12:00:00     private fun computeDegree() {         val secondsInOneRoll = 12 * 60 * 60         val currentSeconds = mCurrentTimeInSecond % secondsInOneRoll         var leftSeconds = currentSeconds         val hours = currentSeconds / 60 / 60         leftSeconds = currentSeconds - hours * 60 * 60         val minutes = leftSeconds / 60         leftSeconds -= minutes * 60         val seconds = leftSeconds % 60         mHoursDegree = hours * 30f         mMinutesDegree = minutes * 6f         mSecondsDegree = seconds * 6f     } }

推荐阅读