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繪制到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自動手繪,圓你兒時畫家夢! - huachao1001的專欄 - 博客頻道 - CSDN.NET
總結
以上是生活随笔為你收集整理的Android手绘效果实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构之最小生成树
- 下一篇: HttpClient实现客户端与服务器的