android实现PhotoShop里的魔棒效果

2024-05-25 17:04

本文主要是介绍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 = 4000dashAnimate.addUpdateListener {val dash = it.animatedValue as IntdashPaint.pathEffect = DashPathEffect(floatArrayOf(20f, 20f), dash.toFloat())invalidate()}dashAnimate.repeatCount = ValueAnimator.INFINITEdashAnimate.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 = 70private var rectF = RectF()private val markedPointMap = HashMap<Int, Boolean>()private val visitedSeedMap = HashMap<Int, Boolean>()private var width: Int = 0private var height: Int = 0private var pointColor: Int = 0private lateinit var pixels: IntArrayprivate val segmentList = arrayListOf<Segment>()fun findColorRegion(x: Int, y: Int, bitmap: Bitmap, action: ((Path, RectF) -> Unit)) {markedPointMap.clear()segmentStack.clear()visitedSeedMap.clear()width = bitmap.widthheight = bitmap.heightif (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 = 1while (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.boundaryPathpath.addPath(tempPath)action.invoke(path, rectF)}private fun processSegment(segment: Segment) {val left = segment.rect.leftval right = segment.rect.rightval y = segment.point.yfor (x in left .. right) {val top = y-1searchLineAtPoint(Point(x, top))val bottom = y+1searchLineAtPoint(Point(x, bottom))}}private fun searchLineAtPoint(point: Point) {if (point.x < 0 || point.x >= width || point.y < 0 || point.y >= height) returnif (visitedSeedMap[point.y * width + point.x] != null) {return}if (!markPointIfMatches(point)) return// search leftvar 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 rightvar right = point.xx = point.x + 1while (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.xval visited = visitedSeedMap[offset]if (visited != null) return falsevar matches = falseif (matchPoint(point)) {matches = truemarkedPointMap[offset] = true}visitedSeedMap[offset] = truereturn matches}private fun matchPoint(point: Point): Boolean {val index = point.y*width + point.xval 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.FILLcanvas.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}// 根据裁剪区域生成bitmapval 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 = rectFpaint.style = Paint.Style.FILLval 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接口,在画布上裁剪出指定区域。

这篇关于android实现PhotoShop里的魔棒效果的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1002096

相关文章

Python实现终端清屏的几种方式详解

《Python实现终端清屏的几种方式详解》在使用Python进行终端交互式编程时,我们经常需要清空当前终端屏幕的内容,本文为大家整理了几种常见的实现方法,有需要的小伙伴可以参考下... 目录方法一:使用 `os` 模块调用系统命令方法二:使用 `subprocess` 模块执行命令方法三:打印多个换行符模拟

SpringBoot+EasyPOI轻松实现Excel和Word导出PDF

《SpringBoot+EasyPOI轻松实现Excel和Word导出PDF》在企业级开发中,将Excel和Word文档导出为PDF是常见需求,本文将结合​​EasyPOI和​​Aspose系列工具实... 目录一、环境准备与依赖配置1.1 方案选型1.2 依赖配置(商业库方案)二、Excel 导出 PDF

Python实现MQTT通信的示例代码

《Python实现MQTT通信的示例代码》本文主要介绍了Python实现MQTT通信的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 安装paho-mqtt库‌2. 搭建MQTT代理服务器(Broker)‌‌3. pytho

使用zip4j实现Java中的ZIP文件加密压缩的操作方法

《使用zip4j实现Java中的ZIP文件加密压缩的操作方法》本文介绍如何通过Maven集成zip4j1.3.2库创建带密码保护的ZIP文件,涵盖依赖配置、代码示例及加密原理,确保数据安全性,感兴趣的... 目录1. zip4j库介绍和版本1.1 zip4j库概述1.2 zip4j的版本演变1.3 zip4

python生成随机唯一id的几种实现方法

《python生成随机唯一id的几种实现方法》在Python中生成随机唯一ID有多种方法,根据不同的需求场景可以选择最适合的方案,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习... 目录方法 1:使用 UUID 模块(推荐)方法 2:使用 Secrets 模块(安全敏感场景)方法

Spring StateMachine实现状态机使用示例详解

《SpringStateMachine实现状态机使用示例详解》本文介绍SpringStateMachine实现状态机的步骤,包括依赖导入、枚举定义、状态转移规则配置、上下文管理及服务调用示例,重点解... 目录什么是状态机使用示例什么是状态机状态机是计算机科学中的​​核心建模工具​​,用于描述对象在其生命

Spring Boot 结合 WxJava 实现文章上传微信公众号草稿箱与群发

《SpringBoot结合WxJava实现文章上传微信公众号草稿箱与群发》本文将详细介绍如何使用SpringBoot框架结合WxJava开发工具包,实现文章上传到微信公众号草稿箱以及群发功能,... 目录一、项目环境准备1.1 开发环境1.2 微信公众号准备二、Spring Boot 项目搭建2.1 创建

IntelliJ IDEA2025创建SpringBoot项目的实现步骤

《IntelliJIDEA2025创建SpringBoot项目的实现步骤》本文主要介绍了IntelliJIDEA2025创建SpringBoot项目的实现步骤,文中通过示例代码介绍的非常详细,对大家... 目录一、创建 Spring Boot 项目1. 新建项目2. 基础配置3. 选择依赖4. 生成项目5.

Linux下删除乱码文件和目录的实现方式

《Linux下删除乱码文件和目录的实现方式》:本文主要介绍Linux下删除乱码文件和目录的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux下删除乱码文件和目录方法1方法2总结Linux下删除乱码文件和目录方法1使用ls -i命令找到文件或目录

SpringBoot+EasyExcel实现自定义复杂样式导入导出

《SpringBoot+EasyExcel实现自定义复杂样式导入导出》这篇文章主要为大家详细介绍了SpringBoot如何结果EasyExcel实现自定义复杂样式导入导出功能,文中的示例代码讲解详细,... 目录安装处理自定义导出复杂场景1、列不固定,动态列2、动态下拉3、自定义锁定行/列,添加密码4、合并