咱们来回顾一下前面 5 篇文章咱们讲解的内容,先是从 View 基础 ,事件分发,View 工做流程,而后是 Paint, Canvas 讲解,该篇咱们仍是分 2 个部分讲解,先是 Path, 而后是 PathMeaure ,若是想作出比较炫的动画,那么必不可少是离不开这 2 个类的,下面仍是以 API 的使用和 demo 实战来说解。css
Path
又叫路径,它是一个比较重要的概念,在自定义 View 中它的重要程度基本上跟 Paint 差很少,那么它能够用来干什么勒, 能够说 Path 是万能的也不为过,为何这么说呢,由于只要给我任何一个 Path 路径,我就能把它绘制出来。下面咱们先来熟悉一下它有哪些 API 吧,请看下表:html
API | 功能 | 说明 |
---|---|---|
moveTo | 移动起点 | 移动下一次操做的起点位置 |
setLastPoint | 设置终点 | 重置当前 path 中最后一个点位置,若是在绘制以前调用,效果和 moveTo 相同 |
lineTo | 链接直线 | 添加上一个点到当前点 |
close | 闭合路径 | 链接第一个点到最后一个点,造成一个闭合区间 |
addRect,addRoundRect,addOval,addCircle,addPath,addArc,arcTo | 添加内容 | 添加(矩形,圆角矩形,椭圆,圆,路径,圆弧)到当前 Path 中 |
isEmpty | 是否为空 | 判断当前 Path 是不是空的 |
isRect | 是否为矩形 | 判断 Path 是不是一个矩形 |
set | 替换路径 | 用新的路径替换到当前路径的全部内容 |
offset | 偏移路径 | 对当前路径以前的操做进行偏移(不会影响以后的操做) |
quadTo,cubicTo | 贝塞尔曲线 | 分别为二次和三次贝塞尔取消的方法 |
rMoveTo, rLineTo,rQuadTo,rCubicTo | rXXX方法 | 不带 r 的方法时基于远点的坐标系(偏移量),rXXX 方法是基于当前点坐标系(偏移量) |
setFillType, getFillType,isInverseFilltype,toggleInverseFilltype | 填充模式 | 设置,获取,判断和切换填充模式 |
incReserve | 提示方法 | 提示 Path 还有多少个点等待加入 |
op | 布尔操做 | 对2个Path进行布尔运算(取交集并集) |
computeBounds | 计算Path 的边界 | 计算边界 |
reset,rewind | 重置路径 | 清除Path中的内容,reset不保留内部数据结构,但会保留 Filltype,rewind会保留内部的数据结构,但不保留 FillType |
transform | 矩阵操做 | 矩阵变换 |
moveTo,lineTo,setLastPoint,closejava
//从0.0 链接 400,600
mPath.lineTo(400f,600f)
//重置上一点至关于 0,0 到 600,200, 设置以前操做的最后一个点位置(会影响以前跟以后的起始点)
//mPath.setLastPoint(600f,200f)
//从 400,600 链接 900,100
mPath.lineTo(900f,100f)
//开始绘制
anvas!!.drawPath(mPath,mPathPaint)
复制代码
咱们把上面注释放开,以下代码:git
//从0.0 链接 400,600
mPath.lineTo(400f,600f)
//重置上一点至关于 0,0 到 600,200, 设置以前操做的最后一个点位置(会影响以前跟以后的起始点)
mPath.setLastPoint(600f,200f)
//从 600,200 链接 900,100
mPath.lineTo(900f,100f)
//开始绘制
anvas!!.drawPath(mPath,mPathPaint)
复制代码
实现效果以下:github
经过上图咱们发现 setLastPoint 设置了以后改变了以前的最后一次的坐标点,能够理解为更新最后一点的坐标,咱们发现每次都是从坐标角 (0,0) 开始绘制,那么有没有一个方法指定从哪一个起点开始绘制,正好,你能够试试 moveTo 它能够指定 path 的起点,以下代码:canvas
//moveTo 设置起点
mPath.moveTo(600f,200f)
//从0.0 链接 400,600
mPath.lineTo(400f,600f)
mPath.lineTo(800f,300f)
//最后一点和起点封闭
mPath.close()
复制代码
经过上图咱们先利用 Path#moveTo
将 Path 起点设置为 (400,600) 开始绘制,最后调用了 Path#close
将为闭合的链接线闭合。数据结构
addXxx 系列ide
咱们就以 矩形,圆角矩形,椭圆,圆,圆弧 的顺序绘制函数
//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 将变量移动到圆弧的起点位移 ,也就意味着:oop
forceMoveTo | 含义 | 等价方法 |
---|---|---|
true | 将最后一个点移动到圆弧起点,即不链接最后一个点与圆弧起点 | public void addArc (RectF oval, float startAngle, float sweepAngle) |
false | 不移动,而是链接最后一个点与圆弧起点 | public void arcTo (RectF oval, float startAngle, float sweepAngle) |
computeBounds,set,setPath
先绘制一个 圆和矩形
mPath.addCircle(500f, 500f, 150f, Path.Direction.CW)
canvas!!.drawPath(mPath, mPathPaint) // 绘制Path
canvas.drawRect(300f,800f,800f,1300f ,mPathPaint) // 绘制矩形
复制代码
重写 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)
}
复制代码
效果
Path 咱们就先学习到这里,该篇文章后面会有 Path 实战。
上部分咱们讲解了 Path 路径的知识,如今来看 Path 除了绘制图形好像也没什么做用,固然若是只是单纯显示 Path 绘制的图形,那我也就不介绍该篇的重点了。Android SDK 提供了一个很是有用的 API 来帮组开发者实现一个 Path 路径追踪,这个 API 就是 PathMeasure
, 经过它能够实现复杂切绚丽的效果。
PathMeasure 相似一个计算器,能够计算出指定路径的一些信息,好比路径总长、指定长度所对应的坐标点等。
构造方法
//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 =${mPathMeasure.length}")
println("forceClosed true pathLength =${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 添加的顺序相同
//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 片断是连续的。
注意:
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("总长度:${mPathMeasure.length} 是否截取成功:$segment + start:$start stop:$stop")
canvas.drawPath(mTempPath,mPathPaint)
复制代码
这个方法是用于获得路径上某一长度的位置以及该位置的正切值
//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 不断重绘就是如今这个效果了。仍是很简单把。
这里咱们简单回顾下三角函数的计算吧
还不会的能够参考这个文章正弦,余弦,正切值计算
这个方法是用于获得路径上某一长度的位置以及该位置的正切值的矩阵:
参数 | 做用 | 备注 |
---|---|---|
返回值(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))
// 将图片绘制中心调整到与当前点重合(偏移加旋转)
mMatrix!!.preTranslate(-mBitmap!!.getWidth() / 2f, -mBitmap!!.getHeight() / 2f);
//绘制Bitmap和path
canvas!!.drawPath(mTempPath, mTempPaint)
canvas!!.drawBitmap(mBitmap!!, mMatrix!!, mTempPaint)
//重绘
postInvalidate()
}
复制代码
实现效果这里跟上图同样就不在贴图了,这里不用在求 tan 角度 什么的,看起来比第一种简单把,具体使用哪种看实际需求场景吧。
详细代码 SpiderWebView.kt 请移步 GitHub
实现原理:
利用 Path 路径绘制眼睛 ,嘴巴,而后在经过Path#computeBounds
拿到 RectF 矩形边界并绘制出来,最后经过动画来执行不断重绘截取,就造成了上面效果了。
(Ps: 上面效果只是一个练习 demo 不建议直接在项目中使用。)
详细代码 FaceLoadingView 请移步 GitHub