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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值