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

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景