自定义控件-圆形刻度盘ui

圆形刻度盘

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:第一个刻度盘的刻度条的颜色
startLineColorfinishLineColor:第一个刻度盘的刻度条的渐变颜色
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
   }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值