本方轉載自https://www.cnblogs.com/itgungnir/p/6217447.html
【Android - 自定義View】之自定義View淺析
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。
這16中情況的具體解釋如下:
1.PorterDuff.Mode.CLEAR:所繪製不會提交到畫布上。 2.PorterDuff.Mode.SRC:顯示上層繪製圖片 3.PorterDuff.Mode.DST:顯示下層繪製圖片 4.PorterDuff.Mode.SRC_OVER:正常繪製顯示,上下層繪製疊蓋。 5.PorterDuff.Mode.DST_OVER:上下層都顯示。下層居上顯示。 6.PorterDuff.Mode.SRC_IN:取兩層繪製交集。顯示上層。 7.PorterDuff.Mode.DST_IN:取兩層繪製交集。顯示下層。 8.PorterDuff.Mode.SRC_OUT:上層繪製非交集部分。 9.PorterDuff.Mode.DST_OUT:取下層繪製非交集部分。 10.PorterDuff.Mode.SRC_ATOP:取下層非交集部分與上層交集部分 11.PorterDuff.Mode.DST_ATOP:取上層非交集部分與下層交集部分 12.PorterDuff.Mode.XOR:異或:去除兩圖層交集部分 13.PorterDuff.Mode.DARKEN:取兩圖層全部區域,交集部分顏色加深 14.PorterDuff.Mode.LIGHTEN:取兩圖層全部,點亮交集部分顏色 15.PorterDuff.Mode.MULTIPLY:取兩圖層交集部分疊加後顏色 16.PorterDuff.Mode.SCREEN:取兩圖層全部區域,交集部分變爲透明色
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個子View 3) 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()方法之後調用一次,也會在用戶手指觸摸屏幕時被調用多次,來繪製控件。