Android Xfermode应用:实现圆角矩形、圆形等图片裁切

先上效果图:

其中第一个是原图,其余都是通过Xfermode处理之后的效果。

代码实现:

kotlin 复制代码
class XfermodeImgView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        style = Paint.Style.FILL
    }
    private val xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) // 显示相交的区域,但图像为DST
    private lateinit var imageBitmap: Bitmap
    private val mRectF = RectF()
    private var mCurShape = SHAPE_CIRCLE

    //边框
    private val mStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.STROKE
        strokeWidth = 1.dp2px().toFloat()
        color = Color.RED // 矩形颜色无所谓,只需要形状
    }
    private val mPath = Path()

    // 圆角半径
    private var cornerRadius = 50f // 默认圆角半径

    // 设置图片资源
    fun setImageBitmap(bitmap: Bitmap, shape: Int) {
        this.mCurShape = shape
        imageBitmap = bitmap
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        mRectF.set(0f, 0f, w.toFloat(), h.toFloat())
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (!::imageBitmap.isInitialized) return

        //canvas.drawRect(mRectF, mStrokePaint) //绘制边框

        if (mCurShape == SHAPE_ORIGIN) {
            //展示原图
            val scaledBitmap = Bitmap.createScaledBitmap(imageBitmap, width, height, false)
            canvas.drawBitmap(scaledBitmap, 0f, 0f, mPaint)
            return
        }

        // 创建一个新的图层,用于混合绘制
        val layerId = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null)
        drawDSTByCanvas(canvas) //绘制DST
        // 设置混合模式
        mPaint.xfermode = xfermode
        // 绘制源图像
        val scaledBitmap = Bitmap.createScaledBitmap(imageBitmap, width, height, false)
        canvas.drawBitmap(scaledBitmap, 0f, 0f, mPaint)
        // 清除混合模式
        mPaint.xfermode = null
        // 恢复图层
        canvas.restoreToCount(layerId)
    }

    private fun drawDSTByCanvas(canvas: Canvas) {
        val halfLength = mRectF.width() / 2
        when (mCurShape) {
            SHAPE_ROUND -> {
                canvas.drawRoundRect(mRectF, cornerRadius, cornerRadius, mPaint)
            }

            SHAPE_ROUND_SUB -> {
                // 定义每个角的圆角半径(左上、右上、右下、左下,每个角2个值:横向半径、纵向半径)
                val radii = floatArrayOf(
                    halfLength, halfLength, // 左上角圆角半径
                    halfLength, halfLength, // 右上角圆角半径
                    0f, 0f, // 右下角圆角半径
                    0f, 0f // 左下角圆角半径
                )
                mPath.reset()
                // 创建 Path,并添加圆角矩形
                mPath.addRoundRect(mRectF, radii, Path.Direction.CW)
                // 使用 Canvas 的 drawDoubleRoundRect 方法绘制
                canvas.drawPath(mPath, mPaint)
            }

            SHAPE_CIRCLE -> {
                canvas.drawCircle(halfLength, halfLength, halfLength, mPaint)
            }

            SHAPE_STAR -> {
                val centerX = mRectF.width() / 2 // 矩形中心 X 坐标
                val centerY = mRectF.height() / 2 // 矩形中心 Y 坐标
                val radiusOuter = mRectF.width() / 2f // 外圆半径(五角星的顶点半径)
                val radiusInner = radiusOuter * 0.5f // 内圆半径(五角星的交点半径)
                // 清空 Path
                mPath.reset()
                // 五角星的顶点数量
                val points = 5
                // 计算每个角度(360°分为 10 个角)
                val angleStep = Math.PI * 2 / (points * 2)
                // 绘制五角星的路径
                for (i in 0 until points * 2) {
                    // 交替使用外圆和内圆的半径
                    val radius = if (i % 2 == 0) radiusOuter else radiusInner
                    // 当前点的角度
                    val angle = angleStep * i - Math.PI / 2 // 起始角度为 -90°(顶点朝上)
                    // 计算点的坐标
                    val x = centerX + (radius * cos(angle)).toFloat()
                    val y = centerY + (radius * sin(angle)).toFloat()

                    if (i == 0) {
                        mPath.moveTo(x, y) // 第一个点移动到起点
                    } else {
                        mPath.lineTo(x, y) // 连接到下一个点
                    }
                }
                mPath.close() // 闭合路径,形成五角星
                canvas.drawPath(mPath, mPaint)
            }

            SHAPE_HEART -> {
                val width = mRectF.width()
                val height = mRectF.height()
                val centerX = mRectF.left + width / 2
                val centerY = mRectF.top + height / 3

                // 心形的上圆弧半径
                val radius = width / 4

                // 左侧圆弧的中心点
                val leftCircleCenterX = centerX - radius
                val leftCircleCenterY = centerY

                // 右侧圆弧的中心点
                val rightCircleCenterX = centerX + radius
                val rightCircleCenterY = centerY

                // 底部点 (心形的尖端)
                val bottomPointX = centerX
                val bottomPointY = mRectF.bottom

                // 起始点 (从左侧圆弧顶部开始)
                val startX = centerX - 2 * radius
                val startY = centerY

                // 开始绘制路径
                mPath.reset()
                mPath.moveTo(startX, startY)
                // 左侧圆弧
                mPath.arcTo(
                    leftCircleCenterX - radius, leftCircleCenterY - radius,
                    leftCircleCenterX + radius, leftCircleCenterY + radius,
                    180f, 180f, false
                )
                // 右侧圆弧
                mPath.arcTo(
                    rightCircleCenterX - radius, rightCircleCenterY - radius,
                    rightCircleCenterX + radius, rightCircleCenterY + radius,
                    180f, 180f, false
                )
                // 连接到底部点
                mPath.lineTo(bottomPointX, bottomPointY)
                // 闭合路径,返回起点
                mPath.close()
                canvas.drawPath(mPath, mPaint)
            }
            SHAPE_LEAF -> {
                // 定义每个角的圆角半径(左上、右上、右下、左下,每个角2个值:横向半径、纵向半径)
                val radii = floatArrayOf(
                    halfLength, halfLength, // 左上角圆角半径
                    0f, 0f, // 右上角圆角半径
                    halfLength, halfLength, // 右下角圆角半径
                    0f, 0f // 左下角圆角半径
                )
                mPath.reset()
                // 创建 Path,并添加圆角矩形
                mPath.addRoundRect(mRectF, radii, Path.Direction.CW)
                // 使用 Canvas 的 drawDoubleRoundRect 方法绘制
                canvas.drawPath(mPath, mPaint)
            }
            SHAPE_TRI -> {

                // 矩形中心点
                val centerX = halfLength
                val centerY = halfLength

                // 顶点坐标
                val topX = centerX
                val topY = mRectF.top

                // 左下角坐标
                val leftX = mRectF.left
                val leftY = mRectF.bottom

                // 右下角坐标
                val rightX = mRectF.right
                val rightY = mRectF.bottom

                // 构建三角形路径
                mPath.reset()
                mPath.moveTo(topX, topY)      // 移动到顶部
                mPath.lineTo(leftX, leftY)    // 绘制左边的线
                mPath.lineTo(rightX, rightY)  // 绘制右边的线
                mPath.close()                 // 闭合路径
                canvas.drawPath(mPath, mPaint)
            }
            SHAPE_DIAMOND -> {
                // 获取矩形的中心点
                val centerX = halfLength
                val centerY = halfLength

                // 计算菱形的四个顶点
                val topX = centerX
                val topY = mRectF.top

                val rightX = mRectF.right
                val rightY = centerY

                val bottomX = centerX
                val bottomY = mRectF.bottom

                val leftX = mRectF.left
                val leftY = centerY

                // 构建菱形路径
                mPath.reset()
                mPath.moveTo(topX, topY)       // 移动到顶部点
                mPath.lineTo(rightX, rightY)   // 绘制右边线
                mPath.lineTo(bottomX, bottomY) // 绘制底部线
                mPath.lineTo(leftX, leftY)     // 绘制左边线
                mPath.close()                  // 闭合路径
                canvas.drawPath(mPath, mPaint)
            }
        }
    }

    companion object {
        const val SHAPE_ORIGIN = 100 // 原图
        const val SHAPE_ROUND = 0 // 圆角矩形
        const val SHAPE_ROUND_SUB = 1 // 不完整的圆角矩形
        const val SHAPE_CIRCLE = 2 // 圆形
        const val SHAPE_STAR = 3 //五角星
        const val SHAPE_HEART = 4 //心形
        const val SHAPE_LEAF = 5 //叶子
        const val SHAPE_TRI = 6 //三角形
        const val SHAPE_DIAMOND = 7 //菱形
    }
}

Fragment中:

kotlin 复制代码
/**
 * 图片处理
 */
class XFerModeIMGFragment : Fragment() {

    data class ViewItem(val titleName: String, val position: Int)

    private val recyclerView: RecyclerView by id(R.id.rv_view)

    override fun getLayoutId(): Int {
        return R.layout.layout_rv
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        recyclerView.layoutManager = GridLayoutManager(context, 2)
        context?.let { recyclerView.createDivider(it, 5.dp2px().toFloat()) }

        // 准备数据
        val dataList = mutableListOf<ViewItem>().apply {
            add(ViewItem("原图", SHAPE_ORIGIN))
            add(ViewItem("圆角矩形", SHAPE_ROUND))
            add(ViewItem("圆角角度不一致", SHAPE_ROUND_SUB))
            add(ViewItem("圆形", SHAPE_CIRCLE))
            add(ViewItem("五角星", SHAPE_STAR))
            add(ViewItem("心形", SHAPE_HEART))
            add(ViewItem("叶子", SHAPE_LEAF))
            add(ViewItem("三角形", SHAPE_TRI))
            add(ViewItem("菱形", SHAPE_DIAMOND))
        }
        // 设置适配器
        val adapter = MyAdapter(dataList) { _, item -> }
        recyclerView.adapter = adapter
    }

    class MyAdapter(
        private val dataList: List<ViewItem>,
        private val onItemClick: (Int, ViewItem) -> Unit
    ) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_xfermode_img, parent, false)
            return MyViewHolder(view)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            val data = dataList[position]
            val context = holder.itemView.context
            val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.icon_cat_w)
            holder.img.setImageBitmap(bitmap, data.position)
            // 设置点击事件
            holder.itemView.setOnClickListener {
                onItemClick(position, data)
            }
        }

        override fun getItemCount(): Int = dataList.size

        class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val img: XfermodeImgView = itemView.findViewById(R.id.iv_xfer_img)
        }
    }
}

对应的XML文件:

kotlin 复制代码
//layout_rv.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rv_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="10dp"
    android:orientation="vertical" />

//item_xfermode_img.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <org.ninetripods.mq.study.widget.XfermodeImgView
        android:id="@+id/iv_xfer_img"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_gravity="center" />
</FrameLayout>

上述代码通过 PorterDuffXfermode 的混合模式,绘制出目标形状的遮罩图层(如圆角矩形、圆形等),然后结合原始图片进行图像合成,最终实现裁剪效果。裁剪过程大致如下:

  • 创建一层目标形状(DST 图层)的遮罩。例如,圆角矩形的裁剪会用 Canvas.drawRoundRect 绘制遮罩。
  • 使用 PorterDuffXfermode 的合成模式(如 PorterDuff.Mode.SRC_IN),将遮罩和源图片(SRC 图层)进行混合,只保留两者重叠部分的内容。
  • 将裁剪后的图片绘制到 View 上。

在 XFerModeIMGFragment 中,主要功能是使用自定义 View 和 RecyclerView 展示不同的图形裁剪效果。

相关推荐
笨鸭先游3 小时前
前台--Android开发
android
fareast_mzh3 小时前
Lightweight App Alternatives
android
pq113_67 小时前
OrangePi Zero 3学习笔记(Android篇)4 - eudev编译(获取libudev.so)
android·笔记·学习
鸿蒙布道师11 小时前
鸿蒙NEXT开发动画案例3
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
鸿蒙布道师11 小时前
AI原生手机:三大技术阵营的终极对决与未来展望
android·人工智能·ios·华为·智能手机·ai-native·hauwei
每次的天空11 小时前
移动应用开发:自定义 View 处理大量数据的性能与交互优化方案
android·java·学习·交互
Huang兄12 小时前
Android 项目中配置了多个 maven 仓库,但依赖还是下载失败,除了使用代理,还有其他方法吗?
android·gradle·maven
snail20121113 小时前
Flutter接入ProtoBuff和原生Android通信【性能最优】
android·flutter
難釋懷13 小时前
Android开发-常用布局
android·gitee
墨菲斯托88814 小时前
fakebook
android