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

相关文章

uniapp接入微信小程序原生代码配置方案(优化版)

uniapp项目需要把微信小程序原生语法的功能代码嵌套过来,无需把原生代码转换为uniapp,可以配置拷贝的方式集成过来 1、拷贝代码包到src目录 2、vue.config.js中配置原生代码包直接拷贝到编译目录中 3、pages.json中配置分包目录,原生入口组件的路径 4、manifest.json中配置分包,使用原生组件 5、需要把原生代码包里的页面修改成组件的方

swiper实例

大家好,我是燐子,今天给大家带来swiper实例   微信小程序中的 swiper 组件是一种用于创建滑动视图的容器组件,常用于实现图片轮播、广告展示等效果。它通过一系列的子组件 swiper-item 来定义滑动视图的每一个页面。 基本用法   以下是一个简单的 swiper 示例代码:   WXML(页面结构) <swiper autoplay="true" interval="3

Java面试题:通过实例说明内连接、左外连接和右外连接的区别

在 SQL 中,连接(JOIN)用于在多个表之间组合行。最常用的连接类型是内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)。它们的主要区别在于它们如何处理表之间的匹配和不匹配行。下面是每种连接的详细说明和示例。 表示例 假设有两个表:Customers 和 Orders。 Customers CustomerIDCus

公共筛选组件(二次封装antd)支持代码提示

如果项目是基于antd组件库为基础搭建,可使用此公共筛选组件 使用到的库 npm i antdnpm i lodash-esnpm i @types/lodash-es -D /components/CommonSearch index.tsx import React from 'react';import { Button, Card, Form } from 'antd'

17.用300行代码手写初体验Spring V1.0版本

1.1.课程目标 1、了解看源码最有效的方式,先猜测后验证,不要一开始就去调试代码。 2、浓缩就是精华,用 300行最简洁的代码 提炼Spring的基本设计思想。 3、掌握Spring框架的基本脉络。 1.2.内容定位 1、 具有1年以上的SpringMVC使用经验。 2、 希望深入了解Spring源码的人群,对 Spring有一个整体的宏观感受。 3、 全程手写实现SpringM

代码随想录算法训练营:12/60

非科班学习算法day12 | LeetCode150:逆波兰表达式 ,Leetcode239: 滑动窗口最大值  目录 介绍 一、基础概念补充: 1.c++字符串转为数字 1. std::stoi, std::stol, std::stoll, std::stoul, std::stoull(最常用) 2. std::stringstream 3. std::atoi, std

Eclipse+ADT与Android Studio开发的区别

下文的EA指Eclipse+ADT,AS就是指Android Studio。 就编写界面布局来说AS可以边开发边预览(所见即所得,以及多个屏幕预览),这个优势比较大。AS运行时占的内存比EA的要小。AS创建项目时要创建gradle项目框架,so,创建项目时AS比较慢。android studio基于gradle构建项目,你无法同时集中管理和维护多个项目的源码,而eclipse ADT可以同时打开

android 免费短信验证功能

没有太复杂的使用的话,功能实现比较简单粗暴。 在www.mob.com网站中可以申请使用免费短信验证功能。 步骤: 1.注册登录。 2.选择“短信验证码SDK” 3.下载对应的sdk包,我这是选studio的。 4.从头像那进入后台并创建短信验证应用,获取到key跟secret 5.根据技术文档操作(initSDK方法写在setContentView上面) 6.关键:在有用到的Mo

android一键分享功能部分实现

为什么叫做部分实现呢,其实是我只实现一部分的分享。如新浪微博,那还有没去实现的是微信分享。还有一部分奇怪的问题:我QQ分享跟QQ空间的分享功能,我都没配置key那些都是原本集成就有的key也可以实现分享,谁清楚的麻烦详解下。 实现分享功能我们可以去www.mob.com这个网站集成。免费的,而且还有短信验证功能。等这分享研究完后就研究下短信验证功能。 开始实现步骤(新浪分享,以下是本人自己实现

Android我的二维码扫描功能发展史(完整)

最近在研究下二维码扫描功能,跟据从网上查阅的资料到自己勉强已实现扫描功能来一一介绍我的二维码扫描功能实现的发展历程: 首页通过网络搜索发现做android二维码扫描功能看去都是基于google的ZXing项目开发。 2、搜索怎么使用ZXing实现自己的二维码扫描:从网上下载ZXing-2.2.zip以及core-2.2-source.jar文件,分别解压两个文件。然后把.jar解压出来的整个c