ImageView
,可以使用ImageView
属性的src
和scaleType
通过src设置的图片会被裁剪,设置准确大小下scaleType会生效
先来看下效果吧
图片角度有两种方式BitmapShader
(图片着色器)和PorterDuffXfermode
(图像叠加覆盖的规则)
通过对画笔Paint
设置shader
和xfermode
来实现图片的圆角效果。
用Bitmap
的像素来作为图片或文字的填充。给Paint
设置shder
来使用
bitMapPaint.shader = bitmapShader
自定义属性:
属性名 | 属性类型 | 含义 | 默认值 |
---|---|---|---|
shiv_bg_color |
color/reference | 控件背景色 | Color.TRANSPARENT |
shiv_border_color |
color/reference | 边框颜色 | Color.WHITE |
shiv_border_width |
dimension/reference | 边框宽度 | 2dp |
shiv_radius |
dimension/reference | 圆角正方形的边长 | 5dp |
shiv_radius_x |
dimension/reference | 非圆角矩形的宽 | -1f |
shiv_radius_y |
dimension/reference | 非圆角矩形的长 | -1f(同时设置x,y大于0 才有效) |
shiv_top_left |
boolean | 左上是否有角度 | true |
shiv_top_right |
boolean | 右上是否有角度 | true |
shiv_bottom_left |
boolean | 左下是否有角度 | true |
shiv_bottom_right |
boolean | 右下是否有角度 | true |
onDraw()重写 :
删除spuer.onDraw(),实现自己的圆角逻辑
override fun onDraw(canvas: Canvas?) { canvas?.drawColor(bgColor) // BitmapShader实现 canvas?.save() val bitmap = (drawable as BitmapDrawable).bitmap val bitmapShader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) val matrix = setBitmapMatrixAndPath(width.toFloat(), height.toFloat(), bitmap) bitmapShader.setLocalMatrix(matrix) bitMapPaint.shader = bitmapShader canvas?.drawPath(clipPath, bitMapPaint) canvas?.restore() borderPaint.style = Paint.Style.STROKE canvas?.drawPath(borderPath, borderPaint) if (!cornerTopLeftAble) { borderPaint.style = Paint.Style.FILL canvas?.drawRect(suppleRectF, borderPaint) } } 复制代码
bitmap
对象,将drawable
转为BitmapDrawable
获取bitmap
对象BitmapShader
对象,需要设置bitmap
,以及端点之外的图片延伸模式TileMode
, 图片的matrix
paint
设置shader
后,用canvas
的drawXXX
方法画出想要的图形,必须使用设置了shader
的paint
borderPaint
设置颜色,填充模式和描边宽度即可正常绘制。ShapeBitmaoshaderImageView
继承AppCompatImageView
,支持一些ImageView
的属性设置,例如src
,scaleType
等。注意:在实际测试中发现,绘制边框时,首尾衔接不上,需要在开始的点的左上位置绘制一个边长为边框宽度的一半,颜色为边框颜色的正方形用于补充空白部分。修补代码及效果:
if (!cornerTopLeftAble) { borderPaint.style = Paint.Style.FILL canvas?.drawRect(suppleRectF, borderPaint) } 复制代码
修补前 | 修补后 |
---|---|
setBitmapMatrixAndPath(w,h,bitmap) 设置图片缩放,平移
根据ScaleType
的枚举值,进行图片的缩放,平移以达到ImageView
的ScaleType
效果。
/** * 设置图片变化的matrix, 裁剪路径,边框路径 */ private fun setBitmapMatrixAndPath(w: Float, h: Float, bitmap: Bitmap): Matrix { // 图片变化的matrix val matrix = Matrix() // 图片缩放比例 val scaleX: Float val scaleY: Float // 缩放后的图片宽高 val bw: Float val bh: Float // 移动圆点 var transX = 0f var transY = 0f if (isSetSize) { when(scaleType) { ScaleType.FIT_XY -> { // 不管图片大小,填充整个view scaleX = w / bitmap.width scaleY = h / bitmap.height matrix.setScale(scaleX, scaleY) setPath(borderWidth, borderWidth, w - borderWidth, h - borderWidth) } ScaleType.FIT_CENTER -> { // fitCenter图片按比例缩放至View的宽度或者高度(取宽和高的最小值),居中显示 val scale: Float if (w < h) { scale = w / bitmap.width transY = (h - bitmap.height * scale) / 2 } else { scale = h / bitmap.height transX = (w - bitmap.width * scale) / 2 } matrix.setScale(scale, scale) matrix.postTranslate(transX, transY) bw = bitmap.width * scale bh = bitmap.height * scale val left = if (transX < 0) borderWidth else transX + borderWidth val top = if (transY < 0) borderWidth else transY + borderWidth val right = if (transX < 0) w - borderWidth else transX + bw -borderWidth val bottom = if (transY < 0) h - borderWidth else transY + bh - borderWidth setPath(left, top, right, bottom) } ScaleType.FIT_START -> { // 图片按比例缩放至View的宽度或者高度(取宽和高的最小值),然后居上或者居左显示 val scale = if (w < h) { w / bitmap.width } else { h / bitmap.height } matrix.setScale(scale, scale) bw = bitmap.width * scale bh = bitmap.height * scale val left = borderWidth val top = borderWidth val right = if (w < bw) w - borderWidth else bw - borderWidth val bottom = if (h < bh) h - borderWidth else bh - borderWidth setPath(left, top, right, bottom) } ScaleType.FIT_END -> { // 图片按比例缩放至View的宽度或者高度(取宽和高的最小值),然后居下或者居右显示 val scale: Float if (w < h) { scale = w / bitmap.width transY = h - bitmap.height * scale } else { scale = h / bitmap.height transX = w - bitmap.width * scale } matrix.setScale(scale, scale) matrix.postTranslate(transX, transY) bw = bitmap.width * scale bh = bitmap.height * scale val left = if (transX < 0) borderWidth else transX + borderWidth val top = if (transY < 0) borderWidth else transY + borderWidth val right = if (transX < 0) w - borderWidth else transX + bw - borderWidth val bottom = if (transY < 0) h - borderWidth else transY + bh - borderWidth setPath(left, top, right, bottom) } ScaleType.CENTER -> { // 按照图片原始大小,居中显示,多余部分裁剪 transX = (w - bitmap.width) / 2 transY = (h - bitmap.height) / 2 matrix.postTranslate(transX, transY) setPath(if (transX < 0) borderWidth else transX + borderWidth, if (transY < 0) borderWidth else transY + borderWidth, if (transX < 0) w - borderWidth else transX + bitmap.width - borderWidth, if (transY < 0) h - borderWidth else transY + bitmap.height - borderWidth) } ScaleType.CENTER_INSIDE -> { // centerInside的目标是将原图完整的显示出来,故按比例缩放原图,居中显示 val scale: Float if (w < h) { scale = w / bitmap.width transY = (h - bitmap.height * scale) / 2 } else { scale = h / bitmap.height transX = (w - bitmap.width * scale) / 2 } matrix.setScale(scale, scale) matrix.postTranslate(transX, transY) bw = bitmap.width * scale bh = bitmap.height * scale val left = if (transX < 0) borderWidth else transX + borderWidth val top = if (transY < 0) borderWidth else transY + borderWidth val right = if (transX < 0) w - borderWidth else transX + bw - borderWidth val bottom = if (transY < 0) h - borderWidth else transY + bh -borderWidth setPath(left, top, right, bottom) } ScaleType.CENTER_CROP -> { // centerCrop的目标是将ImageView填充满,故按比例缩放原图,居中显示 val scale: Float if (w > h) { scale = w / bitmap.width transY = (h - bitmap.height * scale) / 2 } else { scale = h / bitmap.height transX = (w -bitmap.width * scale) / 2 } matrix.setScale(scale, scale) matrix.postTranslate(transX, transY) bw = bitmap.width * scale bh = bitmap.height * scale val left = if (transX < 0) borderWidth else transX + borderWidth val top = if (transY < 0) borderWidth else transY + borderWidth val right = if (transX < 0) w - borderWidth else transX + bw - borderWidth val bottom = if (transY < 0) h - borderWidth else transY + bh -borderWidth setPath(left, top, right, bottom) } ScaleType.MATRIX -> { // 按照原图大小从左上角绘制,多余部分裁剪 bw = if (w < bitmap.width) w else bitmap.width.toFloat() bh = if (h < bitmap.height) h else bitmap.height.toFloat() setPath(borderWidth, borderWidth, bw - borderWidth, bh - borderWidth) } else -> {} } } else { scaleX = w / bitmap.width scaleY = h / bitmap.height matrix.setScale(scaleX, scaleY) setPath(borderWidth, borderWidth, w - borderWidth, h -borderWidth) } return matrix } 复制代码
ScaleType简单说明:
ScaleType | 含义 |
---|---|
FIT_XY |
不管图片大小,填充整个view |
FIT_CENTER |
图片按比例缩放至View的宽度或者高度(取宽和高的最小值),居中显示 |
FIT_START |
图片按比例缩放至View的宽度或者高度(取宽和高的最小值),居上或者居左显示 |
FIT_END |
图片按比例缩放至View的宽度或者高度(取宽和高的最小值),居下或者居右显示 |
CENTER |
按照图片原始大小,居中显示,多余部分裁剪 |
CENTER_INSIDE |
目标是将原图完整的显示出来,故按比例缩放原图,居中显示 |
CENTER_CROP |
目标是将view填充满,故按比例缩放原图,居中显示 |
MATRIX |
按照原图大小从左上角绘制,多余部分裁剪 |
setPath(left, right, top, bottom) 设置裁剪路径和边框路径
setPath
主要根据传进来的裁剪矩形框的值进行裁剪路径和边框路径的合成。
边框四边 = 裁剪框四边向外扩张borderwidth大小;
边框角度值 = 裁剪框角度值 + borderwidth / 2
/** * 设置裁剪路径和边框路径 * @param left 裁剪框的left * @param top 裁剪框的top * @param right 裁剪框的right * @param bottom 裁剪框的bottom */ private fun setPath(left: Float, top: Float, right: Float, bottom: Float) { clipPath.reset() borderPath.reset() val w = right - left val h = bottom - top setRadius(w, h) val borderLeft = left - borderWidth / 2 val borderTop = top - borderWidth / 2 val borderRight = right + borderWidth / 2 val borderBottom = bottom + borderWidth / 2 val borderRadiusX = radiusX + borderWidth / 2 val borderRadiusY = radiusY + borderWidth / 2 val bw = borderRight - borderLeft val bh = borderBottom - borderTop suppleRectF.left = borderLeft - borderWidth / 2 suppleRectF.top = borderTop - borderWidth / 2 suppleRectF.right = borderLeft suppleRectF.bottom = borderTop // 圆角或椭圆角的矩形 val topLeftRectF = RectF() val topRightRectF = RectF() val bottomLeftRectF = RectF() val bottomRightRectF = RectF() if (radiusX <= 0 && radiusY <= 0) { // 没有圆角 clipPath.addRect(left, top, right, bottom, Path.Direction.CW) borderPath.addRect(borderLeft, borderTop, borderRight, borderBottom, Path.Direction.CW) } else { // 有圆角 if (cornerTopLeftAble) { // 裁剪 // 左上角 topLeftRectF.left = left topLeftRectF.top = top topLeftRectF.right = left + radiusX * 2 topLeftRectF.bottom = top + radiusY * 2 clipPath.addArc(topLeftRectF, 180f, 90f) // 边框 // 左上角 topLeftRectF.left = borderLeft topLeftRectF.top = borderTop topLeftRectF.right = borderLeft + borderRadiusX * 2 topLeftRectF.bottom = borderTop + borderRadiusY * 2 borderPath.moveTo(borderLeft, borderTop + borderRadiusY) borderPath.addArc(topLeftRectF, 180f, 90f) borderPath.moveTo(borderLeft + borderRadiusX, borderTop) } else { clipPath.moveTo(left, top) borderPath.moveTo(borderLeft, borderTop) } clipPath.lineTo(if (cornerTopRightAble) right - radiusX else right , top) if (bw != borderRadiusX * 2) { borderPath.lineTo(if (cornerTopRightAble) borderRight - borderRadiusX else borderRight , borderTop) } if (cornerTopRightAble) { // 右上角 topRightRectF.left = right - radiusX * 2 topRightRectF.top = top topRightRectF.right = right topRightRectF.bottom = top + radiusY * 2 clipPath.addArc(topRightRectF, 270f, 90f) // 右上角 topRightRectF.left = borderRight - borderRadiusX * 2 topRightRectF.top = borderTop topRightRectF.right = borderRight topRightRectF.bottom = borderTop + borderRadiusY * 2 borderPath.addArc(topRightRectF, 270f, 90f) borderPath.moveTo(borderRight, borderTop + borderRadiusY) } clipPath.lineTo(right, if (cornerBottomRightAble) bottom - radiusY else bottom) if (bh != borderRadiusY * 2) { borderPath.lineTo(borderRight, if (cornerBottomRightAble) borderBottom - borderRadiusY else borderBottom) } if (cornerBottomRightAble) { // 右下角 bottomRightRectF.left = right - radiusX * 2 bottomRightRectF.top = bottom - radiusY * 2 bottomRightRectF.right = right bottomRightRectF.bottom = bottom clipPath.addArc(bottomRightRectF, 0f, 90f) // 右下角 bottomRightRectF.left = borderRight - borderRadiusX * 2 bottomRightRectF.top = borderBottom - borderRadiusY * 2 bottomRightRectF.right = borderRight bottomRightRectF.bottom = borderBottom borderPath.addArc(bottomRightRectF, 0f, 90f) borderPath.moveTo(borderRight - borderRadiusX ,borderBottom) } clipPath.lineTo(if (cornerBottomLeftAble) left + radiusX else left, bottom) if (bw != borderRadiusX * 2) { borderPath.lineTo(if (cornerBottomLeftAble) borderLeft + borderRadiusX else borderLeft, borderBottom) } if (cornerBottomLeftAble) { // 左下角 bottomLeftRectF.left = left bottomLeftRectF.top = bottom - radiusY * 2 bottomLeftRectF.right = left + radiusX * 2 bottomLeftRectF.bottom = bottom clipPath.addArc(bottomLeftRectF, 90f, 90f) // 左下角 bottomLeftRectF.left = borderLeft bottomLeftRectF.top = borderBottom - borderRadiusY * 2 bottomLeftRectF.right = borderLeft + borderRadiusX * 2 bottomLeftRectF.bottom = borderBottom borderPath.addArc(bottomLeftRectF, 90f, 90f) borderPath.moveTo(borderLeft, borderBottom - borderRadiusY) } clipPath.lineTo(left, if (cornerTopLeftAble) top + radiusY else top) if (cornerTopLeftAble) { clipPath.lineTo(left, top + radiusY) } if (cornerTopRightAble) { clipPath.lineTo(right - radiusX, top) } if (cornerBottomRightAble) { clipPath.lineTo(right, bottom - radiusY) } if (cornerBottomLeftAble) { clipPath.lineTo(left + radiusX, bottom) } if (bh != borderRadiusY * 2) { borderPath.lineTo(borderLeft, if (cornerTopLeftAble) borderTop + borderRadiusY else borderTop) } } } 复制代码
setRadius(w,h) 设置圆角值
/** * 设置圆角值 */ private fun setRadius(w: Float, h: Float) { if (radiusX < 0 || radiusY < 0) { if (cornerRadius < 0) { cornerRadius = 0f } radiusX = cornerRadius radiusY = cornerRadius } if (radiusX > w / 2) { radiusX = w / 2 } if (radiusY > h / 2) { radiusY = h / 2 } } 复制代码
如果设置了角度的x,y且大于等于0,则使用,否则使用圆角值(>=0)
源码传送门
PorterDuffXfermode
是指目标图片和源图片的叠加覆盖规则,给Paint
设置xfermode
来使用,需要注意PorterDuffXfermode
需要设置具体某种绘制规则(PorterDuff.Mode
)
bitMapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
注意: xfermode和BitmapShader角度图片,裁剪路径和边框的设置是一样的。 不同的是xfermode需要用裁剪路径去生成源图,设置的图片用作目标图来进行绘制,而bitmapshader直接使用裁剪路径。
ShapeXfermodeImageView 自定义属性:
属性名 | 属性类型 | 含义 | 默认值 |
---|---|---|---|
sxiv_bg_color |
color/reference | 控件背景色 | Color.TRANSPARENT |
sxiv_border_color |
color/reference | 边框颜色 | Color.WHITE |
sxiv_border_width |
dimension/reference | 边框宽度 | 2dp |
sxiv_radius |
dimension/reference | 圆角正方形的边长 | 5dp |
sxiv_radius_x |
dimension/reference | 非圆角矩形的宽 | -1f |
sxiv_radius_y |
dimension/reference | 非圆角矩形的长 | -1f(同时设置x,y大于0 才有效) |
sxiv_top_left |
boolean | 左上是否有角度 | true |
sxiv_top_right |
boolean | 右上是否有角度 | true |
sxiv_bottom_left |
boolean | 左下是否有角度 | true |
sxiv_bottom_right |
boolean | 右下是否有角度 | true |
onDraw()重写:
删除spuer.onDraw(),实现自己的圆角逻辑
override fun onDraw(canvas: Canvas?) { canvas?.drawColor(bgColor) // BitmapShader实现 val saved = canvas?.saveLayer(null, null, Canvas.ALL_SAVE_FLAG) val dstBitmap = (drawable as BitmapDrawable).bitmap val matrix = setBitmapMatrixAndPath(width.toFloat(), height.toFloat(), dstBitmap) val srcBitmap = createSrcBitmap(width, height) canvas?.drawBitmap(dstBitmap, matrix, bitMapPaint) bitMapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) canvas?.drawBitmap(srcBitmap, 0f, 0f, bitMapPaint) bitMapPaint.xfermode = null canvas?.restoreToCount(saved?: 0) borderPaint.style = Paint.Style.STROKE canvas?.drawPath(borderPath, borderPaint) if (!cornerTopLeftAble) { borderPaint.style = Paint.Style.FILL canvas?.drawRect(suppleRectF, borderPaint) } } 复制代码
Paint
实例进行绘制xfermode
之前的drawBitmap
是绘制目标图,之后的是源图PorterDuff.Mode
是DST_IN
值,保留目标图和源图的交集部分createSrcBitmap(w,h)
,根据裁剪路径绘制源图PorterDuff.ModeJ简单说明:
createSrcBitmap(w,h)
根据裁剪路径绘制源图
/** * 获取源图biatmap,用于截出形状图 */ private fun createSrcBitmap(w: Int, h: Int): Bitmap { val srcBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) srcBitmap.eraseColor(Color.TRANSPARENT) val canvas = Canvas(srcBitmap) val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE style = Paint.Style.FILL } canvas.drawPath(clipPath, paint) return srcBitmap } 复制代码
setBitmapMatrixAndPath(), setPath(), setRadius() 和ShpaeShaderImageView的方法是一样的
源码传送门
以上就是BitmapShader
和Xfermode
实现角度图片的过程
关于我的自定义view项目
了解BitmapShader和Xfermodede,下载自定义View详解apk观看