Android 圆形以及圆角图片处理

预览图

Android原生控件中并没有展示圆形,以及圆角的基础控件,想要实现圆形和圆角样式的展示就需要借助,Shape ,CardView,圆形图片等形式展示圆形以及圆角.最近时间允许就探究下圆形以及圆角图形的实现.

Android原生控件不支持圆形和圆角,那么实现圆形和圆角,无非是将原生控件切割和绘制成圆形和圆角就可以了

关键类解释:

PorterDuffXfermode :将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值

切割和绘制圆形圆角的方案:

    1. PorterDuffXfermode 方案(操作Canvas)
      利用Paint的setXfermode()方法 设置PorterDuffXfermode 叠加展示圆形以及圆角效果
      缺点:
      1:如果你觉得混合模式没有正确使用,可以让调用setLayerType(View.LAYER_TYPE_SOFTWARE, null)方法切换到软件渲染模式
    1. Canvas.clipPath() 方案 (操作Canvas)
      利用画布裁剪路径的方式绘制圆形/圆角形状 ShapeableImageView采用此方案
      缺点:
      1:cliptPath不支持硬件加速,因此在调用前必须禁用硬件加速,setLayerType(View.LAYER_TYPE_SOFTWARE, null)
      2:这种方式剪裁的是Canvas图形,View的实际形状是不变的,
      因此只能对src属性有效,对background属性是无效的
  • 3.BitmapShader 位图着色器 (Paint)
    利用位图着色器,在画布上绘制圆形或者圆角,Glide实现圆角以及圆形采用此方案
    缺点:
    就是如果要定义一个圆角图片,必须调用canvas.drawRoundRect进行绘制,但是这个方法要求API>=21

  • 4.ViewOutlineProvider 将View切割成圆形或者圆角(View)
    OutlineProvider轮廓提供者,可以给View提供一个外轮廓,并且让其根据轮廓进行剪切
    缺点:
    OutlineProvider 需要>21 使用

1.PorterDuffXfermode 方案

思路:
1:利用canvas.drawCircle()绘制一个圆形形状
2:给画笔设置叠加模式 mPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
3:绘制图片,然后取消绘制模式


注意:
如果你觉得混合模式没有正确使用,可以让调用setLayerType(View.LAYER_TYPE_SOFTWARE, null)方法切换到软件渲染模式


package com.wkq.workkotlin.custom.cycle

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import androidx.compose.ui.geometry.Rect
import com.wkq.workkotlin.R

/**
 *@Desc:Xfermode 处理圆形图片
 *
 *@Author: wkq
 *
 *@Time: 2024/11/7 10:55
 *
 */
class CycleXFerModeView : View {


    var mContext: Context
    var mPaint: Paint
    val DEFAULT_SIZE = 500
    var mSize: Int = 0
    var mRadius=0f
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : this(
        context, attrs, defStyleAttr, -1
    )

    @SuppressLint("ResourceAsColor")
    constructor(
        context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        mContext = context
        mPaint = Paint()
        mPaint.isAntiAlias = true
        mPaint.style = Paint.Style.FILL
        val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
        mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
        tp.recycle()
        setLayerType(LAYER_TYPE_SOFTWARE, null);

    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        val width: Int = getMeasureSize(widthMeasureSpec)
        val height: Int = getMeasureSize(heightMeasureSpec)
        mSize = Math.min(width, height)
        setMeasuredDimension(mSize, mSize)
    }

    private fun getMeasureSize(measureSpec: Int): Int {
        val mode = MeasureSpec.getMode(measureSpec)
        val size = MeasureSpec.getSize(measureSpec)
        return if (mode == MeasureSpec.EXACTLY) size else DEFAULT_SIZE
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        if (mRadius==0f){
            processCycleXMode(canvas)
        }else{
            processConnerXMode(canvas)
        }
    }


    /**
     * 处理圆角
     * @param canvas Canvas
     */
    var curBitmap : Bitmap?=null
    private fun processConnerXMode(canvas: Canvas) {
        curBitmap=getScaleBitmap()
        if (curBitmap==null)return
        val mRect= RectF(0f,0f,width.toFloat(), height.toFloat())
        canvas.drawRoundRect(mRect,20f,20f, mPaint)
        mPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
        canvas.drawBitmap(curBitmap!!, 0f, 0f, mPaint)
        mPaint.setXfermode(null)


    }

    /**
     * 处理圆形
     * @param canvas Canvas
     */
    private fun processCycleXMode(canvas: Canvas) {
        curBitmap=getScaleBitmap()
        if (curBitmap==null)return
        canvas.drawCircle((height / 2.0).toFloat(), (height / 2.0).toFloat(), (height / 2.0).toFloat(), mPaint)
        mPaint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
        canvas.drawBitmap(curBitmap!!, 0f, 0f, mPaint)
        mPaint.setXfermode(null)


    }

    private fun getScaleBitmap(): Bitmap {


        val options =  BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, R.mipmap.test, options)
        options.inSampleSize = calcSampleSize(options, mSize, mSize)
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeResource(resources, R.mipmap.test, options)
    }

    /**
     * 计算缩放比例
     *
     * @param option
     * @param width
     * @param height
     * @return
     */
    private fun calcSampleSize(option: BitmapFactory.Options, width: Int, height: Int): Int {
        var originWidth = option.outWidth
        var originHeight = option.outHeight
        var sampleSize = 1
        while (((originWidth shr 1).also {
                originWidth = it
            }) > width && ((originHeight shr 1).also {
                originHeight = it
            }) > height) {
            sampleSize = sampleSize shl 1
        }
        return sampleSize
    }

}

2.clipPath方案

思路:
1:mPath.addRoundRect()/addCircle()绘制路径
2:canvas.clipPath(mPath) 按路径绘制


注意:
1:canvas.clipPath:不支持硬件加速,所以在使用前需要禁止硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null)

2:clipPath要在super.onDraw方法前,调用,否则无效(canvas已经被设置给View了)


package com.wkq.workkotlin.custom.cycle

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatImageView
import androidx.compose.ui.geometry.Rect
import com.wkq.workkotlin.R
import kotlin.math.min

/**
 *@Desc:Xfermode 处理圆形图片
 *
 *@Author: wkq
 *
 *@Time: 2024/11/7 10:55
 *
 */
class CycleClipPathView : AppCompatImageView {


    var mContext: Context
    private var mRect: RectF
    private var mPath: Path
    private var mRadius = 0f
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    ){
        mContext = context

        val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
        mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
        tp.recycle()
        mRect = RectF()
        mPath = Path()
        setLayerType(LAYER_TYPE_HARDWARE, null);
    }





    override fun onDraw(canvas: Canvas) {
        canvas.clipPath(mPath)
        super.onDraw(canvas)


    }


    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        if (mRadius <= 0f) {
            clipCircle(w, h);
        } else {
            clipRoundRect(w, h);
        }
    }

    /**
     * 圆角
     */
    private fun clipRoundRect(width: Int, height: Int) {
        mRect.left = 0f
        mRect.top = 0f
        mRect.right = width.toFloat()
        mRect.bottom = height.toFloat()
        mPath.addRoundRect(mRect, mRadius, mRadius, Path.Direction.CW)
    }

    /**
     * 圆形
     */
    private fun clipCircle(width: Int, height: Int) {
        val radius = (min(width.toDouble(), height.toDouble()) / 2).toInt()
        mPath.addCircle(width.toFloat() / 2, height.toFloat() / 2, radius.toFloat(), Path.Direction.CW)
    }


}

3.BitmapShader 方案

思路:
1:获取Bitmap,生成BitmapShader 着色器
2:将着色器设置给画笔 mPaint.setShader(bitmapShader)
3:在画布上绘制圆形或者圆角 canvas.drawCircle()/drawRoundRect()

package com.wkq.workkotlin.custom.cycle

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import com.wkq.workkotlin.R

/**
 *@Desc:
 *
 *@Author: wkq
 *
 *@Time: 2024/11/7 11:25
 *
 */
class CycleBitmapShaderView:View {
    val DEFAULT_SIZE = 500
    var mContext: Context
    var mPaint: Paint
    private var mRadius = 0f
    var mSize: Int = 0
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    ){
        mContext = context

        val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
        mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
        tp.recycle()
        mPaint = Paint()
        mPaint.isAntiAlias = true
        mPaint.style = Paint.Style.FILL

    }

    /**
     * BitmapShader 方式绘制圆形
     * @param canvas Canvas
     */
    private fun processCycleBitmapShader(canvas: Canvas) {
        val bitmapShader = BitmapShader(getScaleBitmap(), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
        mPaint.setShader(bitmapShader)
        canvas.drawCircle((height / 2.0).toFloat(), (height / 2.0).toFloat(), (height / 2.0).toFloat(), mPaint)
    }

    /**
     * BitmapShader 方式绘制圆形
     * @param canvas Canvas
     */
    private fun processConnerBitmapShader(canvas: Canvas) {
        val bitmapShader = BitmapShader(getScaleBitmap(), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
        mPaint.setShader(bitmapShader)
        val mRect = RectF()
        mRect.left = 0f
        mRect.top = 0f
        mRect.right = width.toFloat()
        mRect.bottom = height.toFloat()
        canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        val width: Int = getMeasureSize(widthMeasureSpec)
        val height: Int = getMeasureSize(heightMeasureSpec)
        mSize = Math.min(width, height)
        setMeasuredDimension(mSize, mSize)
    }

    private fun getMeasureSize(measureSpec: Int): Int {
        val mode = MeasureSpec.getMode(measureSpec)
        val size = MeasureSpec.getSize(measureSpec)
        return if (mode == MeasureSpec.EXACTLY) size else DEFAULT_SIZE
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (mRadius<=0f){

            processCycleBitmapShader(canvas)
        }else{

            processConnerBitmapShader(canvas)
        }
    }

    private fun getScaleBitmap(): Bitmap {


        val options =  BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, R.mipmap.test, options)
        options.inSampleSize = calcSampleSize(options, mSize, mSize)
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeResource(resources, R.mipmap.test, options)
    }

    /**
     * 计算缩放比例
     *
     * @param option
     * @param width
     * @param height
     * @return
     */
    private fun calcSampleSize(option: BitmapFactory.Options, width: Int, height: Int): Int {
        var originWidth = option.outWidth
        var originHeight = option.outHeight
        var sampleSize = 1
        while (((originWidth shr 1).also {
                originWidth = it
            }) > width && ((originHeight shr 1).also {
                originHeight = it
            }) > height) {
            sampleSize = sampleSize shl 1
        }
        return sampleSize
    }
}

4.ViewOutlineProvider 方案

思路:
通过view.setOutlineProvider,给我们的View控件设置一个圆形轮廓,然后让View根据轮廓提供者进行切割(使用任何组件)

扩展:
圆角TextView
圆形文字

package com.wkq.workkotlin.custom.cycle

import android.content.Context
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.Rect
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import androidx.appcompat.widget.AppCompatTextView
import com.wkq.workkotlin.R


/**
 *@Desc: ViewOutlineProvider 实现 圆形
 *
 *@Author: wkq
 *
 *@Time: 2024/11/7 11:25
 *
 */
class CycleViewOutlineProviderView: AppCompatTextView {

    var mContext: Context
    var mPaint: Paint
    private var mRadius = 0f

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, -1)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context, attrs, defStyleAttr
    ){
        mContext = context
        val tp = mContext.obtainStyledAttributes(attrs, R.styleable.CycleXFerModeViewStyle)
        mRadius = tp.getFloat(R.styleable.CycleXFerModeViewStyle_mRadius, 0f)
        tp.recycle()
        mPaint = Paint()
        mPaint.isAntiAlias = true
        mPaint.style = Paint.Style.FILL
        initView()
    }

    private fun initView() {
        if (Build.VERSION.SDK_INT >= 21) {
            val outlineProvider: ViewOutlineProvider = object : ViewOutlineProvider() {
                override fun getOutline(view: View, outline: Outline) {
                    val width = view.width
                    val height = view.height
                    if (mRadius <= 0f) {
                        val rect: Rect = Rect(0, 0, width, height)
                        outline.setOval(rect) // API>=21
                    } else {
                        // 右边圆弧
//                        val rect: Rect = Rect(0, 0, width, height)
//                        outline.setRoundRect(rect, height.toFloat()/2)
                        // 圆角
                        val rect: Rect = Rect(0, 0, width, height)
                        outline.setRoundRect(rect, mRadius)
                    }
                }
            }
            clipToOutline = true
            setOutlineProvider(outlineProvider)
        }
    }

}

总结:

通过四种方式实现了圆形以及圆角形状,主要是通过操作画布,画笔实现了圆形以及圆角的功能.就这四种方案简单说两句

  • 1:PorterDuffXfermode API强大能,灵活能绘制各种需求
  • 2.clipPath 按照路径绘制圆形圆角,能绘制切割出 各式各样的样式
  • 3.BitmapShader 位图着色器 根据画布绘制的形状填充 图片
  • 4:ViewOutlineProvider 给View 设置一个轮廓 不仅限于图片(扩展性强)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容