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

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

效果图如下:

00d6cf38f142

最终效果图

![GIF截图]](http://upload-images.jianshu.io/upload_images/2154124-487f5f5017b0f844.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

心动有木有!

原理

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

代码实现

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

00d6cf38f142

寻找附近点

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

//获取离指定点最近的一个未绘制过的点

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,建议另开线程处理。别问为什么我不去改,一个字:懒!另外,接下来一篇文章中,我将介绍提高运算速度相关内容,提升轮廓提取速度,敬请期待~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
package com.example.linchartdemo.view; import java.util.List; import android.R.color; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.util.AttributeSet; import android.util.Log; import android.view.View; public class ZXView extends View { private List<Integer> xlist;// X坐标标签 private List<Integer> ylist;// Y坐标标签 private List<Integer> params;// 参数集合 private Paint paint; private Paint paintLines; private Paint paintArc; private Paint paintText; private int textsize = 20; private float Xoffset = 0;// X轴偏移或叫间隔 private float Yoffset = 0;// Y轴偏移或叫间隔 public float XSpac = 50; public float Xspac = 50; public float YSpac = 50; public float rightXspac = 50; public float rightYspac = 50; public float circleRadius = 10; private float lastX = -1; private float lastY = -1; public ZXView(Context context, List<Integer> xlists, List<Integer> ylists, List<Integer> paramss) { super(context); xlist = xlists; ylist = ylists; params = paramss; initWidget(); } public ZXView(Context context, AttributeSet attrs) { super(context, attrs); initWidget(); } public ZXView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initWidget(); } public void initViewData(List<Integer> xlists, List<Integer> ylists, List<Integer> paramss) { xlist = xlists; ylist = ylists; params = paramss; } @Override protected void onDraw(Canvas canvas) { drawXY(canvas); drawLine(canvas); super.onDraw(canvas); } public void updataUI() { invalidate(); } /** * 画折线 * * @param canvas */ private void drawLine(Canvas canvas) { Xoffset = ((getWidth()) - 20) / (xlist.size()); Yoffset = (getHeight()) / (ylist.size()); Log.i("texts", "X=" + Xoffset + "Y=" + Yoffset); float rulerOffset = 0; if (xlist.size() < 2) throw new IllegalArgumentException("the params argument is <2"); else rulerOffset = (xlist.get(1) - xlist.get(0)); if (params == null && params.size() <= 0) throw new IllegalArgumentException("the params is null or 0"); // Paint p = new Paint(); // p.setAntiAlias(true); // p.setTextSize(25); // p.setColor(Color.WHITE); // canvas.drawText("KW", 10 + Xoffset, 20, p); for (int i = 0; i < params.size(); i++) { int param = params.get(i); float histigramHight = param * (Yoffset / rulerOffset); float currentX = (Xoffset * i + Xspac + 5); float currentY = (getHeight() - YSpac - histigramHight); canvas.drawCircle(currentX, currentY, circleRadius, paintArc); if (lastX != -1) { canvas.drawLine(lastX, lastY, currentX, currentY, paintLines); } lastX = currentX; lastY = currentY; } // 重置lastX跟Y lastX = -1; lastY = -1; } /** * 画坐标轴 * * @param canvas */ private void drawXY(Canvas canvas) { canvas.drawLine(XSpac, 0, XSpac, getHeight() - 5 - XSpac, paint);// x canvas.drawLine(XSpac, getHeight() - XSpac, getWidth(), getHeight() - XSpac, paint);// y canvas.drawLine(getWidth(), 0, getWidth() - 1, getWidth() - YSpac, paint);// 右边Y float yoffset = getHeight() / ylist.size(); float xoffset = (getWidth() - XSpac) / xlist.size(); // 画字 for (int i = 0; i < ylist.size(); i++) { canvas.drawText(ylist.get(i) + "", 0, getHeight() - yoffset * i - YSpac, paintText); } for (int i = 0; i < xlist.size(); i++) { canvas.drawText(xlist.get(i) + "", XSpac + xoffset * i, getHeight() - YSpac + textsize, paintText); } // for (int i = 0; i < ylist.size(); i++) { // for (int j = 0; j < 50; j++) { // canvas.drawLine(XSpac + j * 303, yoffset * i - YSpac, XSpac + j // * 30 + 20, yoffset * i - YSpac, paint); // } // } } /** * 初始化画笔 */ private void initWidget() { paint = new Paint(); paint.setColor(Color.parseColor("#999999")); paint.setTypeface(Typeface.DEFAULT); paint.setAntiAlias(true); paint.setStrokeWidth(2); paintLines = new Paint(); paintLines.setColor(Color.parseColor("#00B4EA")); paintLines.setTypeface(Typeface.DEFAULT); paintLines.setAntiAlias(true); paintLines.setStrokeWidth(3); paintArc = new Paint(); paintArc.setColor(Color.parseColor("#EBEEEF")); paintArc.setTypeface(Typeface.DEFAULT); paintArc.setAntiAlias(true); paintArc.setStrokeWidth(2); paintText = new Paint(); // paintText.setColor(Color.parseColor("#ffffff")); paintText.setTypeface(Typeface.DEFAULT); paintText.setAntiAlias(true); paintText.setStrokeWidth(2); paintText.setTextSize(textsize); } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值