Android自定義View

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

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


Paste_Image.png

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

<?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需要哪些屬性(當(dāng)然也可以直接在declare-styleable里面聲明屬性的format,這樣就不需要在外面聲明了,但是這樣的話這些屬性也不能被另一個(gè)自定義View重用)。
關(guān)于屬性的format有很多種,reference,color,boolean等等,想看全部可以參考這里。

在attrs.xml聲明了屬性之后,就可以在View的xml里用了,不過(guò)首先要在根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="我是一個(gè)球"
        cust:TextColor="#ffffff"
        cust:TextSize= "34"
        cust:inVelocityX="6"
        cust:inVelocityY="6"/>

</RelativeLayout>

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

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個(gè)構(gòu)造函數(shù)中:如果View是在Java代碼里面new的,則調(diào)用第一個(gè)構(gòu)造函數(shù);如果是在xml里聲明的,則調(diào)用第二個(gè)構(gòu)造函數(shù),我們所需要的自定義屬性也就是從這個(gè)AttributeSet參數(shù)傳進(jìn)來(lái)的;第三第四個(gè)構(gòu)造函數(shù)不會(huì)自動(dòng)調(diào)用,一般是在第二個(gè)構(gòu)造主動(dòng)調(diào)用(例如View有style屬性的時(shí)候)。如果想深入了解構(gòu)造函數(shù),可以參考這里這里 所以,我們就可以重寫第二個(gè)構(gòu)造函數(shù)那里獲取我們?cè)趚ml設(shè)定的自定義屬性:

  //球的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);
    //獲取自定義屬性數(shù)組
    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()
  關(guān)于重寫onMeasure()的解釋,我覺得用BallView不合適,于是就另外開了個(gè)TestMeasureView進(jìn)行測(cè)試:   下面是沒(méi)有重寫onMeasure()來(lái)支持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上使用這個(gè)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>

得出的結(jié)果是這樣的:

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

@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){
        //在這里實(shí)現(xiàn)計(jì)算需要wrap_content時(shí)需要的寬度,這里我直接當(dāng)作賦值處理了
        width =200;
    }
    if (hSpeMode == MeasureSpec.AT_MOST){
        //在這里實(shí)現(xiàn)計(jì)算需要wrap_content時(shí)需要的高度,這里我直接當(dāng)作賦值處理了
        height = 200;
    }
    //傳入處理后的寬高
    setMeasuredDimension(width,height);
}

結(jié)果是成功的:

網(wǎng)上的很多都是這樣做,通過(guò)判斷測(cè)量模式是否AT_MOST來(lái)判斷View的參數(shù)是否是wrap_content,然而,通過(guò)上面的表我們發(fā)現(xiàn)View的AT_MOST模式對(duì)應(yīng)的不只是wrap_content,還有當(dāng)父View是AT_MOST模式的時(shí)候的match_parent,如果我們這樣做的話,父View是AT_MOST的時(shí)候這個(gè)自定義View的match_parent不就失效了嗎。   
測(cè)試一下,我們把TestMeasureView長(zhǎng)寬參數(shù)設(shè)置為match_parent,然后在外面再包一個(gè)模式為AT_MOST的父View(把父View的寬高都設(shè)為wrap_content,這樣就確保了模式是AT_MOST,UNSPECIFIED因?yàn)椴粫?huì)出現(xiàn)在這里可以忽略):

<?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>

運(yùn)行一下,結(jié)果果然是match_parent失效:

  所以說(shuō)看到的東西要思考一下,才能真正地轉(zhuǎn)化為自己的,然后這個(gè)怎么解決呢,很簡(jiǎn)單,直接在onMeasure里面判斷參數(shù)是否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){
        //在這里實(shí)現(xiàn)計(jì)算需要wrap_content時(shí)需要的寬
        width =200;
    }
    if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
        //在這里實(shí)現(xiàn)計(jì)算需要wrap_content時(shí)需要的高
        height =200;
    }
    //傳入處理后的寬高
    setMeasuredDimension(width,height);
}

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

  但是這種方法有一個(gè)缺陷,就是可能會(huì)將UNSPECIFIED的情況也覆蓋掉,但是UNSPECIFIED一般只出現(xiàn)在系統(tǒng)內(nèi)部的View,不會(huì)出現(xiàn)在自定義View,而且當(dāng)它出現(xiàn)的時(shí)候也可以加個(gè)判斷按情況解決。

重寫onDraw()
  這里就是利用onDraw()給出的Canvas畫出各種東西了,這里是BallView的onMeasure()方法和onDraw(),通過(guò)以下代碼,可以實(shí)現(xiàn)在wrap_content的時(shí)候根據(jù)字的內(nèi)容長(zhǎng)度畫出相應(yīng)的圓,然后可以根據(jù)給出的速度移動(dòng),遇到“墻會(huì)碰撞”。

  @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){
        //在這里實(shí)現(xiàn)計(jì)算需要wrap_content時(shí)需要的寬高
        width = bounds.width();

    }else if(getLayoutParams().width != ViewGroup.LayoutParams.MATCH_PARENT){
        width = getLayoutParams().width;
    }
    if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
        //在這里實(shí)現(xiàn)計(jì)算需要wrap_content時(shí)需要的寬高
        height =bounds.height();
    }else if(getLayoutParams().height != ViewGroup.LayoutParams.MATCH_PARENT){
        height = getLayoutParams().height;
    }
    //計(jì)算半徑
    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);
}

//檢測(cè)碰撞,有碰撞就反彈
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 ;
    }
}

最后結(jié)果:


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

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

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


    // 計(jì)算出所有的childView的寬和高
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    int cCount = getChildCount();
    int width = 0;
    int height = 0;
    //處理WRAP_CONTENT情況,把所有子View的寬高加起來(lái)作為自己的寬高
    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);
}

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

@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,根據(jù)它們的寬高定位
    for (int i = 0; i < cCount; i++){
        View childView = getChildAt(i);
        //這里使用getMeasuredXXX()方法是因?yàn)檫€沒(méi)layout完,使用getWidth()和getHeight()獲取會(huì)得不到正確的寬高
        cWidth = childView.getMeasuredWidth();
        cHeight = childView.getMeasuredHeight();
        //定位
        childView.layout(sPointX,sPointY,sPointX + cWidth,sPointY + cHeight);
        sPointX += cWidth;
        sPointY += cHeight;
    }
}

結(jié)果: 參數(shù)為WRAP_CONTENT的時(shí)候,成功地顯示了:

<?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的時(shí)候:

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容