范围裁切
Android自定义View的范围裁切是通过canvas来实现的,主要是 canvas.clipRect() 和 canvas.clipPath() 两个方法
clipRect()用于裁切出一块矩形区域, 比如我们对上面的图先裁切再画
1 canvas.clipRect(padding, padding, padding+ bitmapWidth, padding+ bitmapWidth/2f) 2 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
得到结果:
clipPath()则可以根据给定的path来进行裁切,如:
1 private val path = Path() 2 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 3 path.addCircle(padding+ bitmapWidth/2, padding+ bitmapWidth, bitmapWidth/2, Path.Direction.CCW) 4 } 5 6 override fun onDraw(canvas: Canvas) { 7 super.onDraw(canvas) 8 9 canvas.clipPath(path) 10 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint) 11 }
得到:
-------------------------------------------------------------------------------强行插入小结的分割线--------------------------------------------------------------
强行插入小结,截止目前,我们已经知道了画出圆形图像的三种方法:
1, 利用xferMode, 在同一个位置先画个圆,再画Bitmap
1 private val xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) 2 override fun onDraw(canvas: Canvas) { 3 canvas.drawOval(IMAGE_MARGIN, IMAGE_MARGIN, 4 IMAGE_MARGIN+ IMAGE_WIDTH, IMAGE_MARGIN+ IMAGE_WIDTH, 5 paint) 6 7 paint.xfermode = xfermode 8 canvas.drawBitmap(getBitMap(IMAGE_WIDTH.toInt()), 9 IMAGE_MARGIN, 10 IMAGE_MARGIN, 11 paint) 12 }
2, 利用Paint的setShader(), 将Bitmap作为背景, 再在上面画一个圆
1 private val bitmapShader = BitmapShader(getBitMap(500), Shader.TileMode.MIRROR, Shader.TileMode.CLAMP) 2 override fun onDraw(canvas: Canvas) { 3 super.onDraw(canvas) 4 //画图 5 paint.shader = bitmapShader 6 canvas.drawCircle(width-radius, radius*1.5f, radius, paint) 7 }
3, 就是上文说的裁切, 先clipPath(circlePath), 再drawBitmap
通常情况下我们不用第3种,因为clip之后无法对范围之外的部分进行抗锯齿修复,那么很可能会出现毛边
--------------------------------------------------------------------------------------------over-------------------------------------------------------------------------
范围裁切还有两个反向方法 clipOutRect()和clipOutPath(), 具体意思看下图就明白了
1 canvas.clipOutPath(path)
范围裁切比较简单, 下面重点看看几何变换
几何变换
Andriod自定义view的几何变换可以通过canvas或者Matrix来实现, 先看canvas
比如移动:
1 canvas.translate(100f.toPx, 0f) 2 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
比如旋转, 注意, 默认情况下旋转的轴心是canvas的坐标原点,也就是view的左上角
1 canvas.rotate(30f) 2 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
所以正确的旋转姿势是这样的:
canvas.rotate(30f, padding+ bitmapWidth/2f.toPx, padding+ bitmapWidth/2f.toPx)
现在我们让问题稍微复杂一点点, 想对图片先平移,再旋转, 我们先直接将上面两行代码简单组合起来
1 canvas.translate(100f.toPx, 0f) 2 canvas.rotate(30f, padding+ bitmapWidth/2f.toPx, padding+ bitmapWidth/2f.toPx) 3 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
需要特别注意的一点是: canvas的几何变换改变的都是canvas的坐标系,而不是移动图片
所以上面的代码可以这样理解:
1, 把坐标系右移100
2, 旋转坐标系(这里有个很容易写错的地方, 就是老想着右移了一百,那轴心是不是要右移100呢?其实不是的, 因为移动的是坐标系)
3, 画图
我们也可以这样思考:
我们先在(padding, padding)位置画一张bitmap
然后把这个Bitmap向右移动100
再旋转, 那么此时旋转的轴心位置就是(100+原轴心x, 原轴心y)
然后我们倒着写代码:
1 canvas.rotate(30f, padding+ bitmapWidth/2f.toPx+ 100f.toPx, padding+ bitmapWidth/2f.toPx) 2 canvas.translate(100f.toPx, 0f) 3 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
上面两种方式得到的结果是相同的, 个人觉得第二种思考方式更容易理解, 因为不用去想着移动坐标系
现在我们让情况再复杂一点, 模仿一个三维效果, 这就要用到Camera工具
如图, camera坐标系和canvas坐标系不同,新增了z轴, 朝屏幕里面是正向, y轴向上为正, x轴向右为正
camera相当于上图黄点处的一个虚拟相机,给我们画出来的图形做个投影
1 private val camera = Camera() 2 3 init { 4 camera.rotateX(40f) 5 //单位是英寸,默认值为-8 6 //但是为了适配不同像素密度的手机, 这里不应该写固定值 7 camera.setLocation(0f,0f, -8f*resources.displayMetrics.density) 8 }
1 camera.applyToCanvas(canvas) 2 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
看起来是有效果了, 但它好像是斜的。
这是因为camera的坐标原点是固定再屏幕左上角的那个位置, 而且camera并没有给我们方法去移动它的坐标系
这可怎么办呢?
你不来我过去就行了
我先把我的图片移动到坐标原点的位置, 啪, 照张相, 然后再移回来
1 canvas.translate(padding+ bitmapWidth/2,padding+ bitmapWidth/2) 2 camera.applyToCanvas(canvas) 3 canvas.translate(-(padding+ bitmapWidth/2), -(padding+ bitmapWidth/2)) 4 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
这段代码从下往上读或许更好理解:
1, 画一张图
2, 把图往左上移到camera的坐标原点
3, 拍照
4, 把图再移回来
或者正向理解:
1, 把坐标系往下移
2, 照相,
3, 把坐标系再移回去
4, 画图
两种方式都能想通, 重点是 你移动的是图还是坐标系
现在我们把难度再增加一点, 如何实现一个翻页的效果呢?就是图像的上半部分不动, 让下半部分翻起来
这就要用到裁切了
我们先处理下面翻页的部分
1 canvas.translate(padding+ bitmapWidth/2, padding+ bitmapWidth/2) 2 camera.applyToCanvas(canvas) 3 canvas.clipRect(-bitmapWidth, 0f, bitmapWidth, bitmapWidth) 4 canvas.translate(-(padding+ bitmapWidth/2), -(padding+ bitmapWidth/2)) 5 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint)
几何变换的顺序参考上面的两种思考方式, 不再多说
接下来处理上半部分, 只裁切不拍照
1 canvas.save() 2 canvas.translate(padding+ bitmapWidth/2, padding+ bitmapWidth/2) 3 canvas.clipRect(-bitmapWidth, -bitmapWidth, bitmapWidth, 0f) 4 canvas.translate(-(padding+ bitmapWidth/2), -(padding+ bitmapWidth/2)) 5 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint) 6 canvas.restore()
注意这里的 canvas.restore(), 没有这句的话canvas的这些变换会继续应用到下面的代码
canvas.save()和 canvas.restore(), 要搭配使用, 否则会报异常
java.lang.IllegalStateException: Underflow in restore - more restores than saves
看下运行结果
我们还可以让图片旋转一下, 来营造斜着翻页的感觉, 不多解释了,代码如下
1 //翻页效果 2 //首先范围裁切,分开画上下部分 3 canvas.save() 4 canvas.translate(padding+ bitmapWidth/2, padding+ bitmapWidth/2) 5 canvas.rotate(-30f) 6 canvas.clipRect(-bitmapWidth, -bitmapWidth, bitmapWidth, 0f) 7 canvas.rotate(30f) 8 canvas.translate(-(padding+ bitmapWidth/2), -(padding+ bitmapWidth/2)) 9 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint) 10 canvas.restore() 11 //再画下面部分 12 canvas.save() 13 canvas.translate(padding+ bitmapWidth/2, padding+ bitmapWidth/2) 14 canvas.rotate(-30f) 15 camera.applyToCanvas(canvas) 16 canvas.clipRect(-bitmapWidth, 0f, bitmapWidth, bitmapWidth) 17 canvas.rotate(30f) 18 canvas.translate(-(padding+ bitmapWidth/2), -(padding+ bitmapWidth/2)) 19 canvas.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint) 20 canvas.restore()
最后再简单提下Matrix
Matrix可以用来做常见变换和一些自定义变换
常见变换包括
其中PreTranslate()/ preRotate()/...就相当于canvas的translate()和Rotate()/.....
postTranslate()/... 这些就是我们上面说的倒着写的思维方式
简单使用如下:
1 myMatrix.reset() 2 myMatrix.postRotate(30f, padding+ bitmapWidth/2f.toPx, padding+ bitmapWidth/2f.toPx) 3 myMatrix.postTranslate(100f.toPx, 0f) 4 canvas.withMatrix(myMatrix 5 ) { 6 this.drawBitmap(getBitMap(bitmapWidth.toInt()), padding, padding, paint) 7 }
使用 Matrix 来做自定义变换
Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) 用点对点映射的方式设置变换
setPolyToPoly()
的作用是通过多点的映射的方式来直接设置变换。「多点映射」的意思就是把指定的点移动到给出的位置,从而发生形变。例如:(0, 0) -> (100, 100) 表示把 (0, 0) 位置的像素移动到 (100, 100) 的位置,这个是单点的映射,单点映射可以实现平移。而多点的映射,就可以让绘制内容任意地扭曲
参数里,src
和 dst
是源点集合目标点集;srcIndex
和 dstIndex
是第一个点的偏移;pointCount
是采集的点的个数(个数不能大于 4,因为大于 4 个点就无法计算变换了)