美文网首页
自定义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