標籤:android 自訂視圖 自訂屬性
這是Android UI Fundamentals裡的內容
建立自訂視圖
建立自訂UI組件首先要繼承一個視圖類.
首先建立一個簡單的自訂視圖, 展示一條十字線.
需要做的第一件事是建立一個繼承自View的CrossView類.
public CrossView(Context context, AttributeSet attrs) { super(context, attrs); }
該建構函式的第二個參數是用來傳遞XML參數的, 等會兒會講到. 接下來我們要重寫兩個基礎方法: onMeasure 和 onDraw.
onMeasure
系統調用onMeasure方法來決定視圖及其子視圖的尺寸. 它的兩個參數的類型都是int, 但是這倆參數並非普通的數字, 而是兩個MeasureSpec, MeasureSpec是一個模式和一個整型尺寸值的結合, 被當成一個整數來實現. 其中模式值有如下幾種情況:
| 模式 |
解釋 |
| UNSPECIFIED |
父視圖沒有在這個視圖上做任何限制, 它可以是任意尺寸 |
| AT_MOST |
該視圖可以是小於等於MeasureSpec中尺寸的任意大小 |
| EXACTLY |
父視圖要求該視圖必須是MeasureSpec指定的尺寸大小 |
當你建立一個自訂視圖並重寫onMeasure方法時, 必須正確處理每種情況, 得到相應的尺寸, 然後必須在onMeasure中調用setMeasureDimensions方法, 參數就是你決定的尺寸, 如果不調用就會拋出異常.
下面是重寫的onMeasure方法代碼.
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(calculateMeasure(widthMeasureSpec), calculateMeasure(heightMeasureSpec)); }
注意其中calculateMeasure方法是我們自己定義的, 下面我們來完成這個方法.
我們先定義一個預設的尺寸100, 單位是dp(我暫時不確定是不是dp).
private static final int DEFAULT_SIZE = 100;
乘上裝置的像素密度, 得到實際顯示需要的像素值.
int result = (int) (DEFAULT_SIZE * getResources().getDisplayMetrics().density);
然後我們需要從MeasureSpec中拿到模式和尺寸
int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);
接下來我們根據specMode的情況來判斷result的值到底應該是什麼.
MeasureSpec.UNSPECIFIED
此時父控制項對自訂視圖的尺寸沒有要求, 那麼我就以預設大小為結果, 也就是說
int result = (int) (DEFAULT_SIZE * getResources().getDisplayMetrics().density);
MeasureSpec.AT_MOST
此時父控制項認為最多不能超過指定尺寸值, 那麼此時我們選指定值和預設值中最小的那個就行, 無論哪種情況這種選法都是合法的.
result = Math.min(specSize, result);
MeasureSpec.EXACTLY
此時父控制項要求子視圖必須是給定的尺寸, 那麼我們讓result等於它就好
result = specSize;
綜合上面的討論, 最終我們的方法代碼如下:
private static final int DEFAULT_SIZE = 100; private int calculateMeasure(int measureSpec) { int result = (int) (DEFAULT_SIZE * getResources().getDisplayMetrics().density); int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); if (specMode == MeasureSpec.EXACTLY) { result = specSize; } else if (specMode == MeasureSpec.AT_MOST) { result = Math.min(specSize, result); } return result; }
onDraw
當視圖應當繪製其內容時會調用onDraw方法. 在重寫它之前, 我們先建立一個Paint對象, 它處理諸如顏色和文字大小之類的事情.
通過CrossView的建構函式來建立Paint對象
private Paint mPaint; public CrossView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(0xffff0000); }
上面的代碼建立了Paint對象, 並設定消除鋸齒和顏色.
接下來重寫onDraw方法, 模板如下, canvas.save()和canvas.restore()我就不解釋了, 不影響後面的理解.
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); // code goes here canvas.restore(); }
我們基於視圖的尺寸縮放畫布, 這樣我們可以使用0到1之間的浮點數來作為畫線時的座標
private static final float[] mPoints = {0.5f, 0f, 0.5f, 1f, 0f, 0.5f, 1f, 0.5f}; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.scale(getWidth(), getHeight()); canvas.drawLines(mPoints, mPaint); canvas.restore(); }
我們在activity的xml裡面加入我們的自訂控制項
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <com.shaw.uitest.CrossView android:layout_width="wrap_content" android:layout_height="wrap_content" /> <com.shaw.uitest.CrossView android:layout_marginTop="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /></LinearLayout>
運行一下就可以看到文章開頭的畫面了.
向自訂視圖中添加自訂屬性
有了自訂視圖, 我們希望它能通過自訂XML屬性來配置, 要做到這一點, 需要先聲明屬性, 然後在XML布局中添加一個新的命名空間, 最後處理被傳遞給自訂視圖建構函式的AttributeSet對象.
聲明屬性
在res/values/目錄下建立一個attrs.xml(可以是別的名字)的檔案, 然後在其中添加如下內容:
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="cross"> <attr name="android:color" /> <attr name="rotation" format="string" /> </declare-styleable></resources>
declare-styleable元素有一個name屬性, 用來在代碼中的引用自訂屬性, 每個自訂的屬性都使用一個attr元素來聲明, attr元素有name和format兩個屬性, name用於引用, format代表它的資料類型, 如果使用了預設的系統屬性, 就不需要定義format了, 如果嘗試給已有的屬性定義一個不同的format, 則工程無法build. 在外層聲明的attr可以被其他declare-styleable複用, 就和使用系統屬性一樣, 比如:
<?xml version="1.0" encoding="utf-8"?><resources> <attr name="test" format="string" /> <declare-styleable name="foo"> <attr name="test" /> </declare-styleable> <declare-styleable name="bar"> <attr name="test" /> </declare-styleable></resources>
也可以給屬性建立自訂值, 例如
<attr name="enum_attr"> <enum name="value1" value="1" /> <enum name="value2" value="2" /></attr><attr name="flag_attr"> <flag name="flag1" value="0x01" /> <flag name="flag2" value="0x02" /></attr>
enum和flag都要求是整數. 不同之處在於flag可以使用|來拼接. 比如android:gravity的值就是flag.
在XML中使用屬性
要使用在我們的XML中的新屬性, 首先必須為視圖聲明namespace. 其實我們經常見到namespace的聲明, 比如我們常在activity的xml檔案中看到
xmlns:android="http://schemas.android.com/apk/res/android"
這個namespace聲明了所有以關鍵詞android開頭的屬性都可以在android包中找到. 要使用自訂屬性, 需要聲明一個帶有新包名的新namespace, 下面為CrossView的屬性添加一個新的namespace, 並在自訂視圖中添加相關的xml配置:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:crossview="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <com.shaw.uitest.CrossView android:layout_width="wrap_content" android:layout_height="wrap_content" crossview:rotation="30" android:color="#ff0000ff"/> <com.shaw.uitest.CrossView android:layout_marginTop="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" crossview:rotation="50" android:color="#ff00ff00"/></LinearLayout>
上面聲明了所有以crossview(名字可以用別的)開頭的屬性都可以在res中找到. 這是Gradle要求的寫法.
在代碼中使用XML屬性
在CrossView的建構函式中傳入了一個AttributeSet對象, 我們可以通過它擷取XML布局中聲明的屬性.
更新CrossView的建構函式並添加相應函數和成員變數:
private float mRotation; public CrossView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(); mPaint.setAntiAlias(true); TypedArray arr = getContext().obtainStyledAttributes(attrs, R.styleable.cross); int color = arr.getColor(R.styleable.cross_android_color, Color.BLACK); float rotation = arr.getFloat(R.styleable.cross_rotation, 0f); arr.recycle(); setColor(color); setRotation(rotation); } public void setColor(int color) { mPaint.setColor(color); } public void setRotation(float degree) { mRotation = degree; }
同時更新onDraw的代碼
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.scale(getWidth(), getHeight()); canvas.rotate(mRotation, 0.5f, 0.5f); canvas.drawLines(mPoints, mPaint); canvas.restore(); }
我們的旋轉中心是畫布中心, 而不是左上方.
現在運行這個程式, 如下:
Android自訂視圖與自訂屬性