我的简书同步发布Android自动手绘,圆你儿时画家梦!

从小就喜欢到处乱画,家里一米以下墙上就没有一块干净的地方(那是老房子啦~)~~(⊙﹏⊙)b。好了,废话不多说,进入主题。今天主要跟大家分享一下如何将一张图片转成手绘效果,并模拟画家动态绘制。先把最终效果图亮出来,觉得好的请点个赞,您的点赞是对我的最大鼓励(O(∩_∩)O哈哈~)。
效果图如下:

心动有木有!

原理

大概介绍一下实现原理。首先你得有一张图(废话~),接下来就是把这张图的轮廓提取出来,轮廓提取算法有很多,本人不是搞图像处理的,对图像处理感兴趣的童鞋可以查看相关资料。如果你有好的轮廓提取算法,也可以把源码中的算法替换掉,我们采用的轮廓提取算法是Sobel边缘检测。网上的实现有很多,我懒得去实现一遍,github有开源的实现,直接下载了:GraphicLib.本文不具体介绍轮廓提取算法。得到轮廓图以后,接下来要做的是,按照线条的走势,一个一个像素点绘制出来。注意,一定要按照线条的走势显示对应的像素点,如果用两个嵌套的for循环,动画会像网速不好时浏览器显示图片一样。难点就在于此,如何把像素点按照线条方向绘制。接下来我们一起研究。

代码实现

如何让绘制的点不会跳远太远,使之连贯起来?首先,对于一个刚绘制完成的点,接下来要绘制的点肯定要选择离它最近的点,这样肯定是最佳的下一个绘制点。因此,只要我们找到最近的点就可以,寻找最近的点,可以通过以圆的方式不断改变半径的大小进行探测。但是用圆的话需要各种三角函数运算,影响效率。我们可以换一种方法:根据当前的点可以轻松得到每一层以该点为中心的正方形,一层层遍历,直到找到需要的点就是我们要的点。遍历的方法很简单,就是比较对应的正方形的上下左右四条边上面的像素点。如下图所示:

接下来看看如何用代码去实现寻找最近的点:

 //获取离指定点最近的一个未绘制过的点private Point getNearestPoint(Point p) {if (p == null) return null;//以点p为中心,向外扩大搜索范围,每次搜索的是与p点相距add的正方形for (int add = 1; add < mSrcBmWidth && add < mSrcBmHeight; add++) {//int beginX = (p.x - add) >= 0 ? (p.x - add) : 0;int endX = (p.x + add) < mSrcBmWidth ? (p.x + add) : mSrcBmWidth - 1;int beginY = (p.y - add) >= 0 ? (p.y - add) : 0;int endY = (p.y + add) < mSrcBmHeight ? (p.y + add) : mSrcBmHeight - 1;//搜索正方形的上下边for (int x = beginX; x <= endX; x++) {if (mArray[x][beginY]) {//标记当前点已经访问过mArray[x][beginY] = false;return new Point(x, beginY);}if (mArray[x][endY]) {//标记当前点已经访问过mArray[x][endY] = false;return new Point(x, endY);}}//搜索正方形的左右边for (int y = beginY + 1; y <= endY - 1; y++) {if (mArray[beginX][y]) {//标记当前点已经访问过mArray[beginX][beginY] = false;return new Point(beginX, beginY);}if (mArray[endX][y]) {//标记当前点已经访问过mArray[endX][y] = false;return new Point(endX, y);}}}return null;}

任何一个点,只要还存在没有绘制过的点,就一定能找得到与它最近的点,如果找不到,说明所有的点已经绘制完毕。为了防止查找到重复的点,需要把访问过的点做上记号(即设为false)。我们需要把整张图中每一个像素点位置作好记号,标记哪些点是需要绘制,哪些点是不需要绘制或者是已经绘制过。用一个boolean[][]型数组保存。还需要记录最后一次访问的点,以便继续下一次的绘制。根据最后一次访问的点继续寻找最近点,反复迭代,把所有的点绘制完成后,整张图就出来了。程序开始时,将最后一次访问的点初始化为左上角的点。

private Point mLastPoint = new Point(0, 0);
//获取下一个需要绘制的点private Point getNextPoint() {mLastPoint = getNearestPoint(mLastPoint);return mLastPoint;}

接下来是将点绘制到Bitmap上,在将Bitmap绘制到SurfaceView的Canvas上。这里这么做的目的是,SurfaceView内部使用了双缓存,直接绘制到SurfaceView的Canvas可能会闪屏。

/*** //绘制* return :false 表示绘制完成,true表示还需要继续绘制*/private boolean draw() {mPaint.setStyle(Paint.Style.STROKE);mPaint.setColor(Color.BLACK);//获取count个点后,一次性绘制到bitmap在把bitmap绘制到SurfaceViewint count = 100;Point p = null;while (count-- > 0) {p = getNextPoint();if (p == null) {//如果p为空,说明所有的点已经绘制完成return false;}mTmpCanvas.drawPoint(p.x, p.y + offsetY, mPaint);}//将bitmap绘制到SurfaceView中Canvas canvas = mSurfaceHolder.lockCanvas();canvas.drawBitmap(mTmpBm, 0, 0, mPaint);if (p != null)canvas.drawBitmap(mPaintBm, p.x, p.y - mPaintBm.getHeight() + offsetY, mPaint);mSurfaceHolder.unlockCanvasAndPost(canvas);return true;}

基本上绘制算完成了,附上完整的代码:


package com.hc.myoutline;import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;/*** Package com.hc.myoutline* Created by HuaChao on 2016/5/27.*/
public class DrawOutlineView extends SurfaceView implements SurfaceHolder.Callback {private SurfaceHolder mSurfaceHolder;private Bitmap mTmpBm;private Canvas mTmpCanvas;private int mWidth;private int mHeight;private Paint mPaint;private int mSrcBmWidth;private int mSrcBmHeight;private boolean[][] mArray;private int offsetY = 100;private Bitmap mPaintBm;private Point mLastPoint = new Point(0, 0);public DrawOutlineView(Context context) {super(context);init();}public DrawOutlineView(Context context, AttributeSet attrs) {super(context, attrs);init();}private void init() {mSurfaceHolder = getHolder();mSurfaceHolder.addCallback(this);mPaint = new Paint();mPaint.setColor(Color.BLACK);}//设置画笔图片public void setPaintBm(Bitmap paintBm) {mPaintBm = paintBm;}//获取离指定点最近的一个未绘制过的点private Point getNearestPoint(Point p) {if (p == null) return null;//以点p为中心,向外扩大搜索范围,每次搜索的是与p点相距add的正方形for (int add = 1; add < mSrcBmWidth && add < mSrcBmHeight; add++) {//int beginX = (p.x - add) >= 0 ? (p.x - add) : 0;int endX = (p.x + add) < mSrcBmWidth ? (p.x + add) : mSrcBmWidth - 1;int beginY = (p.y - add) >= 0 ? (p.y - add) : 0;int endY = (p.y + add) < mSrcBmHeight ? (p.y + add) : mSrcBmHeight - 1;//搜索正方形的上下边for (int x = beginX; x <= endX; x++) {if (mArray[x][beginY]) {mArray[x][beginY] = false;return new Point(x, beginY);}if (mArray[x][endY]) {mArray[x][endY] = false;return new Point(x, endY);}}//搜索正方形的左右边for (int y = beginY + 1; y <= endY - 1; y++) {if (mArray[beginX][y]) {mArray[beginX][beginY] = false;return new Point(beginX, beginY);}if (mArray[endX][y]) {mArray[endX][y] = false;return new Point(endX, y);}}}return null;}//获取下一个需要绘制的点private Point getNextPoint() {mLastPoint = getNearestPoint(mLastPoint);return mLastPoint;}/*** //绘制* return :false 表示绘制完成,true表示还需要继续绘制*/private boolean draw() {mPaint.setStyle(Paint.Style.STROKE);mPaint.setColor(Color.BLACK);//获取count个点后,一次性绘制到bitmap在把bitmap绘制到SurfaceViewint count = 100;Point p = null;while (count-- > 0) {p = getNextPoint();if (p == null) {//如果p为空,说明所有的点已经绘制完成return false;}mTmpCanvas.drawPoint(p.x, p.y + offsetY, mPaint);}//将bitmap绘制到SurfaceView中Canvas canvas = mSurfaceHolder.lockCanvas();canvas.drawBitmap(mTmpBm, 0, 0, mPaint);if (p != null)canvas.drawBitmap(mPaintBm, p.x, p.y - mPaintBm.getHeight() + offsetY, mPaint);mSurfaceHolder.unlockCanvasAndPost(canvas);return true;}//重画public void reDraw(boolean[][] array) {if (isDrawing) return;mTmpBm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);mTmpCanvas = new Canvas(mTmpBm);mPaint.setColor(Color.WHITE);mPaint.setStyle(Paint.Style.FILL);mTmpCanvas.drawRect(0, 0, mWidth, mHeight, mPaint);mLastPoint = new Point(0, 0);beginDraw(array);}private boolean isDrawing = false;public void beginDraw(boolean[][] array) {if (isDrawing) return;this.mArray = array;mSrcBmWidth = array.length;mSrcBmHeight = array[0].length;new Thread() {@Overridepublic void run() {while (true) {isDrawing = true;boolean rs = draw();if (!rs) break;try {sleep(20);} catch (InterruptedException e) {e.printStackTrace();}}isDrawing = false;}}.start();}@Overridepublic void surfaceCreated(SurfaceHolder holder) {}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {this.mWidth = width;this.mHeight = height;mTmpBm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);mTmpCanvas = new Canvas(mTmpBm);mPaint.setColor(Color.WHITE);mPaint.setStyle(Paint.Style.FILL);mTmpCanvas.drawRect(0, 0, mWidth, mHeight, mPaint);Canvas canvas = holder.lockCanvas();canvas.drawBitmap(mTmpBm, 0, 0, mPaint);holder.unlockCanvasAndPost(canvas);mPaint.setStyle(Paint.Style.STROKE);}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {}
}

接下来是在MainActivity里面传入参数过来,附上MainActivity的代码:

package com.hc.myoutline;import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;public class MainActivity extends AppCompatActivity {private DrawOutlineView drawOutlineView;private Bitmap sobelBm;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//将Bitmap压缩处理,防止OOMBitmap bm = CommenUtils.getRatioBitmap(this, R.drawable.test, 100, 100);//返回的是处理过的BitmapsobelBm = SobelUtils.Sobel(bm);drawOutlineView = (DrawOutlineView) findViewById(R.id.outline);Bitmap paintBm = CommenUtils.getRatioBitmap(this, R.drawable.paint, 10, 20);drawOutlineView.setPaintBm(paintBm);}//根据Bitmap信息,获取每个位置的像素点是否需要绘制//使用boolean数组而不是int[][]主要是考虑到内存的消耗private boolean[][] getArray(Bitmap bitmap) {boolean[][] b = new boolean[bitmap.getWidth()][bitmap.getHeight()];for (int i = 0; i < bitmap.getWidth(); i++) {for (int j = 0; j < bitmap.getHeight(); j++) {if (bitmap.getPixel(i, j) != Color.WHITE)b[i][j] = true;elseb[i][j] = false;}}return b;}boolean first = true;//点击时开始绘制@Overridepublic boolean onTouchEvent(MotionEvent event) {if (first) {first = false;drawOutlineView.beginDraw(getArray(sobelBm));} elsedrawOutlineView.reDraw(getArray(sobelBm));return true;}
}

关于轮廓提取的具体实现代码这里不粘出,可以去GitHub查看:GraphicLib 或者是下载我的源代码查看。所有内容已经结束,赶紧下载源码运行一下你的照片去秀一下你的逼格吧!

最后的最后:请注意,源码中,轮廓的提取是运行在主线程中,如果图片比较复杂,可能会导致ANR,建议另开线程处理。别问为什么我不去改,一个字:懒!另外,接下来一篇文章中,我将介绍提高运算速度相关内容,提升轮廓提取速度,敬请期待~

源码地址:Android自动手绘,圆你儿时画家梦! 。

Android自动手绘,圆你儿时画家梦!相关推荐

  1. Android自动手绘,Android自动手绘,圆你儿时画家梦!

    从小就喜欢到处乱画,家里一米以下墙上就没有一块干净的地方(那是老房子啦)(⊙﹏⊙)b.好了,废话不多说,进入主题.今天主要跟大家分享一下如何将一张图片转成手绘效果,并模拟画家动态绘制.先把最终效果图亮 ...

  2. Android自动手绘,Android应用开发之Android 实现手绘功能教程

    本文将带你了解Android应用开发Android 实现手绘功能教程,希望本文对大家学Android有所帮助. 布局文件如下. Activity代码如下,其中线的颜色,宽度等属性都可以修改. pack ...

  3. Android自动手绘,Android实现手绘功能

    本文实例为大家分享了android实现手绘功能的具体代码,供大家参考,具体内容如下 布局文件如下 xmlns:app="http://schemas.android.com/apk/res- ...

  4. 基于Android的儿童绘本阅读与收听APP的设计与实现【附项目源码+论文说明】

    基于Android的儿童绘本阅读与收听APP的设计与实现演示 摘要 随着人们生活水平的不断提高,儿童学前教育日益受到重视.绘本作为一种历史古老.效果优秀的早教手段,利用图画和简短的文字构筑出一个跌宕起 ...

  5. 自定义圆形倒计时Android,Android自定义View倒计时圆

    本文实例为大家分享了Android自定义View倒计时圆的具体代码,供大家参考,具体内容如下 创建attr 创建DisplayUtil 类 import android.content.Context ...

  6. android xml画圆,Android自定义View画圆功能

    本文实例为大家分享了Android自定义View画圆的具体代码,供大家参考,具体内容如下 引入布局 xmlns:tools="http://schemas.android.com/tools ...

  7. android 圆动画效果,Android实现任意绕圆或椭圆旋转的动画——SatelliteAnimator使用介绍...

    话说实习也就快一个月了,虽然没干什么活,但是这几天总算是有一些可以写的东西. 代码中应该还存在很多问题要修改,大神们请赐教,不胜感激. 开始正题. 关于Android实现任意绕圆或椭圆旋转动画,我称之 ...

  8. android百度地图定位自定义图标,Android应用开发之android 百度地图自定义圆,更改默认图标等常用方法...

    本文将带你了解Android应用开发之android 百度地图自定义圆,更改默认图标等常用方法,希望本文对大家学Android有所帮助. 总结了一下百度地图常用的方法(前提是集成百度地图环境成功): ...

  9. android百度地图画圆,Android应用开发之android 百度地图自定义圆,更改默认图标等常用方法...

    本文将带你了解Android应用开发android 百度地图自定义圆,更改默认图标等常用方法,希望本文对大家学Android有所帮助. 总结了一下百度地图常用的方法(前提是集成百度地图环境成功): 1 ...

最新文章

  1. 怎么设置java的精度值_Java:如何为double值设置Precision?
  2. boost::hana::string_c用法的测试程序
  3. [LintCode笔记了解一下]64.合并排序数组
  4. 检测和删除多余无用的css
  5. SpringBoot集成Flowable_Jsite已办任务菜单报500
  6. VS2008jQuery智能提示
  7. slice,Array.prototype.slice,Array.protyotype.slice.call
  8. C语言的静态数组初始化
  9. mongodb 的安装使用步骤
  10. SLAM学习------Sophus模板类的安装和使用
  11. 合并报表软件系统_报表难题统统扫除!
  12. 服务器2012分辨率不能修改,F1 2012分辨率修改办法
  13. Axure|【医药、医疗】药企员工内部培训平台原型
  14. 计算机信息安全专业代码0839,(0839)网络空间安全一级学科硕士研究生培养方案...
  15. 小武与剑指offer的恩怨情仇
  16. 新赛季上分神器 vivo X70 Pro+首批适配《王者荣耀》120Hz极高帧率
  17. 组合学:使用10个数字与52个字母生成1477万个不重复的4位串码V4衍生版本
  18. ubuntu安装水星MW150US无线网卡8188eu驱动
  19. linux下下载openwrt源码,OpenWrt下载编译
  20. flac转换成wav的方法,flac转wav步骤

热门文章

  1. 【转帖】药物设计软件【schrodinger】 all manual
  2. Schrodinger 功能模块简介
  3. 零件三维缺陷检测相关基础知识
  4. win10系统显示语言切换
  5. “超男”改“快乐男”,不“超级”也能“快乐”?
  6. (转)数据挖掘岗求职经验:腾讯+百度+华为(均拿到sp offer)
  7. SQL1 插入记录(一)
  8. Django1.11.4 在前端显示图片
  9. 【2023最新】MySQL安装配置教程(5.7+8.0)
  10. 计算机组成原理 | 浮点数和定点数(上):怎么用有限的Bit表示尽可能多的信息?...