自定义View④范围裁切和几何变换

996 阅读2分钟

Canvas 的范围裁切

  • clipRect()
  • clipPath() clipPath() 切出来的圆为什么没有抗锯⻮效果?因为「强行切边」
  • clipOutRect() / clipOutPath()

如下:

  1. 首先我们绘制一个矩形
  2. 然后用clipPath去裁切一个圆形
  3. 最后用clipRect去裁切一个矩形,最后得到了一个扇形

Canvas 的几何变换

  • translate(x, y) 移动
    canvas.save();
    canvas.translate(200.dp, 0.dp);
    canvas.drawBitmap(bitmap, x, y, paint);
    canvas.restore();
    
  • rotate(degree) 旋转
    • 20
    //旋转
    canvas.rotate(45f, 100.dp, 100.dp);
    
  • scale(x, y) 放缩
    //等比放大1.3倍
    //canvas.scale(1.3f, 1.3f, BITMAP_PADDING + BITMAP_SIZE / 2, BITMAP_PADDING + BITMAP_SIZE / 2);
    //不等比放大
    canvas.scale(1.3f, 0.7f, BITMAP_PADDING + BITMAP_SIZE / 2, BITMAP_PADDING + BITMAP_SIZE / 2);
    
    
  • skew(x, y)
    • 22
    //参数里的 sx 和 sy 是 x 方向和 y 方向的错切系数。
    canvas.skew(0f, 0.5f);
    

重点:Canvas 的几何变换方法参照的是 View 的坐标系,而绘制方法 (drawXxx())参照的是 Canvas 自己的坐标系。

关于多重变换:

Canvas 的变换方法多次调用的时候,由于 Canvas 的坐标系会整体被变换,因此当 平移、旋转、放缩、错切等变换多重存在的时候,Canvas 的变换参数会非常难以计 算,因此可以改用倒序的理解方式:

将 Canvas 的变换理解为 Canvas 的坐标系不变,每次变换是只对内部的绘制内 容进行变换,同时把 Canvas 的变换顺序看作是倒序的(即写在下面的变换先 执行),可以更加方便进行多重变换的参数计算。

Matrix 的几何变换

Camera投影效果

private val camera = Camera()
init{
    camera.rotateX(30f)
}

onDraw{
    //投影效果
    canvas.translate(BITMAP_PADDING+ BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2);
    camera.applyToCanvas(canvas)
    canvas.translate(-(BITMAP_PADDING+ BITMAP_SIZE/2),-(BITMAP_PADDING+ BITMAP_SIZE/2))
    canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING,paint)
}
23

翻页效果

24
onDraw{
    //翻页效果
    //上半部分 反着看,先裁切,再移动
    //移动
    canvas.save()
    canvas.translate(BITMAP_PADDING+ BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2);
    //裁切
    canvas.clipRect(-BITMAP_SIZE/2, -BITMAP_SIZE/2, BITMAP_SIZE/2, BITMAP_SIZE/2)
    canvas.translate(-(BITMAP_PADDING+ BITMAP_SIZE/2),-(BITMAP_PADDING+ BITMAP_SIZE/2))
    canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING,paint)
    canvas.restore()

    //画一条线
    canvas.save()
    paint.color = Color.parseColor("#FF0000")
    canvas.drawLine(BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2, BITMAP_PADDING+BITMAP_SIZE,BITMAP_PADDING+ BITMAP_SIZE/2,paint)
    canvas.restore()

    //下半部分,反着看,先裁切,camera旋转,再移动
    //移动
    canvas.save()
    canvas.translate(BITMAP_PADDING+ BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2);
    camera.applyToCanvas(canvas)
    //裁切
    canvas.clipRect(-BITMAP_SIZE/2, 2f, BITMAP_SIZE/2, BITMAP_SIZE/2)
    canvas.translate(-(BITMAP_PADDING+ BITMAP_SIZE/2),-(BITMAP_PADDING+ BITMAP_SIZE/2))
    canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING,paint)
    canvas.restore()
}

完整代码

private val BITMAP_SIZE = 200.dp
private val BITMAP_PADDING = 100.dp
class CameraView (context: Context,attributeSet: AttributeSet): View(context,attributeSet) {

  private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
  private val bitmap = getAvatar(BITMAP_SIZE.toInt())
  @RequiresApi(VERSION_CODES.LOLLIPOP)
  private val clipped = Path().apply {
    addOval(BITMAP_PADDING, BITMAP_PADDING, BITMAP_PADDING+ BITMAP_SIZE, BITMAP_PADDING+ BITMAP_SIZE,CCW)
  }
  private val camera = Camera()

  init {
    camera.rotateX(30f)
    camera.setLocation(0f,0f,-3*resources.displayMetrics.density)
  }

  @RequiresApi(VERSION_CODES.LOLLIPOP)
  override fun onDraw(canvas: Canvas) {
    //1. Canvas范围裁切
//    //椭圆裁切
//    canvas.clipPath(clipped)
//    //矩形裁切,左上角切四分之一
//    canvas.clipRect(BITMAP_PADDING, BITMAP_PADDING, BITMAP_PADDING+ BITMAP_SIZE/2, BITMAP_PADDING+ BITMAP_SIZE/2)
//    canvas.drawBitmap(bitmap, BITMAP_PADDING,BITMAP_PADDING,paint)

    //2. Canvas几何变换
//    canvas.save();
    //平移
//    canvas.translate(200.dp, 0.dp);
    //旋转
//    canvas.rotate(45f, 100.dp, 100.dp);
    //等比放大1.3倍
//    canvas.scale(1.3f, 1.3f, BITMAP_PADDING + BITMAP_SIZE / 2, BITMAP_PADDING + BITMAP_SIZE / 2);
    //不等比放大
//    canvas.scale(1.3f, 0.7f, BITMAP_PADDING + BITMAP_SIZE / 2, BITMAP_PADDING + BITMAP_SIZE / 2);
    //参数里的 sx 和 sy 是 x 方向和 y 方向的错切系数。
//    canvas.skew(0f, 0.5f);
//    canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING, paint);
//    canvas.restore();

    //3. Matrix 几何变换
    //投影效果
//    canvas.translate(BITMAP_PADDING+ BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2);
//    camera.applyToCanvas(canvas)
//    canvas.translate(-(BITMAP_PADDING+ BITMAP_SIZE/2),-(BITMAP_PADDING+ BITMAP_SIZE/2))
//    canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING,paint)

    //翻页效果
    //上半部分 反着看,先裁切,再移动
    //移动
    canvas.save()
    canvas.translate(BITMAP_PADDING+ BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2);
    //裁切
    canvas.clipRect(-BITMAP_SIZE/2, -BITMAP_SIZE/2, BITMAP_SIZE/2, BITMAP_SIZE/2)
    canvas.translate(-(BITMAP_PADDING+ BITMAP_SIZE/2),-(BITMAP_PADDING+ BITMAP_SIZE/2))
    canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING,paint)
    canvas.restore()

    //画一条线
    canvas.save()
    paint.color = Color.parseColor("#FF0000")
    canvas.drawLine(BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2, BITMAP_PADDING+BITMAP_SIZE,BITMAP_PADDING+ BITMAP_SIZE/2,paint)
    canvas.restore()

    //下半部分,反着看,先裁切,camera旋转,再移动
    //移动
    canvas.save()
    canvas.translate(BITMAP_PADDING+ BITMAP_SIZE/2,BITMAP_PADDING+ BITMAP_SIZE/2);
    camera.applyToCanvas(canvas)
    //裁切
    canvas.clipRect(-BITMAP_SIZE/2, 2f, BITMAP_SIZE/2, BITMAP_SIZE/2)
    canvas.translate(-(BITMAP_PADDING+ BITMAP_SIZE/2),-(BITMAP_PADDING+ BITMAP_SIZE/2))
    canvas.drawBitmap(bitmap, BITMAP_PADDING, BITMAP_PADDING,paint)
    canvas.restore()

  }




  fun getAvatar(width: Int): Bitmap {
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    BitmapFactory.decodeResource(resources, R.mipmap.slmh, options)
    options.inJustDecodeBounds = false
    options.inDensity = options.outWidth
    options.inTargetDensity = width
    return BitmapFactory.decodeResource(resources, R.mipmap.slmh, options)
  }
}