Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 關於自定義View的基礎思路以及畫布的解析

關於自定義View的基礎思路以及畫布的解析

編輯:關於Android編程

1、前言

UI作為用戶看得到的東西,已經成為吸引用戶的最重要因素了。在android中提供了大量的widget以及主題和屬性,加上各種動畫,已經可以實現非常多很絢麗的控件了。但是很多情況下,僅僅使用系統提供給我們的控件,總是有那麼點缺憾。即每個控件的存在都有自身的特定功能,當我們卻是需要這些功能的時候,無疑是很好的選擇,但如果我們不需要這些功能,但卻需要其中的某些特性呢?這個時候,就需要我們自定義一個新的控件了。關於自定義控件,按照復雜程度,可以分成三大類,

①、僅僅繼承某個已存在的widget重寫部分方法。

②、組合多個已存在的widget組合成一個復合控件。

③、完全自定義控件,包括外觀。

上面的最復雜的就是第三種,也是本文的重點內容。在了解如何自定義控件之前,我們首先得明白View是個什麼玩意,它的實現機制是什麼,有什麼特點,才能更好的去掌握它。接下來就先了解一下View。

2、自定義View的基礎知識

View是所有的layout和widget的基礎,它是以矩形的形式在屏幕中占領著一定的空間,並且負責處理各種事件以及界面渲染。也就是說,無論是控件還是布局,它都是以矩形的形式在屏幕中出現的,但是通過改變形狀,是可以修改控件可見的形狀,但記得,它本質上還是矩形。而布局則是看不見的VIew,本質上也是一塊矩形。為什麼強調它是矩形,因為後面需要用的這個特征。

View根據用途,比如展示圖片,展示文字,展示滾動內容.....可以分為很多種類,但是無論是什麼類型的View,都有兩種方式可以使用它們,一種是通過代碼的形式,另外一種是通過布局文件的形式。因為布局文件的形式寫法比較符合邏輯與視圖的分離模式,並且用起來自由度也高,所以是比較推薦這種方式的。記住,無論你是通過哪種途徑產生的View,它們最終都會被添加到視圖樹裡面。這樣有助於系統進行事件的攔截處理以及統一管理視圖。

一旦我們建立了View,通常會對它進行一些基本的屬性設置,監聽器設置,或者是焦點處理。但是對於一個需要完全自定義View來說,這些不是重點,重點的是measure,layout,draw這三個方法。如果沒有需要的話,前往不要調用這些方法,否則會破壞系統本身對視圖的渲染。但如果確實又需要,就需要我們好好理解這些方法之間的聯系,處理好界面渲染的關系。所以下面會重點講解這些。

如果要自定義View,那麼接下來的關於View的知識都是需要了解清楚的。雖然關於自定義View的知識很多,但是我們沒必要都重寫所有的有關自定義View的方法。比如,你可以單單重寫onDraw方法,就可以實現一個自己繪圖的View。接下來,了解關於會影響自定義View的方方面面。

2.1 View的創建

View的創建有兩種形式,一種是代碼形式,另外一種是xml布局文件形式,對於XML文件填充的View還需要處理xml裡邊寫的相關屬性。對於View的構造函數有以下幾個:

 

1.public View(Context context)

2.public View(Context context, @Nullable AttributeSet attrs)

3.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)

4.public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
第一種:主要用於使用代碼創建View,context是View的上下文環境,通過它可以獲取主題和資源文件。

 

第二種:用於通過Xml文件填充的View,AttributeSet是一個屬性集,它包含了你在xml文件中指定的屬性集合,記得在調用次構造函數的時候,必須調用父類的對應的構造函數,以便系統初始化視圖的本身的樣式風格。如果,我們自己寫了屬性集,就需要在這裡獲取到我們的TypeArray的對象,根據自己定義的stylable名稱去獲取屬性值,以便後續處理。默認情況下,即你沒有自定義屬性的情況下,使用的是系統提供的主題和屬性集。填充完View之後,會調用onFinishInflate()方法。

第三種:也是用於通過xml文件填充的View,但是這個可以指定一個主題風格。

第四種:在第三種的基礎上,可以指定一種主題風格並指定它的主題資源。

通常我們只需要重寫前面兩個構造函數就ok的了。

2.2 View的布局過程

View的布局過程,主要有onMeasure,onlayout,ondraw,onsizechanger這幾個方法,其中涉及到測量,布局,繪畫各方面。所以很經常自定義View所花的時間就在這上面。同時這也是自定義View的重點所在。接下來我們了解一下這些方法。

2.2.1 onMeasure

這個方法會在View通過視圖樹去遍歷它包括它的所有子View的尺寸要求的時候被調用。在了解onMeasure之前,我們先了解MeasureSpec。


2.2.1.1MeasureSpec

MesusreSpec是當前View的父容器傳給它的關於父容器對它的尺寸的要求,這個要求是根據父容器的layoutParam和當前View的寬高綜合給出的測量規格。每個MesureSpec都包含兩方面的內容,一個是mode,一個是size。其中mode表示當前父容器對view的限制模式,size表示父容器給出的建議尺寸。

view的限制模式有三種:

①、UNSPECIFIED:即父容器不對當前View做任何限制,因此View可以使用任何尺寸。一般用於設置默認的尺寸。

②、EXACTLY:即父容器給當前View指定了一個確定的尺寸,無論你給View設置了什麼值,view都將會是指定的尺寸。

③、AT_MOST:即父容器給出了一個最大的尺寸,view設置的尺寸大小不能超過這個范圍。

根據上面三種描述,總結一下,假設現在有一個默認尺寸,一個父容器建議的尺寸。第一種情況,可以設置為任何你想要的值,第二種情況只能使用建議的尺寸,第三種情況只能使用小於建議尺寸的值。由於我們聲明一個View的時候,一般都會指定寬高的大小,所以我們遇到的情況只會是第二第三種。第二種發生在在布局中指定了確定大小或者match_parent的情況,第三種發生在指定的大小是Wrap_content的情況。標准的使用方法如下:

 

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }


2.2.2 onMeasure方法概況

 

OnMeasure方法在View需要確認它的內容和子View所占據的測量空間的時候調用。一般是measure方法的回調,所以,如果View有重寫這個回調方法的話,一定要提供一個有效的測量值。即在根據測量的mode和size綜合得出的測量高度和寬度的時候,一定要調用setMeasureDimensions方法將測量的寬高穿進去,以便父容器知道當前View的要求的寬高。此回調方法在View測量的過程中可能會被回調多次,因為有時候所有子View給的測量寬高的綜合,當前View並不能滿足,所以又需要重新測量一次。


2.2.3 onLayout布局過程

當View需要給所有的子View分配空間和大小的時候調用。一般我們把這個過程交給系統去做就好,layout分為兩個過程,分別是measure過程和layout過程,前者用於遍歷視圖樹中子View的所有尺寸要求。注意measure過程可能會不止調用一遍,因為view可以第一次用不確定的值去處理子View的尺寸要求,然後確定了所有View的要求之後,在第二次遍歷得到一個確定的值。一旦onMeausre方法被回調之後,就可以獲取測量的高度和寬度了。layout過程就是根據測量的尺寸要求,給所有的View包括他的子View進行尺寸分配以及位置的確定。

2.2.4 onSizeChanged

如果需要在布局過程中判斷View的尺寸是否發生變化,那麼就可以重寫這個方法了。這個方法包含四個形參,分別是舊的寬高和現在的寬高。


2.2.5 onDraw渲染內容

當View進行內容的渲染的時候,就會調用此方法。此方法會將View的內容和它的子View在給定的Canvas對象裡面進行渲染。注意,調用此方法之前布局必須先確定下來。

2.2.5.1 canvas的理解

前面提到draw的時候,是在canvas上進行繪畫的,那麼現在來了解一下canvas是什麼,可以干什麼。

canvas是來承受繪畫的內容的,所有在View中呈現的內容,都是先渲染到canvas中,然後再由canvas寫進bitmap裡面,然後就呈現出來了。因此canvas含有大量的繪制圖形,文本的方法,並且還可以進行圖形的旋轉,移動,放大縮小......即所有相關的繪制方法都包含在這裡面了。

2.2.5.2 canvas指定背景

 public void setBitmap(@Nullable Bitmap bitmap)

此方法會將指定的bitmap對象作為canvas的背景圖,同時,canvas的像素密度也會更新以匹配當前的bitmap的像素密度。

2.2.5.3 獲取canvas的寬高

 public void setBitmap(@Nullable Bitmap bitmap)
 public void setBitmap(@Nullable Bitmap bitmap)

獲取當前繪制圖像的寬高。

 

2.2.5.4 canvas的像素密度

 

public int getDensity()
 public void setDensity(int density)

獲取像素密度的時候,如果有設置背景圖層,則返回的是背景圖層的像素密度。如果有設置像素密度,則返回設置的值,如果都沒有,則返回一個0,表示沒有指定任何像素密度。設置像素密度會影響背景圖像的像素密度,同時也會影響canvas的像素密度。


2.2.5.5 canvas狀態的保存與恢復

 public int save() 

 public void restore()

sava用於保存當前canvas的狀態,restore用於恢復。注意,一定要注意,sava和restore的數量一定要對等,如果restore的調用數次大於save的話,會導致異常。現在述說報攢canvas的狀態有什麼作用。由於canvas可以執行旋轉,移動等影響畫布的操作,因此後續的操作都是會基於畫布當前的狀態來進行的。比如:我現在將畫布旋轉90讀,然後再在畫布裡畫一個矩形,它也是出於90度的狀態的。但如果我們在旋轉畫布之前調用save方法進行狀態保存,在旋轉畫布之後,再調用restore方法,會將畫布的旋轉狀態去除,此後再次畫布上的操作就不會受旋轉的影響了。後面會有詳細的例子說明。

 

2.2.5.6 對canvas進行旋轉移動等操作

移動

 

public void translate(float dx, float dy)

表示會將當前的畫布在x方向是移動dx距離,y方向上移動dy距離。

 

縮放比例

 

  public void scale(float sx, float sy)

 

 

public final void scale(float sx, float sy, float px, float py)

上述兩個方法都是用於將當前的畫布進行放大縮小,sx,sy表示的是在x,y方向上的縮放比例,px,py表示的是縮放圍繞的中心點坐標。

 

旋轉

 

public void rotate(float degrees)
public final void rotate(float degrees, float px, float py)

將當前畫布進行旋轉,degress別是旋轉的角度,px,py是旋轉的中心點坐標。

 

傾斜

 

public void skew(float sx, float sy)

將當前畫布進行傾斜,sx,sy表示傾斜的范圍。

 

2.2.5.7 對畫布進行顏色填充

 

public void drawRGB(int r, int g, int b)
public void drawARGB(int a, int r, int g, int b)
public void drawColor(@ColorInt int color) 
public void drawPaint(@NonNull Paint paint)
上面四種方法都可以用於給畫布填充顏色,r,g,b分別表示紅綠藍的比例,a是透明度,也可以直接用Color的常量填充,或者使用畫筆paint直接對畫布噴油漆。

 

 

2.2.5.8 對畫布進行畫點

public void drawPoints(@Size(multiple=2) float[] pts, int offset, int count,
            @NonNull Paint paint)
 public void drawPoints(@Size(multiple=2) @NonNull float[] pts, @NonNull Paint paint)
 public void drawPoint(float x, float y, @NonNull Paint paint)

以上方法可以在畫布上畫小點,pts保存著所以的點的x,y坐標,比如pts[0],pts[1],pts[2].pts[3]分別表示第一第二個點的坐標,offset表示要跳過的數量,比如1,則從pts[2]開始計算。count表示畫的點的數量,paint有兩個作用,一個是控制點的形狀,通過setStrokeCap方法控制,另外一個是paint的strokeWidth是控制點的直徑的大小,如果是矩形則是寬。第三個方法是之花一個點。

 


2.2.5.9 對畫布進行畫線

 

public void drawLine(float startX, float startY, float stopX, float stopY,
            @NonNull Paint paint) 

public void drawLines(@Size(min=4,multiple=2) float[] pts, int offset, int count, Paint paint) 

 

 

public void drawLines(@Size(min=4,multiple=2) @NonNull float[] pts, @NonNull Paint paint) 

第一個方法用於畫一條線,下面的兩個用於話多條線。各參數值結合字面意思以及前面的解釋,讀者可以自行推測出來。

 

2.2.5.10 對畫布進行其它形狀的繪制

 

1、public void drawRect(@NonNull RectF rect, @NonNull Paint paint)
2、public void drawRect(@NonNull Rect r, @NonNull Paint paint)
3、public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)
4、public void drawOval(@NonNull RectF oval, @NonNull Paint paint)
5、public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)
6、public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
7、public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)
8、public void drawText(@NonNull char[] text, int index, int count, float x, float y,
            @NonNull Paint paint)
9、public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
10、public void drawText(@NonNull String text, int start, int end, float x, float y,
            @NonNull Paint paint)

1-3用於繪制矩形,可以直接傳遞rext,也可以傳遞矩形四個角的坐標。4-5繪制橢圓形,6繪制圓形,7繪制Bitmap圖像,8-10繪制文字。

 

 

2.3 View的坐標


View是以矩形的形式出現的,因此他含有一對坐標分別表示它的ledt,top的位置,以及一隊尺寸width,height。長寬的單位是像素。我們可以通過getLeft,getTop方法來獲取當前View相對於它的父View的距離,比如getLeft返回20,表示當前View距離父View的右邊緣20個像素點。getTop同理。當然也有getBottom,getRight方法,也可以以getLedt+getWidth的方式得到getRight。兩種方式都可以獲得View的右邊的坐標。

 

2.4 View的Size,padding,margin

事實上View有兩種Size類型,一種是測量尺寸,一種是布局尺寸。測量尺寸表達的是當前View所期望的尺寸,布局尺寸是父容器結合實際給出的具體尺寸,所以測量尺寸和布局尺寸不一定會相等。獲取測量尺寸的方法,getMeasuredWidth和getMeasuredHeight。獲取布局尺寸的方法:getWidth,getHeight。

padding表示的是間距,即當前View的內容距離邊緣的距離。比如padding=2,表示當前View的內容距離View的邊緣有2個像素點。要注意的是,如果是我們自定義View過程需要Draw渲染內容,一定要處理padding,否則即使你給View設置了padding而不去處理它,會導致View根本起不到padding的效果。獲取padding值得方法,getPaddingLeft,getPaddingRight,getPaddingTop,getPaddingBottom。

Margin表示的是間隔,即當前View相對於父容器的距離,此特征會有父容器進行處理,所以我們自定義View不必關注這個屬性。

 

三、自定義View的例子

上面關於自定義View的測量布局繪畫過程如果都了解清除了,就可以做很多不規則的View了。本文僅局限與對自定義View的渲染方面的解析。如果需要自己處理事件,焦點,需要了解View的事件分發機制,焦點處理,觸摸模式。同時如果希望有更絢麗的效果,還需要知道android中動畫的相關知識。在下面的例子裡,將只對前面提到的基礎知識進行舉例,只有明確知道了這些基礎知識,才能結合View的各方面特征,作出更好的更絢麗的自由度更高的View。

 




首先是attr_xml文件定義自定義屬性:

 



    
    
        
        
    

然後是自定義View

 

 

package cn.com.chinaweal.customview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

/**
 * 自定義View注意這裡繼承的是View所以除了系統處理的事件之外任何我們要的東西都要自己寫
 * Created by Myy on 2016/8/20.
 */
public class CustomerView extends View {

    float strokeWidth=1;
    int paintColor=Color.RED;
    Paint paint;
    Context context;
    /**
     * 次構造函數有代碼生成的View調用
     * @param context
     */
    public CustomerView(Context context) {
        super(context);
        this.context=context;
        init();
    }

    /**
     * 此構造函數一般有xml填充的View調用
     */
    public CustomerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context=context;
        //獲取自定義的主題的相關信息
        TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.CustomerView);
        strokeWidth=typedArray.getDimension(R.styleable.CustomerView_strokeWidth,1);
        paintColor=typedArray.getColor(R.styleable.CustomerView_paintColor,Color.RED);
        init();
    }

    private void init() {
        paint=new Paint();
        paint.setColor(paintColor);
        paint.setStrokeWidth(strokeWidth);
    }

    /**
     * 重寫onMeasure用於得出一個合適的測量尺寸,因為我們繼承的是View,所以這些方法必須自己實現
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width=getBetterSize(widthMeasureSpec);
        int height=getBetterSize(heightMeasureSpec);
        int size=width>height?height:width;//用最小的尺寸作為view的尺寸
        setMeasuredDimension(size,size);//一定要調用此方法設置測量尺寸
    }

    /**
     * 根據測量規則獲取最佳尺寸
     * @param measureSpec
     * @return
     */
    private int getBetterSize(int measureSpec) {
        int size=200;
        int mode=MeasureSpec.getMode(measureSpec);
        int requestSize=MeasureSpec.getSize(measureSpec);
        switch (mode)
        {
            case MeasureSpec.UNSPECIFIED:size=200;break;
            case MeasureSpec.AT_MOST:size=requestSize;break;
            case MeasureSpec.EXACTLY:size=requestSize;break;

        }
        return size;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪畫基本圖形
        //記得處理padding
        int left=getPaddingLeft();
        int right=getPaddingRight();
        int bottom=getPaddingBottom();
        int top=getPaddingTop();
        int width=getMeasuredWidth();
        int height=getMeasuredHeight();

        //繪制圓形
        paint.setFlags(Paint.ANTI_ALIAS_FLAG);//這樣畫出來的不是實心的
        int radius=Math.min(width-left-right,height-bottom-top)/2;//如果不處理padding會導致padding屬性失效
        canvas.drawCircle(width/2,height/2,radius,paint);

        //繪制直線

        canvas.drawLine(left,top,width-left-right,height-bottom-top,paint);

        //移動畫布
        canvas.save();
        //將畫布繞著0,0旋轉10度
        canvas.rotate(10);
        //在旋轉後的畫布上畫圖,會發現圖也被選擇了10度
        canvas.drawBitmap(BitmapFactory.decodeResource(context.getResources(),R.mipmap.ic_launcher),20,20,paint);
        paint.setColor(Color.GREEN);
        //可以發現線也被旋轉了十度
        canvas.drawLine(left,top,width-left-right,height-bottom-top,paint);
        canvas.restore();//恢復狀態
        /**
         * 此時要注意畫筆的顏色的改變,在save和restore之間畫的圖像和線段會隨著resotre的調用而被排除在繪制棧外
         * 當調用resotre的時候,需要重新繪制save之前繪畫的東西,所以當系統需要再次刷新View的時候,級下面調用drawRect的時候
         * 系統會重新恢復之前的繪畫圖像,由於在resotre之後我將paint的顏色修改了,所以之前的繪畫圖像都會用此paint進行繪制。導致
         * 顏色被修改
         */
        paint.setColor(Color.YELLOW);
        //此時畫矩形,發現矩形並沒有被旋轉10度,因為resotre將畫布狀態恢復了
        //此時會發現之前繪制的灰色的圖像變成了黃色
        canvas.drawRect(50,50,100,100,paint);

    }
}

布局文件:

 

 




    




 

 

運行效果:


\

具體解釋請看我的注釋。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved