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

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja