更新:本文的內容只是一部分,這段時間添加了橡皮擦這個新功能,因而問題連續不斷的來,好比說:若是用本文的內容去作橡皮擦的話,難!(至少我沒解決,不是沒背景圖,就是有背景圖可是更新要在下一下刷橡皮擦的時候才能更新效果),而後有個setbackgroundresource的函數,這個函數就能夠了,可是問題又來了,好比說保存,清屏,可是我都解決了(清屏的話就是從新構造一個圖,當clear的時候就把這張圖賦值給之前的圖片。保存的話我就是把繪下個圖放到一張有背景的canvas上面,至是分辨率的問題本身去解決就好了,保證存下來的跟你用setbackgoundresource繪圖看到的效果一致,須要源碼的請聯繫我) java
本人也是在網上查了不少文章後才作出來的,感受網上的一些塗鴉功能不是很完善,在此就稍微完善了一下。先看下效果圖吧(想作成全屏的話須要弄一張跟你屏幕同樣大小的背景圖,找不到也不要緊,我有改尺寸的代碼,一併獻上)。代碼下載請到http://www.oschina.net/code/snippet_729469_20445 其實塗鴉的難點就是如何能在canvas上進行清屏又能保存,至少目前我碰到的狀況是這樣,這就須要對canvas與bitmap的較爲深刻的理解了,這個你多寫這方面的代碼就好了,網上有許多塗鴉的做品,看看源代碼。 android
塗鴉中關於canvas的學習須要掌握三點吧:1:view 2:onDraw函數 3:onTouchEvent canvas
public class HandWrite extends View { Paint paint = null; private Bitmap originalBitmap = null; Bitmap new1Bitmap = null; private Bitmap new2Bitmap = null; private float clickX = 0,clickY = 0; private float startX = 0,startY = 0; private boolean isMove = true; private boolean isClear = false; int color = Color.WHITE; float strokeWidth = 3.0f; public HandWrite(Context context, AttributeSet attrs) { super(context, attrs); // originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.t); originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a).copy(Bitmap.Config.ARGB_8888, true); new1Bitmap = Bitmap.createBitmap(originalBitmap); } public void clear(){ isClear = true; new2Bitmap = Bitmap.createBitmap(originalBitmap); invalidate(); } public void setstyle(float strokeWidth){ this.strokeWidth = strokeWidth; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawBitmap(HandWriting(new1Bitmap), 0, 0,null); } public Bitmap HandWriting(Bitmap originalBitmap) { Canvas canvas = null; if(isClear){ canvas = new Canvas(new2Bitmap); } else{ canvas = new Canvas(originalBitmap); } paint = new Paint(); paint.setStyle(Style.STROKE); paint.setAntiAlias(true); paint.setColor(color); paint.setStrokeWidth(strokeWidth); if(isMove){ canvas.drawLine(startX, startY, clickX, clickY, paint); } startX = clickX; startY = clickY; if(isClear){ return new2Bitmap; } return originalBitmap; } @Override public boolean onTouchEvent(MotionEvent event) { clickX = event.getX(); clickY = event.getY(); if(event.getAction() == MotionEvent.ACTION_DOWN){ isMove = false; invalidate(); return true; } else if(event.getAction() == MotionEvent.ACTION_MOVE){ isMove = true; invalidate(); return true; } return super.onTouchEvent(event); }這個view的代碼網上有,這裏面必須得實現兩個重要的方法,一個是onDraw一個是onTouchEvent,onDraw是在你每次觸碰屏幕的時候都會觸發,包括你初始化的時候。onTouchEvent就是在你觸碰屏幕後採起的相應操做。其實塗鴉的關鍵就是經過drawLine將瞬間變化的兩點連起來畫成直線,而後畫在canvas上的bitmap上。
在mainActivity中我實現了一個菜單按鈕 數組
public class CanvasDrawActivity extends Activity { private static final String TAG = "CanvasDrawActivity"; /** Called when the activity is first created. */ private int width; private int height; private HandWrite handWrite = null; private Button clear = null; private int whichColor = 0; private int whichStrokeWidth = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.main); handWrite = (HandWrite)findViewById(R.id.handwriteview); } @Override public boolean onCreateOptionsMenu(Menu menu) { // TODO Auto-generated method stub menu.add(0, 1, 1, "清屏"); menu.add(0, 2, 2, "顏色"); menu.add(0, 3, 3, "畫筆"); menu.add(0, 4, 4, "保存"); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { // TODO Auto-generated method stub if(item.getItemId() == 4){ File f = new File(Environment.getExternalStorageDirectory() .getAbsolutePath() + "/aaa.jpg"); try { saveMyBitmap(f, handWrite.new1Bitmap); } catch (IOException e) { e.printStackTrace(); } }else if(item.getItemId() == 1){ handWrite.clear(); }else if(item.getItemId() == 2){ Dialog mDialog = new AlertDialog.Builder(CanvasDrawActivity.this) .setTitle("顏色設置") .setSingleChoiceItems(new String[]{"白色","綠色","紅色"}, whichColor, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub switch(which) { case 0: { handWrite.color = Color.WHITE; whichColor = 0; break; } case 1: { handWrite.color = Color.GREEN; whichColor = 1; break; } case 2: { handWrite.color = Color.RED; whichColor = 2; break; } } } }) .setPositiveButton("肯定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub dialog.dismiss(); } }) .create(); mDialog.show(); }else if(item.getItemId() == 3){ Dialog mDialog = new AlertDialog.Builder(CanvasDrawActivity.this) .setTitle("畫筆設置") .setSingleChoiceItems(new String[]{"細","中","粗"}, whichStrokeWidth, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub switch(which) { case 0: { handWrite.strokeWidth = 3.0f; whichStrokeWidth = 0; break; } case 1: { handWrite.strokeWidth = 6.0f; whichStrokeWidth = 1; break; } case 2: { handWrite.strokeWidth = 9.0f; whichStrokeWidth = 2; break; } } } }) .setPositiveButton("肯定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // TODO Auto-generated method stub dialog.dismiss(); } }) .create(); mDialog.show(); } return super.onOptionsItemSelected(item); } public void saveMyBitmap(File f, Bitmap mBitmap) throws IOException { try { f.createNewFile(); FileOutputStream fOut = null; fOut = new FileOutputStream(f); mBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); fOut.flush(); fOut.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }這些就是經過菜單選項對相應的canvas上面的paint進行設置便可達到效果。
那麼如今就看看塗鴉這個程序上與canvas相關的幾個函數的源碼吧!(初始函數,drawLine,drawBitmap這3個)canvas.java文件位於android2.3.3/frameworks/base/graphics/java/android/graphics app
canvas(Bitmap bitmap) less
// assigned in constructors, freed in finalizer final int mNativeCanvas; private Bitmap mBitmap; // if not null, mGL must be null // Package-scoped for quick access. int mDensity = Bitmap.DENSITY_NONE; public Canvas(Bitmap bitmap) { if (!bitmap.isMutable()) { throw new IllegalStateException( "Immutable bitmap passed to Canvas constructor"); } throwIfRecycled(bitmap); mNativeCanvas = initRaster(bitmap.ni()); mBitmap = bitmap; mDensity = bitmap.mDensity; }這個初始化比較簡單,就主要是一個叫作initRaster(bitmap.ni())這個函數
private static native int initRaster(int nativeBitmapOrZero);這個須要涉及到向底層本地函數傳遞一個int參數,那麼這個bitmap.ni()是什麼呢?查看bitmap.java一樣位於android2.3.3/frameworks/base/graphics/java/android/graphics
/* package */ final int ni() { return mNativeBitmap; } // Note: mNativeBitmap is used by FaceDetector_jni.cpp // Don't change/rename without updating FaceDetector_jni.cpp private final int mNativeBitmap;也就是想下面傳遞的是mNativeBitmap這個值,而這個值會在FaceDetector_jni.cpp中使用。
那麼initRaster怎麼實現的呢,看Canvas.cpp位於android/frameworks/base/core/jni/android/graphics,註冊函數有 ide
{"initRaster","(I)I", (void*) SkCanvasGlue::initRaster}, {"native_drawBitmap","(IIFFIIII)V", (void*) SkCanvasGlue::drawBitmap__BitmapFFPaint}, {"native_drawLine","(IFFFFI)V", (void*) SkCanvasGlue::drawLine__FFFFPaint}, {"native_drawColor","(III)V", (void*) SkCanvasGlue::drawColor__II}, {"native_drawPaint","(II)V", (void*) SkCanvasGlue::drawPaint}, {"drawPoint", "(FFLandroid/graphics/Paint;)V", (void*) SkCanvasGlue::drawPoint}, {"drawPoints", "([FIILandroid/graphics/Paint;)V", (void*) SkCanvasGlue::drawPoints}, {"drawLines", "([FIILandroid/graphics/Paint;)V", (void*) SkCanvasGlue::drawLines}, {"native_drawLine","(IFFFFI)V", (void*) SkCanvasGlue::drawLine__FFFFPaint}, {"native_drawRect","(ILandroid/graphics/RectF;I)V", (void*) SkCanvasGlue::drawRect__RectFPaint}, {"native_drawRect","(IFFFFI)V", (void*) SkCanvasGlue::drawRect__FFFFPaint}, {"native_drawOval","(ILandroid/graphics/RectF;I)V", (void*) SkCanvasGlue::drawOval}, {"native_drawCircle","(IFFFI)V", (void*) SkCanvasGlue::drawCircle}, {"native_drawArc","(ILandroid/graphics/RectF;FFZI)V",其實這些就是對canva的常見操做,包括下面遇到的drawline,drawBitmap,請看: drawBitmap(Bitmap bitmap,float, left,float top, Paint ,paint)
public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { throwIfRecycled(bitmap); native_drawBitmap(mNativeCanvas, bitmap.ni(), left, top, paint != null ? paint.mNativePaint : 0, mDensity, mScreenDensity, bitmap.mDensity); }
drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 函數
public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) { native_drawLine(mNativeCanvas, startX, startY, stopX, stopY, paint.mNativePaint); }他們實際上都是使用的SkCanvasGlue中的對應函數,而這個時候,canvas已經再也不撒過去那個canvas了,它換成了SKCanvas。
好比說drawBitmap,這個是在註冊函數中表示的對應的函數 學習
static void drawBitmap__BitmapFFPaint(JNIEnv* env, jobject jcanvas, SkCanvas* canvas, SkBitmap* bitmap, jfloat left, jfloat top, SkPaint* paint, jint canvasDensity, jint screenDensity, jint bitmapDensity) { SkScalar left_ = SkFloatToScalar(left); SkScalar top_ = SkFloatToScalar(top); if (canvasDensity == bitmapDensity || canvasDensity == 0 || bitmapDensity == 0) { if (screenDensity != 0 && screenDensity != bitmapDensity) { SkPaint filteredPaint; if (paint) { filteredPaint = *paint; } filteredPaint.setFilterBitmap(true); canvas->drawBitmap(*bitmap, left_, top_, &filteredPaint); } else { canvas->drawBitmap(*bitmap, left_, top_, paint); } } else { canvas->save(); SkScalar scale = SkFloatToScalar(canvasDensity / (float)bitmapDensity); canvas->translate(left_, top_); canvas->scale(scale, scale); SkPaint filteredPaint; if (paint) { filteredPaint = *paint; } filteredPaint.setFilterBitmap(true); canvas->drawBitmap(*bitmap, 0, 0, &filteredPaint); canvas->restore(); } }它最終調用的就是SKCanvas中的
canvas->drawBitmap(*bitmap, left_, top_, &filteredPaint);相似的,其餘的一些canvas的操做都是調用的SKCanvas的對應的方法。(skcanvas與skia的關係請你們網上查詢)
那麼SKCnvas的源碼究竟存放在哪裏呢?android/external/skia/src/core,由於全部的方法的實現都是相似的,這裏我就單獨選擇一個簡單的drawLine吧 ui
void SkCanvas::drawLine(SkScalar x0, SkScalar y0, SkScalar x1, SkScalar y1, const SkPaint& paint) { SkPoint pts[2]; pts[0].set(x0, y0); pts[1].set(x1, y1); this->drawPoints(kLines_PointMode, 2, pts, paint); }就是將亮點的座標值存放在一個SKPoint數組中而後做爲參數傳遞給drawPoints函數,繼續找drawPoints
void SkCanvas::drawPoints(PointMode mode, size_t count, const SkPoint pts[], const SkPaint& paint) { if ((long)count <= 0) { return; } SkASSERT(pts != NULL); ITER_BEGIN(paint, SkDrawFilter::kPoint_Type) while (iter.next()) { iter.fDevice->drawPoints(iter, mode, count, pts, paint); } ITER_END }首先確保這個數組的非空的存在性,而後用一個迭代器去不斷的drawPoint,由於fDevice十一個SKDevice*類型。而後查看DKDevice.cpp文件,一樣位於android/external/skia/src/core
void SkDevice::drawPoints(const SkDraw& draw, SkCanvas::PointMode mode, size_t count, const SkPoint pts[], const SkPaint& paint) { draw.drawPoints(mode, count, pts, paint); }調用的是SkDraw中的drawPoints方法
void SkDraw::drawPoints(SkCanvas::PointMode mode, size_t count, const SkPoint pts[], const SkPaint& paint) const { // if we're in lines mode, force count to be even if (SkCanvas::kLines_PointMode == mode) { count &= ~(size_t)1; } if ((long)count <= 0) { return; } SkAutoRestoreBounder arb; if (fBounder) { if (!bounder_points(fBounder, mode, count, pts, paint, *fMatrix)) { return; } // clear the bounder for the rest of this function, so we don't call it // again later if we happen to call ourselves for drawRect, drawPath, // etc. arb.clearBounder(this); } SkASSERT(pts != NULL); SkDEBUGCODE(this->validate();) // nothing to draw if (fClip->isEmpty() || (paint.getAlpha() == 0 && paint.getXfermode() == NULL)) { return; } PtProcRec rec; if (rec.init(mode, paint, fMatrix, fClip)) { SkAutoBlitterChoose blitter(*fBitmap, *fMatrix, paint); SkPoint devPts[MAX_DEV_PTS]; const SkMatrix* matrix = fMatrix; SkBlitter* bltr = blitter.get(); PtProcRec::Proc proc = rec.chooseProc(bltr); // we have to back up subsequent passes if we're in polygon mode const size_t backup = (SkCanvas::kPolygon_PointMode == mode); do { size_t n = count; if (n > MAX_DEV_PTS) { n = MAX_DEV_PTS; } matrix->mapPoints(devPts, pts, n); proc(rec, devPts, n, bltr); pts += n - backup; SkASSERT(count >= n); count -= n; if (count > 0) { count += backup; } } while (count != 0); } else { switch (mode) { case SkCanvas::kPoints_PointMode: { // temporarily mark the paint as filling. SkAutoPaintStyleRestore restore(paint, SkPaint::kFill_Style); SkScalar width = paint.getStrokeWidth(); SkScalar radius = SkScalarHalf(width); if (paint.getStrokeCap() == SkPaint::kRound_Cap) { SkPath path; SkMatrix preMatrix; path.addCircle(0, 0, radius); for (size_t i = 0; i < count; i++) { preMatrix.setTranslate(pts[i].fX, pts[i].fY); // pass true for the last point, since we can modify // then path then this->drawPath(path, paint, &preMatrix, (count-1) == i); } } else { SkRect r; for (size_t i = 0; i < count; i++) { r.fLeft = pts[i].fX - radius; r.fTop = pts[i].fY - radius; r.fRight = r.fLeft + width; r.fBottom = r.fTop + width; this->drawRect(r, paint); } } break; } case SkCanvas::kLines_PointMode: case SkCanvas::kPolygon_PointMode: { count -= 1; SkPath path; SkPaint p(paint); p.setStyle(SkPaint::kStroke_Style); size_t inc = (SkCanvas::kLines_PointMode == mode) ? 2 : 1; for (size_t i = 0; i < count; i += inc) { path.moveTo(pts[i]); path.lineTo(pts[i+1]); this->drawPath(path, p, NULL, true); path.rewind(); } break; } } } }我起初看的時候瞬間想砸電腦,這得耽誤我晚上的dota時間啊,可是細看,你會注意到那個case語句,由於咱們分析的是drawLine函數,而drawLine函數傳入的是kLines_PointMode,那麼咱們就分析這個語句,其實就這一個for循環
for (size_t i = 0; i < count; i += inc) { path.moveTo(pts[i]); path.lineTo(pts[i+1]); this->drawPath(path, p, NULL, true); path.rewind(); }它再一次不甘寂寞地調用了SKpath的兩個函數以及自身的drawPath函數
void SkPath::moveTo(SkScalar x, SkScalar y) { SkDEBUGCODE(this->validate();) int vc = fVerbs.count(); SkPoint* pt; if (vc > 0 && fVerbs[vc - 1] == kMove_Verb) { pt = &fPts[fPts.count() - 1]; } else { pt = fPts.append(); *fVerbs.append() = kMove_Verb; } pt->set(x, y); fBoundsIsDirty = true; } void SkPath::lineTo(SkScalar x, SkScalar y) { SkDEBUGCODE(this->validate();) if (fVerbs.count() == 0) { fPts.append()->set(0, 0); *fVerbs.append() = kMove_Verb; } fPts.append()->set(x, y); *fVerbs.append() = kLine_Verb; fBoundsIsDirty = true; } void SkDraw::drawPath(const SkPath& origSrcPath, const SkPaint& paint, const SkMatrix* prePathMatrix, bool pathIsMutable) const { SkDEBUGCODE(this->validate();) // nothing to draw if (fClip->isEmpty() || (paint.getAlpha() == 0 && paint.getXfermode() == NULL)) { return; } SkPath* pathPtr = (SkPath*)&origSrcPath; bool doFill = true; SkPath tmpPath; SkMatrix tmpMatrix; const SkMatrix* matrix = fMatrix; if (prePathMatrix) { if (paint.getPathEffect() || paint.getStyle() != SkPaint::kFill_Style || paint.getRasterizer()) { SkPath* result = pathPtr; if (!pathIsMutable) { result = &tmpPath; pathIsMutable = true; } pathPtr->transform(*prePathMatrix, result); pathPtr = result; } else { if (!tmpMatrix.setConcat(*matrix, *prePathMatrix)) { // overflow return; } matrix = &tmpMatrix; } } // at this point we're done with prePathMatrix SkDEBUGCODE(prePathMatrix = (const SkMatrix*)0x50FF8001;) /* If the device thickness < 1.0, then make it a hairline, and modulate alpha if the thickness is even smaller (e.g. thickness == 0.5 should modulate the alpha by 1/2) */ SkAutoPaintRestoreColorStrokeWidth aprc(paint); // can we approximate a thin (but not hairline) stroke with an alpha-modulated // hairline? Only if the matrix scales evenly in X and Y, and the device-width is // less than a pixel if (paint.getStyle() == SkPaint::kStroke_Style && paint.getXfermode() == NULL) { SkScalar width = paint.getStrokeWidth(); if (width > 0 && map_radius(*matrix, &width)) { int scale = (int)SkScalarMul(width, 256); int alpha = paint.getAlpha() * scale >> 8; // pretend to be a hairline, with a modulated alpha ((SkPaint*)&paint)->setAlpha(alpha); ((SkPaint*)&paint)->setStrokeWidth(0); } } if (paint.getPathEffect() || paint.getStyle() != SkPaint::kFill_Style) { doFill = paint.getFillPath(*pathPtr, &tmpPath); pathPtr = &tmpPath; } if (paint.getRasterizer()) { SkMask mask; if (paint.getRasterizer()->rasterize(*pathPtr, *matrix, &fClip->getBounds(), paint.getMaskFilter(), &mask, SkMask::kComputeBoundsAndRenderImage_CreateMode)) { this->drawDevMask(mask, paint); SkMask::FreeImage(mask.fImage); } return; } // avoid possibly allocating a new path in transform if we can SkPath* devPathPtr = pathIsMutable ? pathPtr : &tmpPath; // transform the path into device space pathPtr->transform(*matrix, devPathPtr); SkAutoBlitterChoose blitter(*fBitmap, *fMatrix, paint); // how does filterPath() know to fill or hairline the path??? <mrr> if (paint.getMaskFilter() && paint.getMaskFilter()->filterPath(*devPathPtr, *fMatrix, *fClip, fBounder, blitter.get())) { return; // filterPath() called the blitter, so we're done } if (fBounder && !fBounder->doPath(*devPathPtr, paint, doFill)) { return; } if (doFill) { if (paint.isAntiAlias()) { SkScan::AntiFillPath(*devPathPtr, *fClip, blitter.get()); } else { SkScan::FillPath(*devPathPtr, *fClip, blitter.get()); } } else { // hairline if (paint.isAntiAlias()) { SkScan::AntiHairPath(*devPathPtr, fClip, blitter.get()); } else { SkScan::HairPath(*devPathPtr, fClip, blitter.get()); } } }再調用skmatrix,skmask,skscan。。。。圖形化的東西瞭解太少了,鄙人就作拋磚引玉的做用吧,分析到此結束,但願大俠補充了。
最後,在背景圖上須要改大小的,這裏有代碼
public class test { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub BufferedImage image; try { image = ImageIO.read(new File("D:\\t.JPG")); resize(image, 300, 300); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } private static void resize(BufferedImage source, int targetW, int targetH) throws IOException { // TODO Auto-generated method stub int type = source.getType(); BufferedImage target = null; double sx = (double) targetW / source.getWidth(); double sy = (double) targetH / source.getHeight(); // 這裏想實如今targetW,targetH範圍內實現等比縮放。若是不須要等比縮放 // 則將下面的if else語句註釋便可 // if (sx > sy) // { // sx = sy; // targetW = (int) (sx * source.getWidth()); // } // else // { // sy = sx; // targetH = (int) (sy * source.getHeight()); // } // if (type == BufferedImage.TYPE_CUSTOM) // { // handmade ColorModel cm = source.getColorModel(); WritableRaster raster = cm.createCompatibleWritableRaster(targetW, targetH); boolean alphaPremultiplied = cm.isAlphaPremultiplied(); target = new BufferedImage(cm, raster, alphaPremultiplied, null); // } // else // { // //固定寬高,寬高必定要比原圖片大 // //target = new BufferedImage(targetW, targetH, type); // target = new BufferedImage(800, 600, type); // } Graphics2D g = target.createGraphics(); //寫入背景 g.drawImage(ImageIO.read(new File("D:\\t.jpg")), 0, 0, null); // smoother than exlax: g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.drawRenderedImage(source, AffineTransform.getScaleInstance(sx, sy)); g.dispose(); ImageIO.write(target, "png", new FileOutputStream("D:\\a.JPG")); }這個java工程就是把D盤的t.jpg圖片改爲300*300的a.jpg圖片,這個就得根據你屏幕的大小了。