您好,登錄后才能下訂單哦!
這篇文章主要介紹了Android如何實(shí)現(xiàn)自定義view之畫圖板,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。
看效果: 中間一個(gè)畫圖板 上方小控件用來顯示實(shí)時(shí)畫出的圖形 下方小控件用來做一些畫圖的控制 2個(gè)小控件都能移動
順帶還有一個(gè)刮刮卡效果,只需要改一個(gè)參數(shù):
自定義view首先要自定義屬性:
在values下面創(chuàng)建attrs.xml:
<!--畫圖板--> <declare-styleable name="DrawImg"> <attr name="PaintColor" /> //畫筆顏色 <attr name="PaintWidth" /> // 畫筆寬度 <attr name="CanvasImg" /> //畫板圖片 </declare-styleable> <!--指定單位--> <attr name="PaintColor" format="color" /> <attr name="PaintWidth" format="dimension" /> <attr name="CanvasImg" format="reference" />
對于下面3行指定單位的代碼可以放出來,可以讓多個(gè)自定義view 都能使用。
接下來新建自定義view類繼承view,重寫前3個(gè)構(gòu)造方法
紅線標(biāo)注是android studio 3.0.0對于參數(shù)提示的新特性
通過this 讓前2個(gè)構(gòu)造方法都實(shí)現(xiàn)3個(gè)參數(shù)的構(gòu)造方法。
簡單說一下構(gòu)造方法。一個(gè)參數(shù)的構(gòu)造方法是在代碼中 new 時(shí)用到,2個(gè)參數(shù)的構(gòu)造方法在布局xml中用到,3個(gè)參數(shù)的基本就是自定義view類中使用,大概就是這樣。
接下來從attrs.xml中通過TypedArray取出自定義屬性:
//從attrs文件中取出各個(gè)屬性 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DrawImg, defStyleAttr, 0); for (int i = 0; i < a.getIndexCount(); i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.DrawImg_PaintWidth: //畫筆寬度 paintWidth = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, -1, getResources().getDisplayMetrics())); break; case R.styleable.DrawImg_PaintColor: //畫筆顏色 paintColor = a.getColor(attr, Color.GREEN); break; case R.styleable.DrawImg_CanvasImg: //畫板圖片 hasCanvasImg = a.getResourceId(attr, -1); break; } } //設(shè)置默認(rèn)畫筆寬度 if (paintWidth == -1) { paintWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics()); } //取出bitmap if (hasCanvasImg != -1) { bitmap = BitmapFactory.decodeResource(getResources(), hasCanvasImg); } //onMeasure可能走多次,onDraw創(chuàng)建對象更不好 所以把畫筆路徑new在這里 path = new Path();
需要默認(rèn)值的設(shè)置默認(rèn)值,以免布局中沒有用到自定義屬性導(dǎo)致報(bào)錯(cuò)。
重寫自定義view關(guān)鍵方法onMeasure(),onDraw()。onMeasure()用來指定這個(gè)自定義view 的大小,onDraw()用來進(jìn)行實(shí)時(shí)繪圖
最重要的3個(gè)東西:畫布Canvas,畫筆Paint,路徑Path
代碼略長但是注釋很全,把需要注意的提出來
在newPaint()方法中,paint有一個(gè)setXfermode()方法,這個(gè)表示圖形混合方式,有18種 ~(比下圖多了ADD和OVERLAY)~。給張圖看一下。這里我們用到2種 SRC_IN和 DST_OUT。
SRC_IN:取兩層交集部分,顯示上層
DST_OUT:取兩層非交集部分,顯示下層
說實(shí)話這么說也很難懂,還是要自己動手試一試,不過這里只要知道:
使用SRC_IN就會有一個(gè)畫圖板的效果
使用DST_OUT就會有一個(gè)刮刮卡的效果
/** * onMeasure常見方法 * 1) getChildCount():獲取子View的數(shù)量; * 2) getChildAt(i):獲取第i個(gè)子控件; * 3) subView.getLayoutParams().width/height:設(shè)置或獲取子控件的寬或高; * 4) measureChild(child, widthMeasureSpec, heightMeasureSpec):測量子View的寬高; * 5) child.getMeasuredHeight/width():執(zhí)行完measureChild()方法后就可以通過這種方式獲取子View的寬高值; * 6) getPaddingLeft/Right/Top/Bottom():獲取控件的四周內(nèi)邊距; * 7) setMeasuredDimension(width, height):重新設(shè)置控件的寬高。如果寫了這句代碼,就需要?jiǎng)h除 * “super. onMeasure(widthMeasureSpec, heightMeasureSpec);”這行代碼。 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { /** * getMode獲取測量模式(下面3種) 和 getSize獲取測量值 * * EXACTLY:當(dāng)寬高值設(shè)置為具體值時(shí)使用,如100dp、match_parent等,此時(shí)取出的size是精確的尺寸; * AT_MOST:當(dāng)寬高值設(shè)置為wrap_content時(shí)使用,此時(shí)取出的size是控件最大可獲得的空間; * UNSPECIFIED:當(dāng)沒有指定寬高值時(shí)使用(很少見)。 * * */ //測量模式_寬 int widthMode = MeasureSpec.getMode(widthMeasureSpec); //測量模式_高 int heightMode = MeasureSpec.getMode(heightMeasureSpec); //寬度 int widthSize = MeasureSpec.getSize(widthMeasureSpec); //高度 int heightSize = MeasureSpec.getSize(heightMeasureSpec); //設(shè)置view寬度 //如果布局中給出了準(zhǔn)確的寬度,直接使用寬度,否則設(shè)置圖片寬度為view寬度 if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else { if (hasCanvasImg != -1) { //如果設(shè)置了圖片,使用圖片寬 width = bitmap.getWidth(); } else { //沒有設(shè)置圖片并且也沒給準(zhǔn)確的view寬高 設(shè)置一個(gè)寬默認(rèn)值 width = 500; } } //設(shè)置view高度同上 if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else { if (hasCanvasImg != -1) { height = bitmap.getHeight(); } else { height = 500; } } //重新設(shè)置view的寬高 setMeasuredDimension(width, height); //設(shè)置畫布以及畫筆 newPaint(); } private void newPaint() { //根據(jù)參數(shù)創(chuàng)建一個(gè)新的bitmap 最后一個(gè)參數(shù)為為儲存形式 newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); //保存bitmap中所有像素點(diǎn)的數(shù)組 bmPixels = new int[newBitmap.getWidth() * newBitmap.getHeight()]; //new帶參的Canvas,其中的bitmap參數(shù) 必須通過createBitmap得到; //否則會報(bào)錯(cuò):IllegalStateException : Immutable bitmap passed to Canvas constructor canvas = new Canvas(newBitmap); if (hasCanvasImg == -1) { //如果沒有設(shè)置圖片,則默認(rèn)用灰色覆蓋 canvas.drawColor(Color.GRAY); } else { //把設(shè)置的圖片縮放到view大小 bitmap = zoomBitmap(this.bitmap, width, height); canvas.drawBitmap(bitmap, 0, 0, null); } // 準(zhǔn)備繪制刮卡線條的畫筆 paint = new Paint(); paint.setColor(paintColor); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(paintWidth); //設(shè)置是否使用抗鋸齒功能,抗鋸齒功能會消耗較大資源,繪制圖形的速度會減慢 paint.setAntiAlias(true); //設(shè)置是否使用圖像抖動處理,會使圖像顏色更加平滑飽滿,更加清晰 paint.setDither(true); //當(dāng)設(shè)置畫筆樣式為STROKE或FILL_OR_STROKE時(shí),設(shè)置筆刷的圖形樣式 paint.setStrokeCap(Paint.Cap.ROUND); //設(shè)置繪制時(shí)各圖形的結(jié)合方式 paint.setStrokeJoin(Paint.Join.ROUND); //設(shè)置圖形重疊時(shí)的處理方式 /** * SRC_IN:取兩層繪制交集。顯示上層 */ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); } //這個(gè)onDraw方法只有一句代碼,意思是在手指移動的同時(shí)把畫板圖片繪制出來 @Override protected void onDraw(Canvas canvas) { canvas.drawBitmap(newBitmap, 0, 0, null); super.onDraw(canvas); } //將指定圖片縮放到指定寬高,返回新的圖片Bitmap對象 public static Bitmap zoomBitmap(Bitmap bm, int newWidth, int newHeight) { // 獲得圖片的寬高 int width = bm.getWidth(); int height = bm.getHeight(); // 計(jì)算縮放比例 float scaleWidth = ((float) newWidth) / width; float scaleHeight = ((float) newHeight) / height; // 取得想要縮放的matrix參數(shù) Matrix matrix = new Matrix(); matrix.postScale(scaleWidth, scaleHeight); // 得到新的圖片 return Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true); }
這是一堆對于這個(gè)view來說比較復(fù)雜的代碼,但是功能很簡單,我們做了2件事:
1.通過MeasureSpec.getMode(測量模式),計(jì)算出整個(gè)控件的寬高
2.通過canvas.drawBitmap在畫布上畫出bitmap,同時(shí) new 出畫筆 Paint 給它設(shè)置顏色,粗細(xì)等屬性
注意:
1.onDraw()方法在每次調(diào)用invalidate(),或者視圖變化時(shí)都會重走,所以不能在里面 new 東西.
2.有一個(gè)int[]類型的數(shù)組 bmPixels,這里大概說一下是個(gè)什么意思,具體的解釋在Bitmap類getPixels和createBitmap方法詳解中有說道。
bmPixels: 我們通過bitmap的寬度乘以高度,可以的到一個(gè)int[]類型的數(shù)組,這個(gè)數(shù)組就是組成bitmap的所有像素點(diǎn),某一個(gè)像素點(diǎn)為0的時(shí)候就說明他是沒有顏色,!0就說明是有顏色的。
既然是畫圖,那肯定要監(jiān)聽手指移動,onTouchEvent()方法:
@Override public boolean onTouchEvent(MotionEvent event) { int currX = (int) event.getX(); int currY = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //按下時(shí),設(shè)置線條的起始點(diǎn)準(zhǔn)備繪制 path.moveTo(currX, currY); break; case MotionEvent.ACTION_MOVE: //滑動時(shí),繪制路徑 path.lineTo(currX, currY); break; case MotionEvent.ACTION_UP: } // 繪制線條,請求重繪整個(gè)控件 canvas.drawPath(path, paint); //請求View樹進(jìn)行重繪,即draw()方法,如果視圖的大小發(fā)生了變化,還會調(diào)用layout()方法。 invalidate(); return true; }
這個(gè)就很簡單,手指按下時(shí)記錄位置,path.moveTo給path設(shè)置起始點(diǎn)位置,移動時(shí)通過path.lineTo()方法記錄路徑,同時(shí)使用 canvas.drawPath(path, paint)直接繪制出來,invalidate()通知視圖更新。
寫到這里,在xml布局中使用這個(gè)view,已經(jīng)能畫一畫了
我們的畫筆Paint類,可以指定顏色,粗細(xì),模式,等等,這樣我們就可以寫一些公開的方法,給它動態(tài)的設(shè)置這些屬性,從而讓畫筆更加多樣性。
//設(shè)置畫筆顏色 public void setPaintColor(int color) { //path = new Path(); path.reset(); paint.setColor(color); } //設(shè)置畫筆類型 public void setPaintMode(int style) { //path = new Path(); path.reset(); /** * SRC_IN:取兩層交集部分,顯示上層 * DST_OUT:取兩層非交集部分,顯示下層 */ if (style == 1) { paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); } else { paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); } resetCanvaas(); } //設(shè)置畫布重置 public void resetCanvaas() { //path = new Path(); path.reset(); canvas.drawBitmap(bitmap, 0, 0, null); invalidate(); listener.bitmapChangeListener(bitmap); }
上面代碼 設(shè)置畫筆顏色 ,設(shè)置畫筆類型以及畫布重置為什么都要new Path呢,因?yàn)槿绻恍麻_一個(gè)路徑給畫筆,當(dāng)你設(shè)置了新的顏色,用的還是以前的Path,畫筆就會把以前的Path也重新設(shè)置新顏色,而不是保持原來的顏色。
這樣就會出現(xiàn)一個(gè)問題,每次都在new Path,new一次創(chuàng)建一次,占用一次內(nèi)存,想到一些避免方法,但是本文畫圖不是重點(diǎn),就不在論述。(已改用path.reset())
效果中的右上角,顯示了一個(gè)float類型的數(shù),它是在刮刮卡模式下,已經(jīng)抹掉部分所占bitmap的比例,onMeasure()方法中有一個(gè)int[]類型的數(shù)組 bmPixels ,這個(gè)時(shí)候我們就要利用這個(gè)數(shù)組來得到這個(gè)比例。
在onTouchEvent()方法的case MotionEvent.ACTION_UP加上一些代碼:
@Override public boolean onTouchEvent(MotionEvent event) { int currX = (int) event.getX(); int currY = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //按下時(shí),設(shè)置線條的起始點(diǎn)準(zhǔn)備繪制 path.moveTo(currX, currY); break; case MotionEvent.ACTION_MOVE: //滑動時(shí),繪制路徑 path.lineTo(currX, currY); //通過回調(diào),實(shí)時(shí)把bitmap顯示出去 listener.bitmapChangeListener(newBitmap); break; case MotionEvent.ACTION_UP: //抬起手指時(shí),計(jì)算圖片抹去了多少 int nullPixel = 0; newBitmap.getPixels(bmPixels, 0, width, 0, 0, width, height); for (int i = 0; i < bmPixels.length; i++) { //抹去部分的像素點(diǎn)在數(shù)組中就會表示為0,找出為0的個(gè)數(shù) if (bmPixels[i] == 0) { nullPixel++; } } //計(jì)算抹去部分所占的百分比 listener.showBitmapClear((float) nullPixel / (float) bmPixels.length); break; } // 繪制線條,請求重繪整個(gè)控件 canvas.drawPath(path, paint); //請求View樹進(jìn)行重繪,即draw()方法,如果視圖的大小發(fā)生了變化,還會調(diào)用layout()方法。 invalidate(); return true; }
有一句 newBitmap.getPixels(bmPixels, 0, width, 0, 0, width, height);在getPixels方法詳解中有解釋,它的作用就是把newBitmap 中所有的像素點(diǎn)全部取出來,放到方法中的第一個(gè)參數(shù)bmPixels中。這個(gè)時(shí)候,我們再通過for循環(huán)遍歷bmPixels數(shù)組,等于0的說明是沒有顏色被抹掉的,統(tǒng)計(jì)他們的數(shù)量,計(jì)算他們所占的比例,就能算出抹掉的比例。同理我們也可以改變等于0這個(gè)判斷條件,讓他等于其他顏色,這樣也就可以計(jì)算其他顏色所占比例。
寫個(gè)回調(diào)接口,在代碼中取出來就OK了。
//回調(diào)接口 public interface bitmapListener { //實(shí)時(shí)的把繪制的bitmap顯示在imageview 上 void bitmapChangeListener(Bitmap bitmap); //顯示抹掉比例 void showBitmapClear(float clear); } public void addBitmapListener(bitmapListener bitmapListener) { this.listener = bitmapListener; }
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“Android如何實(shí)現(xiàn)自定義view之畫圖板”這篇文章對大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。