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

相关文章

MyBatis-Plus逻辑删除实现过程

《MyBatis-Plus逻辑删除实现过程》本文介绍了MyBatis-Plus如何实现逻辑删除功能,包括自动填充字段、配置与实现步骤、常见应用场景,并展示了如何使用remove方法进行逻辑删除,逻辑删... 目录1. 逻辑删除的必要性编程1.1 逻辑删除的定义1.2 逻辑删php除的优点1.3 适用场景2.

C#借助Spire.XLS for .NET实现在Excel中添加文档属性

《C#借助Spire.XLSfor.NET实现在Excel中添加文档属性》在日常的数据处理和项目管理中,Excel文档扮演着举足轻重的角色,本文将深入探讨如何在C#中借助强大的第三方库Spire.... 目录为什么需要程序化添加Excel文档属性使用Spire.XLS for .NET库实现文档属性管理Sp

Python+FFmpeg实现视频自动化处理的完整指南

《Python+FFmpeg实现视频自动化处理的完整指南》本文总结了一套在Python中使用subprocess.run调用FFmpeg进行视频自动化处理的解决方案,涵盖了跨平台硬件加速、中间素材处理... 目录一、 跨平台硬件加速:统一接口设计1. 核心映射逻辑2. python 实现代码二、 中间素材处

Java数组动态扩容的实现示例

《Java数组动态扩容的实现示例》本文主要介绍了Java数组动态扩容的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1 问题2 方法3 结语1 问题实现动态的给数组添加元素效果,实现对数组扩容,原始数组使用静态分配

Python实现快速扫描目标主机的开放端口和服务

《Python实现快速扫描目标主机的开放端口和服务》这篇文章主要为大家详细介绍了如何使用Python编写一个功能强大的端口扫描器脚本,实现快速扫描目标主机的开放端口和服务,感兴趣的小伙伴可以了解下... 目录功能介绍场景应用1. 网络安全审计2. 系统管理维护3. 网络故障排查4. 合规性检查报错处理1.

Python轻松实现Word到Markdown的转换

《Python轻松实现Word到Markdown的转换》在文档管理、内容发布等场景中,将Word转换为Markdown格式是常见需求,本文将介绍如何使用FreeSpire.DocforPython实现... 目录一、工具简介二、核心转换实现1. 基础单文件转换2. 批量转换Word文件三、工具特性分析优点局

Springboot3统一返回类设计全过程(从问题到实现)

《Springboot3统一返回类设计全过程(从问题到实现)》文章介绍了如何在SpringBoot3中设计一个统一返回类,以实现前后端接口返回格式的一致性,该类包含状态码、描述信息、业务数据和时间戳,... 目录Spring Boot 3 统一返回类设计:从问题到实现一、核心需求:统一返回类要解决什么问题?

Java使用Spire.Doc for Java实现Word自动化插入图片

《Java使用Spire.DocforJava实现Word自动化插入图片》在日常工作中,Word文档是不可或缺的工具,而图片作为信息传达的重要载体,其在文档中的插入与布局显得尤为关键,下面我们就来... 目录1. Spire.Doc for Java库介绍与安装2. 使用特定的环绕方式插入图片3. 在指定位

Java使用Spire.Barcode for Java实现条形码生成与识别

《Java使用Spire.BarcodeforJava实现条形码生成与识别》在现代商业和技术领域,条形码无处不在,本教程将引导您深入了解如何在您的Java项目中利用Spire.Barcodefor... 目录1. Spire.Barcode for Java 简介与环境配置2. 使用 Spire.Barco

Java利用Spire.Doc for Java实现在模板的基础上创建Word文档

《Java利用Spire.DocforJava实现在模板的基础上创建Word文档》在日常开发中,我们经常需要根据特定数据动态生成Word文档,本文将深入探讨如何利用强大的Java库Spire.Do... 目录1. Spire.Doc for Java 库介绍与安装特点与优势Maven 依赖配置2. 通过替换