android实现PhotoShop里的魔棒效果

魔棒是画板工具一个重要的功能,非常实用,只要轻轻一点,就能把触摸到的颜色区域选中,做复制、剪切、擦除等工作。

那怎么实现呢?

先来看看效果:

要实现这个效果,需要对安卓canvas和paint理解比较深才行。

原理:

1、获取画板上用户触摸点的颜色, bitmap.getPixel;

2、根据目标色对画布进行检索,符合容差范围内的像素纳入到选区内。上下左右4个方向检索,检索到连续的Point汇集成Rect,把Rect合并成Region;

3、对Region取boundaryPath,获取到选区是个Path对象

4、对Path对象描述的范围做虚线框选中显示,同时得到Rect作为选中的位置锚定。

5、把Path跟画布结合生成出剪切、复制的图像进行后续操作。

关键实现:

整个实现都在一个单独的View中操作,即在原来的画布View上添加一层半透明View。即CutView。代码太长,这里给出关键代码:

复制代码
    private fun startDashAnimate() {
        dashAnimate.setIntValues(dashMin, dashMax)
        dashAnimate.duration = 4000
        dashAnimate.addUpdateListener {
            val dash = it.animatedValue as Int
            dashPaint.pathEffect = DashPathEffect(floatArrayOf(20f, 20f), dash.toFloat())
            invalidate()
        }
        dashAnimate.repeatCount = ValueAnimator.INFINITE
        dashAnimate.start()
    }

    private fun pauseAnim() {
        dashAnimate.pause()
    }

    private fun resumeAnim() {
        dashAnimate.resume()
    }

    private fun findRegionPath(event: MotionEvent) {
        actionShowLoading?.invoke()
        GlobalScope.launch(Dispatchers.IO) {
            pvsEditView?.let {
                it.saveToPhoto(true)?.let {bitmap ->
                    filterRegionUtils.findColorRegion(event.x.toInt(), event.y.toInt(), bitmap) {path, r ->
                        addPath(path, r)
                        GlobalScope.launch(Dispatchers.Main) {
                            invalidate()
                            actionHideLoading?.invoke()
                        }
                    }
                }
            }
        }
    }

这里其他的都是选区动画与绘制。主要看魔棒的入口方法:findRegionPath

findRegionPath由于耗时较长,使用了协程进行计算。

把真正的findColorRegion查找色块放到了工具类filterRegionUtils

这是核心,它返回找到的Path和Rect

整个色块查找类:

复制代码
class FilterRegionUtils {

    data class Point(val x: Int, val y: Int)

    data class Segment(val point: Point, val rect: Rect)

    private val segmentStack = Stack<Segment>()

    private val tolerance = 70

    private var rectF = RectF()

    private val markedPointMap = HashMap<Int, Boolean>()

    private val visitedSeedMap = HashMap<Int, Boolean>()

    private var width: Int = 0
    private var height: Int = 0

    private var pointColor: Int = 0

    private lateinit var pixels: IntArray

    private val segmentList = arrayListOf<Segment>()

    fun findColorRegion(x: Int, y: Int, bitmap: Bitmap, action: ((Path, RectF) -> Unit)) {
        markedPointMap.clear()
        segmentStack.clear()
        visitedSeedMap.clear()
        width = bitmap.width
        height = bitmap.height
        if (x < 0 || x >= width || y < 0 || y >= height) {
            return
        }

        val region = Region()

        val path = Path()
        path.moveTo(x.toFloat(), y.toFloat())
        rectF = RectF(x.toFloat(), y.toFloat(), x.toFloat(), y.toFloat())

        // 拿到该bitmap的颜色数组
        pixels = IntArray(width * height)

        bitmap.getPixels(pixels, 0, width, 0, 0, width, height)

        pointColor = bitmap.getPixel(x, y)
        val point = Point(x, y)

        searchLineAtPoint(point)
        var index = 1
        while (segmentStack.isNotEmpty()) {
            val segment = segmentStack.pop()
            processSegment(segment)
            region.union(segment.rect)
            rectF.left = min(rectF.left, segment.rect.left.toFloat())
            rectF.top = min(rectF.top, segment.point.y.toFloat())
            rectF.right = max(rectF.right, segment.rect.right.toFloat())
            rectF.bottom = max(rectF.bottom, segment.point.y.toFloat())
            index++
        }
        val tempPath = region.boundaryPath
        path.addPath(tempPath)

        action.invoke(path, rectF)
    }

    private fun processSegment(segment: Segment) {
        val left = segment.rect.left
        val right = segment.rect.right
        val y = segment.point.y
        for (x in left .. right) {
            val top = y-1
            searchLineAtPoint(Point(x, top))
            val bottom = y+1
            searchLineAtPoint(Point(x, bottom))
        }
    }

    private fun searchLineAtPoint(point: Point) {
        if (point.x < 0 || point.x >= width || point.y < 0 || point.y >= height) return
        if (visitedSeedMap[point.y * width + point.x] != null) {
            return
        }
        if (!markPointIfMatches(point)) return
        // search left
        var left = point.x;
        var x = point.x - 1;
        while (x >= 0) {
            val lPoint = Point(x, point.y)
            if (markPointIfMatches(lPoint)) {
                left = x
            } else {
                break
            }
            x--
        }
        // search right
        var right = point.x
        x = point.x + 1
        while (x < width) {
            val rPoint = Point(x, point.y)
            if (markPointIfMatches(rPoint)) {
                right = x
            } else {
                break
            }
            x++
        }
        val segment = Segment(point, Rect(left, point.y-1, right, point.y+1))
        segmentList.add(segment)
        segmentStack.push(segment)
    }

    private fun markPointIfMatches(point: Point): Boolean {
        val offset = point.y*width + point.x
        val visited = visitedSeedMap[offset]
        if (visited != null) return false
        var matches = false
        if (matchPoint(point)) {
            matches = true
            markedPointMap[offset] = true
        }
        visitedSeedMap[offset] = true
        return matches
    }

    private fun matchPoint(point: Point): Boolean {
        val index = point.y*width + point.x
        val c1 = pixels[index]
        val t = max(max(abs(Color.red(c1)-Color.red(pointColor)), abs(Color.green(c1)-Color.green(pointColor))),
            abs(Color.blue(c1)-Color.blue(pointColor)))
        val alpha = abs(Color.alpha(c1)-Color.alpha((pointColor)))
        // 容差值范围内的都视作同一颜色
        return t < tolerance && alpha < tolerance
    }
}

整个算法流程还是比较简洁高效的。

再看后面,拿到了选区的Path和Rect后,怎么跟画布结合实现复制或剪切。

复制代码
/**
     * 剪切选区
     */
    fun cutPath(path: Path, isNormal: Boolean) {
        bitmap?.let {
            bitmap = Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888)
            canvas = Canvas(bitmap!!)
            val paint = Paint()
            paint.style = Paint.Style.FILL
            canvas.drawPath(path, paint)
            paint.xfermode = if (isNormal) {
                // 取原bitmap的非交集部分
                PorterDuffXfermode(PorterDuff.Mode.SRC_OUT)
            } else {
                // 取原bitmap的交集部分
                PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
            }
            canvas.drawBitmap(it, 0f, 0f, paint)
        }
    }

这是剪切的方法,很简单,就是利用Paint的xfermode,用isNormal控制是正选还是反选,即取交集还是非交集。

复制选区方法也类似:

复制代码
fun genAreaBitmap(src: Bitmap, action: ((Bitmap, RectF) -> Unit)){
        if (!canOperate()) {
            return
        }
        // 根据裁剪区域生成bitmap
        val srcCopy = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(srcCopy)
        val rectF = region.bounds
        // 避免溢出
        rectF.right = min(src.width, rectF.right)
        rectF.bottom = min(src.height, rectF.bottom)
        val paint = Paint()
        var r = rectF
        paint.style = Paint.Style.FILL
        val op = if (isNormal) {
            Region.Op.INTERSECT
        } else {
            r = Rect(0, 0, width, height)
            Region.Op.DIFFERENCE
        }
        canvas.clipPath(targetPath, op)
        canvas.drawBitmap(src, 0f, 0f, paint)
        val fBitmap = Bitmap.createBitmap(srcCopy, r.left, r.top,
            r.width(), r.height())
        action.invoke(fBitmap, RectF(r))
        finish()
    }

利用Cavnas的clipPath接口,在画布上裁剪出指定区域。

相关推荐
2501_9160074716 小时前
iOS开发中抓取HTTPS请求的完整解决方法与步骤详解
android·网络协议·ios·小程序·https·uni-app·iphone
ZC跨境爬虫16 小时前
跟着 MDN 学CSS day_3:(为一个传记页面添加样式)
前端·javascript·css·ui·音视频·html5
lvronglee18 小时前
【数字图传第四步】Android App查看图传视频
android·音视频
90后的晨仔18 小时前
Android 程序入口与核心组件详解
android
90后的晨仔18 小时前
Kotlin 简介与开发环境搭建
android
UI设计兰亭妙微18 小时前
兰亭妙微|打破色彩对比度迷思:UI设计公司中的无障碍设计灵活之道
ui·b端界面设计·高端网站设计
BU摆烂会噶19 小时前
【LangGraph】House_Agent 实战(四):预定流程 —— 中断与人工干预
android·人工智能·python·langchain
AI玫瑰助手19 小时前
Python运算符:比较运算符(等于不等等于大于小于)与返回值
android·开发语言·python
轻口味19 小时前
HarmonyOS 6.1 全栈实战录 - 14 渲染树透镜:FrameNode 渲染状态感知与高性能 UI 调优实战
ui·华为·harmonyos
new_dev19 小时前
Python实现Android自动化打包工具:加固、签名、多渠道一键完成
android·python·自动化