美文网首页
自定义View之BezierProgressView贝塞尔曲线实

自定义View之BezierProgressView贝塞尔曲线实

作者: 钦_79f7 | 来源:发表于2019-12-17 12:37 被阅读0次

    贝塞尔曲线实现百分比注水球

    几大块

    • 正弦曲线绘制
    • startAnimation 水波动画
    • 多行文字居中绘制

    实现思路

    水纹绘制

    • 裁剪圆形画布,不可超出区域绘制
    • 移动画布将View的中心作为原点,便于顺向思维处理坐标
    • 根据百分比计算出水面的坐标位置即Y坐标
    • 求出水面与圆的交点与y的正半轴的夹角度数
    • 根据度数计算出交点的X坐标
    • 利用贝塞尔曲线(这里采用二阶的)模拟绘制水纹(圆内2个周期的正弦曲线)
    • 绘制圆弧,根据之前的夹角计算出当前圆弧的度数
    • 闭环绘制充满水的效果

    文字绘制

    • 算出多行文本的总高度
    • 原点需要处于文字的中线位置,但是文字的绘制在Y轴上是基于baseline的,故需要计算出baseline的Y轴坐标,即baseline的偏移量
    • 循环进行文案的多行绘制

    水纹荡漾的动画效果

    • 水纹采用正弦曲线实现的,而正弦曲线是一个周期性函数
    • 动态变化正弦曲线的开始角度,即可模拟荡漾的效果
    • 而上述有说明当前圆内是两个周期的曲线,并且做了圆外绘制的限制,则在圆外做一个周期曲线的不可见绘制,随着起始位置的变化,就会改变圆内正弦曲线的起始角度的效果
    • 另外由于随着百分比的变化,会使得正弦曲线的周期随之变化,所以动画(即ValueAnimator的取值范围)也要跟随周期的变化而变化
    BezierProgressView.gif

    代码实现

    package com.stone.templateapp.module.view
    
    import android.animation.ValueAnimator
    import android.content.Context
    import android.graphics.*
    import android.util.AttributeSet
    import android.view.View
    import android.view.animation.LinearInterpolator
    import com.stone.commonutils.ctx
    import com.stone.log.Logs
    import com.stone.templateapp.R
    import java.text.NumberFormat
    
    
    /**
     * Created By: sqq
     * Created Time: 2019/1/7 18:39.
     *
     * Bezier实现百分比进度的注水球
     *
     */
    class BezierProgressView : View {
        constructor(ctx: Context) : this(ctx, null)
        constructor(ctx: Context, attributeSet: AttributeSet?) : this(ctx, attributeSet, 0)
        constructor(ctx: Context, attributeSet: AttributeSet?, defStyle: Int) : super(ctx, attributeSet, defStyle) {
            initAttr(attributeSet, defStyle)
        }
    
        private fun initAttr(attributeSet: AttributeSet?, defStyle: Int) {
            val ta = ctx.obtainStyledAttributes(attributeSet, R.styleable.BezierProgressView, defStyle, 0)
            mPercent = ta.getFloat(R.styleable.BezierProgressView_bezier_percent, 0.5f).toDouble()
            ta.recycle()
        }
    
        private val mPaint = Paint()
        private val mCirclePaint = Paint()
        private val mTextPaint = Paint()
        private val mPath = Path()
        /**
         * 当前进度的百分比
         */
        private var mPercent = 0.3
    
        var percent = 50
            set(value) {
                field = when {
                    value > 100 -> 100
                    value < 0 -> 0
                    else -> value
                }
                mPercent = field.toDouble() / 100
                postInvalidate()
            }
    
        init {
            mPaint.isAntiAlias = true
            mPaint.color = Color.parseColor("cyan")
            mPaint.strokeWidth = 5f
            mPaint.style = Paint.Style.FILL
            mPaint.isDither = true
    
            mCirclePaint.isAntiAlias = true
            mCirclePaint.color = Color.parseColor("black")
            mCirclePaint.strokeWidth = 15f
            mCirclePaint.style = Paint.Style.STROKE
            mCirclePaint.isDither = true
    
            mTextPaint.isAntiAlias = true
            mTextPaint.color = Color.parseColor("purple")
            mTextPaint.strokeWidth = 5f
            mTextPaint.style = Paint.Style.STROKE
            mTextPaint.isDither = true
            mTextPaint.textSize = 140f
        }
    
        //View的大小
        private var mViewWidth: Int = 0
        private var mViewHeight: Int = 0
        //可绘制区域大小(去除Padding之后)
        private var mWidth: Int = 0
        private var mHeight: Int = 0
    
        private var r: Float = 0f
        private lateinit var rectF: RectF
        private val mPointF = PointF(0f, 0f)
        private var mAnimateDx = 0f
        private var mAnimateMaxDx = 0f
    
        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
            mViewWidth = w
            mViewHeight = h
            mWidth = mViewWidth - paddingLeft - paddingRight
            mHeight = mViewHeight - paddingTop - paddingBottom
    
            r = Math.min(mWidth, mHeight) * 0.4f
            mAnimateMaxDx = r
            rectF = RectF(-r, -r, r, r)
        }
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            //保存之前Canvas的状态,save之后可以调用Canvas的平移、旋转等操作
            canvas.save()
            canvas.translate(measuredWidth / 2f, measuredHeight / 2f)
    //        canvas.drawPoint(0f, 0f, mCirclePaint)//绘制原点
    
            mPath.rewind()
            mPath.addCircle(0f, 0f, r + mCirclePaint.strokeWidth / 2, Path.Direction.CW)
            //裁剪画布为圆形,不绘制超出圆边界的path
            canvas.clipPath(mPath)
    
            //递增or递减控制点的Y坐标
            mPath.rewind()
    
            //根据当前百分比,计算出当前圆的Y坐标
            val yCoordinate = r * (1 - 2 * mPercent)
            //根据y坐标计算出,原点与交点形成的直线 与 Y的正半轴形成的夹角度数
            val angle = Math.acos(yCoordinate / r)
            //根据夹角度数计算出当前交点的x坐标
            val xCoordinate = r * Math.sin(angle)
            //正弦周期的x轴长度
            val t = xCoordinate.toFloat()
            mPath.moveTo(-2 * t + mAnimateDx, yCoordinate.toFloat())
    //        mPath.moveTo(-xCoordinate.toFloat(), yCoordinate.toFloat())
    
            //利用二阶Bezier实现正弦波水纹效果
    //        mPath.rQuadTo(xCoordinate.toFloat() / 2, -r / 8, xCoordinate.toFloat(), 0f)
    //        mPath.rQuadTo(xCoordinate.toFloat() / 2, r / 8, xCoordinate.toFloat(), 0f)
    
            for (i in 0..2) {
                mPath.rQuadTo(t / 4, t / 8, t / 2, 0f)
                mPath.rQuadTo(t / 4, -t / 8, t / 2, 0f)
            }
    
            //利用三阶Bezier实现水纹效果
    //        mPath.rCubicTo(xCoordinate.toFloat() / 2, -r / 6, xCoordinate.toFloat() * 3 / 2, r / 6, 2 * xCoordinate.toFloat(), 0f)
            val dDegree = Math.toDegrees(angle).toFloat()
            mPath.addArc(rectF, 90 - dDegree, dDegree * 2)
    //        mPath.close()
            canvas.drawPath(mPath, mPaint)
            mPath.rewind()
    
            val numberFormat = NumberFormat.getPercentInstance()
            //百分比保留几位小数:0:10%;1:10.0%
            numberFormat.minimumFractionDigits = 1
            numberFormat.maximumFractionDigits = 3
            val format = numberFormat.format(mPercent)
    //        Logs.i("onDraw: $format")
            textCenter(arrayOf(format), mTextPaint, canvas, mPointF, Paint.Align.CENTER)
    //        textCenter(arrayOf("进度", format), mTextPaint, canvas, mPointF, Paint.Align.CENTER)
    
            canvas.drawCircle(0f, 0f, r, mCirclePaint)
    
            //与 save() 成对出现,恢复之前保存的canvas状态,防止上述save之后的canvas操作对后续的绘制产生影响
            canvas.restore()//
    
            if (mAnimateMaxDx.toInt() != t.toInt()) {
                startAnimation(t)
            }
    //        postInvalidateDelayed(200)
            Logs.d("onDraw: r: $r,mWith: $mWidth,mAnimateDx:$mAnimateDx")
        }
    
        private var animator: ValueAnimator? = null
        fun startAnimation(max: Float) {
            mAnimateMaxDx = max
            animator?.cancel()
            animator = ValueAnimator.ofFloat(0f, max)
    //        animator.repeatMode = ValueAnimator.REVERSE
            animator?.repeatCount = ValueAnimator.INFINITE
            animator?.duration = 1000
            animator?.interpolator = LinearInterpolator()
            animator?.addUpdateListener {
                mAnimateDx = it.animatedValue as Float
                invalidate()
            }
            animator?.start()
        }
    
        override fun onDetachedFromWindow() {
            super.onDetachedFromWindow()
            animator?.cancel()
            animator = null
        }
    
        /**
         * 多行文本居中、居右、居左
         * @param strings 文本字符串列表
         * @param paint 画笔
         * @param canvas 画布
         * @param point 点的坐标
         * @param align 居中、居右、居左
         */
        protected fun textCenter(strings: Array<String>, paint: Paint, canvas: Canvas, point: PointF, align: Paint.Align) {
            paint.textAlign = align
            val fontMetrics = paint.fontMetrics
            val top = fontMetrics.top
            val bottom = fontMetrics.bottom
            val length = strings.size
            val total = (length - 1) * (-top + bottom) + (-fontMetrics.ascent + fontMetrics.descent)
            val offset = total / 2 - bottom
            for (i in 0 until length) {
                val yAxis = -(length - i - 1) * (-top + bottom) + offset
                canvas.drawText(strings[i], point.x, point.y + yAxis, paint)
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:自定义View之BezierProgressView贝塞尔曲线实

          本文链接:https://www.haomeiwen.com/subject/wdssnctx.html