標籤:
概述
在開發過程中,經常會遇到系統中提供的控制項無法滿足產品的設計需求,這時可能就需要考慮使用自訂的View來實現產品的設計細節了。對於自訂View,可以分為兩種,一種是自訂控制項(繼承View),另一種是自訂版面配置容器(繼承ViewGroup),下面就針對自訂控制項View的應用進行簡單的總結。
自訂View
自訂View時,我們大部分只需要重寫兩個方法onMeasure(),onDraw()。onMeasure()負責對當前View尺寸進行測量,onDraw()負責把當前這個View繪製出來。另外在自訂View時,還需要寫一個帶AttributeSet參數的構造方法。
定義構造方法
public CustView(Context context) { super(context); } public CustView(Context context, AttributeSet attrs) { super(context, attrs); }
上面兩個不同的構造方法,表示了在建立該View時,可以有兩種不同的方式。如果直接使用代碼的形式建立CustView,調用的是第一種構造方法。如:
CustView custView = new CustView(this);
如果通過XML布局檔案建立這個View的對象時,那麼系統自動調用第二個構造方法。該方法不能被省略,因為系統從這個方法中獲得該View設定在XML布局檔案中的屬性。否則的話,系統將會拋出異常。有關這個方法,Google 文檔這樣描述:
public View(Context context, AttributeSet attrs)
Constructor that is called when inflating a view from XML. This is called when a view is being constructed from an XML file, supplying attributes that were specified in the XML file. This version uses a default style of 0, so the only attribute values applied are those in the Context’s Theme and the given AttributeSet.
這段話大概的意思就是,當從XML布局中填充一個View的時候,這個構造方法就會被調用。當View正在從XML布局中建立並提供在XML檔案中指定的屬性時會被調用。
所以說XML布局中設定該View的屬性通過該方法傳遞進來。
重寫onMeasure()方法
上面所說onMeasure()方法是用來測量View的寬高尺寸的。在XML布局檔案中,我們可以對屬性layout_width和layout_height可以不寫具體的尺寸,而是用wrap_content或match_parent。這兩個參數的意思就是“包裹住內容”和“匹配父布局給我們的所有空間”。預設情況下系統會針對上面的屬性值進行處理。我們可以通過重寫onMeasure(),來設定自己想要的尺寸。方法原型如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
這個方法中包含了兩個參數widthMeasureSpec和heightMeasureSpec。這兩個參數代表了水平空間上父容器施加的要素和垂直空間上父容器施加的要素。說的通俗點,就是它們包含了寬高的尺寸大小和測量模式。
我們都知道設定寬高時可以有三個選擇,一個具體的數值,match_parent,和 wrap_content。而測量模式同樣也有三種:UNSPECIFIED,EXACTLY,AT_MOST。我們可以用MeasureSpec類取得上面參數對應的大小和測量模式。比如:
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
這三種測量模式分別代表的意義:
UNSPECIFIED:父容器沒有對當前View強加任何限制,當前View可以顯示任意尺寸大小。
EXACTLY:父容器確定了當前View的具體尺寸,View被限制在該尺寸中。當前的尺寸就是View應該設定的尺寸大小。
AT_MOST:當前尺寸是View能取得的最大尺寸。
上面三種測量模式與我們設定寬高屬性的值對應的關係如下:
match_parent 對應的是 EXACTLY:因為match_parent 表示的匹配父控制項的大小,而父View的大小是卻定的,所以該View的尺寸同樣確定,所以測量模式為EXACTLY。
wrap_content 對應的是 AT_MOST:wrap_content 表示的是大小為包裹View中的內容,以父View作為參考尺寸,不超過父View的大小就可以。
固定尺寸(100dp)對應的是EXACTLY:使用者指定了尺寸大小,與match_parent 性質一致。
下面我們重寫onMeasure方法實現View的寬高為正方形。預設寬高為100px,可以設定其他大小值(大於預設值)
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getSize(100, widthMeasureSpec); int height = getSize(100, heightMeasureSpec); if(width > height){ width = height; }else{ height = width; } setMeasuredDimension(width, height); //將新計算的寬高測量值進行儲存,否則不生效 } private int getSize(int defaultSize, int measureSpec) { int mySize = defaultSize; int size = MeasureSpec.getSize(measureSpec); int mode = MeasureSpec.getMode(measureSpec); switch (mode) { case MeasureSpec.UNSPECIFIED: // 沒有指定大小,設定預設大小. mySize = defaultSize; break; case MeasureSpec.EXACTLY: // 如果布局中設定的值大於預設值,則使用布局中設定的值,對應match_parent和固定值 if(size > defaultSize){ mySize = size; } break; case MeasureSpec.AT_MOST: // 如果測量模式中的值大於預設值,取預設值,對應wrap_content if(size > defaultSize){ mySize = defaultSize; } break; } return mySize; }
布局檔案設定:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/view_pager_box" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <com.yuminfeng.myviewpager.CustView android:id="@+id/custView" android:layout_width="match_parent" android:layout_height="100dp" android:background="@android:color/holo_red_dark" /></LinearLayout>
運行後如下:
如果登出掉onMeasure方法時,如下:
重寫onDraw()
方法onDraw用來繪製使用者想要的圖形效果。下面我們實現了在View上繪製一個空心圓,如下:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int r = getMeasuredWidth()/2; //使用getMeasuredWidth 可以獲得在onMeasure方法中新計算的寬度 int cx = getLeft() + r; //getLeft 表示的是當前View的左邊到螢幕左邊框的位置距離。這裡不建議使用,因為如果view在左邊使用其他View時,會影響該值。 canvas.drawCircle(r, r, r-20, mPaint); //該方法中的四個參數,前兩個表示圓心的x,y的座標。這兩個值表示的是相對於該View中的位置,與其他view的位置無關。 }
上面還涉及到了一個Paint類,該類只需要在構造方法中初始化即可,方法:
private void initPaint() { mPaint = new Paint(); mPaint.setAntiAlias(true); //設定消除鋸齒的效果 mPaint.setStyle(Paint.Style.STROKE); //設定畫筆樣式為描邊 mPaint.setStrokeWidth(3); //設定筆刷的粗細度 mPaint.setColor(Color.BLUE); //設定畫筆的顏色 }
上面代碼的執行效果,
添加自訂屬性
我們為自訂View添加自訂屬性,這樣可以方便使用者使用該View應對不同的情境。使得該自訂View的擴充性更好。
首先,我們需要在/res/values目錄下建立一個名為attrs.xml的檔案
<?xml version="1.0" encoding="utf-8"?><resources> <!-- name為屬性的名稱,可以隨便取,但是建議使用view一樣的名稱 --> <declare-styleable name="CustView"> <!--聲明我們的屬性,這裡要注意format的值,不可亂用--> <attr name="paintColor" format="color" /> <attr name="paintSize" format="dimension" /> </declare-styleable></resources>
上面定義屬性的檔案中,每個屬性的名稱都有一個對應的format值,每個format值都代表這不同的含義,主要有一下幾個值:
建立完attrs.xml檔案後,然後我們將attrs.xml中屬性應用到布局檔案中
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custView="http://schemas.android.com/apk/res/com.yuminfeng.myviewpager" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <com.yuminfeng.myviewpager.CustView android:id="@+id/custView" android:layout_width="match_parent" android:layout_height="100dp" custView:paintColor="@android:color/holo_orange_dark" custView:paintSize="10dp" /></LinearLayout>
注意:布局檔案中使用自訂的屬性時,必須要設定命名空間。命名控制項的名稱可以隨便取,如:custView。但是命名空間的值是由固定的首碼“http://schemas.android.com/apk/res/”+“包名”組成。完成命名空間的設定後,我們才可以在控制項屬性中添加自訂的屬性了,如:custView:paintColor和custView:paintSize。
最後,我們在代碼中調用自訂屬性,如下:
private void initAttrs(Context context,AttributeSet attrs) { //通過該方法,可以取出attrs中的CustView屬性。CustView就是我們在attrs.xml檔案中declare-styleable標籤名稱。 TypedArray type = context.obtainStyledAttributes(attrs, R.styleable.CustView); //取出每個標籤屬性,命名方式為:R.styleable + 屬性集合名稱 + 底線 + 屬性名稱,如果沒有該值,則使用預設值 paintColor = type.getColor(R.styleable.CustView_paintColor, Color.BLACK); paintSize = (int) type.getDimension(R.styleable.CustView_paintSize, 3); //最後需要將TypedArray對象回收 type.recycle(); }
完整的代碼如下:
package com.yuminfeng.myviewpager;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.util.AttributeSet;import android.view.View;public class CustView extends View { private Paint mPaint; private int paintColor; private int paintSize; public CustView(Context context) { super(context); initPaint(); //初始化畫筆 } public CustView(Context context, AttributeSet attrs) { super(context, attrs); initAttrs(context,attrs); //初始化屬性值 initPaint(); //初始化畫筆 } private void initAttrs(Context context,AttributeSet attrs) { //通過該方法,可以取出attrs中的CustView屬性。CustView就是我們在attrs.xml檔案中declare-styleable標籤名稱。 TypedArray type = context.obtainStyledAttributes(attrs, R.styleable.CustView); //取出每個標籤屬性,命名方式為:R.styleable + 屬性集合名稱 + 底線 + 屬性名稱,如果沒有該值,則使用預設值 paintColor = type.getColor(R.styleable.CustView_paintColor, Color.BLACK); paintSize = (int) type.getDimension(R.styleable.CustView_paintSize, 3); //最後需要將TypedArray對象回收 type.recycle(); } private void initPaint() { mPaint = new Paint(); mPaint.setAntiAlias(true); //設定消除鋸齒的效果 mPaint.setStyle(Paint.Style.STROKE); //設定畫筆樣式為描邊 mPaint.setStrokeWidth(paintSize); //設定筆刷的粗細度 mPaint.setColor(paintColor); //設定畫筆的顏色 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getSize(100, widthMeasureSpec); int height = getSize(100, heightMeasureSpec); if(width > height){ width = height; }else{ height = width; } setMeasuredDimension(width, height); //將新計算的寬高測量值進行儲存,否則不生效 } private int getSize(int defaultSize, int measureSpec) { int mySize = defaultSize; int size = MeasureSpec.getSize(measureSpec); int mode = MeasureSpec.getMode(measureSpec); switch (mode) { case MeasureSpec.UNSPECIFIED: // 沒有指定大小,設定預設大小. mySize = defaultSize; break; case MeasureSpec.EXACTLY: // 如果布局中設定的值大於預設值,則使用布局中設定的值,對應match_parent和固定值 if(size > defaultSize){ mySize = size; } break; case MeasureSpec.AT_MOST: // 如果測量模式中的值大於預設值,取預設值,對應wrap_content if(size > defaultSize){ mySize = defaultSize; } break; } return mySize; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int r = getMeasuredWidth()/2; //使用getMeasuredWidth 可以獲得在onMeasure方法中新計算的寬度 int cx = getLeft() + r; //getLeft 表示的是當前View的左邊到螢幕左邊框的位置距離。這裡不建議使用,因為如果view在左邊使用其他View時,會影響該值。 canvas.drawCircle(r, r, r-20, mPaint); //該方法中的四個參數,前兩個表示圓心的x,y的座標。這兩個值表示的是相對於該View中的位置,與其他view的位置無關。 }}
上面代碼的執行效果,
到此,自訂View就結束了,下一篇我將繼續講述自定ViewGroup。
Android 中自訂View的初步總結