Canvas 的范围裁切
一旦涉及到canvas的坐标变化的(旋转、缩放、平移)都需要逆向写代码,然后放大,在做偏移,最后画图
clipRect()
clipPath() // 切出来的圆为什么没有抗锯⻮效果?因为「强⾏切边」
clipOutRect() / clipOutPath()
clipRect 和 clipPath 是用来裁剪绘制区域的方法,而 clipOutRect 和 clipOutPath 是它们的反向操作。以下是这些方法的作用及相关的抗锯齿问题的解释:
1. clipRect()
-
功能:裁剪一个矩形区域,只有这个矩形区域内的内容会被绘制,矩形区域外的内容会被裁剪掉。
-
用法:
canvas.clipRect(left, top, right, bottom) -
常见用途:用来限制绘制区域,比如绘制一个部分视图。
2. clipPath()
-
功能:裁剪一个由
Path定义的区域,只有这个区域内的内容会被绘制,区域外的内容会被裁剪掉。 -
用法:
val path = Path() path.addCircle(cx, cy, radius, Path.Direction.CW) // 添加圆形路径 canvas.clipPath(path) -
抗锯齿问题:
-
使用
clipPath时,裁剪的区域边缘是“硬切边”,因此会产生锯齿。 -
原因:
clipPath强制裁剪像素,未使用抗锯齿处理。 -
解决方法:
- 开启抗锯齿:确保
Paint对象的抗锯齿属性isAntiAlias为true。 - 绘制边缘时,尽量避免复杂的路径操作,使用抗锯齿的形状填充代替裁剪。
- 或者在裁剪前后,手动处理边缘区域的光滑效果。
- 开启抗锯齿:确保
-
3. clipOutRect()
-
功能:从当前裁剪区域中裁剪掉指定的矩形区域,剩余区域继续绘制。
-
用法:
js canvas.clipOutRect(left, top, right, bottom) ```
- 常见用途:用于反向裁剪,即排除指定矩形外的部分作为绘制区域。
4. clipOutPath()
-
功能:从当前裁剪区域中裁剪掉指定路径(
Path)所定义的区域,剩余部分继续绘制。 -
用法:
val path = Path() path.addCircle(cx, cy, radius, Path.Direction.CW) canvas.clipOutPath(path) -
特点:这是
clipPath的反向操作,裁剪掉Path定义的区域。
clipPath 的抗锯齿问题(切出来的圆没有抗锯齿效果)
-
本质原因:
clipPath是“硬裁剪”操作,不支持抗锯齿的边缘处理,它直接基于像素点计算裁剪区域。 -
解决方法:
-
替代方法 1:使用遮罩绘制 在裁剪区域外设置一个半透明的遮罩,而不是直接使用
clipPath,这样可以让边缘更平滑:```kotlin val paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.color = Color.BLACK paint.alpha = 128 // 半透明 val path = Path() path.addCircle(cx, cy, radius, Path.Direction.CW) canvas.drawPath(path, paint) ``` -
替代方法 2:分步绘制 使用圆形填充而非裁剪,以减少边缘锯齿的显现:
```kotlin val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG) circlePaint.color = Color.WHITE canvas.drawCircle(cx, cy, radius, circlePaint) ```
-
总结
clipRect和clipPath用于限制绘制区域,clipOutRect和clipOutPath是它们的反向操作。clipPath的边缘没有抗锯齿效果,因为它是强制裁剪像素。可以通过替代绘制方法(如遮罩、填充路径等)来实现更平滑的边缘效果。
Canvas 的⼏何变换
重点:Canvas 的⼏何变换⽅法参照的是 View 的坐标系,⽽绘制⽅法(drawXxx())参照的是 Canvas ⾃⼰的坐标系。
translate(x, y)
rotate(degree)
scale(x, y)
skew(x, y)
1. translate(x, y)
-
功能:移动 Canvas 的坐标系。
-
作用:
- 将当前坐标系的原点移动到新的位置
(x, y)。 - 后续绘制操作都会基于新的原点进行。
- 将当前坐标系的原点移动到新的位置
-
用法:
canvas.translate(50f, 100f)上述代码将坐标系的原点从
(0, 0)移动到(50, 100)。 -
特点:
- 不会改变绘制内容的形状或大小。
- 常用于调整绘制内容的位置。
2. rotate(degree)
-
功能:旋转 Canvas 的坐标系。
-
作用:
- 绕当前坐标系的原点将坐标系顺时针旋转指定角度(以度为单位)。
- 后续绘制操作会基于旋转后的坐标系进行。
-
用法:
canvas.rotate(45f)上述代码会让坐标系顺时针旋转 45°。
-
特点:
-
旋转的中心点是当前原点。
-
如果需要绕特定点旋转,可以先用
translate移动原点,再旋转,然后还原原点:
-
canvas.translate(cx, cy)
canvas.rotate(45f)
canvas.translate(-cx, -cy)
3. scale(x, y)
-
功能:缩放 Canvas 的坐标系。
-
作用:
- 将坐标系按照
x和y方向分别缩放指定倍数。 - 后续绘制的内容会按照缩放后的坐标系大小进行缩放。
- 将坐标系按照
-
用法:
canvas.scale(2f, 1.5f)上述代码将 x 方向放大 2 倍,y 方向放大 1.5 倍。
-
特点:
-
默认缩放中心是原点
(0, 0),如果需要绕特定点缩放,可以先平移原点:canvas.translate(cx, cy) canvas.scale(2f, 2f) canvas.translate(-cx, -cy)
-
4. skew(x, y)
-
功能:倾斜 Canvas 的坐标系。
-
作用:
- 将坐标系按
x和y方向分别倾斜指定比例。 - 会导致绘制的内容产生斜切变换。
- 将坐标系按
-
用法:
canvas.skew(0.5f, 0.5f)上述代码会让 x 方向倾斜 0.5 倍,y 方向倾斜 0.5 倍。
-
特点:
x倾斜是指 x 方向的坐标随着 y 值的增加按比例变化。y倾斜是指 y 方向的坐标随着 x 值的增加按比例变化。- 常用于创造 3D 效果或仿射变换。
总结
| 方法 | 作用 | 影响内容 | 常见用途 |
|---|---|---|---|
translate | 移动坐标系 | 位置 | 改变绘制的起点 |
rotate | 旋转坐标系 | 位置和方向 | 绘制旋转图形 |
scale | 缩放坐标系 | 尺寸 | 放大或缩小绘制内容 |
skew | 倾斜坐标系 | 形状(斜切变换) | 制造透视效果或特殊形状 |
这些方法可以组合使用,通过先后顺序叠加各种变换,生成复杂的效果。需要注意变换会累积,所以通常在变换前后使用 save 和 restore 保持状态一致。
关于多重变换: Cavas的绘制过程中,Cavas的坐标是会变化的,因此,如果我们以图片为准,以倒着的方式写绘制的逻辑更加合适(针对图片,而不是针对坐标,采用正向思考,但是倒着写代码,也可以先把代码正向写完,然后再重新排列顺序,后边的放到前边,只需要把draw的代码放到最后就行了)。将 Canvas 的变换理解为 Canvas 的坐标系不变,每次变换是只对内部的绘制内容进行变换,同时把Canvas 的变换顺序看作是倒序的(即写在下⾯的变换先执⾏),可以更加⽅便进⾏多重变换的参数计算。
Matrix 的⼏何变换
preTranslate(x, y) / postTranslate(x, y)
preRotate(degree) / postRotate(degree)
preScale(x, y) / postScale(x, y)
preSkew(x, y) / postSkew(x, y)
其中 preXxx() 效果和 Canvas 的准同名⽅法相同, postXxx() 效果和 Canvas的准同名⽅法顺 序相反。如果多次绘制时重复使用 Matrix,在使用之前需要⽤ Matrix.reset() 来把 Matrix 重置。
使⽤ Camera 做三维旋转
rotate() / rotateX() / rotateY() / rotateZ()
translate()
setLocation()
其中,⼀般只⽤ rotateX() 和 rorateY() 来做沿 x 轴或 y 轴的旋转,以及使⽤ setLocation() 来调整放缩的视觉幅度。对 Camera 变换之后,要⽤ Camera.applyToCanvas(Canvas) 来应⽤到 Canvas。setLocation()这个⽅法⼀般前两个参数都填 0,第三个参数为负值。由于这个值的单位是硬编码写死的,因此像素密 度越⾼的⼿机,相当于Camera 距离View 越近,所以最好把这个值写成与机器的 density 成正⽐的⼀ 个负值,例如 -6 * density。
Camera 类可以用来进行 3D 旋转、平移等变换,常用于实现视图的三维效果。Camera 提供了多种方法来进行三维变换,使得应用能够创建更复杂的视觉效果,比如旋转、平移、缩放等。
常用方法说明:
1. rotate()
-
作用:用于旋转
Camera绘制的对象,旋转的角度以度为单位。 -
说明:它对所有的三维坐标轴进行旋转,默认为绕
Z轴旋转。 -
用法:
camera.rotate(degree); // 绕 Z 轴旋转
2. rotateX()
-
作用:绕
X轴旋转三维对象。 -
说明:这会改变物体的纵深效果,模拟物体绕水平轴旋转的效果。
-
用法:
camera.rotateX(degree); // 绕 X 轴旋转
3. rotateY()
-
作用:绕
Y轴旋转三维对象。 -
说明:这会改变物体的宽度效果,模拟物体绕垂直轴旋转的效果。
-
用法:
camera.rotateY(degree); // 绕 Y 轴旋转
4. rotateZ()
-
作用:绕
Z轴旋转三维对象。 -
说明:这会改变物体的前后效果,模拟物体绕屏幕的轴旋转的效果。通常这个方法用于旋转二维平面中的物体。
-
用法:
camera.rotateZ(degree); // 绕 Z 轴旋转
5. translate(x, y, z)
-
作用:用于平移物体到指定的三维坐标。
-
说明:它接受
x,y,z参数,用来改变物体在三维空间中的位置。 -
用法:
camera.translate(x, y, z); // 平移x:沿着X轴平移。y:沿着Y轴平移。z:沿着Z轴平移,通常用于模拟物体远离或接近视点。
6. setLocation(x, y, z)
-
作用:设置
Camera的位置。 -
说明:这与
translate()不同,setLocation()用来设置相机本身的位置,影响的是视点的位置,而非绘制的对象的位置。 -
用法:
camera.setLocation(x, y, z); // 设置 Camera 的位置x,y,z:指定相机在三维空间中的位置。此操作影响相机的视角,而不是物体本身的位置。
示例:使用 Camera 进行三维旋转
例1,通过 Camera 对某个 View 进行三维旋转,可以使用以下代码来实现:
private void rotate3DView(Canvas canvas, Bitmap bitmap, Paint paint) {
// 创建一个 Camera 对象
Camera camera = new Camera();
// 保存当前画布状态
canvas.save();
// 将画布移至中心点
canvas.translate(300f, 300f);
// 使用 Camera 旋转方法进行旋转
camera.rotateX(30f); // 绕 X 轴旋转
camera.rotateY(60f); // 绕 Y 轴旋转
// 将 Camera 的变换应用到画布
camera.applyToCanvas(canvas);
// 画出 Bitmap
canvas.drawBitmap(bitmap, -bitmap.getWidth() / 2f, -bitmap.getHeight() / 2f, paint);
// 恢复画布状态
canvas.restore();
}
代码说明:
camera.rotateX(30f); :绕 X 轴旋转 30 度。
camera.rotateY(60f); :绕 Y 轴旋转 60 度。
camera.applyToCanvas(canvas); :将 Camera 的变换应用到当前画布。这样,后续的绘制操作会受这些三维变换的影响。
canvas.drawBitmap(bitmap, -bitmap.getWidth() / 2f, -bitmap.getHeight() / 2f, paint); :在变换后的画布上绘制 Bitmap,这时 Bitmap 会呈现出三维旋转效果。
例2,对一张图片的部分区域进行旋转,实现类似的翻页效果:
:
class CameraView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// 创建一个画笔对象,设置抗锯齿
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
// 获取一个图像用于绘制
private val bitmap = getAvatar(BITMAP_SIZE.toInt())
/**
* 用来实现投影的一个类,通常作用于Z轴上的变换。
* Camera 类允许你在三维空间中对画布进行旋转和平移。
*/
private val camera = Camera()
init {
// 在初始化时,绕X轴旋转30度,模拟一个投影效果
camera.rotateX(30f)
// 通过设置 setLocation,可以调整 camera 的位置,以实现更深层次的投影效果(注释掉了)
// camera.setLocation(0f, 0f, -6 * resources.displayMetrics.density)
}
// 重写 onDraw 方法来进行绘制操作
override fun onDraw(canvas: Canvas) {
// 在绘制上半部分时,先保存当前 canvas 状态
canvas.save()
// 将画布平移,使得投影从图像的正中心开始
canvas.translate(BITMAP_PADDING + BITMAP_SIZE / 2, BITMAP_PADDING + BITMAP_SIZE / 2)
// 旋转画布 -30度,用来模拟图像的倾斜效果
canvas.rotate(-30f)
// 裁剪画布,只保留上半部分区域(从 -BITMAP_SIZE 到 0 的矩形区域)
canvas.clipRect(-BITMAP_SIZE, -BITMAP_SIZE, BITMAP_SIZE, 0f)
// 旋转画布回原来的角度
canvas.rotate(30f)
// 将画布移回原位置,这样就能将图像绘制在正确的地方
canvas.translate(-(BITMAP_PADDING + BITMAP_SIZE / 2), -(BITMAP_PADDING + BITMAP_SIZE / 2))
// 绘制图像
canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING, paint)
// 恢复之前保存的 canvas 状态,准备绘制下半部分
canvas.restore()
// 下半部分的绘制开始
canvas.save()
// 同样的方式,将画布平移到图像的正中心
canvas.translate(BITMAP_PADDING + BITMAP_SIZE / 2, BITMAP_PADDING + BITMAP_SIZE / 2)
// 旋转画布 -30度
canvas.rotate(-30f)
// 应用 camera 的投影效果
camera.applyToCanvas(canvas)
// 裁剪画布,只保留下半部分区域(从 0 到 BITMAP_SIZE 的矩形区域)
canvas.clipRect(-BITMAP_SIZE, 0f, BITMAP_SIZE, BITMAP_SIZE)
// 旋转画布回原来的角度
canvas.rotate(30f)
// 将画布移回原位置
canvas.translate(-(BITMAP_PADDING + BITMAP_SIZE / 2), -(BITMAP_PADDING + BITMAP_SIZE / 2))
// 绘制图像
canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING, paint)
// 恢复 canvas 状态
canvas.restore()
}
// 获取指定宽度的头像图像
private fun getAvatar(width: Int): Bitmap {
// 创建 BitmapFactory.Options 对象,设置只获取图像的宽高
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
// 读取资源文件,但不真正解码,获取图像的宽高
BitmapFactory.decodeResource(resources, R.drawable.avatar_rengwuxian, options)
// 重新设置 inJustDecodeBounds 为 false,准备进行实际解码
options.inJustDecodeBounds = false
// 设置目标密度,用来按指定宽度缩放图像
options.inDensity = options.outWidth
options.inTargetDensity = width
// 解码资源文件,返回指定宽度的 Bitmap
return BitmapFactory.decodeResource(resources, R.drawable.avatar_rengwuxian, options)
}
}
}
上边的代码实现的是沿着箭头所指向的线条对下边进行旋转,结果如下:
原图:
我们再进行旋转等效果变化的时候,实际上canvas的坐标系也会变化,这样转来转去容易头晕,那么我们如何解决这个问题呢?就是顺着想,倒着写,如何理解这个话?我们试想,如果canvas的坐标系不变化,是不是做很多变化都能比较容易的实现了,但是实际上不是这样的,所以我们在写代码的时候,比如上边的这种效果,对于上半部分,左上平移->正向旋转->裁剪->逆向旋转->右前平移->绘制,不过我们写代码的时候,需要按照 右前平移->逆向旋转->裁剪->正向旋转->左上平移->绘制,可以看到,我们写代码与实际思考的方式是倒序的,这样就能避免canvas坐标变化而导致的问题。
注意事项:
-
Camera的变换是相对于相机位置和视点的,因此setLocation()可以用来设置视点的位置,改变相机的观察角度。 -
对于
rotate()、rotateX()、rotateY()和rotateZ(),它们会影响绘制的对象,尤其在做复杂的动画效果时,需要灵活使用。 -
Camera变换一般用于给 2D 图形添加 3D 效果,且它主要通过画布的变换来实现,不会改变对象本身的坐标。 -
不使用
Camera或不设置camera.setLocation()时,图像会保持原始大小,旋转只改变物体的方向。 -
使用
Camera并设置camera.setLocation()后,图像的大小会因为相机位置的变化而发生变化,尤其是相机靠近或远离物体时,物体的投影会变小或变大。这是因为实际是在操作一个三维空间中的物体,而相机位置的变化影响了它在二维屏幕上的投影。
学后检测
一、单选题
-
关于
canvas.clipPath()的裁切效果,以下哪项说法正确?- A. 边缘总是带有抗锯齿效果
- B. 对应区域内的内容会被绘制,外部内容被裁剪掉
- C.
clipPath()只能用于矩形区域 - D.
clipPath()支持动态软边裁剪
答案:B
解析:clipPath 是硬裁剪,区域外全部被裁剪,边缘无抗锯齿。 -
以下关于
canvas.rotate(45f)的效果描述,哪项是错误的?- A. 会将 canvas 当前坐标系顺时针旋转 45°
- B. 绘制内容的形状不会变,只有坐标轴变了
- C. 默认绕 (0,0) 原点旋转
- D. 绘制内容会绕自身中心旋转
答案:D
解析:不指定原点时旋转中心是 (0,0),不是内容自身中心。
二、多选题
-
关于 Canvas 几何变换,下列哪些描述是正确的?
- A.
translate可以改变后续绘制的参考点 - B. 多次 scale 会累积,最终缩放是所有 scale 的乘积
- C. skew 只能用于 X 方向的斜切
- D. 变换顺序影响最终效果
答案:A、B、D
解析:C 错在 skew 既可 x 也可 y,顺序极其重要。 - A.
-
关于
Camera的使用,下列哪些说法正确?- A. Camera 可以让 2D 图形产生 3D 立体感
- B. setLocation 可改变投影视觉深度
- C. Camera 的变换需要 applyToCanvas 才生效
- D. Camera 只能旋转 Z 轴
答案:A、B、C
解析:D 错,Camera 可 rotateX/Y/Z。
三、判断题
-
( ) 使用
canvas.clipPath()裁剪一个圆形区域时,抗锯齿效果完全取决于 Paint 的 isAntiAlias 属性。答案:错
解析:clipPath 是像素级裁剪,本质不支持抗锯齿,Paint 的抗锯齿属性不影响 clipPath 的裁剪边缘。 -
( ) 多次调用
canvas.save()后,需要同样次数的canvas.restore()保证状态不混乱。答案:对
解析:save/restore 必须成对,否则变换栈混乱。
四、简答题
-
简述 clipPath 产生锯齿的本质原因,并列举两种实现平滑圆形裁切边缘的方法。
参考答案:
- 本质原因是 clipPath 属于像素级“硬裁切”,直接舍弃不在区域内的像素,边界没有过渡,所以锯齿明显。
- 方法一:用 Paint 绘制带抗锯齿属性的 Path 遮罩覆盖非圆区域(实现软遮罩)。
- 方法二:直接 drawCircle 绘制白色底圆实现圆形区域填充,而不用裁剪。
五、编程题
-
编写一个自定义 View,实现图片的上半部分正常显示,下半部分绕 X 轴旋转 30 度显示,要求使用 Camera 和 clipRect 实现,并简单注释主要步骤。
参考实现(伪代码+关键说明):
class RotateHalfView(context: Context, attrs: AttributeSet?) : View(context, attrs) { private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val bitmap = ... // your bitmap private val camera = Camera() private val bitmapWidth = bitmap.width private val bitmapHeight = bitmap.height override fun onDraw(canvas: Canvas) { // 上半部分 canvas.save() canvas.clipRect(0, 0, bitmapWidth, bitmapHeight / 2) canvas.drawBitmap(bitmap, 0f, 0f, paint) canvas.restore() // 下半部分 + 3D 旋转 canvas.save() canvas.translate(bitmapWidth / 2f, bitmapHeight / 2f) camera.save() camera.rotateX(30f) camera.applyToCanvas(canvas) camera.restore() canvas.clipRect(-bitmapWidth / 2f, 0f, bitmapWidth / 2f, bitmapHeight / 2f) canvas.translate(-bitmapWidth / 2f, -bitmapHeight / 2f) canvas.drawBitmap(bitmap, 0f, 0f, paint) canvas.restore() } }解析:关键点是 clipRect+Camera 旋转的配合,以及注意变换中心与顺序。