美文网首页
PathMeasure

PathMeasure

作者: echoSuny | 来源:发表于2020-06-28 18:36 被阅读0次

PathMeasure是一个路径计算类,可以计算路径总长,指定长度所对应的坐标点等。可以用来实现一些比较复杂的动画效果。

初始化

// 第一种
        val path = Path()
        val pathMeasure = PathMeasure()
        pathMeasure.setPath(path, true)
// 第二种
        val path = Path()
        val pathMeasure = PathMeasure(path, true)

在setPath()和构造方法的第二个参数中都传入了一个布尔变量-forceClosed。这个变量表示Path是否需要闭合。如果为true,则不管传入的Path是否是闭合的,都会被闭合。但是forceClosed参数对传入的Path不会产生任何影响。例如一个没有闭合的Path,当forceClosed为true时,只是PathMeasure在计算的时候是以闭合的方式计算的,而Path本身没有变化,也就是说只对测量的结果有影响。

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        val paint = Paint()
        paint.strokeWidth = 12f
        paint.color = Color.RED
        paint.style = Paint.Style.STROKE

        canvas.translate(100f, 100f)
        val path = Path()
        path.moveTo(0f, 0f)
        path.lineTo(0f, 100f)
        path.lineTo(100f, 100f)
        path.lineTo(100f, 0f)
        val pathMeasure1 = PathMeasure(path, false)
        val pathMeasure2 = PathMeasure(path, true)
        Log.d("----->", "pathMeasure1: ${pathMeasure1.length}")
        Log.d("----->", "pathMeasure2: ${pathMeasure2.length}")
        canvas.drawPath(path, paint)
    }

可以看到绘制了一个没有闭口的正方形,每条边长都是100



当forceClosed为false时,计算得到这个path的长度是300,而为true的时候则是400,多了一条边的长度。但是创建出来的path依旧是个没有闭口的路径。

常用函数

  • getLength() 获取计算路径的长度
  • isClosed() 用于判断测量Path时是否计算闭合。如果在构造方法或者setPath()中设置forceClosed为true,那么isClosed()一定返回true
  • nextContour() 如果Path由多条曲线构成,调用其他函数时都只会针对第一条进行计算,而nextContour()则可以跳转到下一条曲线。跳转成功返回true,反之返回false。顺序则是按照Path添加的顺序。另外getLength()等函数都是针对的当前的曲线,而不是整个Path。
  • getSegment() 用于截取整个Path中的某个片段。参数共有四个分别是起点和终点,对应startD和stopD。如果起点和终点的数值不在[0,getLength]的范围内,或者startD == stopD,则返回false。另外还需要传入一个Path对象,用于保存截取出来的片段。最后一个参数是startWithMoveTo,表示起始点是否使用moveTo将路径的新起始点移到结果 Path的起始点。这是一个布尔变量,通常设置为true。
        val paint = Paint()
        paint.strokeWidth = 12f
        paint.color = Color.RED
        paint.style = Paint.Style.STROKE

        canvas.translate(100f, 100f)
        val path = Path()
        path.moveTo(0f, 0f)
        path.lineTo(0f, 100f)
        path.lineTo(100f, 100f)
        path.lineTo(100f, 0f)
        path.close()
        val dst = Path()
        val pathMeasure = PathMeasure(path, false)
        pathMeasure.getSegment(0f, 160f, dst, true)
        canvas.drawPath(path, paint)
        canvas.translate(200f, 0f)
        canvas.drawPath(dst, paint)

可以看到右边的路径是左边的一部分。需要注意的是因为我们在添加路径的时候是按照下图的顺序添加的,那么截取的时候也是这样。也就是说添加的时候是什么顺序,截取的时候就是什么顺序。



前面说了最后一个参数是true还是false对最终的结果是会产生影响的。下面把最后一个参数改为false之后看一下效果:




非常奇怪的是没有任何区别。这是因为保存路径片段的dst为空(不是说dst没有初始化,而是它里面没有任何的路径)。下面给dst添加一条路径,再看一下效果:
        val dst = Path()
        dst.moveTo(200f,200f)
        dst.lineTo(200f,100f)
        val pathMeasure = PathMeasure(path, false)
        pathMeasure.getSegment(0f, 160f, dst, true)
        canvas.drawPath(path, paint)
        canvas.translate(200f, 0f)
        canvas.drawPath(dst, paint)

当把getSegment()中最后一个参数改为false之后:



其实只是把两条曲线连接起来了:



也就是把截取的路径片段的起点与dst中的原来路径的终点连接起来了。也就是原来路径的终点作为新的路径的起点了。
下面通过一个例子再熟悉一下PathMeasure的基本使用:
class EasyPathView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    val paint: Paint
    val dst: Path
    val circlePath: Path
    val pathMeasure: PathMeasure
    var currentValue by Delegates.notNull<Float>()

    init {

        setLayerType(LAYER_TYPE_SOFTWARE,null)
        paint = Paint()
        paint.color = Color.RED
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 6f

        dst = Path()

        circlePath = Path()
        circlePath.addCircle(300f, 300f, 160f, Path.Direction.CW)

        pathMeasure = PathMeasure(circlePath, true)

        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.repeatCount = ValueAnimator.INFINITE
        animator.duration = 2000
        animator.addUpdateListener {
            currentValue = it.animatedValue as Float
            invalidate()
        }
        animator.start()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val stop = pathMeasure.length * currentValue
        dst.reset()
        pathMeasure.getSegment(0f, stop, dst, true)
        canvas.drawPath(dst, paint)
    }
}

稍微修改一下,使其变成一个简单的加载动画:

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val stop = pathMeasure.length * currentValue
        var start = 0f
        if (currentValue >= .5f) {
            start = (2 * currentValue - 1f) * pathMeasure.length
        }
        dst.reset()
        pathMeasure.getSegment(start, stop, dst, true)
        canvas.drawPath(dst, paint)
    }

PathMeasure进阶

getPosTan()

PathMeasure除了上面的基本用法之外还包括getPosTan()函数。这个方法的作用是得到路径某一长度的位置以及该位置的正切值。
boolean getPosTan(float distance,float[] pos , float[) tan)

  • distance 距离Path起始点的长度,取值范围为 0<= distance<= getLength
  • float[] pos 该点的坐标值。pos[0]表示x坐标,pos[1]表示y坐标
  • float[] tan 该点的正切值
    正切值是数学三角函数当中的概念,指的是一个直角三角形中,对边比上邻边的值。也就是下图中的Y的长度除以X:



    把上面的EasyPathView稍作修改来应用一下getPosTan()

class EasyPathView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    val paint: Paint
    val dst: Path
    val circlePath: Path
    val pathMeasure: PathMeasure
    var currentValue by Delegates.notNull<Float>()
    val arrow: Bitmap
    // 创建两个数组用来接收对应点的x、y坐标和正切值
    val pos: FloatArray = floatArrayOf(0f, 0f)
    val tan: FloatArray = floatArrayOf(0f, 0f)

    init {
        // 加载箭头图标
        arrow = BitmapFactory.decodeResource(resources, R.drawable.heart)
        setLayerType(LAYER_TYPE_SOFTWARE, null)
        paint = Paint()
        paint.color = Color.RED
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 6f

        dst = Path()

        circlePath = Path()
        circlePath.addCircle(300f, 300f, 160f, Path.Direction.CW)

        pathMeasure = PathMeasure(circlePath, true)

        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.repeatCount = ValueAnimator.INFINITE
        animator.duration = 2000
        animator.addUpdateListener {
            currentValue = it.animatedValue as Float
            invalidate()
        }
        animator.start()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val stop = pathMeasure.length * currentValue
        dst.reset()
        pathMeasure.getSegment(0f, stop, dst, true)
        canvas.drawPath(dst, paint)
        pathMeasure.getPosTan(stop, pos, tan)
        // 得到箭头所在位置的正切值所对应的弧度
        val atan = Math.atan2(tan[1].toDouble(), tan[0].toDouble())
        // 将弧度值转换为角度值
        val degree = atan * 180.0f / Math.PI
        val matrix = Matrix()
        // 将箭头图标按照正切角度旋转
        matrix.postRotate(degree.toFloat(), arrow.width / 2.toFloat(), arrow.height / 2.toFloat())
        // 将箭头图标平移到相应的位置
        matrix.postTranslate(pos[0] - arrow.width / 2, pos[1] - arrow.height / 2)
        canvas.drawBitmap(arrow, matrix, paint)
    }
}
getMatrix()

boolean getMatrix(float distance,Matrix matrix , int flags)
这个函数用于得到路径上某一长度的位置以及该位置的正切值的矩阵。

  • distance 距离Path起始点的长度
  • matrix 用来接收相关的数据,并保存到matrix当中
  • flags 用于指定哪些内容会存到matix当中。有两个取值:PathMeasre.POSITION_
    MATRIX_FLAG用于保存位置信息,PathMeasure. TANGENT_MATRIX_FLAG用来获取切边信息,是图片按Path旋转。可以指定一个,也可以使用 ‘|’符号来同时指定两个。
    了解了这个函数的作用之后,很明显可以想到getMatrix()是getPosTan()的一种实现,省去了使用数组来接收参数以及一些计算。

相关文章

网友评论

      本文标题:PathMeasure

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