倒霉的菜鸟

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

范围裁切

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 个点就无法计算变换了)

 

posted on 2021-10-16 14:52  倒霉的菜鸟  阅读(286)  评论(0编辑  收藏  举报