Android進階——自定義View的必修課之關於Canvas繪圖與Android坐標系的總結

引言

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.saveLayersaveLayerAlpha創建新的(帶有透明度的)圖層並且放入到圖層棧中,出棧則是通過方法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);
    }
}

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *