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接口,在画布上裁剪出指定区域。

相关推荐
lw向北.14 分钟前
Qt For Android之环境搭建(Qt 5.12.11 Qt下载SDK的处理方案)
android·开发语言·qt
不爱学习的啊Biao22 分钟前
【13】MySQL如何选择合适的索引?
android·数据库·mysql
Clockwiseee1 小时前
PHP伪协议总结
android·开发语言·php
mmsx7 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
众拾达人10 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌11 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley12 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
界面开发小八哥14 小时前
DevExpress WPF中文教程:Grid - 如何移动和调整列大小?(二)
ui·.net·wpf·界面控件·devexpress·ui开发
hedalei14 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng14 小时前
安卓多渠道apk配置不同签名
android