當初剛入門Android時用的都是原生的控件,剛開始覺得原生的控件其實也可以滿足當時的一些學校的小項目開發,也就沒怎麼深入自定義view。但參加工作後,發現有時美工給的設計圖某些功能實現起來還是挺刁鑽的,於是便開始了自定義view的學習。或許很多人都覺得自定義view是個很難的東西,其實當你真正用心去弄了幾個自定義view之後就會發現其實也並沒有那麼難。由於個人工作效率還是蠻快的,項目之餘閒蛋疼的很,常常自己看到那些好玩的東西就用自定義view畫下來。
自定義view的基本步驟無非也就那麼幾步:
1. values文件夾下創建attrs.xml文件,在attrs裏添加你想給自己view添加的屬性。例:
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyTextView">
<attr name="text" format="string"/>
<attr name="textSize" format="dimension"/>
</declare-styleable>
</resources>
declare-styleable是你自定義view的一套新定義的屬性,下面包含了你要定義的各種屬性attr
format是指定attr屬性的單位,其中包括:
(1) reference: 引用某一資源,如:src="@drawable/sourcename";
(2)color:顏色,如color="#ff0000";
(3)boolean:布爾值,true或false;
(4)dimension:尺寸值,如sp,dp,px;
(5)float:浮點型,也就是小數,如0.5, 1.8;
(6)integer:整形, 如 1, 100;
(7)string:字符串
(8)fraction:百分數, 如100%
(9)enum:枚舉,如 orientation="vertical"
(10)flag:位或運算,如gravity="centerHorizontal | right"
2. 創建類文件,添加構造體,獲取屬性並初始化變量。例:
public class MyTextView extends View {
private String text;
private int textSize;
private Paint paint;
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
//顧名思義,獲取風格和屬性,得到一個包含各種屬性的數組array,包括你自定義的attr屬性
//R.styleable.MyTextView就是一個指向你剛在attrs.xml中自定義的屬性數組的id
TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
//獲取文本內容
text=array.getString(R.styleable.MyTextView_text);
//獲取文本字體大小,第二個參數是默認值,就是沒有使用你定義屬性時的提供值, sp2px()是sp轉px函數。
textSize=array.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(18));
//這玩意初始化完成後務必回收
array.recycle();
//畫筆初始化,用於後面的繪圖;
paint=new Paint();
//至此,完成變量的初始化
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
// TODO Auto-generated constructor stub
//若使用xml加載view,必須要重寫上面或這個構造體
}
public MyTextView(Context context) {
this(context, null);
// TODO Auto-generated constructor stub
//統一到第一個構造體進行初始化
}
}
3. 重寫onMeasure(),,測量view,確定view的尺寸。(這步並不是自定義view的必要步驟,但重寫後可以適應wrap_content這參數等)
這一步因爲不是必須,可以跳過,但當你在設置layout_width和layout_height的時候只能設match_parent或指定值,不然設置wrap_content會很彆扭。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
//widthMeasureSpec參數可以被MeasureSpec類的靜態方法解析出寬度計算的模式和值
//模式有AT_MOST, EXACTLY, UNSPECIFIED
int width=measureViewWidth(widthMeasureSpec);
//計算高度,和寬度處理差不多
int height=measureViewHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
//處理view的寬度
private int measureViewWidth(int widthSpec){
int result=0;
int mode=MeasureSpec.getMode(widthSpec);
int width=MeasureSpec.getSize(widthSpec);
//對應wrap_content, viewgroup只提供一個最大值,子view尺寸不能超過這個值
//這種情況下,可以根據內容大小設置view的大小,如令view的width=text的寬度
if(mode==MeasureSpec.AT_MOST){
int textWidth=measureTextWidth();
result=Math.min(textWidth, width);
}
//對應match_parent或指定的值,viewgourp提供的值爲parent的寬度或指定的寬度
if(mode==MeasureSpec.EXACTLY){
result=width;
}
return result;
}
//處理view的高度
private int measureViewHeight(int heightSpec){
int result=0;
int mode=MeasureSpec.getMode(heightSpec);
int height=MeasureSpec.getSize(heightSpec);
if(mode==MeasureSpec.AT_MOST){
int textHeight=measureTextHeight();
result=Math.min(textHeight, height);
}
if(mode==MeasureSpec.EXACTLY){
result=height;
}
return result;
}
//測量text的寬度
private int measureTextWidth(){
int textWidth=(int) paint.measureText(text);
return textWidth;
}
//測量text的高度
private int measureTextHeight(){
FontMetrics fm=paint.getFontMetrics();
int textHeight=(int) (fm.bottom-fm.top);
return textHeight;
}
widthMeasureSpec和heightMeasureSpec兩個值是viewgroup傳給子view的,通過MeasureSpec的解析後再根據模式來計算最後的值,若是wrap_content則計算view內容的尺寸再計算view的尺寸,若是match_parent或指定值,則直接使用viewgroup傳過來的值,經過處理後,最後還要調用setMeasuredDimension來確定view的最終尺寸。
view的尺寸設置爲wrap_content情況下,左邊沒有重寫onMeasure,viewgroup會傳一個父組件可分配給子view的最大尺寸,所以子view的尺寸便和父容器一樣大了;而右邊的重寫了onMeasure之後,因爲經過處理,使子view尺寸等於文本內容大小,所以尺寸只有文本大小。
4.重寫onDraw(), 在一塊空白的View上繪製你想要的東西,這一步是最重要的。如:
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
//把view的背景繪成黃色
canvas.drawColor(Color.YELLOW);
//測量繪製字體的高度
FontMetrics fm=paint.getFontMetrics();
int textHeight=(int) (fm.bottom-fm.top);
//參數1.要繪製的文本, 2.文本左邊位於view的x座標, 3.文本baseline位於view的y座標, 4.畫筆
//因爲baseline到文本底部的距離無法獲取,只能取文本高度的3/10
canvas.drawText(text, 0, textHeight-textHeight*0.3f, paint);
}
canvas類封裝了一大堆繪圖工具,所以畫圖並不是很難的事,不過若要實現比較複雜的圖,那就需要懂得一些幾何計算知識了,此處只是把文本簡單地畫上去而已。
還有那個文本的高度處理不懂的可以百度搜索android baseline或android測量字體高度。
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.test"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.test.MyTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:text="aaaaaaaaaaagggggggggggggggg"
custom:textSize="18sp"/>
</LinearLayout>
下面的custom屬性命名空間必須加上xmlns:custom="http://schemas.android.com/apk/res/com.example.test"才能用
格式: xmlns:定義的空間名稱="http://schemas.android.com/apk/res/在AndroidManifest中的包名。
至此,一個簡單的自定義view就實現了,看起來代碼挺多的,但真正去把它寫完後,就感覺其實自定義view也就這樣而已。當然,簡單的view只要實現以上幾個步驟,基本就可以滿足需要了,如果要實現華麗的效果,僅僅是上面幾個步驟不夠的, 還要重新onTouchEvent等函數,使view能處理觸摸事件從而達到交互效果。
下面貼出完整代碼:
MyTextView.java
public class MyTextView extends View {
private String text;
private int textSize;
private Paint paint;
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
//顧名思義,獲取風格和屬性,得到一個包含各種屬性的數組array,包括你自定義的attr屬性
//R.styleable.MyTextView就是一個指向你剛在attrs.xml中自定義的屬性數組的id
TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
//獲取文本內容
text=array.getString(R.styleable.MyTextView_text);
//獲取文本字體大小,第二個參數是默認值,就是沒有使用你定義屬性時的提供值, sp2px()是sp轉px函數。
textSize=array.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(18));
//這玩意初始化完成後務必回收
array.recycle();
//畫筆初始化,用於後面的繪圖;
paint=new Paint();
//至此,完成變量的初始化
paint.setTextSize(textSize);
}
public MyTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
// TODO Auto-generated constructor stub
//若使用xml加載view,必須要重寫上面或這個構造體
}
public MyTextView(Context context) {
this(context, null);
// TODO Auto-generated constructor stub
//統一到第一個構造體進行初始化
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
//widthMeasureSpec參數可以被MeasureSpec類的靜態方法解析出寬度計算的模式和值
//模式有AT_MOST, EXACTLY, UNSPECIFIED
int width=measureViewWidth(widthMeasureSpec);
//計算高度,和寬度處理差不多
int height=measureViewHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
//處理view的寬度
private int measureViewWidth(int widthSpec){
int result=0;
int mode=MeasureSpec.getMode(widthSpec);
int width=MeasureSpec.getSize(widthSpec);
//對應wrap_content, viewgroup只提供一個最大值,子view尺寸不能超過這個值
//這種情況下,可以根據內容大小設置view的大小,如令view的width=text的寬度
if(mode==MeasureSpec.AT_MOST){
int textWidth=measureTextWidth();
result=Math.min(textWidth, width);
}
//對應match_parent或指定的值,viewgourp提供的值爲parent的寬度或指定的寬度
if(mode==MeasureSpec.EXACTLY){
result=width;
}
return result;
}
//處理view的高度
private int measureViewHeight(int heightSpec){
int result=0;
int mode=MeasureSpec.getMode(heightSpec);
int height=MeasureSpec.getSize(heightSpec);
if(mode==MeasureSpec.AT_MOST){
int textHeight=measureTextHeight();
result=Math.min(textHeight, height);
}
if(mode==MeasureSpec.EXACTLY){
result=height;
}
return result;
}
//測量text的寬度
private int measureTextWidth(){
int textWidth=(int) paint.measureText(text);
return textWidth;
}
//測量text的高度
private int measureTextHeight(){
FontMetrics fm=paint.getFontMetrics();
int textHeight=(int) (fm.bottom-fm.top);
return textHeight;
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
//把view的背景繪成黃色
canvas.drawColor(Color.YELLOW);
//測量繪製字體的高度
FontMetrics fm=paint.getFontMetrics();
int textHeight=(int) (fm.bottom-fm.top);
//參數1.要繪製的文本, 2.文本左邊位於view的x座標, 3.文本baseline位於view的y座標, 4.畫筆
//因爲baseline到文本底部的距離無法獲取,只能取文本高度的3/10
canvas.drawText(text, 0, textHeight-textHeight*0.3f, paint);
}
//sp轉px單位
private int sp2px(int sp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}
}
佈局文件很簡單就不貼了, attrs文件也很簡單,在上面了。