前言
本文是对 完全自定义View 的一次实践。实现了一个 扇形圆环. 包括渐变色,增长动画等. 好了话不多少, 我们先上图
一、分析
两个画笔, 一个背景, 一个前景; 然后绘制环形即可; 属性动画, 不断增加彩色环的角度; 然后不断重绘;
涉及问题点:
自定义属性接收, 包括色彩集合 onMeasure 适配View自适应的情况 色彩环 在 0° 位置 色彩分界线处理
二、上代码
1.自定义View代码
class FanRingView(context: Context, attrs: AttributeSet?)
: View(context, attrs) {
private val mPaint: Paint
private val mBgPaint: Paint
private var mRectF: RectF? = null
private var mViewCenterX = 0f
private var mViewCenterY = 0f
private var mCurrentRing = 0f
private var mSweepAngle = 270f
private var mRingWidth = 18f
private var duration = 1500L
private var valueAnimator: ValueAnimator? = null
private lateinit var color: IntArray
private val defaultWidth: Int
companion object{
const val MAX_VALUE = 100
const val TAG = "FanRingView"
}
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar)
mSweepAngle = typedArray.getFloat(R.styleable.CircleProgressBar_sweepAngle, 270f)
val bgArcColor = typedArray.getColor(R.styleable.CircleProgressBar_bgArcColor, Color.LTGRAY)
mRingWidth = typedArray.getDimension(R.styleable.CircleProgressBar_arcWidth, 18f)
duration = typedArray.getInt(R.styleable.CircleProgressBar_animTime, 1500).toLong()
val gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0)
Log.i(TAG, "initAttrs: gradientArcColors--$gradientArcColors")
if (gradientArcColors != 0) {
try {
val gradientColors = resources.getIntArray(gradientArcColors)
Log.i(TAG, "initAttrs: gradientColors.length::"+gradientColors.size)
when(gradientColors.size) {
0 -> {
val colorSingle = resources.getColor(gradientArcColors,null)
color = IntArray(1)
color[0] = colorSingle
}
1 -> {
color = IntArray(1)
color[0] = gradientColors[0]
}
else -> {
color = gradientColors
}
}
} catch (e: Resources.NotFoundException) {
throw Resources.NotFoundException("the give resource not found.")
}
}
typedArray.recycle()
mBgPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mBgPaint.style = Paint.Style.STROKE
mBgPaint.strokeWidth = mRingWidth
mBgPaint.color = bgArcColor
mBgPaint.strokeCap = Paint.Cap.ROUND
mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mPaint.style = Paint.Style.STROKE
mPaint.strokeWidth = mRingWidth
mPaint.strokeCap = Paint.Cap.ROUND
defaultWidth = AndroidUtils.dp2px(context, 200f)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val heithtSpecSize = MeasureSpec.getSize(heightMeasureSpec)
when{
widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST -> {
setMeasuredDimension(defaultWidth, defaultWidth)
}
widthSpecMode == MeasureSpec.AT_MOST -> {
setMeasuredDimension(heithtSpecSize, heithtSpecSize)
}
heightSpecMode == MeasureSpec.AT_MOST -> {
setMeasuredDimension(widthSpecSize, widthSpecSize)
}
else -> super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
resetBlock(w, h)
}
private fun resetBlock(width: Int, height: Int){
if (width > 0 && height > 0) {
mViewCenterX = width / 2f
mViewCenterY = height / 2f
mPaint.shader = SweepGradient(mViewCenterX, mViewCenterX, color, null)
val radius = (width.coerceAtMost(height) - mRingWidth) / 2f
mRectF = RectF(mViewCenterX - radius, mViewCenterY - radius, mViewCenterX + radius, mViewCenterY + radius)
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if(mRectF == null){
resetBlock(measuredWidth, measuredHeight)
}
val start = 90f + (360f - mSweepAngle) / 2f
drawNormalRing(canvas, start)
drawColorRing(canvas, start)
}
private fun drawNormalRing(canvas: Canvas?, start: Float) {
val startReal = start + mCurrentRing - 5f
val ringReal = mSweepAngle - mCurrentRing + 5f
canvas?.drawArc(mRectF!!, startReal, ringReal, false, mBgPaint)
}
private fun drawColorRing(canvas: Canvas?, start: Float) {
if(mCurrentRing == 0f) return
canvas?.rotate(start - 5f, mViewCenterX, mViewCenterY)
canvas?.drawArc(mRectF!!, 5f, mCurrentRing, false, mPaint)
}
fun setValue(value: Int, textView: TextView) {
valueAnimator?.cancel()
val current = if (value > MAX_VALUE) MAX_VALUE else value
startAnimator(current, textView)
}
private fun startAnimator(end: Int, textView: TextView) {
valueAnimator = ValueAnimator.ofFloat(0f, end.toFloat()).also {
it.duration = duration
it.addUpdateListener { animation ->
Log.i(TAG, "startAnimator: AnimatedValue()--${animation.animatedValue}")
val i: Float = animation.animatedValue as Float
textView.text = i.toInt().toString()
mCurrentRing = mSweepAngle / 100f * i
Log.i(TAG, "startAnimator: mSelectRing::$mCurrentRing")
invalidate()
if(i >= end) valueAnimator = null
}
it.start()
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
valueAnimator?.cancel()
valueAnimator = null
}
}
2.布局及Activity的代码
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".test.customview.FanRingActivity">
<com.example.kotlinmvpframe.test.customview.custom.FanRingView
android:id="@+id/frv_ring"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:arcWidth="10dp"
app:arcColors="@array/gradient_arc_color"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/tv_value"
style="@style/tv_base_16_dark"
android:layout_marginBottom="40dp"
android:padding="15dp"
android:background="?selectableItemBackground"
app:layout_constraintStart_toStartOf="@id/frv_ring"
app:layout_constraintEnd_toEndOf="@id/frv_ring"
app:layout_constraintBottom_toBottomOf="@id/frv_ring" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Activity:
binding.frvRing.setValue(100, binding.tvValue)
binding.tvValue.setOnClickListener {
val score: Int = (1..100).random()
binding.frvRing.setValue(score, binding.tvValue)
}
3.其他代码
自定义属性: values/attr.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="sweepAngle" format="float" />
<attr name="animTime" format="integer" />
<attr name="bgArcColor" format="color|reference" />
<attr name="arcWidth" format="dimension" />
<attr name="arcColors" format="color|reference" />
<declare-styleable name="CircleProgressBar">
<attr name="sweepAngle" />
<attr name="animTime" />
<attr name="arcWidth" />
<attr name="arcColors" />
<attr name="bgArcColor" />
</declare-styleable>
</resources>
颜色集合文件: values/array.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer-array name="gradient_arc_color">
<item>0xFFFF0000</item>
<item>0xFFFFAA00</item>
<item>0xFFFFFF00</item>
</integer-array>
</resources>
三、部分讲解
1.Paint 设置:
mBgPaint.strokeCap = Paint.Cap.ROUND
上面这句是让圆环 末端圆角的关键代码. 不然末端为直线样式, 并不好看;
mPaint.shader = SweepGradient(mViewCenterX, mViewCenterX, color, null)
上面这句是设置 着色器. 颜色渐变的关键代码;
2. 零度位置, 色彩分界线处理
由于我们的圆环末端为圆角. 而着色器在 0°位置会产生一个分界线.
假如我们的代码类似这样:
canvas?.drawArc(mRectF!!, -90f, 350f, false, mPaint)
效果如下图所示: 即便我们旋转画布. 那么这个分界线也会在左下角产生. 因此: 我们将旋转画布, 并预留出5°的位置, 来避开这个分界线!
private fun drawColorRing(canvas: Canvas?, start: Float) {
if(mCurrentRing == 0f) return
canvas?.rotate(start - 5f, mViewCenterX, mViewCenterY)
canvas?.drawArc(mRectF!!, 5f, mCurrentRing, false, mPaint)
}
3.View自适应
defaultWidth 该属性作为 默认的宽高值(目前为200dp) 在 onMeasure 回调中, 当宽高都为 wrap_content 时, 启用该属性;
4.属性动画:
需要注意的点:
当View销毁时, 关闭属性动画, 以免内存泄漏 再次设定数值时, 首先关掉未执行完毕的先前动画 动画执行完毕时, 对象置为 null. 以免内存泄漏
5.减少过度绘制
private fun drawNormalRing(canvas: Canvas?, start: Float) {
val startReal = start + mCurrentRing - 5f
val ringReal = mSweepAngle - mCurrentRing + 5f
canvas?.drawArc(mRectF!!, startReal, ringReal, false, mBgPaint)
}
色彩环和背景环 会有重叠部分. 重叠部分背景的绘制 并无必要. 因此我们计算色彩环角度, 绘制背景环时 适当减去 色彩环的部分
总结
没有总结
上一篇: WorkManager笔记: 三、加急工作 下一篇: 记一次自定义View:滑动标尺
|