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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【html】网页布局模板01---简谱风

模板效果: 这是一种最简单,最干净的一种网页布局。 模板介绍: 模板概述: 这个模板是一个基础的网页布局模板,包括一个头部区域(header),其中包含网站标题(logo)和导航菜单(nav),以及一个页脚区域(copy),用于显示版权信息。整体布局简洁明了,适合作为各种类…

不靠后端,前端也能搞定接口!

嘿&#xff0c;前端开发达人们&#xff01;有个超酷的消息要告诉你们&#xff1a;MemFire Cloud来袭啦&#xff01;这个神奇的东东让你们不用依赖后端小伙伴们&#xff0c;也能妥妥地搞定 API 接口。是不是觉得有点不可思议&#xff1f;但是事实就是这样&#xff0c;让我们一起…

探索编程乐趣:绘制螺旋图的奇幻之旅

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;编程的魔法世界 二、绘制螺旋图的准备工作 三、代码实战&#xff1a;…

X-SCAN:Rust从零实现一个命令行端口扫描工具

0. 成品预览 本文将基于Rust构建一个常见的网络工具&#xff0c;端口扫描器。 按照惯例&#xff0c;还是和之前实现的文本编辑器一样&#xff0c;我给这个工具起名为X-SCAN,它的功能很简单&#xff0c;通过命令行参数的方式对指定IP进行扫描&#xff0c;扫描结束之后返回该IP…

Spring AMQP 随笔 8 Retry MessageRecoverer ErrorHandler

0. 列位&#xff0c;响应式布局好麻烦的 … 有意思的&#xff0c;chrome devtool 在调试响应式的分辨率的时候&#xff0c;比如说在 宽度远远大于 768 的时候&#xff0c;按说浏览器也知道大概率是 web端方式打开&#xff0c;样式也是如此渲染&#xff0c;但一些事件(没有鼠标…

【IC设计】牛客网-序列检测习题总结

文章目录 状态机基础知识VL25 输入序列连续的序列检测VL26 含有无关项的序列检测VL27 不重叠序列检测VL28 输入序列不连续的序列检测参考资料 状态机基础知识 VL25 输入序列连续的序列检测 timescale 1ns/1ns module sequence_detect(input clk,input rst_n,input a,output re…

csdn的insCode怎么用IDE和linux终端

1.进入insCode&#xff0c;选择工作台 找到我的项目&#xff0c;没有项目的话可以新建一个。 选择在IDE中编辑&#xff0c;界面如下&#xff1a; 右边有个终端&#xff0c;点击即可出现linux的xterm终端。

边用边充电影响寿命吗?看看计算机指令组成与操作类型

计算机指令集体系结构之指令 指令由操作码和地址码字段组成。 操作码指明了指令要完成的操作。 长度可以固定&#xff1a;比如RISC&#xff08;reduced instruction set computer&#xff09;精简指令集计算机 与之对应的RISC&#xff08;复杂指令集计算机&#xff09;&…

福昕PDF使用技巧

因为突然间学校的企业版WPS突然很多功能就不能使用了&#xff0c;所以转向福昕PDF。 一、合并文件 添加需要合并的文件&#xff0c;可以使用ctrla等方式全选 找到最上方的“合并文件” 二、文本注释

IDEA打开项目报错

IDEA打开项目报错&#xff1a; Cannot read scheme C:\Users\xxxxxx\AppData\Roaming\JetBrains\IntelliJIdea2023.2\qaplug_profiles\Default.xmljava.lang.AbstractMethodError: Receiver class com.soldevelo.qaplug.scanner.AnalysisProfileManager$2 does not define or i…

【讲解下PDM,PDM是什么?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

【汽车之家注册/登录安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

借助Kong记录接口的请求和响应内容

和APISIX类似&#xff0c;Kong也是一个Api GateWay。 运行在调用Api之前&#xff0c;以插件的扩展方式为Api提供管理, 如 鉴权、限流、监控、健康检查等. Kong是基于Lua语言、Nginx以及OpenResty开发的&#xff0c;拥有动态路由、负载均衡、高可用、高性能、熔断&#xff08;基…

通过RAG架构LLM应用程序

在之前的博客文章中&#xff0c;我们已经描述了嵌入是如何工作的&#xff0c;以及RAG技术是什么。本节我们我们将使用 LangChain 库以及 RAG 和嵌入技术在 Python 中构建一个简单的 LLM 应用程序。 我们将使用 LangChain 库在 Python 中构建一个简单的 LLM 应用程序。LangChai…

自己手写一个单向链表【C风格】

//单链表 #include <iostream> #define MAX_SIZE 20 #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0typedef int ElemType;//元素的类型 typedef int Status;//返回状态typedef struct Node {ElemType data;//链表中保存的数据struct Node* next;//指向下…

【CSP CCF记录】201909-1 小明种苹果

题目 过程 #include<bits/stdc.h> using namespace std; int N,M; long long tree[1010]; int main() {cin>>N>>M;long long result0,max0;//result剩余苹果&#xff0c;max最大疏果个数 int id0;//id最大疏果的果树编号 for(int i1;i<N;i){long long b0…

构建php环境

目录 php简介 官网php安装包 选择下载稳定版本 &#xff08;建议使用此版本&#xff0c;文章以此版本为例&#xff09; 安装php解析环境 准备工作 安装依赖 zlib-devel 和 libxml2-devel包。 安装扩展工具库 安装 libmcrypt 安装 mhash 安装mcrypt 安装php 选项含…

Gin框架学习笔记(六)——gin中的日志使用

gin内置日志组件的使用 前言 在之前我们要使用Gin框架定义路由的时候我们一般会使用Default方法来实现&#xff0c;我们来看一下他的实现&#xff1a; func Default(opts ...OptionFunc) *Engine {debugPrintWARNINGDefault()engine : New()engine.Use(Logger(), Recovery())…

uniapp微信小程序解决type=“nickname“获取昵称,v-model绑定值为空问题!

解决获取 type"nickname"值为空问题 文章目录 解决获取 type"nickname"值为空问题效果图Demo解决方式通过表单收集内容通过 uni.createSelectorQuery 效果图 开发工具效果图&#xff0c;真机上还会显示键盘输入框 Demo 如果通过 v-model 结合 blur 获取不…

【Linux】写时拷贝技术COW (copy-on-write)

文章目录 Linux写时拷贝技术(copy-on-write)进程的概念进程的定义进程和程序的区别PCB的内部构成 程序是如何被加载变成进程的&#xff1f;写时复制&#xff08;Copy-On-Write, COW&#xff09;写时复制机制的原理写时拷贝的场景 fork与COWvfork与fork Linux写时拷贝技术(copy-…