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

485 阅读7分钟

一、范围裁切

1、先举个例子:如何通过裁切得到一个圆角图片?

private val IMAGE_WIDTH = 200f.dp2px   //图片的目标宽度
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var bitmap: Bitmap = getAvatar(IMAGE_WIDTH.toInt())   //转换后的位图
private val clippedPath = Path()   //需要裁切的路径
private lateinit var ovalBounds: RectF   //矩形区域


override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    ovalBounds = RectF(    //设置矩形区域
        width / 2 - IMAGE_WIDTH / 2,
        height / 2 - IMAGE_WIDTH / 2,
        width / 2 + IMAGE_WIDTH / 2,
        height / 2 + IMAGE_WIDTH / 2
    )
    clippedPath.addOval(ovalBounds, Path.Direction.CCW)   //给需要裁切的路径通过矩形区域添加一个圆
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    //给canvas限定裁切区域
    canvas.clipPath(clippedPath)
    //绘制位图
    canvas.drawBitmap(
        bitmap,
        width / 2 - IMAGE_WIDTH / 2,
        height / 2 - IMAGE_WIDTH / 2,
        paint
    )
}

/**
 * 获取符合尺寸宽度的位图
 * @param width 位图的目标宽度
 */
fun getAvatar(width: Int): Bitmap {
    //获取options对象
    val options = BitmapFactory.Options()
    //配置中设置属性获取图片的长宽设置
    options.inJustDecodeBounds = true
    //对图片进行解码
    BitmapFactory.decodeResource(resources, R.drawable.girl, options)
    //取消获取图片的长宽的设置
    options.inJustDecodeBounds = false
    options.inDensity = options.outWidth   //实际宽度
    options.inTargetDensity = width   //目标宽度
    return BitmapFactory.decodeResource(resources, R.drawable.girl, options)
}

效果展示

image.png

于是得到了一张裁切后的圆形图片。但是不推荐这种方式获取圆角图片,因为这样得到的圆角图片放大后会有很明显的毛边(锯齿),还是推荐用Xfermode

2、常用的裁切方法

clipPath:裁切路径

clipOutPath:裁切路径外区域

clipRect:裁切矩形

clipOutRect:裁切矩形外区域

通过设置path或者rect就能得到想要目标图形。

二、二维几何变换

1、二维变换的类别

translate(x,y):平移
rotate(degree):旋转
scale(x,y):缩放
skew(sx,sy):斜切(菱形)

弄懂以上的变换关系需要理解手机范围、自定义View范围、矩阵Matrix位置的关系。每一个View都自带Matrix,用于确认View中元素的摆放位置,平移、旋转、缩放、斜切都是改变View的Matrix。如图:

image.png

2、平移

View(占全屏)的Matrix平移屏幕的一半,然后绘制位图,代码如下:

canvas.translate(width / 2f,0f)
//绘制位图
canvas.drawBitmap(
   bitmap,
   0f,
   0f,
   paint
)

效果展示

image.png

3、旋转

先将View(占全屏)的Matrix平移到View的中心,然后旋转180°,会得到一张倒立的头像。

image.png

代码如下:

canvas.translate(width / 2f, height / 2f)
canvas.rotate(180f)
//绘制位图
canvas.drawBitmap(
    bitmap,
    0f,
    0f,
    paint
)

效果展示

image.png

4、缩放

Matrix矩阵虽然缩放,但是矩阵单元与像素单元之间的比例关系没有发生变化(1:1),矩阵单元变大(宽高放大2倍,1变为4),导致绘制目标像素(比如画100个像素单元,就需要画100个矩阵单元)的图像也跟着变大。如图:

image.png

代码如下:

private lateinit var  bitmap: Bitmap
private var imageWidth = 0   //图片的目标宽度
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
   super.onSizeChanged(w, h, oldw, oldh)
   //图片宽度为屏幕的1/2
   imageWidth = width / 2
   //获取位图
   bitmap = getAvatar(imageWidth)
}
//宽高分别放大二倍
canvas.scale(2f,2f)
//绘制位图
canvas.drawBitmap(
   bitmap,
   0f,
   0f,
   paint
)

效果展示

image.png 得到一张宽度铺满屏幕的图片

5、斜切(菱形)

理解斜切需要一定的三角函数的知识,当θ范围为0°到90°时,tanθ的范围为0到+∞。skew有二个参数,一个为sx,一个为sy,而这二个值真是tanθ的值,通过给定sx和sy就能倒推θ角度的大小,就能画出菱形。但是要特别注意的是sx是横坐标除以纵坐标,而sy是纵坐标除以横坐标。如图所示:

image.png

比如控制β角,代码如下(常识tan45°=1):

canvas.skew(1f,0f)
//绘制位图
canvas.drawBitmap(
    bitmap,
    0f,
    0f,
    paint
)

效果展示

image.png 可以试一下skew(1f,1f)图片就切没了。

6、Marix的几何变换

Matrix变换的Api:

matrix.preTranslate(x,y)  在之前平移
matrix.postTranslate(x,y)  在之后平移

matrix.preRotate(degree)  在之前旋转
matrix.postRotate(degree)  在之后旋转

matrix.preScale(x,y)  在之前缩放
matrix.postScale(x,y)  在之后缩放

matrix.preSkew(x,y)  在之前斜切
matrix.postSkew(x,y)  在之后斜切

//特别注意:下面4个Api会让matrix的设置重置
matrix.setTranslate(x,y)
matrix.setRotate(degree)
matrix.setScale(x,y)
matrix.setSkew(x,y)

特别注意:matrix的4个set的api会让之前设置的matrix变换失效(重置),包括set之间也是互斥的,只有最后一次set有效。如果想要使用多组旋转、平移、缩放效果,用上面的Api。

代码如下:

//创建matrix
var customMatrix = Matrix()
customMatrix.preTranslate(width / 4f, 0f)  //横向移动屏幕1/4
customMatrix.postTranslate(width / 4f, 0f)  //横向移动屏幕1/4
//绘制位图
canvas.drawBitmap(
    bitmap,
    customMatrix,  //设置matrix
    paint
)

效果展示

image.png

添加setTranslate(x,y)后:

//创建matrix
var customMatrix = Matrix()
customMatrix.preTranslate(width / 4f, 0f)  //横向移动屏幕1/4
customMatrix.postTranslate(width / 4f, 0f)  //横向移动屏幕1/4
customMatrix.setTranslate(0f, 0f)  //x,y轴平移0f
//绘制位图
canvas.drawBitmap(
    bitmap,
    customMatrix,  //设置matrix
    paint
)

效果展示

image.png 之前的设置都被重置了

三、三维几何变换

1、如何得到一个绕图片中心旋转的图片

三维几何变换需要用到Camera,这里不是调用摄像头的Camera,而是三维旋转后Camera拍照投影后的图形。假如位图沿X轴翻转30°后放置到屏幕中心。代码如下:

private val IMAGE_WIDTH = 150f.dp2px   //图片的目标宽度

class CanvasTranslateView(context: Context, attributeSet: AttributeSet) :
    View(context, attributeSet) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var bitmap: Bitmap = getAvatar(IMAGE_WIDTH.toInt())   //转换后的位图
    private var camera = Camera()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.save()
        canvas.translate(width / 2f, height / 2f)   //注释一
        camera.rotateX(30f)
        camera.applyToCanvas(canvas)   //camera应用于canvas
        canvas.translate(-width / 2f, -height / 2f)  //注释二
        //绘制位图
        canvas.drawBitmap(
            bitmap,
            width / 2f - IMAGE_WIDTH / 2,
            height / 2f - IMAGE_WIDTH / 2,
            paint
        )
        canvas.restore()
    }

    /**
     * 获取符合尺寸宽度的位图
     * @param width 位图的目标宽度
     */
    fun getAvatar(width: Int): Bitmap {
        //获取options对象
        val options = BitmapFactory.Options()
        //配置中设置属性获取图片的长宽设置
        options.inJustDecodeBounds = true
        //对图片进行解码
        BitmapFactory.decodeResource(resources, R.drawable.girl, options)
        //取消获取图片的长宽的设置
        options.inJustDecodeBounds = false
        options.inDensity = options.outWidth   //实际宽度
        options.inTargetDensity = width   //目标宽度
        return BitmapFactory.decodeResource(resources, R.drawable.girl, options)
    }
}

效果展示

image.png

在写上面这个小demo的时候试了几次最后才成功,首先要明确一点camera的位置在负Z轴上,默认位置是setLocation(0,0,-8),8不是8像素,而是8*72像素。如果想让图片沿着图片中心的X,Y,Z轴旋转,需要将图片先移动然后再移回来,所以才有了注释一和注释二。注意点:①注释一移动的为正坐标,注释二移动的为负坐标。②注释一移多少注释二就要对等移回多少,如果不对等会有问题。

2、三维旋转的UI适配

如果图片的大小比较大(设计师给定的尺寸),然后旋转一定的角度就会出现图片超出屏幕或者太突出的问题,如下效果:

image.png

解决办法就是根据屏幕的分辨率动态改变camera在负Z轴上的位置,代码如下:

private const val CUSTOM_VALUE = -12  //三维的适配自定义的值
camera.setLocation(0f, 0f, CUSTOM_VALUE * resources.displayMetrics.density)  //根据不同的手机动态改变camera在负Z轴上的位置

适配后的效果

image.png

3、日历中翻起一半的效果的实现

思路就是绘制三次,一次绘制上一张图的上半部分,一次绘制下一张图的下半部分,一次绘制上一张图下半部分的翻折效果。

先看绘制二张图上半部分和下半部分的效果:

image.png

绘制翻折效果,完整代码如下(demo写明原理代码未精简):


private val IMAGE_WIDTH = 200f.dp2px   //图片的目标宽度
private const val CUSTOM_VALUE = -7  //三维的适配自定义的值

class CanvasTranslateView(context: Context, attributeSet: AttributeSet) :
    View(context, attributeSet) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var bitmap: Bitmap = getAvatar(IMAGE_WIDTH.toInt(), R.drawable.girl)   //转换后的位图
    private var bitmap2: Bitmap = getAvatar(IMAGE_WIDTH.toInt(), R.drawable.girl2)   //转换后的位图
    private var camera = Camera()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //绘制上一张图上半部分
        canvas.save()
        canvas.clipRect(
            width / 2f - IMAGE_WIDTH / 2,
            height / 2f - IMAGE_WIDTH / 2,
            width / 2f + IMAGE_WIDTH / 2,
            height / 2f
        )
        //绘制位图
        canvas.drawBitmap(
            bitmap,
            width / 2f - IMAGE_WIDTH / 2,
            height / 2f - IMAGE_WIDTH / 2,
            paint
        )
        canvas.restore()

        //绘制下一张图的下半部分
        canvas.save()
        canvas.clipRect(
            width / 2f - IMAGE_WIDTH / 2,
            height / 2f,
            width / 2f + IMAGE_WIDTH / 2,
            height / 2f + IMAGE_WIDTH / 2
        )
        //绘制位图
        canvas.drawBitmap(
            bitmap2,
            width / 2f - IMAGE_WIDTH / 2,
            height / 2f- IMAGE_WIDTH/2,
            paint
        )
        canvas.restore()

        //绘制上一张图下半翻折部分
        canvas.save()
        canvas.translate(width / 2f, height / 2f)
        camera.save()
        camera.setLocation(0f, 0f, CUSTOM_VALUE * resources.displayMetrics.density)  //三维适配
        camera.rotateX(60f)  //翻转角度
        camera.applyToCanvas(canvas)
        camera.restore()
        canvas.translate(-width / 2f, -height / 2f)
        canvas.clipRect(  //裁剪
            width / 2f - IMAGE_WIDTH / 2,
            height / 2f,
            width / 2f + IMAGE_WIDTH / 2,
            height / 2f + IMAGE_WIDTH / 2
        )
        //绘制位图
        canvas.drawBitmap(
            bitmap,
            width / 2f - IMAGE_WIDTH / 2,
            height / 2f - IMAGE_WIDTH / 2,
            paint
        )
        canvas.restore()
    }

    /**
     * 获取符合尺寸宽度的位图
     * @param width 位图的目标宽度
     */
    fun getAvatar(width: Int, @DrawableRes imag: Int): Bitmap {
        //获取options对象
        val options = BitmapFactory.Options()
        //配置中设置属性获取图片的长宽设置
        options.inJustDecodeBounds = true
        //对图片进行解码
        BitmapFactory.decodeResource(resources, imag, options)
        //取消获取图片的长宽的设置
        options.inJustDecodeBounds = false
        options.inDensity = options.outWidth   //实际宽度
        options.inTargetDensity = width   //目标宽度
        return BitmapFactory.decodeResource(resources, imag, options)
    }
}

效果展示

image.png

--个人学习笔记--