Android-高级-UI-进阶之路--(六)-PathMeasure-制作路径动画,近期想跳槽的程序员必看

本文详细介绍了Android中的Path和PathMeasure类,展示了如何使用Path绘制各种图形并计算其长度,以及如何通过PathMeasure获取路径信息,如长度、坐标点和曲线顺序。文章还涉及了PathMeasure的构造方法、getLength、isClosed、nextContour和getSegment函数的实际应用。
摘要由CSDN通过智能技术生成

通过上图我们先利用 Path#moveTo 将 Path 起点设置为 (400,600) 开始绘制,最后调用了 Path#close 将为闭合的连接线闭合。

  1. addXxx 系列

我们就以 矩形,圆角矩形,椭圆,圆,圆弧 的顺序绘制

//Path.Direction.CW/CCW 顺时针/逆时针

//1. 添加矩形到 Path
void addRect (float left, float top, float right, float bottom, Path.Direction dir)
//2. 添加 圆角矩形到 Path
void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)
//3. 添加 椭圆 到 Path
void addOval (RectF oval, Path.Direction dir)
//4. 添加 圆 到 Path
void addCircle (float x, float y, float radius, Path.Direction dir)
//5. 添加 圆弧 到 Path ,直接添加一个圆弧到path中
void addArc (RectF oval, float startAngle, float sweepAngle)
//添加一个圆弧到 path,如果圆弧的起点和上次最后一个坐标点不相同,就连接两个点
void arcTo (RectF oval, float startAngle, float sweepAngle)
void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)

mPathPaint.textSize = 50f
//1. 添加矩形到 Path
mPath.addRect(100f,300f,400f,700f,Path.Direction.CW)//顺时针
canvas!!.drawText(“1”,200f,500f,mPathPaint)

//2. 添加 圆角矩形到 Path
mPath.addRoundRect(100f + 500,300f,1000f ,700f,30f,30f,Path.Direction.CCW)//逆时针
canvas!!.drawText(“2”,800f,500f,mPathPaint)

//3. 添加 椭圆 到 Path
mPath.addOval(100f,1300f,600f ,1000f,Path.Direction.CCW)//逆时针
canvas!!.drawText(“3”,300f,1150f,mPathPaint)

//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)

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

代码真的是重质不重量,质量高的代码,是当前代码界提倡的,当然写出高质量的代码肯定需要一个相当高的专业素养,这需要在日常的代码书写中逐渐去吸收掌握,谁不是每天都在学习呀,目的还不是为了一个,为实现某个功能写出高质量的代码。

所以,长征路还长,大家还是好好地做个务实的程序员吧。

最后,小编这里有一系列Android提升学习资料,有兴趣的小伙伴们可以来看下哦~

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

img2.imgtp.com/2024/03/13/H4lCoPEF.jpg" />

最后

代码真的是重质不重量,质量高的代码,是当前代码界提倡的,当然写出高质量的代码肯定需要一个相当高的专业素养,这需要在日常的代码书写中逐渐去吸收掌握,谁不是每天都在学习呀,目的还不是为了一个,为实现某个功能写出高质量的代码。

所以,长征路还长,大家还是好好地做个务实的程序员吧。

最后,小编这里有一系列Android提升学习资料,有兴趣的小伙伴们可以来看下哦~

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值