先来看效果图,gif图效果不是很好,可以在真机上看一波:
仔细分析效果:
接到UI设计的效果,你觉得我会怎么做?
一般的步骤:
- Google一波,没有找到类似的效果,所以就老老实实的分析一下效果,该如何实现?
- 内外圆弧、刻度、文字、动画、模糊、渐变的效果,分析完之后就开始吧:
画内圆:
private fun drawInnerCircle(canvas: Canvas) {
canvas.save()
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.maskFilter = BlurMaskFilter(2f, BlurMaskFilter.Blur.NORMAL)
paint.alpha = 0x70
paint.style = Paint.Style.STROKE
paint.strokeWidth = dp2px(mInnerStrokeWidth.toInt()).toFloat()
canvas.translate(measuredWidth / 2f, measuredHeight / 2f)
val radius = Math.min(measuredWidth / 4f, measuredHeight / 4f)
val rectF = RectF(-radius, -radius, radius, radius)
canvas.drawArc(rectF, mStartAngle, mSweepAngle, false, paint)
canvas.restore()
}
分析:
- BlurMaskFilter:模糊效果我是用的是内外都模糊绘制(BlurMaskFilter.Blur.NORMA);
- canvas.translate(measuredWidth / 2f, measuredHeight / 2f),将canvas的圆点坐标移至View的中心,方便计算;
外圆:
private fun drawOutCircle(canvas: Canvas) {
canvas.save()
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.setShadowLayer(dp2px(5).toFloat(), 2f, 2f, Color.BLACK)
paint.alpha = 0x70
paint.style = Paint.Style.STROKE
paint.strokeWidth = dp2px(mOutStrokeWidth.toInt()).toFloat()
canvas.translate(measuredWidth / 2f, measuredHeight / 2f)
val radius = Math.min(measuredWidth / 4f, measuredHeight / 4f) + mInnerStrokeWidth + paint.strokeWidth + dp2px(5)
val rectF = RectF(-radius, -radius, radius, radius)
canvas.drawArc(rectF, mStartAngle, mSweepAngle, false, paint)
canvas.restore()
}
这里就不分析了,和画外圆一样,注意:paint.setShadowLayer(dp2px(5).toFloat(), 2f, 2f, Color.BLACK),需要关闭硬件加速,因为在Android中,只有文本才支持阴影,其他GG了。
画刻度:
private fun drawScale(canvas: Canvas) {
canvas.save()
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.color = Color.RED
paint.alpha = 0x70
paint.style = Paint.Style.STROKE
paint.strokeWidth = dp2px(2).toFloat()
canvas.translate(measuredWidth / 2f, measuredHeight / 2f)
val radius = Math.min(measuredWidth / 4f, measuredHeight / 4f)
canvas.rotate(-270 + mStartAngle)
for (index in 0..mScacleNumber) {
canvas.drawLine(0f, -radius + dp2px(mInnerStrokeWidth.toInt()) / 2.toFloat(), 0f, -radius - dp2px(mInnerStrokeWidth.toInt()).toFloat() / 2, paint)
canvas.rotate(mScaleValue)
}
canvas.restore()
}
分析:
mStartAngle开始画圆弧的角度,上面画内外圆都是直接给的值,不需要旋转,而这里为什么要旋转坐标系,其实是为了好计算坐标,这里逆时针旋转-270+mStartAngle,刚好和前面画的内圆开始的mStartAngle相同位置,不信你计算一下。然后循环mSweepAngle / mScacleNumber刻度,这里就顺时针旋转了,其中有些API如果不清楚作用这时候就该点击官网看看了。
画进度条、小圆点:
private fun drawIndicator(canvas: Canvas) {
canvas.save()
canvas.translate(measuredWidth / 2f, measuredHeight / 2f)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.maskFilter = BlurMaskFilter(2f, BlurMaskFilter.Blur.NORMAL)
paint.alpha = 0x70
paint.color = Color.RED
paint.style = Paint.Style.STROKE
paint.strokeWidth = dp2px(mOutStrokeWidth.toInt()).toFloat()
val intArrayOf = intArrayOf(0xffffffff.toInt(), 0x00ffffff, 0x99ffffff.toInt(), 0xffffffff.toInt())
paint.shader = SweepGradient(measuredWidth / 2f, measuredHeight / 2f, intArrayOf, null)
val sweep = currentProgress.toFloat() / mMaxProgress * mSweepAngle
val radius = Math.min(measuredWidth / 4f, measuredHeight / 4f) + mInnerStrokeWidth + paint.strokeWidth + dp2px(5)
val rectF = RectF(-radius, -radius, radius, radius)
canvas.drawArc(rectF, mStartAngle, sweep, false, paint)
/*
画小圆点
*/
paint.style = Paint.Style.FILL
val y = Math.sin(Math.toRadians((sweep + mStartAngle).toDouble())) * radius
val x = Math.cos(Math.toRadians((sweep + mStartAngle).toDouble())) * radius
canvas.drawCircle(x.toFloat(), y.toFloat(), dp2px(3).toFloat(), paint)
canvas.restore()
}
其实也简单,注意:三角函数使用的是弧度,用到一些数学知识,自行百度,这里就不做介绍了,模糊效果和渐变效果BlurMaskFilter和SweepGradient,还是自行官网
最后就是画文本:
private fun drawText(canvas: Canvas) {
canvas.save()
canvas.translate(measuredWidth / 2f, measuredHeight / 2f)
val paint = TextPaint(Paint.ANTI_ALIAS_FLAG)
paint.textSize = dp2px(25).toFloat()
paint.color = Color.WHITE
val textClass = getClassText()
val rect2 = Rect()
paint.getTextBounds(textClass, 0, textClass.length, rect2)
canvas.drawText(textClass, 0, textClass.length, -rect2.width() / 2f, rect2.height() / 2f + dp2px(5), paint)
paint.reset()
paint.textSize = dp2px(25).toFloat()
paint.color = Color.WHITE
val radius = Math.min(measuredWidth / 4f, measuredHeight / 4f)
val str = "$currentProgress"
val rect = Rect()
paint.getTextBounds(str, 0, str.length, rect)
canvas.drawText(str, 0, str.length, -rect.width() / 2f, rect.height() / 2f - radius / 3, paint)
canvas.restore()
}
更新进度这里我用了属性动画,所以还是看码吧:
/**
* 进度更新
*/
fun update(progress: Int) {
val tmpCurrentProgress = if (progress >= mMaxProgress) mMaxProgress else progress
mIndicatorAnimator = ObjectAnimator.ofInt(this, "currentProgress", tmpCurrentProgress)
mIndicatorAnimator?.apply {
this.duration = 2000
this.interpolator = BounceInterpolator()
this.start()
}
}
注意:这个属性必须提供getter和setter方法,毕竟属性动画嘛,操作的就是属性:
fun setCurrentProgress(currentProgress: Int) {
this.currentProgress = currentProgress
invalidate()
}
fun getCurrentProgress() = currentProgress
如何使用?
<com.kotlin.hc.one.AntClassView
android:id="@+id/mCustomView"
android:layout_width="400dp"
android:layout_height="400dp"
android:layout_centerInParent="true"
app:max_progress="800"
app:start_angle="150"
app:sweep_angle="245"
app:text_size="25sp" />
其实自定义View并不难,可能ViewGroup会难一些,复杂一点的其实也不难,主要的难点在于如何结合去计算位置,还有就是性能了。
demo源码