//需要按下事件
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)
}
- 效果
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)
简单函数使用
- 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 。
- isClosed() 函数
该函数用于判断测量 Path 时是否计算闭合。所以,如果在关联 Path 的时候设置 forceClosed 为 true ,那么这个函数的返回值也一定为 true.
- 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 路径的连续性。
实现一个实时截取的动画:
代码实现:
- 定义一个值动画
val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
valueAnimator.addUpdateListener {
animation -> stopValues = animation.animatedValue as Float
invalidate()
}
valueAnimator.repeatCount = ValueAnimator.INFINITE
valueAnimator.setDuration(1500)
valueAnimator.start()
- 实时截取绘制
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()
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
题外话
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料免费分享出来。
【Android学习PDF+学习视频+面试文档+知识点笔记】
【Android思维脑图(技能树)】
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
需要的朋友,可以点赞关注+转发”前往免费领取!
自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料免费分享出来。
【Android学习PDF+学习视频+面试文档+知识点笔记】
【Android思维脑图(技能树)】
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
[外链图片转存中…(img-fIHlKStz-1710773333204)]
需要的朋友,可以点赞关注+转发”前往免费领取!
希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展~