引言
Android進階——自定義View的必修課之關於Canvas繪圖與Android坐標系的總結。Canvas相信大傢都不會陌生,雖然看來很簡單,也知道各種API的用法和作用,但是很多人覺得自定義View很難,很大一部分原因就是對於Canvas不夠熟悉,或許看教材和視頻隻是教你要移動translate、rotate、save、restore等,很少告知你為什麼這樣做,導致你隻能照敲不能靈活應用。所以知其然更要知其所以然,授人以魚不如授人以漁,這篇文章我爭取把Android 2D繪畫的一些相關知識點總結出來。
一、Android系統坐標系
把Android繪畫當成現實中的畫傢作畫,Canvas自然就是畫傢筆下的畫板,而畫傢自然就是Android系統本身,在現實生活中畫傢可以自主決定從哪個點開始起筆,又延伸到哪點,而在機器世界裡都是需要去一系列的邏輯計算的,因而坐標系應運而生,在Android中主要有兩大坐標系:Android坐標系和視圖坐標系。
1、Android坐標系
Android坐標系可以看成是物理存在的坐標系,也可以理解為絕對坐標,以屏幕為參照物,就是以屏幕的左上角是坐標系統原點(0,0),原點向右延伸是X軸正方向,原點向下延伸是Y軸正方向。比如系統的getLocationOnScreen(int[] location)實際上獲取Android坐標系中位置(即該View左上角在Android坐標系中的坐標),還有getRawX()、getRawY()獲取的坐標也是Android坐標系的坐標。
2、視圖坐標系
視圖坐標系是相對坐標系,是以父視圖為參照物,以父視圖的左上角為坐標原點(0,0),原點向右延伸是X軸正方向,原點向下延伸是Y軸正方向,getX()、getY()就是獲取視圖坐標系下的坐標。
3、兩種坐標系在Android的應用
3.1、子View獲取自身尺寸信息
getHeight():獲取View自身高度 getWidth():獲取View自身寬度
3.2、子View獲取自身坐標信息
子View的存在是依附於父View的,所以用的是相對坐標來表示,如下方法可以獲得子View到其父View(ViewGroup)的距離:
getLeft():獲取子View自身左邊到其父View左邊的距離 getTop():獲取子View自身頂邊到其父View頂邊的距離 getRight():獲取子View自身右邊到其父View左邊的距離 getBottom():獲取子View自身底邊到其父View頂邊的距離 getMargingXxxx:獲取子View的邊框距離父ViewGroup邊框的距離即外邊距,Xxxx代表Left、Right、Top、Bootom。 getPaddingXxxx:獲取子View內部的內容的邊框距離子View的邊框的距離即內邊距,Xxxx代表Left、Right、Top、Bootom。
3.3、獲取MotionEvent中對應坐標信息
無論是View還是ViewGroup,Touch事件都會經由onTouchEvent(MotionEvent event)方法來處理,通過MotionEvent實例event可以獲取相關坐標信息。
getX():獲取Touch事件當前觸摸點距離控件左邊的距離,即視圖坐標下對應的X軸的值 getY():獲取Touch事件距離控件頂邊的距離,即視圖坐標系下對應的Y軸的值 getRawX():獲取Touch事件距離整個屏幕左邊距離,即絕對坐標系下對應的X軸的值 getRawY():獲取Touch事件距離整個屏幕頂邊的的距離,即絕對坐標系下對應的Y軸的值
3.4、獲取view在屏幕中的位置
如果在Activity的OnCreate()事件調用這些方法,那麼輸出那些參數全為0,必須要等UI控件都加載完瞭才能獲取到。
– getLocalVisibleRect() :返回一個填充的Rect對象, 所有的View都是以一塊矩形內存空間存在的
getGlobalVisibleRect() :獲取Android坐標系的一個視圖區域, 返回一個填充的Rect對象且該Rect是基於總整個屏幕的
getLocationOnScreen :計算該視圖在Android坐標系中的x,y值,獲取在當前屏幕內的絕對坐標
(這個值是要從屏幕頂端算起,當然包括瞭通知欄和狀態欄的高度)
getLocationInWindow ,計算該視圖在它所在的widnow的坐標x,y值,獲取在整個window的絕對坐標
int[] location = new int[2]; view.getLocationOnScreen(location); int x = location[0]; int y = location[1];
二、Paint
Paint即畫筆,在繪圖過程中起到瞭極其重要的作用,畫筆主要保存瞭顏色, 樣式等繪制信息,指定瞭如何繪制文本和圖形,畫筆對象有很多設置方法,大體上可以分為兩類,一類與圖形繪制相關,一類與文本繪制相關。
方法 | 說明 |
---|---|
圖形繪制相關 | |
setARGB(int a,int r,int g,int b); | 用於繪制圖形,設置繪制的顏色,a代表透明度,r,g,b代表顏色值。 |
setAlpha(int a) | 設置繪制圖形的透明度 |
setColor(int color) | 設置繪制的顏色,使用顏色值來表示,該顏色值包括透明度和RGB顏色 |
setAntiAlias(boolean aa) | 設置是否使用抗鋸齒功能,會消耗較大資源,繪制圖形速度會變慢 |
setDither(boolean dither) | 設定是否使用圖像抖動處理,會使繪制出來的圖片顏色更加平滑和飽滿,圖像更加清晰 |
setFilterBitmap(boolean filter) | 如果該項設置為true,則圖像在動畫進行中會濾掉對Bitmap圖像的優化操作,加快顯示速度,本設置項依賴於dither和xfermode的設置 |
setMaskFilter(MaskFilter maskfilter) | 設置MaskFilter,可以用不同的MaskFilter實現濾鏡的效果,如濾化,立體等 |
setColorFilter(ColorFilter colorfilter) | 設置顏色過濾器,可以在繪制顏色時實現不用顏色的變換效果 |
setPathEffect(PathEffect effect) | 設置繪制路徑的效果,如點畫線 |
setShader(Shader shader) | 設置圖像效果,使用Shader可以繪制出各種漸變效果 |
setShadowLayer(float radius ,float dx,float dy,int color) | 在圖形下面設置陰影層,產生陰影效果,radius為陰影的角度,dx和dy為陰影在x軸和y軸上的距離,color為陰影的顏色 |
setStyle(Paint.Style style) | 設置畫筆的樣式,為FILL,FILL_OR_STROKE,或STROKE |
setStrokeCap(Paint.Cap cap) | 當畫筆樣式為STROKE或FILL_OR_STROKE時,設置筆刷的圖形樣式,如圓形樣式Cap.ROUND,或方形樣式Cap.SQUARE |
setSrokeJoin(Paint.Join join) | 設置繪制時各圖形的結合方式,如平滑效果等 |
setStrokeWidth(float width) | 當畫筆樣式為STROKE或FILL_OR_STROKE時,設置筆刷的粗細度 |
setXfermode(Xfermode xfermode) | 設置圖形重疊時的處理方式,如合並,取交集或並集,經常用來制作橡皮的擦除效果 |
文本繪制相關 | |
setFakeBoldText(boolean fakeBoldText) | 模擬實現粗體文字,但設置在小字體上效果會非常差 |
setSubpixelText(boolean subpixelText) | 設置該項為true,將有助於文本增強在LCD屏幕上的顯示效果 |
setTextAlign(Paint.Align align) | 設置繪制文字的對齊方向 |
setTextScaleX(float scaleX) | 設置繪制文字x軸的縮放比例,可以實現文字的拉伸的效果 |
setTextSize(float textSize) | 設置繪制文字的字號大小 |
setTextSkewX(float skewX) | 設置斜體文字,skewX為傾斜弧度 |
setTypeface(Typeface typeface) | 設置Typeface對象,即字體風格,包括粗體,斜體以及襯線體,非襯線體等 |
setUnderlineText(boolean underlineText) | 設置帶有下劃線的文字效果 |
setStrikeThruText(boolean strikeThruText) | 設置帶有刪除線的效果 |
三、Canvas繪圖
1、canvas.translate(x,y)
translate其實是把圓心坐標移動,比如說canvas.translate(200,200),則是把圓心移動到原來(200,200)處,若是移動canvas.translate(200,0)也是如此,相當於改變圓心的x坐標,y坐標不變。
2、canvas.rotate(degree)
rotate(float degrees)這個方法的旋轉中心是坐標的原點
3、translate和rotate
4、onMeasure方法詳解及實現
在自定義View時重寫onDraw方法是為瞭繪制控件或者UI,而要想能在佈局文件中正確的使用,往往還需要重寫onMeasure方法計算位置 ,要想計算位置就得先測量自身的尺寸大小。至於實現請關註下篇文章。
5、測試代碼
package crazymo.com.drawcanvas; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new CustomView(this)); } class CustomView extends View { Paint paint; public CustomView(Context context) { super(context); //初始化畫筆 paint = new Paint(Paint.ANTI_ALIAS_FLAG);//在畫圖的時候,圖片如果旋轉或縮放之後,總是會出現那些華麗的鋸齒。給Paint加上抗鋸齒標志 paint .setAntiAlias(true); paint.setColor(Color.GREEN); paint.setStrokeJoin(Paint.Join.ROUND); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeWidth(16); } //在這裡我們將測試canvas提供的繪制圖形方法 @Override protected void onDraw(Canvas canvas) { //canvas.translate(200,200); //canvas.rotate(30); //drawCircle(50,50,90,canvas,paint); drawLine(canvas); paint.setColor(0xFFDD0000); drawALine(250+30, 250, 400+30, 400,canvas, paint); drawCircle(0,0,90,canvas,paint); } /** * 畫圓 * @param cx 圓心x坐標 * @param cy 圓心y坐標 * @param radius 半徑 * @param paint paint對象 */ private void drawCircle(int cx,int cy,float radius,Canvas canvas,Paint paint){ canvas.drawCircle(cx,cy,radius,paint); } private void drawLine(Canvas canvas){ //畫一條線 canvas.drawLine(250, 250, 400, 400, paint); } private void drawALine(float startX,float startY,float stopX,float stopY,Canvas canvas,Paint paint){ //畫一條線 canvas.drawLine( startX, startY, stopX, stopY,paint); } } }
6、Layer圖層
Android中的繪圖機制很多都是借鑒瞭Photoshop的概念,在Photoshop中一張原始的素材可能是由很多圖層疊加而成,Android也借鑒瞭這一機制,所謂Layer圖層其本質就是內存中一塊矩形的區域,在Android中圖層是基於棧的數據結果進行管理的,通過方法canvas.saveLayer或saveLayerAlpha來創建新的(帶有透明度的)圖層並且放入到圖層棧中,出棧則是通過方法restore、restoreToCount,出入棧造成的操作區別是:入棧時所有的繪制操作都發生在當前這個圖層,而出棧之後則會把操作繪制到上一個圖層。
@Override protected void onDraw(Canvas canvas) { //相當於是默認繪制白色背景、藍色圓在整個畫佈上,可以看成PS中的背景 canvas.drawColor(Color.WHITE); paintOutSide.setColor(Color.BLUE); canvas.drawCircle(100,100,100,paintOutSide); canvas.saveLayerAlpha(0,0,400,400,125,ALL_SAVE_FLAG);//執行saveLayerAlpha 相當於是創建瞭一個新的圖層繪制紅色圓,其中125代表alpha值0~255,你可以嘗試著修改透明值進行測試可以加深對於圖層的理解 paintOutSide.setColor(Color.RED); canvas.drawCircle(150,150,100,paintOutSide); canvas.restore(); }
7、canvas.save()和canvas.restore()
save和restore通俗解釋就是:save的作用就是將之前所有的繪制操作保存到內存中,讓後續的操作在新的內存空間操作,就像是Photoshop中的前面的操作保存到一個圖層中,save之後的操作再保存到新的圖層;而restore的話就相當於是Photoshop中的合並圖層的操作比如你可以先保存目前畫紙的位置(save),然後旋轉90度,向下移動100像素後畫一些圖形,畫完後調用restore方法合並圖層。
/** * Auther: Crazy.Mo * DateTime: 2017/4/28 16:34 * Summary: */ public class ClockView extends View { private Context context; private Paint paintOutSide,paintDegree; private float outWidth,outHeight; public ClockView(Context context) { this(context, null); init(context); } public ClockView(Context context, AttributeSet attrs) { this(context, attrs,0); init(context); } public ClockView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context){ this.context=context; initOutSize(); } private void initOutSize(){ WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);//獲取WM對象 DisplayMetrics dm = new DisplayMetrics(); manager.getDefaultDisplay().getMetrics(dm); outHeight=(float) dm.heightPixels;//獲取真實屏幕的高度以px為單位 outWidth=(float)dm.widthPixels; } /** * 畫外圈圓 * @param canvas */ private void drawOutCircle(Canvas canvas){ paintOutSide=new Paint(); paintOutSide.setColor(Color.GREEN); paintOutSide.setStyle(Paint.Style.STROKE); paintOutSide.setAntiAlias(true); paintOutSide.setDither(true); paintOutSide.setStrokeWidth(6f); canvas.drawCircle(outWidth/2.0f,outHeight/2.0f,outWidth/2.0f,paintOutSide); } /** * 畫刻度 */ private void drawDegree(Canvas canvas){ paintDegree=new Paint(); paintDegree.setColor(Color.RED); paintDegree.setStyle(Paint.Style.STROKE); paintDegree.setAntiAlias(true); paintDegree.setDither(true); paintDegree.setStrokeWidth(3f); for(int i=0;i<24;i++){ if(i==0||i==6||i==12||i==18){ paintDegree.setStrokeWidth(6f); paintDegree.setTextSize(30); canvas.drawLine(outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f),outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f+60),paintDegree); String degreeTxt=String.valueOf(i); canvas.drawText(degreeTxt,(outWidth/2-paintDegree.measureText(degreeTxt)/2),(outHeight/2-outWidth/2+90),paintDegree); }else { paintDegree.setStrokeWidth(4f); paintDegree.setTextSize(20); canvas.drawLine(outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f),outWidth/2.0f,(outHeight/2.0f-outWidth/2.0f+40),paintDegree); String degreeTxt=String.valueOf(i); canvas.drawText(degreeTxt,(outWidth/2-paintDegree.measureText(degreeTxt)/2)+20,(outHeight/2-outWidth/2+40),paintDegree); } canvas.rotate(15,outWidth/2,outHeight/2); } } private void drawPointor(Canvas canvas){ Paint paintHour=new Paint(); paintHour.setColor(Color.RED); paintHour.setStyle(Paint.Style.STROKE); paintHour.setAntiAlias(true); paintHour.setDither(true); paintHour.setStrokeWidth(12f); Paint paintMin=new Paint(); paintMin.setColor(Color.RED); paintMin.setStyle(Paint.Style.STROKE); paintMin.setAntiAlias(true); paintMin.setDither(true); paintMin.setStrokeWidth(8f); canvas.save(); canvas.translate(outWidth/2,outHeight/2); canvas.drawLine(0,0,100,100,paintHour); canvas.drawLine(0,0,100,150,paintMin); canvas.restore(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawOutCircle(canvas); drawDegree(canvas); drawPointor(canvas); } }