Android-高级-UI-进阶之路--(六)-PathMeasure-制作路径动画

//4. 添加 圆 到 Path
mPath.addCircle(850f,1200f ,150f,Path.Direction.CCW)//逆时针
canvas!!.drawText(“4”,850f,1200f,mPathPaint)

//5. 添加 圆弧 到 Path ,直接添加一个圆弧到path中
//添加一个圆弧到 path,如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
mPath.addArc(100f,1500f,600f,1800f,0f,300f)
canvas!!.drawText(“5”,300f,1550f,mPathPaint)

mPath.arcTo(650f,1500f,800f,1800f,0f,180f,true)
canvas!!.drawText(“6”,750f,1550f,mPathPaint)

canvas!!.drawPath(mPath, mPathPaint)
复制代码

这里注意一点 addTo 最后一个参数 forceMoveTo 它的意思为“是否强制使用 moveTo ”,也就是说,是否使用 moveTo 将变量移动到圆弧的起点位移 ,也就意味着:

forceMoveTo含义等价方法
true将最后一个点移动到圆弧起点,即不连接最后一个点与圆弧起点public void addArc (RectF oval, float startAngle, float sweepAngle)
false不移动,而是连接最后一个点与圆弧起点public void arcTo (RectF oval, float startAngle, float sweepAngle)
  1. computeBounds,set,setPath

  2. 先绘制一个 圆和矩形

mPath.addCircle(500f, 500f, 150f, Path.Direction.CW)

canvas!!.drawPath(mPath, mPathPaint) // 绘制Path

canvas.drawRect(300f,800f,800f,1300f ,mPathPaint) // 绘制矩形

  1. 重写 onTouchEvent 实现点击事件

override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
//需要按下事件
MotionEvent.ACTION_DOWN -> return true
MotionEvent.ACTION_UP -> {
val rectF = RectF()
//计算 Path 边界
mPath.computeBounds(rectF, true)
//将边界放入矩形区域内
region.setPath(
mPath,
Region(rectF.left.toInt(), rectF.top.toInt(), rectF.right.toInt(), rectF.bottom.toInt())
)
if (region.contains(event.x.toInt(), event.y.toInt())) {
Toast.makeText(context, “点击了圆”, Toast.LENGTH_SHORT).show()
}
//用新的路径替换到当前路径所有内容
region.set(300, 800, 800, 1300)
if (region.contains(event.x.toInt(), event.y.toInt())) {
Toast.makeText(context, “点击了矩形”, Toast.LENGTH_SHORT).show()
}
}
}
return super.onTouchEvent(event)
}

  1. 效果

Path 我们就先学习到这里,该篇文章后面会有 Path 实战。

PathMeasure

上部分我们讲解了 Path 路径的知识,现在来看 Path 除了绘制图形好像也没什么作用,当然如果只是单纯显示 Path 绘制的图形,那我也就不介绍该篇的重点了。Android SDK 提供了一个非常有用的 API 来帮组开发者实现一个 Path 路径追踪,这个 API 就是 PathMeasure , 通过它可以实现复杂切绚丽的效果。

概念

PathMeasure 类似一个计算器,可以计算出指定路径的一些信息,比如路径总长、指定长度所对应的坐标点等。

API 使用

构造方法

//1.空参
public PathMeasure()
//2.path 代表一个已经完成的 Path,forceClosed 代表是否最后闭合
public PathMeasure(Path path, boolean forceClosed)

简单函数使用

  1. getLength() 函数

PathMeasure#getLength() 函数的使用非常广泛,其作用就是获取计算的路径长度,下面以一个例子来看下它的用法。

效果:

代码:

override fun draw(canvas: Canvas) {
super.draw(canvas)
/**

  • 1. getLength
    */
    //将起点移动到 100,100 的位置
    mPath.moveTo(100f,100f)
    //绘制连接线
    mPath.lineTo(100f,450f)
    mPath.lineTo(450f,500f)
    mPath.lineTo(500f,100f)
    mPathMeasure.setPath(mPath,false)//不被闭合
    mPathMeasure2.setPath(mPath,true)//闭合
    println(“forceClosed false pathLength = m P a t h M e a s u r e . l e n g t h " ) p r i n t l n ( " f o r c e C l o s e d t r u e p a t h L e n g t h = {mPathMeasure.length}") println("forceClosed true pathLength = mPathMeasure.length")println("forceClosedtruepathLength={mPathMeasure2.length}”)
    canvas.drawPath(mPath,mPathPaint)
    }

输出:

System.out: forceClosed false pathLength =1106.6663
System.out: forceClosed true pathLength =1506.6663

可以看见,如果 forceClosed 设置为 true/false 测量的是各自的 path 。

  1. isClosed() 函数

该函数用于判断测量 Path 时是否计算闭合。所以,如果在关联 Path 的时候设置 forceClosed 为 true ,那么这个函数的返回值也一定为 true.

  1. nextContour() 函数

我们知道,Path 可以由多条曲线构成,但无论是 getLength()、getSegment() 还是其它函数,都只会对针对其中第一条线段进行计算。而 nextContour 就是用于跳转到下一条曲线的函数,如果跳转成功,则返回 true ; 如果跳转失败,则返回 false.下面看一个示例,分别创建 3 条闭合 Path,然后利用 PathMeasure 来依次测量。

效果:

代码:

/**

  • 2. nextContour
    */
    mPath.addCircle(500f,500f,10f,Path.Direction.CW)
    mPath.addCircle(500f,500f,80f,Path.Direction.CW)
    mPath.addCircle(500f,500f,150f,Path.Direction.CW)
    mPath.addCircle(500f,500f,200f,Path.Direction.CW)

mPathMeasure.setPath(mPath,false)//不被闭合

canvas.drawPath(mPath,mPathPaint)

do {
println(“forceClosed pathLength =${mPathMeasure.length}”)
}while (mPathMeasure.nextContour())

输出:

2019-12-03 22:37:22.340 18501-18501/? I/System.out: forceClosed pathLength =62.42697
2019-12-03 22:37:22.341 18501-18501/? I/System.out: forceClosed pathLength =501.84265
2019-12-03 22:37:22.341 18501-18501/? I/System.out: forceClosed pathLength =942.0967
2019-12-03 22:37:22.341 18501-18501/? I/System.out: forceClosed pathLength =1256.1292

在这里,我们通过 do…while 循环和 measure.nextContour() 函数相结合,依次拿到 Path 中所有的曲线

通过这个例子我们可以知道,通过 PathMeasure#nextContour 函数得到的曲线顺序与 Path 添加的顺序相同

getSegment() 函数

//startD:开始截取位置距离 Path 起始点的长度
//stopD: 结束截取位置距离 Path 起始点的长度
//dst: 截取的 Path 将会被添加到 dst 中,注意是添加,而不是替换
//startWithMoveTo: 起始点是否使用 moveTo
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

getSegment 用于截取整个 Path 中的某个片段,通过参数 startD 和 stopD 来控制截取的长度,并将截取后的 Path 保存到参数 dst 中。最后一个参数 startWithMoveTo 表示起始点是否使用 moveTo 将路径的新起始点移到结果 Path 的起始点,通常设置为 true ,以保证每次截取的 Path 都是正常完整的,通常和 dst 一起使用,因为 dst 中保存的 Path 是被不断添加的,而不是每次被覆盖的;如果设置为 false ,则新增的片段会从上一次 Path 终点开始计算,这样可以保证截取的 Path 片段是连续的。

注意:

  • 如果 startD ,stopD 数值不在取值范围内,或者 startD == stopD ,那么就会返回 false ,并且 dst 不会有 Path 数据。
  • 开启硬件加速后,绘图会出现问题,因此,在使用 getSegment 是需要 在构造函数中调用 setLayerType(LAYER_TYPE_SOFTWARE,null) 函数来禁用硬件加速

getSegment 举例:

/**

  • 3. getSegment
    */
    mPath.addCircle(500f,500f,200f,Path.Direction.CCW)
    mPathMeasure.setPath(mPath,false)//不被闭合
    val segment = mPathMeasure.getSegment(50f, 500f, mTempPath, true)
    println(“是否截取成功:$segment”)
    canvas.drawPath(mTempPath,mPathPaint)

效果:

注意:

如果 startWithMoveTo 为 true,则被截取出来的 path 片段保持原状;如果 startWithMoveTo 为 false ,则会将截取出来的 Path 片段的起始点移动到 dst 的最后一个点,以保证 dst 路径的连续性。

实现一个实时截取的动画:

代码实现:

  1. 定义一个值动画

val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
valueAnimator.addUpdateListener {
animation -> stopValues = animation.animatedValue as Float
invalidate()
}
valueAnimator.repeatCount = ValueAnimator.INFINITE
valueAnimator.setDuration(1500)
valueAnimator.start()

  1. 实时截取绘制

mPath.addCircle(500f,500f,200f,Path.Direction.CCW)
mPathMeasure.setPath(mPath,false)//不被闭合
mTempPath.rewind()
stop = mPathMeasure.length * stopValues
val start = (stop - (0.5 - Math.abs(stopValues - 0.5)) * mPathMeasure.length).toFloat()
val segment = mPathMeasure.getSegment(start, stop, mTempPath, true)
println(“总长度: m P a t h M e a s u r e . l e n g t h 是否截取成功 : {mPathMeasure.length} 是否截取成功: mPathMeasure.length是否截取成功:segment + start: s t a r t s t o p : start stop: startstop:stop”)
canvas.drawPath(mTempPath,mPathPaint)

getPosTan

这个方法是用于得到路径上某一长度的位置以及该位置的正切值

//distance:距离 Path 起点的长度,取值范围: 0 <= distance <= getLength
//pos:该点的坐标值 , 当前点在画布上的位置,有两个数值,分别为x,y坐标。
//tan:该点的正切值, 当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。
boolean getPosTan (float distance, float[] pos, float[] tan)

下面以一个 demo 来讲解 getPosTan 具体使用,先来看一个效果图:

感觉是不是很炫,那么我们是怎么实现的呢?先来看一下核心代码,如下:

override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//清楚 path 数据
mTempPath.rewind()
//绘制一个模拟公路
addLineToPath()
//测量 path,闭合
mPathMeasure!!.setPath(mTempPath, true)
//动态变化的值
mCurValues += 0.002f
if (mCurValues >= 1) mCurValues = 0f
//拿到当前点上的 正弦值坐标
mPathMeasure!!.getPosTan(mPathMeasure!!.length * mCurValues, pos, tan)
//通过正弦值拿到当前角度
val y = tan!![1].toDouble()
val x = tan!![0].toDouble()
var degrees = (Math.atan2(y, x) * 180f / Math.PI).toFloat()
println(“角度:$degrees”)
mMatrix!!.reset()
//拿到 bitmap 需要旋转的角度,之后将矩阵旋转
mMatrix!!.postRotate(degrees, mBitmap!!.width / 2.toFloat(), mBitmap!!.height / 2.toFloat())
//拿到 path 上的 pos 点随着点移动
mMatrix!!.postTranslate(pos!![0] - mBitmap!!.getWidth() / 2, pos!![1] - mBitmap!!.getHeight() / 2)
//绘制Bitmap和path
canvas!!.drawPath(mTempPath, mTempPaint)
canvas!!.drawBitmap(mBitmap!!, mMatrix!!, mTempPaint)

//重绘
postInvalidate()
}

这里涉及到了初中数学,正弦值,当然 Android SDK API 也给我们封装了一个求正弦值的类 Math ,我们可以根据 PathMeasure#getPosTan 拿到当前点上的坐标 tan[] ,然后根据 Math#atan 求出 tan ,最后根据 degrees * 180 / π 公式来求出角度。然后矩阵旋转得到一个旋转之后的 car 不断重绘就是现在这个效果了。还是很简单把。

这里我们简单回顾下三角函数的计算吧

还不会的可以参考这个文章正弦,余弦,正切值计算

getMatrix

这个方法是用于得到路径上某一长度的位置以及该位置的正切值的矩阵:

参数作用备注
返回值(boolean)判断获取是否成功true表示成功,数据会存入matrix中,false 失败,matrix内容不会改变
distance距离 Path 起点的长度取值范围: 0 <= distance <= getLength
matrix根据 falgs 封装好的matrix会根据 flags 的设置而存入不同的内容
flags规定哪些内容会存入到matrix中可选择 POSITION_MATRIX_FLAG(位置) ANGENT_MATRIX_FLAG(正切)

其实这个方法就相当于我们在前一个例子中封装 matrix 的过程由 getMatrix 替我们做了,我们可以直接得到一个封装好到 matrix,岂不快哉。

但是我们看到最后到 flags 选项可以选择 位置 或者 正切 ,如果我们两个选项都想选择怎么办?

如果两个选项都想选择,可以将两个选项之间用 | 连接起来,如下:

measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);

我们可以将上面都例子中 getPosTan 替换为 getMatrix, 看看是不是会显得简单很多:

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

//清除 path 数据
mTempPath.rewind()
//绘制一个模拟公路
addLineToPath()
//测量 path,闭合
mPathMeasure!!.setPath(mTempPath, true)
//动态变化的值
mCurValues += 0.002f
if (mCurValues >= 1) mCurValues = 0f

// 获取当前位置的坐标以及趋势的矩阵
mPathMeasure!!.getMatrix(mPathMeasure!!.getLength() * mCurValues, mMatrix!!,
(PathMeasure.TANGENT_MATRIX_FLAG or PathMeasure.POSITION_MATRIX_FLAG))
// 将图片绘制中心调整到与当前点重合(偏移加旋转)

文末

那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试

  • 技能

  • 面试技巧+面试题

    《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
    我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
    Android希望=技能+面试

  • 技能
    [外链图片转存中…(img-P0gbNABF-1714703624651)]

  • 面试技巧+面试题
    [外链图片转存中…(img-tYBLAiyT-1714703624652)]
    《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 28
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值