Android手绘效果实现
效果图
原理
大概介绍一下实现原理。首先你得有一张图(废话~),接下来就是把这张图的轮廓提取出来,轮廓提取算法有很多,本人不是搞图像处理的,对图像处理感兴趣的童鞋可以查看相关资料。如果你有好的轮廓提取算法,也可以把源码中的算法替换掉,我们采用的轮廓提取算法是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绘制到SurfaceView
        int 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绘制到SurfaceView
        int 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() {
            @Override
            public void run() {
                while (true) {
                    isDrawing = true;
                    boolean rs = draw();
                    if (!rs) break;
                    try {
                        sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                isDrawing = false;
            }
        }.start();
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }
    @Override
    public 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);
    }
    @Override
    public 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;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //将Bitmap压缩处理,防止OOM
        Bitmap bm = CommenUtils.getRatioBitmap(this, R.drawable.test, 100, 100);
        //返回的是处理过的Bitmap
        sobelBm = 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;
                else
                    b[i][j] = false;
            }
        }
        return b;
    }
    boolean first = true;
   //点击时开始绘制
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (first) {
            first = false;
            drawOutlineView.beginDraw(getArray(sobelBm));
        } else
            drawOutlineView.reDraw(getArray(sobelBm));
        return true;
    }
}关于轮廓提取的具体实现代码这里不粘出,可以去GitHub查看:GraphicLib 或者是下载我的源代码查看。所有内容已经结束,赶紧下载源码运行一下你的照片去秀一下你的逼格吧!
最后的最后:请注意,源码中,轮廓的提取是运行在主线程中,如果图片比较复杂,可能会导致ANR,建议另开线程处理。别问为什么我不去改,一个字:懒!另外,接下来一篇文章中,我将介绍提高运算速度相关内容,提升轮廓提取速度,敬请期待~
源码地址:Android自动手绘,圆你儿时画家梦
参考链接
Android手绘效果实现的更多相关文章
- 使用numpy和PIL实现图像的手绘效果
		输入 输出 代码如下 图像的手绘效果的实现 from PIL import Image import numpy as np a = np.array(Image.open("index.j ... 
- Python——图像手绘效果
		1.图像的RGB色彩模式 PIL PIL, Python Image Library PIL库是一个具有强大图像处理能力的第三方库 在命令行下的安装方法: pip install pillow fro ... 
- 永中dcs实现浏览器上面的手绘效果
		永中dcs是一款在线预览各种办公文件的网络产品,我们可以只用一个浏览器就可以实现对word,ppt和excel等文件的在线浏览,在其中有一个在线手绘功能很有特色,让我们来探一探它的实现原理吧. 第一, ... 
- 图像的手绘效果(Python)
		PIL库,Python Image Library PIL库是一个具有强大图像处理能力的第三方库 在命令行下的安装方法:pip install pillow from PIL import Image ... 
- python之实现图像的手绘效果
		https://blog.csdn.net/riba2534/article/details/74152285 原图: b: c: d: 最终图: 
- UWP 手绘视频创作工具技术分享系列 - 有 AI 的手绘视频
		AI(Artificial Intelligence)正在不断的改变着各个行业的形态和人们的生活方式,图像识别.语音识别.自然语言理解等 AI 技术正在自动驾驶.智能机器人.人脸识别.智能助理等领域中 ... 
- Microsoft Tech Summit 2018 课程简述:利用 Windows 新特性开发出更好的手绘视频应用
		概述 Microsoft Tech Summit 2018 微软技术暨生态大会将于10月24日至27日在上海世博中心举行,这也会是国内举办的最后一届 Tech Summit,2019 年开始会以 Mi ... 
- Android基于mAppWidget实现手绘地图(一)--简介
		http://lemberg.github.io/mappwidget/user_guide.html 最近在看一些导游类应用,发现一些景区的导览图使用的完全是自定义地图,也就是手绘地图.这种小范围使 ... 
- UWP 手绘视频创作工具 “来画Pro” 技术分享系列
		开篇先来说一下我和来画的故事,以及写这篇文章的初衷. 今年年初时,我还在北京,在 Face++,做着人脸识别技术的 Windows 和 Android 端,做着人工智能终将实现世间所有美好的梦.这时的 ... 
随机推荐
- php写入txt换行符
			1.问题 写入txt文件想换行,老是直接输出了\r\n. 2.解决 要用双引号对\r\n进行解释,否则php会直接当字符输出. 3.例子 要求:往test.txt文本每一行后面加abc $a=file ... 
- python 函数的调用 和执行 小知识
			1.符号表 执行一个函数会引入一个用于函数的局部变量的新符号表. 更确切地说, 函数中的所有的赋值都是将值存储在局部符号表: 而变量引用首先查找局部符号表, 然后是上层函数的局部符号表, 然后是全局符 ... 
- python trackback的使用心得
			以前在读代码的时候总是要花很久时间去找在哪里调用的某个函数,现在好了,直接使用:trackback.print_stack()就可以打印出调用栈了,在那个地方调用的一目了然... 而如果是异常栈的话就 ... 
- Linux之Shell的算术运算
			在Bash的算术运算中有以下几种方法:名称 语法 范例算术扩展 $((算术式)) r ... 
- (转)JAVA之桥接模式
			原文地址:http://blog.sina.com.cn/s/blog_4080505a0101dzib.html 桥模式:将某个问题抽象的不同形式分别与该问题的具体实现部分相分离,使他们都可以独立变 ... 
- Qt5 任务栏托盘功能实现
			23333 有一阵子没写博客了,研究了挺长时间qt,学到任务栏托盘时简直无语,网上找得到的代码大多是废码,Qt5不支持或者本身就有毛病不能实现却被n多人转来转去的,甚是无语. 简单托盘功能以下在Qt5 ... 
- ndk学习5: ndk中使用c++
			默认情况下ndk不支持标准C++库,异常, rtti等 在ndk文档有关于C++ support的详细介绍 一. 使用C++标准库 介绍: 默认是使用最小额度的C++运行时库, 在Applic ... 
- python——常用功能之文本处理
			前言 在生活.工作中,python一直都是一个好帮手.在python的众多功能中,我觉得文本处理是最常用的.下面是平常使用中的一些总结.环境是python 3.3 0. 基础 在python中,使用s ... 
- JAVA调用动态链接库DLL之JNative学习
			package com.ehfscliax; import java.io.UnsupportedEncodingException;import java.net.URLEncoder;import ... 
- C#对图片文件的压缩、裁剪操作
			在做项目时,对图片的处理,以前都采用在上传时,限制其大小的方式,这样带来诸多不便.毕竟网站运维人员不一定会对图片做处理,经常超出大小限制,即使会使用图片处理软件的,也由于个人水平方面原因,处理效果差强 ... 
