先來看效果圖,gif圖效果不是很好,可以在真機上看一波:

螞蟻信用.gif
仔細分析效果:
接到UI設(shè)計的效果,你覺得我會怎么做?
一般的步驟:
- Google一波,沒有找到類似的效果,所以就老老實實的分析一下效果,該如何實現(xiàn)?
- 內(nèi)外圓弧、刻度、文字、動畫、模糊、漸變的效果,分析完之后就開始吧:

螞蟻信用.png
畫內(nèi)圓:
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:模糊效果我是用的是內(nèi)外都模糊繪制(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),需要關(guān)閉硬件加速,因為在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開始畫圓弧的角度,上面畫內(nèi)外圓都是直接給的值,不需要旋轉(zhuǎn),而這里為什么要旋轉(zhuǎn)坐標系,其實是為了好計算坐標,這里逆時針旋轉(zhuǎn)-270+mStartAngle,剛好和前面畫的內(nèi)圓開始的mStartAngle相同位置,不信你計算一下。然后循環(huán)mSweepAngle / mScacleNumber刻度,這里就順時針旋轉(zhuǎn)了,其中有些API如果不清楚作用這時候就該點擊官網(wǎng)看看了。
畫進度條、小圓點:
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()
}
其實也簡單,注意:三角函數(shù)使用的是弧度,用到一些數(shù)學(xué)知識,自行百度,這里就不做介紹了,模糊效果和漸變效果BlurMaskFilter和SweepGradient,還是自行官網(wǎng)
最后就是畫文本:
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會難一些,復(fù)雜一點的其實也不難,主要的難點在于如何結(jié)合去計算位置,還有就是性能了。
demo源碼