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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/641994.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构----堆的实现(附代码)

当大家看了鄙人的上一篇博客栈后&#xff0c;稍微猜一下应该知道鄙人下一篇想写的博客就是堆了吧。毕竟堆栈在C语言中常常是一起出现的。那么堆是什么&#xff0c;是如何实现的嘞。接下来我就带大家去尝试实现一下堆。 堆的含义 首先我们要写出一个堆&#xff0c;那么我们就需…

SQOOP详细讲解

SQOOP安装及使用 SQOOP安装及使用SQOOP安装1、上传并解压2、修改文件夹名字3、修改配置文件4、修改环境变量5、添加MySQL连接驱动6、测试准备MySQL数据登录MySQL数据库创建student数据库切换数据库并导入数据另外一种导入数据的方式使用Navicat运行SQL文件导出MySQL数据库impo…

ElasticSearch - 删除已经设置的认证密码(7.x)

文章目录 Pre版本号 7.x操作步骤检查当前Elasticsearch安全配置停止Elasticsearch服务修改Elasticsearch配置文件删除密码重启Elasticsearch服务验证配置 小结 Pre Elasticsearch - Configuring security in Elasticsearch 开启用户名和密码访问 版本号 7.x ES7.x 操作步骤 …

阿里云产品DTU评测报告(一)

阿里云产品DTU评测报告&#xff08;一&#xff09; 名词解释物联网平台控制台产品设备 DTU设备模拟器 体验评价针对业务场景&#xff0c;您觉得该产品还有哪些可改进的地方&#xff1f;什么场景下使用该产品产品的优势是什么个人建议 在正式进行DTU测评之前&#xff0c;说一点题…

【Vue】input框自动聚焦且输入验证码后跳至下一位

场景&#xff1a;PC端 样式&#xff1a; <div class"verification-code-input"><input v-model"code[index]" v-for"(_, index) in 5" :key"index" type"text" maxlength"1" input"handleInput(i…

【idea】idea2024最新版本下载_安装_破解

1、下载 下载地址&#xff1a;下载 IntelliJ IDEA – 领先的 Java 和 Kotlin IDE 下载完成&#xff1a; idea破解脚本下载链接&#xff1a;https://pan.baidu.com/s/1L5qq26cRABw8XuEn_CngKQ 提取码&#xff1a;6666 下载完成&#xff1a; 2、安装 1、双击idea的安装包&…

电赛经验分享——赛前准备

⏩ 大家好哇&#xff01;我是小光&#xff0c;想要成为系统架构师的嵌入式爱好者。 ⏩在之前的电赛中取得了省一的成绩&#xff0c;本文对电赛比赛前需要准备什么做一个经验分享。 ⏩感谢你的阅读&#xff0c;不对的地方欢迎指正。 加入小光嵌入式交流群&#xff08;qq群号&…

FPGA 纯逻辑arinc818 ip core

1、 符合FC-FS、FC-AV、FC-ADVB协议规范&#xff1b; 2、符合ARINC818协议规范&#xff1b; 3、支持光纤通信Class1、Class3服务&#xff1b; 5、可动态配置光纤端口速率&#xff0c;支持1.0625Gbps、2.125Gbps、3.1875Gbps、4.25Gbps可配置&#xff1b; 6、DDR控制接口简洁…

力扣--字符串58.最后一个单词的长度

思路分析 初始化变量: num 用于记录当前单词的长度。before 用于记录上一个单词的长度。 遍历字符串: 如果字符不是空格&#xff0c;增加 num 计数。如果字符是空格&#xff0c;检查 num 是否为 0&#xff1a; 如果 num 为 0&#xff0c;说明之前没有记录到单词&#xff0c;所以…

刷代码随想录有感(78):回溯算法——关于树枝/树层去重的思考(涉及break/continue的使用)

在复原IP地址中&#xff0c;剪枝操作我们使用的是break: if(isvalid(s, start, i)){s.insert(s.begin() i 1, .);pointNum;backtracking(s, i 2, pointNum);s.erase(s.begin() i 1);pointNum--; }else break;在其他情况&#xff0c;举个例子&#xff0c;在含有重复元素求…

基于UDP的tftp的文件传输

#define SER_PORT 69 #define SER_IP "192.168.125.71" #define CLT_PORT 6666 #define CLT_IP "192.168.125.158" int main(int argc, const char *argv[]) {//创建套接字文件描述符int cfd socket(AF_INET,SOCK_DGRAM,0);if(cfd -1){perror("sock…

Less语言

Less是一门预编译语言&#xff0c;它扩展了CSS语言&#xff0c;增加了变量、Mixin、函数等特性&#xff0c;使CSS更易维护和扩展 Less也扩充了CSS语言&#xff0c;增加了诸如变量、混合运算、函数等功能。Less既可以运行在服务端(Node.js和Rhino平台)也可以运行在客户端(浏览器…

Zookeeper 安装教程和使用指南

一、Zookeeper介绍 ZooKeeper 是 Apache 软件基金会的一个开源项目&#xff0c;主要基于 Java 语言实现。 Apache ZooKeeper 是一个开源的分布式应用程序协调服务&#xff0c;提供可靠的数据管理通知、数据同步、命名服务、分布式配置服务、分布式协调等服务。 关键特性 分布…

提取 Chrome、Firefox 中储存的用户密码用于凭据发现

操作环境 Chrome 浏览器 Version 125.0.6422.112 (Official Build) (64-bit)Firefox 浏览器 Version 126.0 (64 位) Chrome 浏览器储存密钥原理 新的 Chrome 浏览器储存密码的方案是使用 Chrome 生成的 AES 密钥对用户密码进行加密之后储存在 Sqlite 数据库文件中&#xff0c;A…

图论(从数据结构的三要素出发)

文章目录 逻辑结构物理结构邻接矩阵定义性能分析性质存在的问题 邻接表定义性能分析存在的问题 十字链表(有向图)定义性能分析 邻接多重表(无向图)定义性能分析 数据的操作图的基本操作图的遍历广度优先遍历&#xff08;BFS&#xff09;算法思想和实现性能分析深度优先最小生成…

Python项目:数据可视化_下载数据【笔记】

源自《Python编程&#xff1a;从入门到实践》 作者&#xff1a; Eric Matthes 02 下载数据 2.1 sitka_weather_07-2021_simple.csv from pathlib import Path import matplotlib.pyplot as plt import csv from datetime import datetimepath Path(D:\CH16\sitka_weather_0…

Python--List列表

list列表⭐⭐ 1高级数据类型 Python中的数据类型可以分为&#xff1a;数字型&#xff08;基本数据类型&#xff09;和非数字型&#xff08;高级数据类型&#xff09; ●数字型包含&#xff1a;整型int、浮点型float、布尔型bool、复数型complex ●非数字型包含&#xff1a;字符…

杰理-耳机进入关机关闭内内置触摸-节省功耗

杰理-耳机进入关机关闭内内置触摸-节省功耗 if (__this->init 0) {return LP_TOUCH_SOFTOFF_MODE_LEGACY; }if ((__this -> softoff_mode LP_TOUCH_SOFTOFF_MODE_ADVANCE) && (__this->softoff_keep 0)) {lp_touch_key_disable(); } __this->softoff_k…

干货 | 2024 EISS 企业信息安全高峰论坛(脱敏)PPT(7份可下载)

2024 EISS 企业信息安全高峰论坛&#xff08;脱敏&#xff09;PPT&#xff0c;共7份。 AI在出海业务的安全实践.pdf Palo Alto Networks为中国企业全球化布局保驾护航.pdf 安全建设与治理思路.pdf 车路云一体化安全体系建设实践.pdf 企业研发安全DevSecOps流程落地实践.pdf 浅谈…