一、范围裁切
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)
}
效果展示
于是得到了一张裁切后的圆形图片。但是不推荐这种方式获取圆角图片,因为这样得到的圆角图片放大后会有很明显的毛边(锯齿),还是推荐用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。如图:
2、平移
View(占全屏)的Matrix平移屏幕的一半,然后绘制位图,代码如下:
canvas.translate(width / 2f,0f)
//绘制位图
canvas.drawBitmap(
bitmap,
0f,
0f,
paint
)
效果展示
3、旋转
先将View(占全屏)的Matrix平移到View的中心,然后旋转180°,会得到一张倒立的头像。
代码如下:
canvas.translate(width / 2f, height / 2f)
canvas.rotate(180f)
//绘制位图
canvas.drawBitmap(
bitmap,
0f,
0f,
paint
)
效果展示
4、缩放
Matrix矩阵虽然缩放,但是矩阵单元与像素单元之间的比例关系没有发生变化(1:1),矩阵单元变大(宽高放大2倍,1变为4),导致绘制目标像素(比如画100个像素单元,就需要画100个矩阵单元)的图像也跟着变大。如图:
代码如下:
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
)
效果展示
得到一张宽度铺满屏幕的图片
5、斜切(菱形)
理解斜切需要一定的三角函数的知识,当θ范围为0°到90°时,tanθ的范围为0到+∞。skew有二个参数,一个为sx,一个为sy,而这二个值真是tanθ的值,通过给定sx和sy就能倒推θ角度的大小,就能画出菱形。但是要特别注意的是sx是横坐标除以纵坐标,而sy是纵坐标除以横坐标。如图所示:
比如控制β角,代码如下(常识tan45°=1):
canvas.skew(1f,0f)
//绘制位图
canvas.drawBitmap(
bitmap,
0f,
0f,
paint
)
效果展示
可以试一下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
)
效果展示
添加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
)
效果展示
之前的设置都被重置了
三、三维几何变换
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)
}
}
效果展示
在写上面这个小demo的时候试了几次最后才成功,首先要明确一点camera的位置在负Z轴上,默认位置是setLocation(0,0,-8),8不是8像素,而是8*72像素。如果想让图片沿着图片中心的X,Y,Z轴旋转,需要将图片先移动然后再移回来,所以才有了注释一和注释二。注意点:①注释一移动的为正坐标,注释二移动的为负坐标。②注释一移多少注释二就要对等移回多少,如果不对等会有问题。
2、三维旋转的UI适配
如果图片的大小比较大(设计师给定的尺寸),然后旋转一定的角度就会出现图片超出屏幕或者太突出的问题,如下效果:
解决办法就是根据屏幕的分辨率动态改变camera在负Z轴上的位置,代码如下:
private const val CUSTOM_VALUE = -12 //三维的适配自定义的值
camera.setLocation(0f, 0f, CUSTOM_VALUE * resources.displayMetrics.density) //根据不同的手机动态改变camera在负Z轴上的位置
适配后的效果
3、日历中翻起一半的效果的实现
思路就是绘制三次,一次绘制上一张图的上半部分,一次绘制下一张图的下半部分,一次绘制上一张图下半部分的翻折效果。
先看绘制二张图上半部分和下半部分的效果:
绘制翻折效果,完整代码如下(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)
}
}
效果展示
--个人学习笔记--