【Android - 進階】之自訂視圖淺析

來源:互聯網
上載者:User

標籤:ida   display   重用   ova   tom   .com   精確   效果   方向   

 

1       概述

Android自訂View / ViewGroup的步驟大致如下:

1)  自訂屬性;2)  選擇和設定構造方法;3)  重寫onMeasure()方法;4)  重寫onDraw()方法;5)  重寫onLayout()方法;6)  重寫其他事件的方法(滑動監聽等)。

 

2       自訂屬性

Android自訂屬性主要有定義、使用和擷取三個步驟。

 

2.1    定義自訂屬性

參考:http://blog.csdn.net/lmj623565791/article/details/45022631/

我們通常將自訂屬性定義在/values/attr.xml檔案中(attr.xml檔案需要自己建立)。

先來看一段範例程式碼:

<?xml version="1.0" encoding="utf-8"?><resources>    <attr name="rightPadding" format="dimension" />    <declare-styleable name="CustomMenu">        <attr name="rightPadding" />    </declare-styleable></resources> 

可以看到,我們先是定義了一個屬性rightPadding,然後又在CustomMenu中引用了這個屬性。下面說明一下:

  • 首先,我們可以在declare-stylable標籤中直接定義屬性而不需要引用外部定義好的屬性,但是為了屬性的重用,我們可以選擇上面的這種方法:先定義,後引用;
  • declare-stylable標籤只是為了給自訂屬性分類。一個項目中可能又多個自訂控制項,但只能又一個attr.xml檔案,因此我們需要對不同自訂控制項中的自訂屬性進行分類,這也是為什麼declare-stylable標籤中的name屬性往往定義成自訂控制項的名稱;
  • 所謂的在declare-stylable標籤中的引用,就是去掉了外部定義的format屬性,如果沒有去掉format,則會報錯;如果外部定義中沒有format而在內部引用中又format,也一樣會報錯。

常用的format類型:

1)  string:字串類型;2)  integer:整數類型;3)  float:浮點型;4)  dimension:尺寸,後面必須跟dp、dip、px、sp等單位;5)  Boolean:布爾值;6)  reference:參考型別,傳入的是某一資源的ID,必須以“@”符號開頭;7)  color:顏色,必須是“#”符號開頭;8)  fraction:百分比,必須是“%”符號結尾;9)  enum:枚舉類型

下面對format類型說明幾點:

  • format中可以寫多種類型,中間使用“|”符號分割開,表示這幾種類型都可以傳入這個屬性;
  • enum類型的定義樣本如下代碼所示:
<resources>    <attr name="orientation">        <enum name="horizontal" value="0" />        <enum name="vertical" value="1" />    </attr>    <declare-styleable name="CustomView">        <attr name="orientation" />    </declare-styleable></resources>

使用時通過getInt()方法擷取到value並判斷,根據不同的value進行不同的操作即可。

 

2.2    使用自訂屬性

在XML布局檔案中使用自訂的屬性時,我們需要先定義一個namespace。Android中預設的namespace是android,因此我們通常可以使用“android:xxx”的格式去設定一個控制項的某個屬性,android這個namespace的定義是在XML檔案的頭標籤中定義的,通常是這樣的:

xmlns:android="http://schemas.android.com/apk/res/android"

我們自訂的屬性不在這個命名空間下,因此我們需要添加一個命名空間。

自訂屬性的命名空間如下:

xmlns:app="http://schemas.android.com/apk/res-auto"

可以看出來,除了將命名空間的名稱從android改成app之外,就是將最後的“res/android”改成了“res-auto”。

注意:自訂namespace的名稱可以自己定義,不一定非得是app。

 

2.3    擷取自訂屬性

在自訂View / ViewGroup中,我們可以通過TypedArray擷取到自訂的屬性。範例程式碼如下:

public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomMenu, defStyleAttr, 0);    int indexCount = a.getIndexCount();    for (int i = 0; i < indexCount; i++) {        int attr = a.getIndex(i);        switch (attr) {            case R.styleable.CustomMenu_rightPadding:                mMenuRightPadding = a.getDimensionPixelSize(attr, 0);                break;        }    }    a.recycle();}

這裡需要說明一下:

  • 擷取自訂屬性的代碼通常是在三個參數的構造方法中編寫的(具體為什麼是三個參數的構造方法,下面的章節中會有解釋);
  • 在擷取TypedArray對象時就為其綁定了該自訂View的自訂屬性集(CustomMenu),通過getIndexCount()方法擷取到自訂屬性的數量,通過getIndex()方法擷取到某一個屬性,最後通過switch語句判斷屬性並進行相應的操作;
  • 在TypedArray使用結束後,需要調用recycle()方法回收它。

 

3       構造方法

當我們定義一個新的類繼承了View或ViewGroup時,系統都會提示我們重寫它的構造方法。View / ViewGroup中又四個構造方法可以重寫,它們分別有一、二、三、四個參數。四個參數的構造方法我們通常用不到,因此這個章節中我們主要介紹一個參數、兩個參數和三個參數的構造方法(這裡以CustomMenu控制項為例)。

 

3.1    一個參數的構造方法
構造方法的代碼: public CustomMenu(Context context) { …… } 

這個構造方法只有一個參數Context上下文。當我們在JAVA代碼中直接通過new關鍵在建立這個控制項時,就會調用這個方法。

 

3.2    兩個參數的構造方法
構造方法的代碼: public CustomMenu(Context context, AttributeSet attrs) { …… } 

這個構造方法有兩個參數:Context上下文和AttributeSet屬性集。當我們需要在自訂控制項中擷取屬性時,就預設調用這個構造方法。AttributeSet對象就是這個控制項中定義的所有屬性。

我們可以通過AttributeSet對象的getAttributeCount()方法擷取屬性的個數,通過getAttributeName()方法擷取到某條屬性的名稱,通過getAttributeValue()方法擷取到某條屬性的值。

注意:不管有沒有使用自訂屬性,都會預設調用這個構造方法,“使用了自訂屬性就會預設調用三個參數的構造方法”的說法是錯誤的。

 

3.3    三個參數的構造方法
構造方法的代碼: public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { …… } 

這個構造方法中有三個參數:Context上下文、AttributeSet屬性集和defStyleAttr自訂屬性的引用。這個構造方法不會預設調用,必須要手動調用,這個構造方法和兩個參數的構造方法的唯一區別就是這個構造方法給我們預設傳入了一個預設屬性集。

defStyleAttr指向的是自訂屬性的<declare-styleable>標籤中定義的自訂屬性集,我們在建立TypedArray對象時需要用到defStyleAttr。

 

3.4    三個構造方法的整合

一般情況下,我們會將這三個構造方法串聯起來,即層層調用,讓最終的業務處理都集中在三個參數的構造方法。我們讓一參的構造方法引用兩參的構造方法,兩參的構造方法引用三參的構造方法。範例程式碼如下:

public CustomMenu(Context context) {    this(context, null);}public CustomMenu(Context context, AttributeSet attrs) {    this(context, attrs, 0);}public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    // 業務代碼}

這樣一來,就可以保證無論使用什麼方式建立這個控制項,最終都會到三個參數的構造方法中處理,減少了重複代碼。

 

4       onMeasure()

onMeasure()方法中主要負責測量,決定控制項本身或其子控制項所佔的寬高。我們可以通過onMeasure()方法提供的參數widthMeasureSpec和heightMeasureSpec來分別擷取控制項寬度和高度的測量模式測量值(測量 = 測量模式 + 測量值)。

widthMeasureSpec和heightMeasureSpec雖然只是int類型的值,但它們是通過MeasureSpec類進行了編碼處理的,其中封裝了測量模式和測量值,因此我們可以分別通過MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)來擷取到控制項或其子View的測量模式和測量值。

測量模式分為以下三種情況:

1)  EXACTLY:當寬高值設定為具體值時使用,如100DIP、match_parent等,此時取出的size是精確的尺寸;2)  AT_MOST:當寬高值設定為wrap_content時使用,此時取出的size是控制項最大可獲得的空間;3)  UNSPECIFIED:當沒有指定寬高值時使用(很少見)。

onMeasure()方法中常用的方法:

1)  getChildCount():擷取子View的數量;2)  getChildAt(i):擷取第i個子控制項;3)  subView.getLayoutParams().width/height:設定或擷取子控制項的寬或高;4)  measureChild(child, widthMeasureSpec, heightMeasureSpec):測量子View的寬高;5)  child.getMeasuredHeight/width():執行完measureChild()方法後就可以通過這種方式擷取子View的寬高值;6)  getPaddingLeft/Right/Top/Bottom():擷取控制項的四周內邊距;7)  setMeasuredDimension(width, height):重新設定控制項的寬高。如果寫了這句代碼,就需要刪除“super. onMeasure(widthMeasureSpec, heightMeasureSpec);”這行代碼。

注意:onMeasure()方法可能被調用多次,這是因為控制項中的內容或子View可能對分配給自己的空間“不滿意”,因此向父空間申請重新分配空間。

 

5       onDraw()

onDraw()方法負責繪製,即如果我們希望得到的效果在Android原生控制項中沒有現成的支援,那麼我們就需要自己繪製我們的自訂控制項的顯示效果。

要學習onDraw()方法,我們就需要學習在onDraw()方法中使用最多的兩個類:Paint和Canvas。

注意:每次觸摸了自訂View/ViewGroup時都會觸發onDraw()方法。

 

5.1    Paint類

Paint畫筆對象,這個類中包含了如何繪製幾何圖形、文字和位元影像的樣式和顏色資訊,指定了如何繪製文本和圖形。畫筆對象右很多設定方法,大體上可以分為兩類:一類與圖形繪製有關,一類與文本繪製有關。

 

Paint類中有如下方法:

1、圖形繪製:

1)  setArgb(int a, int r, int g, int b):設定繪製的顏色,a表示透明度,r、g、b表示顏色值;2)  setAlpha(int a):設定繪製的圖形的透明度;3)  setColor(int color):設定繪製的顏色;4)  setAntiAlias(boolean a):設定是否使用消除鋸齒功能,消除鋸齒功能會消耗較大資源,繪製圖形的速度會減慢;5)  setDither(boolean b):設定是否使用映像抖動處理,會使映像顏色更加平滑飽滿,更加清晰;6)  setFileterBitmap(Boolean b):設定是否在動畫中濾掉Bitmap的最佳化,可以加快顯示速度;7)  setMaskFilter(MaskFilter mf):設定MaskFilter來實現濾鏡的效果;8)  setColorFilter(ColorFilter cf):設定顏色過濾器,可以在繪製顏色時實現不同顏色的變換效果;9)  setPathEffect(PathEffect pe):設定繪製的路徑的效果;10) setShader(Shader s):設定Shader繪製各種漸層效果;11) setShadowLayer(float r, int x, int y, int c):在圖形下面設定陰影層,r為陰影角度,x和y為陰影在x軸和y軸上的距離,c為陰影的顏色;12) setStyle(Paint.Style s):設定畫筆的樣式:FILL實心;STROKE空心;FILL_OR_STROKE同時實心與空心;13) setStrokeCap(Paint.Cap c):當設定畫筆樣式為STROKE或FILL_OR_STROKE時,設定筆刷的圖形樣式;14) setStrokeJoin(Paint.Join j):設定繪製時各圖形的結合方式;15) setStrokeWidth(float w):當畫筆樣式為STROKE或FILL_OR_STROKE時,設定筆刷的粗細度;16) setXfermode(Xfermode m):設定圖形重疊時的處理方式;

2、文本繪製:

1)  setTextAlign(Path.Align a):設定繪製的文本的對齊;2)  setTextScaleX(float s):設定文本在X軸的縮放比例,可以實現文字的展開效果;3)  setTextSize(float s):設定字型大小;4)  setTextSkewX(float s):設定斜體文字,s是文字傾斜度;5)  setTypeFace(TypeFace tf):設定字型風格,包括粗體、斜體等;6)  setUnderlineText(boolean b):設定繪製的文本是否帶有底線效果;7)  setStrikeThruText(boolean b):設定繪製的文本是否帶有刪除線效果;8)  setFakeBoldText(boolean b):類比實現粗體文字,如果設定在小字型上效果會非常差;9)  setSubpixelText(boolean b):如果設定為true則有助於文本在LCD螢幕上顯示效果;

3、其他方法:

1)  getTextBounds(String t, int s, int e, Rect b):將頁面中t文本從s下標開始到e下標結束的所有字元所佔的地區寬高封裝到b這個矩形中;2)  clearShadowLayer():清除陰影層;3)  measureText(String t, int s, int e):返回t文本中從s下標開始到e下標結束的所有字元所佔的寬度;4)  reset():重設畫筆為預設值。

這裡需要就幾個方法解釋一下:

1、setPathEffect(PathEffect pe):設定繪製的路徑的效果:

常見的有以下幾種可選方案:

1)  CornerPathEffect:可以用圓角來代替尖銳的角;2)  DathPathEffect:虛線,由短線和點組成;3)  DiscretePathEffect:荊棘狀的線條;4)  PathDashPathEffect:定義一種新的形狀並將其作為原始路徑的輪廓標記;5)  SumPathEffect:在一條路徑中順序添加參數中的效果;6)  ComposePathEffect:將兩種效果組合起來,先使用第一種效果,在此基礎上應用第二種效果。

 

2、setXfermode(Xfermode m):設定圖形重疊時的處理方式:

關於Xfermode的多種效果,我們可以參考下面一張圖:

在使用的時候,我們需要通過paint. setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))來設定,XXX是中的某種模式對應的常量參數,如DST_OUT。

 

5.2    Canvas類

Canvas即畫布,其上可以使用Paint畫筆對象繪製很多東西。

Canvas對象中可以繪製:

1)  drawArc():繪製圓弧;2)  drawBitmap():繪製Bitmap映像;3)  drawCircle():繪製圓圈;4)  drawLine():繪製線條;5)  drawOval():繪製橢圓;6)  drawPath():繪製Path路徑;7)  drawPicture():繪製Picture圖片;8)  drawRect():繪製矩形;9)  drawRoundRect():繪製圓角矩形;10) drawText():繪製文本;11) drawVertices():繪製頂點。

Canvas對象的其他方法:

1)  canvas.save():把當前繪製的映像儲存起來,讓後續的操作相當於是在一個新圖層上繪製;2)  canvas.restore():把當前畫布調整到上一個save()之前的狀態;3)  canvas.translate(dx, dy):把當前畫布的原點移到(dx, dy)點,後續操作都以(dx, dy)點作為參照;4)  canvas.scale(x, y):將當前畫布在水平方向上縮放x倍,豎直方向上縮放y倍;5)  canvas.rotate(angle):將當前畫布順時針旋轉angle度。

 

 

6       onLayout()

onLayout()方法負責布局,大多數情況是在自訂ViewGroup中才會重寫,主要用來確定子View在這個布局空間中的擺放位置。

onLayout(boolean changed, int l, int t, int r, int b)方法有5個參數,其中changed表示這個控制項是否有了新的尺寸或位置;l、t、r、b分別表示這個View相對於父布局的左/上/右/下方的位置。

以下是onLayout()方法中常用的方法:

1)  getChildCount():擷取子View的數量;2)  getChildAt(i):擷取第i個子View3)  getWidth/Height():擷取onMeasure()中返回的寬度和高度的測量值;4)  child.getLayoutParams():擷取到子View的LayoutParams對象;5)  child.getMeasuredWidth/Height():擷取onMeasure()方法中測量的子View的寬度和高度值;6)  getPaddingLeft/Right/Top/Bottom():擷取控制項的四周內邊距;7)  child.layout(l, t, r, b):設定子View布局的上下左右邊的座標。

 

 

7       其他方法7.1    generateLayoutParams()

generateLayoutParams()方法用在自訂ViewGroup中,用來指明子控制項之間的關係,即與當前的ViewGroup對應的LayoutParams。我們只需要在方法中返回一個我們想要使用的LayoutParams類型的對象即可。

在generateLayoutParams()方法中需要傳入一個AttributeSet對象作為參數,這個對象是這個ViewGroup的屬性集,系統根據這個ViewGroup的屬性集來定義子View的布局規則,供子View使用。

例如,在自訂流式布局中,我們只需要關心子控制項之間的間隔關係,因此我們需要在generateLayoutParams()方法中返回一個new MarginLayoutParams()即可。

 

7.2    onTouchEvent()

onTouchEvent()方法用來監測使用者手指操作。我們通過方法中MotionEvent參數對象的getAction()方法來即時擷取使用者的手勢,有UP、DOWN和MOVE三個枚舉值,分別表示用於手指抬起、按下和滑動的動作。每當使用者有操作時,就會回掉onTouchEvent()方法。

 

7.3    onScrollChanged()

如果我們的自訂View / ViewGroup是繼承自ScrollView / HorizontalScrollView等可以滾動的控制項,就可以通過重寫onScrollChanged()方法來監聽控制項的滾動事件。

這個方法中有四個參數:l和t分別表示當前滑動到的點在水平和豎直方向上的座標;oldl和oldt分別表示上次滑動到的點在水平和豎直方向上的座標。我們可以通過這四個值對滑動進行處理,如添加屬性動畫等。

 

7.4    invalidate()

invalidate()方法的作用是請求View樹進行重繪,即draw()方法,如果視圖的大小發生了變化,還會調用layout()方法。

一般會引起invalidate()操作的函數如下:

1)  直接調用invalidate()方法,請求重新draw(),但只會繪製調用者本身;2)  調用setSelection()方法,請求重新draw(),但只會繪製調用者本身;3)  調用setVisibility()方法,會間接調用invalidate()方法,繼而繪製該View;4)  調用setEnabled()方法,請求重新draw(),但不會重新繪製任何視圖,包括調用者本身。

 

 

7.5    postInvalidate()

功能與invalidate()方法相同,只是postInvalidate()方法是非同步請求重繪視圖。

 

7.6    requestLayout()

requestLayout()方法只是對View樹進行重新布局layout過程(包括measure()過程和layout()過程),不會調用draw()過程,即不會重新繪製任何視圖,包括該調用者本身。

 

7.7    requestFocus()

請求View樹的draw()過程,但只會繪製需要重繪的視圖,即哪個View或ViewGroup調用了這個方法,就重繪哪個視圖。

 

8       總結

最後,讓我們來總覽一下自訂View / ViewGroup時調用的各種函數的順序,如所示:

 

 

在這些方法中:

onMeasure()會在初始化之後調用一到多次來測量控制項或其中的子控制項的寬高;

onLayout()會在onMeasure()方法之後被調用一次,將控制項或其子控制項進行布局;

onDraw()會在onLayout()方法之後調用一次,也會在使用者手指觸控螢幕幕時被調用多次,來繪製控制項。

 

【Android - 進階】之自訂視圖淺析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.