Android不规则封闭区域填充色彩的实例代码

2024-03-19 13:12

本文主要是介绍Android不规则封闭区域填充色彩的实例代码,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、概述

在上一篇的叙述中,我们通过图层的方式完成了图片颜色的填充(详情请戳:Android不规则图像填充颜色小游戏),不过在着色游戏中更多的还是基于边界的图像的填充。本篇博客将详细描述。

图像的填充有2种经典算法。

一种是种子填充法。

种子填充法理论上能够填充任意区域和图形,但是这种算法存在大量的反复入栈和大规模的递归,降低了填充效率。

另一种是扫描线填充法。

注意:实际上图像填充的算法还是很多的,有兴趣可以去Google学术上去搜一搜。

ok,下面先看看今天的效果图:

img

ok,可以看到这样的颜色填充比上一篇的基于层的在素材的准备上要easy 很多~~~

二、原理分析

首先我们简述下原理,我们在点击的时候拿到点击点的”颜色”,然后按照我们选择的算法进行填色即可。

算法1:种子填充法,四联通/八联通

算法简介:假设要将某个区域填充成红色。

从用户点击点的像素开始,上下左右(八联通还有左上,左下,右上,右下)去判断颜色,如果四个方向上的颜色与当前点击点的像素一致,则改变颜色至目标色。然后继续上述这个过程。

ok,可以看到这是一个递归的过程,1个点到4个,4个到16个不断的去延伸。如果按照这种算法,你会写出类似这样的代码:

/*** @param pixels 像素数组* @param w 宽度* @param h 高度* @param pixel 当前点的颜色* @param newColor 填充色* @param i 横坐标* @param j 纵坐标*/private void fillColor01(int[] pixels, int w, int h, int pixel, int newColor, int i, int j){int index = j * w + i;if (pixels[index] != pixel || i  = w || i < 0 || j < 0 || j  = h)return;pixels[index] = newColor;//上fillColor01(pixels, w, h, pixel, newColor, i, j - 1);//右fillColor01(pixels, w, h, pixel, newColor, i + 1, j);//下fillColor01(pixels, w, h, pixel, newColor, i, j + 1);//左fillColor01(pixels, w, h, pixel, newColor, i - 1, j);}

代码很简单,但是如果你去运行,会发生StackOverflowException异常,这个异常主要是因为大量的递归造成的。虽然简单,但是在移动设备上使用该方法不行。

于是,我就想,这个方法不是递归深度过多么,那么我可以使用一个Stack去存像素点,减少递归的深度和次数,于是我把代码改成如下的方式:

/*** @param pixels 像素数组* @param w 宽度* @param h 高度* @param pixel 当前点的颜色* @param newColor 填充色* @param i 横坐标* @param j 纵坐标*/private void fillColor(int[] pixels, int w, int h, int pixel, int newColor, int i, int j){mStacks.push(new Point(i, j));while (!mStacks.isEmpty()){Point seed = mStacks.pop();Log.e("TAG", "seed = " + seed.x + " , seed = " + seed.y);int index = seed.y * w + seed.x;pixels[index] = newColor;if (seed.y   0){int top = index - w;if (pixels[top] == pixel){mStacks.push(new Point(seed.x, seed.y - 1));}}if (seed.y < h - 1){int bottom = index + w;if (pixels[bottom] == pixel){mStacks.push(new Point(seed.x, seed.y + 1));}}if (seed.x   0){int left = index - 1;if (pixels[left] == pixel){mStacks.push(new Point(seed.x - 1, seed.y));}}if (seed.x < w - 1){int right = index + 1;if (pixels[right] == pixel){mStacks.push(new Point(seed.x + 1, seed.y));}}}}

方法的思想也比较简单,将当前像素点入栈,然后出栈着色,接下来分别判断四个方向的,如果符合条件也进行入栈(只要栈不为空持续运行)。ok,这个方法我也尝试跑了下,恩,这次不会报错了,但是速度特别的慢~~~~慢得我是不可接受的。(有兴趣可以尝试,记得如果ANR,点击等待)。

这样来看,第一种算法,我们是不考虑了,没有办法使用,主要原因是假设对于矩形同色区域,都是需要填充的,而算法一依然是各种入栈。于是考虑第二种算法

扫描线填充法

算法思想[4]:

初始化一个空的栈用于存放种子点,将种子点(x, y)入栈; 判断栈是否为空,如果栈为空则结束算法,否则取出栈顶元素作为当前扫描线的种子点(x, y),y是当前的扫描线; 从种子点(x, y)出发,沿当前扫描线向左、右两个方向填充,直到边界。分别标记区段的左、右端点坐标为xLeft和xRight; 分别检查与当前扫描线相邻的y – 1和y + 1两条扫描线在区间[xLeft, xRight]中的像素,从xRight开始向xLeft方向搜索,假设扫描的区间为AAABAAC(A为种子点颜色),那么将B和C前面的A作为种子点压入栈中,然后返回第(2)步;

上述参考自参考文献[4],做了些修改,文章[4]中描述算法,测试有一点问题,所以做了修改.

可以看到该算法,基本上是一行一行着色的,这样的话在大块需要着色区域的效率比算法一要高很多。

ok,关于算法的步骤大家目前觉得模糊,一会可以参照我们的代码。选定了算法以后,接下来就开始编码了。

三、编码实现

我们代码中引入了一个边界颜色,如果设置的话,着色的边界参考为该边界颜色,否则会只要与种子颜色不一致为边界。

(一)构造方法与测量

public class ColourImageView extends ImageView
{private Bitmap mBitmap;/*** 边界的颜色*/private int mBorderColor = -1;private boolean hasBorderColor = false;private Stack<Point  mStacks = new Stack<Point ();public ColourImageView(Context context, AttributeSet attrs){super(context, attrs);TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColourImageView);mBorderColor = ta.getColor(R.styleable.ColourImageView_border_color, -1);hasBorderColor = (mBorderColor != -1);L.e("hasBorderColor = " + hasBorderColor + " , mBorderColor = " + mBorderColor);ta.recycle();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){super.onMeasure(widthMeasureSpec, heightMeasureSpec);int viewWidth = getMeasuredWidth();int viewHeight = getMeasuredHeight();//以宽度为标准,等比例缩放view的高度setMeasuredDimension(viewWidth,getDrawable().getIntrinsicHeight() * viewWidth / getDrawable().getIntrinsicWidth());L.e("view's width = " + getMeasuredWidth() + " , view's height = " + getMeasuredHeight());//根据drawable,去得到一个和view一样大小的bitmapBitmapDrawable drawable = (BitmapDrawable) getDrawable();Bitmap bm = drawable.getBitmap();mBitmap = Bitmap.createScaledBitmap(bm, getMeasuredWidth(), getMeasuredHeight(), false);}

可以看到我们选择的是继承ImageView,这样只需要将图片设为src即可。 构造方法中获取我们的自定义边界颜色,当然可以不设置~~ 重写测量的目的是为了获取一个和View一样大小的Bitmap便于我们操作。

接下来就是点击啦~

(二)onTouchEvent

@Overridepublic boolean onTouchEvent(MotionEvent event){final int x = (int) event.getX();final int y = (int) event.getY();if (event.getAction() == MotionEvent.ACTION_DOWN){//填色fillColorToSameArea(x, y);}return super.onTouchEvent(event);}/*** 根据x,y获得改点颜色,进行填充** @param x* @param y*/private void fillColorToSameArea(int x, int y){Bitmap bm = mBitmap;int pixel = bm.getPixel(x, y);if (pixel == Color.TRANSPARENT || (hasBorderColor && mBorderColor == pixel)){return;}int newColor = randomColor();int w = bm.getWidth();int h = bm.getHeight();//拿到该bitmap的颜色数组int[] pixels = new int[w * h];bm.getPixels(pixels, 0, w, 0, 0, w, h);//填色fillColor(pixels, w, h, pixel, newColor, x, y);//重新设置bitmapbm.setPixels(pixels, 0, w, 0, 0, w, h);setImageDrawable(new BitmapDrawable(bm));}

可以看到,我们在onTouchEvent中获取(x,y),然后拿到改点坐标:

获得点击点颜色,获得整个bitmap的像素数组 改变这个数组中的颜色 然后重新设置给bitmap,重新设置给ImageView

重点就是通过fillColor去改变数组中的颜色

/*** @param pixels 像素数组* @param w 宽度* @param h 高度* @param pixel 当前点的颜色* @param newColor 填充色* @param i 横坐标* @param j 纵坐标*/private void fillColor(int[] pixels, int w, int h, int pixel, int newColor, int i, int j){//步骤1:将种子点(x, y)入栈;mStacks.push(new Point(i, j));//步骤2:判断栈是否为空,// 如果栈为空则结束算法,否则取出栈顶元素作为当前扫描线的种子点(x, y),// y是当前的扫描线;while (!mStacks.isEmpty()){/*** 步骤3:从种子点(x, y)出发,沿当前扫描线向左、右两个方向填充,* 直到边界。分别标记区段的左、右端点坐标为xLeft和xRight;*/Point seed = mStacks.pop();//L.e("seed = " + seed.x + " , seed = " + seed.y);int count = fillLineLeft(pixels, pixel, w, h, newColor, seed.x, seed.y);int left = seed.x - count + 1;count = fillLineRight(pixels, pixel, w, h, newColor, seed.x + 1, seed.y);int right = seed.x + count;/*** 步骤4:* 分别检查与当前扫描线相邻的y - 1和y + 1两条扫描线在区间[xLeft, xRight]中的像素,* 从xRight开始向xLeft方向搜索,假设扫描的区间为AAABAAC(A为种子点颜色),* 那么将B和C前面的A作为种子点压入栈中,然后返回第(2)步;*///从y-1找种子if (seed.y - 1  = 0)findSeedInNewLine(pixels, pixel, w, h, seed.y - 1, left, right);//从y+1找种子if (seed.y + 1 < h)findSeedInNewLine(pixels, pixel, w, h, seed.y + 1, left, right);}}

可以看到我已经很清楚的将该算法的四个步骤标识到该方法中。好了,最后就是一些依赖的细节上的方法:

 /*** 在新行找种子节点** @param pixels* @param pixel* @param w* @param h* @param i* @param left* @param right*/private void findSeedInNewLine(int[] pixels, int pixel, int w, int h, int i, int left, int right){/*** 获得该行的开始索引*/int begin = i * w + left;/*** 获得该行的结束索引*/int end = i * w + right;boolean hasSeed = false;int rx = -1, ry = -1;ry = i;/*** 从end到begin,找到种子节点入栈(AAABAAAB,则B前的A为种子节点)*/while (end  = begin){if (pixels[end] == pixel){if (!hasSeed){rx = end % w;mStacks.push(new Point(rx, ry));hasSeed = true;}} else{hasSeed = false;}end--;}}/*** 往右填色,返回填充的个数** @return*/private int fillLineRight(int[] pixels, int pixel, int w, int h, int newColor, int x, int y){int count = 0;while (x < w){//拿到索引int index = y * w + x;if (needFillPixel(pixels, pixel, index)){pixels[index] = newColor;count++;x++;} else{break;}}return count;}/*** 往左填色,返回填色的数量值** @return*/private int fillLineLeft(int[] pixels, int pixel, int w, int h, int newColor, int x, int y){int count = 0;while (x  = 0){//计算出索引int index = y * w + x;if (needFillPixel(pixels, pixel, index)){pixels[index] = newColor;count++;x--;} else{break;}}return count;}private boolean needFillPixel(int[] pixels, int pixel, int index){if (hasBorderColor){return pixels[index] != mBorderColor;} else{return pixels[index] == pixel;}}/*** 返回一个随机颜色** @return*/private int randomColor(){Random random = new Random();int color = Color.argb(255, random.nextInt(256), random.nextInt(256), random.nextInt(256));return color;}

ok,到此,代码就介绍完毕了~~~

最后贴下布局文件~~

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:zhy="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"android:paddingBottom="@dimen/activity_vertical_margin"tools:context=".MainActivity" <com.zhy.colour_app_01.ColourImageViewzhy:border_color="#FF000000"android:src="@drawable/image_007"android:background="#33ff0000"android:layout_width="match_parent"android:layout_centerInParent="true"android:layout_height="match_parent"/ </RelativeLayout <?xml version="1.0" encoding="utf-8"? 
<resources <declare-styleable name="ColourImageView" <attr name="border_color" format="color|reference" </attr </declare-styleable 
</resources 

参考链接

扫描线种子填充算法的解析

图像处理之泛洪填充算法(Flood Fill Algorithm)

递归种子填充算法

扫描线种子填充算法

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对ZaLou.Cn的支持。

更多Android进阶指南 可以扫码 解锁 《Android十大板块文档》

1.Android车载应用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2024最新Android中高级面试题汇总+解析,告别零offer

4.企业级Android音视频开发学习路线+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技术解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高级Android插件化与组件化(含实战教程和源码)

9.Android 性能优化实战+360°全方面性能调优

10.Android零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔

这篇关于Android不规则封闭区域填充色彩的实例代码的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

Python实例题之pygame开发打飞机游戏实例代码

《Python实例题之pygame开发打飞机游戏实例代码》对于python的学习者,能够写出一个飞机大战的程序代码,是不是感觉到非常的开心,:本文主要介绍Python实例题之pygame开发打飞机... 目录题目pygame-aircraft-game使用 Pygame 开发的打飞机游戏脚本代码解释初始化部

Java中Map.Entry()含义及方法使用代码

《Java中Map.Entry()含义及方法使用代码》:本文主要介绍Java中Map.Entry()含义及方法使用的相关资料,Map.Entry是Java中Map的静态内部接口,用于表示键值对,其... 目录前言 Map.Entry作用核心方法常见使用场景1. 遍历 Map 的所有键值对2. 直接修改 Ma

MyBatis Plus 中 update_time 字段自动填充失效的原因分析及解决方案(最新整理)

《MyBatisPlus中update_time字段自动填充失效的原因分析及解决方案(最新整理)》在使用MyBatisPlus时,通常我们会在数据库表中设置create_time和update... 目录前言一、问题现象二、原因分析三、总结:常见原因与解决方法对照表四、推荐写法前言在使用 MyBATis

Spring组件实例化扩展点之InstantiationAwareBeanPostProcessor使用场景解析

《Spring组件实例化扩展点之InstantiationAwareBeanPostProcessor使用场景解析》InstantiationAwareBeanPostProcessor是Spring... 目录一、什么是InstantiationAwareBeanPostProcessor?二、核心方法解

深入解析 Java Future 类及代码示例

《深入解析JavaFuture类及代码示例》JavaFuture是java.util.concurrent包中用于表示异步计算结果的核心接口,下面给大家介绍JavaFuture类及实例代码,感兴... 目录一、Future 类概述二、核心工作机制代码示例执行流程2. 状态机模型3. 核心方法解析行为总结:三

python获取cmd环境变量值的实现代码

《python获取cmd环境变量值的实现代码》:本文主要介绍在Python中获取命令行(cmd)环境变量的值,可以使用标准库中的os模块,需要的朋友可以参考下... 前言全局说明在执行py过程中,总要使用到系统环境变量一、说明1.1 环境:Windows 11 家庭版 24H2 26100.4061

java String.join()方法实例详解

《javaString.join()方法实例详解》String.join()是Java提供的一个实用方法,用于将多个字符串按照指定的分隔符连接成一个字符串,这一方法是Java8中引入的,极大地简化了... 目录bVARxMJava String.join() 方法详解1. 方法定义2. 基本用法2.1 拼接

pandas实现数据concat拼接的示例代码

《pandas实现数据concat拼接的示例代码》pandas.concat用于合并DataFrame或Series,本文主要介绍了pandas实现数据concat拼接的示例代码,具有一定的参考价值,... 目录语法示例:使用pandas.concat合并数据默认的concat:参数axis=0,join=

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

C#代码实现解析WTGPS和BD数据

《C#代码实现解析WTGPS和BD数据》在现代的导航与定位应用中,准确解析GPS和北斗(BD)等卫星定位数据至关重要,本文将使用C#语言实现解析WTGPS和BD数据,需要的可以了解下... 目录一、代码结构概览1. 核心解析方法2. 位置信息解析3. 经纬度转换方法4. 日期和时间戳解析5. 辅助方法二、L