Android 在 Kotlin 中 圆角图片,椭圆角图片的实现

2,664 阅读6分钟

推荐下,我自己的关于自定义view的一个演示demo

推荐下,我自己的关于自定义view的一个演示demo

推荐下,我自己的关于自定义view的一个演示demo

一.效果介绍

  • 设置四个圆角的展现和隐藏
  • 控件继承ImageView,可以使用ImageView属性的srcscaleType
  • 设置角度的x和y值,x==y 圆角,x!=y 椭圆角
  • 设置边框的颜色,边框宽度
通过src设置的图片会被裁剪,设置准确大小下scaleType会生效

先来看下效果吧

图片角度有两种方式BitmapShader(图片着色器)和PorterDuffXfermode(图像叠加覆盖的规则)

通过对画笔Paint设置shaderxfermode来实现图片的圆角效果。

二.ShapeShaderImageView

BitmapShader实现的圆角图片

Bitmap的像素来作为图片或文字的填充。给Paint设置shder来使用

bitMapPaint.shader = bitmapShader

自定义属性:

属性名属性类型含义默认值
shiv_bg_colorcolor/reference控件背景色Color.TRANSPARENT
shiv_border_colorcolor/reference边框颜色Color.WHITE
shiv_border_widthdimension/reference边框宽度2dp
shiv_radiusdimension/reference圆角正方形的边长5dp
shiv_radius_xdimension/reference非圆角矩形的宽-1f
shiv_radius_ydimension/reference非圆角矩形的长-1f(同时设置x,y大于0 才有效)
shiv_top_leftboolean左上是否有角度true
shiv_top_rightboolean右上是否有角度true
shiv_bottom_leftboolean左下是否有角度true
shiv_bottom_rightboolean右下是否有角度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后,用canvasdrawXXX方法画出想要的图形,必须使用设置了shaderpaint
  • 设置边框,给borderPaint设置颜色,填充模式和描边宽度即可正常绘制。
  • ShapeBitmaoshaderImageView 继承AppCompatImageView,支持一些ImageView的属性设置,例如srcscaleType等。
注意:在实际测试中发现,绘制边框时,首尾衔接不上,需要在开始的点的左上位置绘制一个边长为边框宽度的一半,颜色为边框颜色的正方形用于补充空白部分。修补代码及效果:
if (!cornerTopLeftAble) {
            borderPaint.style = Paint.Style.FILL
            canvas?.drawRect(suppleRectF, borderPaint)
    }
修补前修补后

setBitmapMatrixAndPath(w,h,bitmap) 设置图片缩放,平移

根据ScaleType的枚举值,进行图片的缩放,平移以达到ImageViewScaleType效果。

     /**
     * 设置图片变化的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)

源码传送门

三.ShapeXfermodeImageView

PorterDuffXfermode实现的圆角图片

PorterDuffXfermode是指目标图片和源图片的叠加覆盖规则,给Paint设置xfermode来使用,需要注意PorterDuffXfermode需要设置具体某种绘制规则(PorterDuff.Mode)

bitMapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)

注意:
     xfermode和BitmapShader角度图片,裁剪路径和边框的设置是一样的。
     不同的是xfermode需要用裁剪路径去生成源图,设置的图片用作目标图来进行绘制,而bitmapshader直接使用裁剪路径。
     

ShapeXfermodeImageView 自定义属性:

属性名属性类型含义默认值
sxiv_bg_colorcolor/reference控件背景色Color.TRANSPARENT
sxiv_border_colorcolor/reference边框颜色Color.WHITE
sxiv_border_widthdimension/reference边框宽度2dp
sxiv_radiusdimension/reference圆角正方形的边长5dp
sxiv_radius_xdimension/reference非圆角矩形的宽-1f
sxiv_radius_ydimension/reference非圆角矩形的长-1f(同时设置x,y大于0 才有效)
sxiv_top_leftboolean左上是否有角度true
sxiv_top_rightboolean右上是否有角度true
sxiv_bottom_leftboolean左下是否有角度true
sxiv_bottom_rightboolean右下是否有角度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.ModeDST_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的方法是一样的

源码传送门

以上就是BitmapShaderXfermode实现角度图片的过程

结束

了解BitmapShader和Xfermodede,下载自定义View详解apk观看