Android 在 Kotlin 中 圓角圖片,橢圓角圖片的實現

一.效果介紹

  • 設置四個圓角的展現和隱藏
  • 控件繼承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_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後,用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_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.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實現角度圖片的過程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章