Android自定義View

自定義View的有好幾種分類,可以分成4種:
1.特定的View的子類:Android的API已經為我們提供了不少可以使用的View,如TextView、ImageView、Button等等,但是有時候我們需要在這些基礎的View上擴展一些功能,例如在Button里綁定一個TextWatch監測若干個EditText的輸入情況時,就是繼承Button類,在它的子類進行擴展了。這種自定義View實現難度低,不需要自己支持wrap_content和padding等屬性,非常常見。
2.特定的ViewGroup子類:Android的API也為我們提供了不少可以使用的ViewGroup,如LinearLayout、RelativeLayout等等,但是有時候我們想把實現同一個需求若干個View組合起來,就可以用這種方式的自定義View來打包了。這種自定義View的實現難度低,也不需要自己處理ViewGroup對每個子View的測量和布局,非常常見。
3.View的子類:View是一個很基礎的父類,有一個空的onDraw()方法,繼承它首先就是要實現這個方法,在里面利用Canvas畫出自己想要的內容,不然View是不會顯示任何東西的,使用這種自定義View主要用于實現一些非常規的圖形效果,例如一些動態變化的View等等。這種自定義View的實現難度比較高,除了需要自己重寫onDraw(),還要自己支持wrap_content和padding等屬性,不過這種View也很常見。
4.ViewGroup的子類:ViewGroup是用于實現View的組合布局的基礎類,直接繼承ViewGroup的子類主要是用于實現一些非常規的布局,即不同于官方API給出的LinearLayout等這些的布局。這種這種自定義View的實現難度高,需要處理好ViewGroup和它子View的測量和布局,比較少見。

** 4種自定義View所需的步驟**


Paste_Image.png

自定義屬性
  想要實現自定義的功能,我們有時候就需要一些自己定義的屬性,怎么讓這些屬性可以通過在xml上設置呢?只需要在res/value文件夾里新建一個attrs.xml(名字隨便,建立位置對就行):

<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="Color" format="color"/>
<attr name="inVelocityX" format="integer"/>
<attr name="inVelocityY" format="integer"/>
<attr name="Text" format="string"/>
<attr name="TextColor" format="color"/>

<declare-styleable name="BallView">
    <attr name="color"/>
    <attr name="inVelocityX" />
    <attr name="inVelocityY" />
    <attr name="Text" />
    <attr name="TextColor"/>
</declare-styleable>
</resources>

BallView就是我demo里面的自定義View名字,在declare-styleable外面聲明一些自定義屬性和屬性的類型format,在里面申明BallView需要哪些屬性(當然也可以直接在declare-styleable里面聲明屬性的format,這樣就不需要在外面聲明了,但是這樣的話這些屬性也不能被另一個自定義View重用)。
關于屬性的format有很多種,reference,color,boolean等等,想看全部可以參考這里

在attrs.xml聲明了屬性之后,就可以在View的xml里用了,不過首先要在根ViewGroup里聲明變量空間:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">

    <com.zhjohow.customview.BallView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        cust:color="#ff0000"
        cust:Text="我是一個球"
        cust:TextColor="#ffffff"
        cust:TextSize= "34"
        cust:inVelocityX="6"
        cust:inVelocityY="6"/>

</RelativeLayout>

然后我們就要在自定義View里面獲取這些屬性了,自定義View的構造函數有4個,自定義View必須重寫至少一個構造函數:

public BallView(Context context) {
    super(context);
}

public BallView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public BallView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

//API21之后才使用
public BallView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

4個構造函數中:如果View是在Java代碼里面new的,則調用第一個構造函數;如果是在xml里聲明的,則調用第二個構造函數,我們所需要的自定義屬性也就是從這個AttributeSet參數傳進來的;第三第四個構造函數不會自動調用,一般是在第二個構造主動調用(例如View有style屬性的時候)。如果想深入了解構造函數,可以參考這里這里 所以,我們就可以重寫第二個構造函數那里獲取我們在xml設定的自定義屬性:

  //球的x,y方向速度
private int velocityX = 0,velocityY = 0;
//球的顏色
private int color;
//球里面的文字
private String text;
//文字的顏色
private int textColor;

public BallView(Context context, AttributeSet attrs) {
    super(context, attrs);
    //獲取自定義屬性數組
    TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BallView, 0, 0);
    int n = a.getIndexCount();
    for (int i = 0;i < n;i++){
        int attr = a.getIndex(i);
        switch (attr){
            case R.styleable.BallView_inVelocityX:
                velocityX = a.getInt(attr,0);
                break;
            case R.styleable.BallView_inVelocityY:
                velocityY = a.getInt(attr,0);
                break;
            case R.styleable.BallView_color:
                color = a.getColor(attr,Color.BLUE);
                break;
            case R.styleable.BallView_Text:
                text = a.getString(attr);
                break;
            case R.styleable.BallView_TextColor:
                textColor = a.getColor(attr,Color.RED);
                break;

        }
    }

}

可以看到輸出:

System.out: text:球
System.out: textColor:-1
System.out: velocityX:3
System.out: velocityY:3
System.out: color:-65536

重寫onMeasure()
  關于重寫onMeasure()的解釋,我覺得用BallView不合適,于是就另外開了個TestMeasureView進行測試:   下面是沒有重寫onMeasure()來支持wrap_content的例子:

public class TestMeasureView extends View {
private Paint paint;
public TestMeasureView(Context context) {
    super(context);
}

public TestMeasureView(Context context, AttributeSet attrs) {
    super(context, attrs);

}

public TestMeasureView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.BLUE);

}

}

在xml上使用這個View:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.zhjh.customview.TestMeasureView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
</RelativeLayout>

得出的結果是這樣的:

  這就是為什么View的之類要自己支持wrap_parent的原因了,如果不重寫wrap_parent就被當成match_parent。具體原因可以看一下View的Measure過程,這個是必須了解的,下面的圖(從鏈接里面盜的)是關鍵。
  了解Measure過程之后我們發現我們現在這個TestMeasureView的長寬參數是由父View的測量模式(RelativeLayout的EXACTLY)和自身的參數(wrap_content)決定的(AT_MOST),所以我們就可以重寫onMeasure()讓View支持wrap_content了,下面網上流傳很廣的方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
    int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);
    int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
    int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);
    int width = wSpeSize;
    int height = hSpeSize;

    if (wSpeMode == MeasureSpec.AT_MOST){
        //在這里實現計算需要wrap_content時需要的寬度,這里我直接當作賦值處理了
        width =200;
    }
    if (hSpeMode == MeasureSpec.AT_MOST){
        //在這里實現計算需要wrap_content時需要的高度,這里我直接當作賦值處理了
        height = 200;
    }
    //傳入處理后的寬高
    setMeasuredDimension(width,height);
}

結果是成功的:

網上的很多都是這樣做,通過判斷測量模式是否AT_MOST來判斷View的參數是否是wrap_content,然而,通過上面的表我們發現View的AT_MOST模式對應的不只是wrap_content,還有當父View是AT_MOST模式的時候的match_parent,如果我們這樣做的話,父View是AT_MOST的時候這個自定義View的match_parent不就失效了嗎。   
測試一下,我們把TestMeasureView長寬參數設置為match_parent,然后在外面再包一個模式為AT_MOST的父View(把父View的寬高都設為wrap_content,這樣就確保了模式是AT_MOST,UNSPECIFIED因為不會出現在這里可以忽略):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <com.zhjh.customview.TestMeasureView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
</RelativeLayout>

運行一下,結果果然是match_parent失效:

  所以說看到的東西要思考一下,才能真正地轉化為自己的,然后這個怎么解決呢,很簡單,直接在onMeasure里面判斷參數是否wrap_content就好:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
    int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);
    int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
    int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);
    int width = wSpeSize;
    int height = hSpeSize;
    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
        //在這里實現計算需要wrap_content時需要的寬
        width =200;
    }
    if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
        //在這里實現計算需要wrap_content時需要的高
        height =200;
    }
    //傳入處理后的寬高
    setMeasuredDimension(width,height);
}

然后我把參數設回wrap_content(xml就不貼代碼了),結果是正確的:

  但是這種方法有一個缺陷,就是可能會將UNSPECIFIED的情況也覆蓋掉,但是UNSPECIFIED一般只出現在系統內部的View,不會出現在自定義View,而且當它出現的時候也可以加個判斷按情況解決。

重寫onDraw()
  這里就是利用onDraw()給出的Canvas畫出各種東西了,這里是BallView的onMeasure()方法和onDraw(),通過以下代碼,可以實現在wrap_content的時候根據字的內容長度畫出相應的圓,然后可以根據給出的速度移動,遇到“墻會碰撞”。

  @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
    int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
    int width = wSpeSize ;
    int height = hSpeSize;


    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
        //在這里實現計算需要wrap_content時需要的寬高
        width = bounds.width();

    }else if(getLayoutParams().width != ViewGroup.LayoutParams.MATCH_PARENT){
        width = getLayoutParams().width;
    }
    if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
        //在這里實現計算需要wrap_content時需要的寬高
        height =bounds.height();
    }else if(getLayoutParams().height != ViewGroup.LayoutParams.MATCH_PARENT){
        height = getLayoutParams().height;
    }
    //計算半徑
    radius = Math.max(width,height)/2;

    //傳入處理后的寬高
    setMeasuredDimension((int) (radius*2+1), (int) (radius*2+1));
}


@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawCircle(getWidth()/2,getHeight()/2,radius,paintFill);
    //讓字體處于球中間
    canvas.drawText(text,getWidth()/2,getHeight()/2+bounds.height()/2,paintText);
    checkCrashScreen();
    offsetLeftAndRight(velocityX);
    offsetTopAndBottom(velocityY);
    postInvalidateDelayed(10);
}

//檢測碰撞,有碰撞就反彈
private void checkCrashScreen(){
    if ((getLeft() <= 0 && velocityX < 0)){
        velocityX = -velocityX ;

    }
    if (getRight() >= screenWidth && velocityX > 0){
        velocityX = -velocityX ;
    }
    if ((getTop() <= 0 && velocityY < 0)) {
        velocityY = -velocityY ;

    }
    if (getBottom() >= screenHeight -sbHeight && velocityY > 0){
        velocityY = -velocityY ;
    }
}

最后結果:


  
重寫自身和子類的onMesure()和onLayout()
     上面是以自定義View為例子,這次就以一個自定義ViewGroup做為例子,做一個很簡單的可以按照斜向下依次排列View的ViewGroup,類似于LinearLayout。要做一個新的ViewGroup,首先就是要重寫它的onMesure()方法,讓它可以按照需求測量子View和自身的寬高,還可以在這里支持wrap_content。
     onMesure()和onLayout()是干什么的呢?為什么需要重寫的是它們?因為View的繪制過程大概是Measure(測量)→Layout(定位)→Draw(繪圖)三個過程,至于具體是怎樣的呢?可以看工匠若水的這篇文章,看不懂沒關系,可以看圖。。。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
    int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);


    // 計算出所有的childView的寬和高
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    int cCount = getChildCount();
    int width = 0;
    int height = 0;
    //處理WRAP_CONTENT情況,把所有子View的寬高加起來作為自己的寬高
    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
        for (int i = 0; i < cCount; i++){
            View childView = getChildAt(i);
            width += childView.getMeasuredWidth();
        }
    }else {
        width = sizeWidth;
    }
    if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
        for (int i = 0; i < cCount; i++){
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();
        }
    }else {
        height =sizeHeight;
    }
    //傳入處理后的寬高
    setMeasuredDimension(width,height);
}

還有通過重寫onLayout()把子View一個個排序斜向放好:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int cCount = getChildCount();
    int sPointX = 0;
    int sPointY = 0;
    int cWidth = 0;
    int cHeight = 0;
    //遍歷子View,根據它們的寬高定位
    for (int i = 0; i < cCount; i++){
        View childView = getChildAt(i);
        //這里使用getMeasuredXXX()方法是因為還沒layout完,使用getWidth()和getHeight()獲取會得不到正確的寬高
        cWidth = childView.getMeasuredWidth();
        cHeight = childView.getMeasuredHeight();
        //定位
        childView.layout(sPointX,sPointY,sPointX + cWidth,sPointY + cHeight);
        sPointX += cWidth;
        sPointY += cHeight;
    }
}

結果: 參數為WRAP_CONTENT的時候,成功地顯示了:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.zhjh.customview.InclinedLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000fff">
    <TextView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:text="1"
        android:background="#fff000"/>
    <TextView
        android:layout_width="20dp"
        android:layout_height="50dp"
        android:text="2"
        android:background="#00ff00"/>
    <TextView
        android:layout_width="50dp"
        android:layout_height="30dp"
        android:text="3"
        android:background="#ff0000"/>
 </com.zhjh.customview.InclinedLayout>

</RelativeLayout>

還有match_parent的時候:

  這樣斜向下排列的ViewGroup就完成了,這些只是最簡單的一個demo,用于我們熟悉自定義View的步驟,掌握了這些,復雜的自定義View也可以一步一步地完成了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容