圆形刻度盘
UI效果
新项目又来了,做的手表功能,有个圆形刻度盘的ui,图如下:
思路
1.刻度条其实是直线,先将坐标系移到圆心,画一条刻度(直线),再将坐标系旋转一定角度,再画一条刻度(直线),直到旋转一周。
2.总共画两层刻度盘,第一层是白色刻度条的刻度盘,再画第二次有颜色刻度条的刻度盘,第二个刻度盘的刻度条从0到阀值,通过动画实现。
3.取消的话,其实也可以增加一个动画,就是第二个刻度盘的刻度条从阀值到0
关于坐标系
画了一个图,简单易懂:
代码
直接放代码了:
1.attrs.xml 自定义了一些属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircularMeter">
<attr name="lineWidth" format="dimension" />
<attr name="lineNumber" format="integer" />
<attr name="lineLength" format="dimension" />
<attr name="backColor" format="color" />
<attr name="backLineColor" format="color" />
<attr name="startLineColor" format="color" />
<attr name="finishLineColor" format="color" />
<attr name="fraction" format="fraction" />
<attr name="startDuration" format="integer" />
<attr name="stopDuration" format="integer" />
</declare-styleable>
</resources>
lineWidth:刻度条的线宽
lineNumber:刻度条的总共数量,要被360整除,这样每条线旋转的角度就是360/lineNumber
lineLength:刻度条的长度,肯定要小于半径
backColor:刻度盘的背景色
backLineColor:第一个刻度盘的刻度条的颜色
startLineColor,finishLineColor:第一个刻度盘的刻度条的渐变颜色
fraction:取值0到1,1就是一圈,0.5就是半圈,第四个UI图的fraction就是0.2,红色刻度条的数量就是lineNumber*fraction
startDuration:刻度条增加的动画时间
stopDuration:刻度条减少的动画时间
2.CircularMeterView.kt
class CircularMeterView(context: Context, @Nullable attrs: AttributeSet?, defStyleAttr: Int) :
View(context, attrs, defStyleAttr) {
constructor(context: Context) : this(context, null)
constructor(context: Context, @Nullable attrs: AttributeSet?) : this(context, attrs, 0)
private val TAG = "CircularMeterView"
private var mBackPaint: Paint
private var mBackLinePaint: Paint
private var mProgLinePaint: Paint
//半径
private var mRadius: Float = 0.0f
//圆心坐标
private var mCenterX: Float = 0.0f
private var mCenterY: Float = 0.0f
//刻度条数量
private var mLineNumber: Int
//刻度条长度
private var mLineLength: Float
//启动动画时间
private var mStartDuration: Int = 0
//关闭动画时间
private var mStopDuration: Int = 0
//渐变色
private var mStartLineColor: Int
private var mFinishLineColor: Int
//背景色
private var mBackColor: Int
//有色刻度盘的比例(0到1)
private var mFraction: Float = 0F
//动画实时改变的值,小于mFraction
private var currentFraction = 0F
private var animator: ValueAnimator? = null
private var mListenerList: ArrayList<ListenerBuilder> = arrayListOf()
init {
@SuppressLint("Recycle")
val typedArray: TypedArray =
context.obtainStyledAttributes(attrs, R.styleable.CircularMeter)
mBackPaint = Paint()
mBackPaint.run {
style = Paint.Style.FILL_AND_STROKE
isAntiAlias = true
color = typedArray.getColor(R.styleable.CircularMeter_backColor, Color.TRANSPARENT)
}
mBackLinePaint = Paint()
mBackLinePaint.run {
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND // 设置圆角
isAntiAlias = true // 设置抗锯齿
isDither = true // 设置抖动
strokeWidth = typedArray.getDimension(R.styleable.CircularMeter_lineWidth, 2f)
color = typedArray.getColor(R.styleable.CircularMeter_backLineColor, resources.getColor(R.color.colorMeterBack))
}
mProgLinePaint = Paint()
mProgLinePaint.run {
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
isAntiAlias = true
isDither = true
strokeWidth = typedArray.getDimension(R.styleable.CircularMeter_lineWidth, 2f)
color = Color.BLUE
}
typedArray.run {
mLineNumber = getInt(R.styleable.CircularMeter_lineNumber, 60)
mLineLength = getDimension(R.styleable.CircularMeter_lineLength, 30f)
mStartLineColor = getColor(R.styleable.CircularMeter_startLineColor, -1)
mFinishLineColor = getColor(R.styleable.CircularMeter_finishLineColor, -1)
mBackColor = getColor(R.styleable.CircularMeter_backColor, -1)
mStartDuration = getInt(R.styleable.CircularMeter_startDuration, 2000)
mStopDuration = getInt(R.styleable.CircularMeter_stopDuration, 200)
mFraction = getFraction(R.styleable.CircularMeter_fraction, 1, 1, 0.0f)
recycle()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val viewWith: Float = (measuredWidth - paddingLeft - paddingRight).toFloat()
val viewHigh: Float = (measuredHeight - paddingTop - paddingBottom).toFloat()
mCenterX = measuredWidth.toFloat() / 2
mCenterY = measuredHeight.toFloat() / 2
mRadius = min(viewWith, viewHigh) / 2
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawBackGroud(canvas)
drawBackLine(canvas)
drawFrogLine(canvas)
}
private fun drawBackGroud(canvas: Canvas) {
canvas.drawCircle(mCenterX,mCenterY,mRadius,mBackPaint)
}
private fun drawFrogLine(canvas: Canvas) {
//第二个刻度盘实时的刻度条数量
val number = (mLineNumber * currentFraction).toInt()
if (number > 0) {
canvas.save()
canvas.translate(mCenterX, mCenterX)
//旋转180度,使坐标系的Y轴方向和数学坐标系的Y轴方向一致
canvas.rotate(180.0f)
for (i in 1..number) {
if (mStartLineColor != -1 && mFinishLineColor != -1) {
mProgLinePaint.shader = LinearGradient(0.0f, mRadius, 0.0f, mRadius - mLineLength, mStartLineColor, mFinishLineColor, Shader.TileMode.CLAMP)
}
//-0.5f为了避免毛刺效果
canvas.drawLine(0.0f, mRadius -0.5f , 0.0f, mRadius - mLineLength, mProgLinePaint)
canvas.rotate((360 / mLineNumber).toFloat())
}
canvas.restore()
}
}
private fun drawBackLine(canvas: Canvas) {
canvas.save()
//坐标系移到圆心
canvas.translate(mCenterX, mCenterX)
for (i in 1..mLineNumber) {
//画一条刻度线,坐标(0, mRadius, 0, mRadius - mLineLength)
canvas.drawLine(0.0f, mRadius, 0.0f, mRadius - mLineLength, mBackLinePaint)
//坐标系旋转
canvas.rotate((360 / mLineNumber).toFloat())
}
canvas.restore()
}
/**
* fraction 0到1,有动画过程
* duration 动画时间(毫秒)
* */
fun startCircular(fraction: Float = mFraction, duration: Int = mStartDuration) {
if (fraction < 0 || fraction > 1) {
"startCircular error , fraction = $fraction".d(TAG)
return
}
if (duration < 0) {
"startCircular duration < 0".d(TAG)
return
}
mFraction = fraction
mStartDuration = duration
"mFraction = $mFraction".d(TAG)
animator?.run { cancel() }
animator = ValueAnimator.ofFloat(0.0f, mFraction)
animator?.run {
addUpdateListener { animation ->
currentFraction = animation.animatedValue as Float
invalidate()
if (currentFraction == mFraction) {
mListenerList.takeUnless { it.isEmpty() }?.run { forEach { it.mComplete?.invoke() } }
}
}
interpolator = LinearInterpolator()
this.duration = mStartDuration.toLong()
start()
}
}
/**
* fraction 0到1,无动画过程
* */
fun setCircular(fraction: Float = mFraction) {
if (fraction < 0 || fraction > 1) {
"setCircular error , fraction = $fraction".d(TAG)
return
}
animator?.run { cancel() }
mFraction = fraction
currentFraction = fraction
invalidate()
mListenerList.takeUnless { it.isEmpty() }?.run { forEach { it.mComplete?.invoke() } }
}
/**
* duration 动画时间(毫秒)
* */
fun stopCircular(duration: Int = mStopDuration) {
if (duration < 0) {
"stopCircular duration < 0".d(TAG)
mStopDuration = 0
}
mStopDuration = duration
mStopDuration = (mStopDuration * currentFraction).toInt()
animator?.run { cancel() }
animator = ValueAnimator.ofFloat(currentFraction, 0.0f)
animator?.run {
addUpdateListener { animation ->
currentFraction = animation.animatedValue as Float
invalidate()
/*if (currentFraction <= 0.0f) {
mListenerList.takeUnless { it.isEmpty() }?.run { forEach { it.mComplete?.invoke() } }
}*/
}
interpolator = LinearInterpolator()
this.duration = mStopDuration.toLong()
start()
}
}
fun setFrogLineColor(startColor: Int, finishColor: Int) {
mStartLineColor = startColor
mFinishLineColor = finishColor
invalidate()
}
fun registerListener(listenerBuilder: ListenerBuilder.() -> Unit) {
mListenerList.add(ListenerBuilder().also(listenerBuilder))
}
fun removeAllListener() {
mListenerList.clear()
}
class ListenerBuilder {
//动画结束的回调
internal var mComplete: (() -> Unit)? = null
fun onComplete(action: () -> Unit) {
mComplete = action
}
}
}
3.fragment_lock.xml 布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center">
<com.qinggan.app.havalapp.view.CircularMeterView
android:layout_width="214px"
android:layout_height="214px"
android:id="@+id/f_lock_circularMeterView"
app:backColor="#EEEEEE"
app:lineWidth="2px"
app:lineNumber="60"
app:lineLength="29px"
app:backLineColor="@color/colorMeterBack"
app:startLineColor="@color/colorOilMeterStartGreen"
app:finishLineColor="@color/colorOilMeterFinishGreen"
app:fraction = "100%"
app:startDuration = "3000"
app:stopDuration = "400"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
3.简单调用
class LockFragment : Fragment() {
private val TAG = "LockFragment"
private lateinit var meterView: CircularMeterView
override fun onResume() {
super.onResume()
"onResume ".d(TAG)
meterView.startCircular()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
"onCreateView ".d(TAG)
val view = inflater.inflate(R.layout.fragment_lock, container, false)
meterView = view.findViewById<CircularMeterView>(R.id.f_lock_circularMeterView)
// meterView.setColor(resources.getColor(R.color.colorOilMeterStartRed),resources.getColor(R.color.colorOilMeterFinishRed))
meterView.registerListener {
onComplete {
"onComplete animation".d(TAG)
}
}
meterView.setOnClickListener{
//meterView.setCircular(0.0f)
meterView.stopCircular()
}
return view
}
}