天天看點

Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

1、前言

在上篇部落格中我主要介紹了自定義屬性的定義和擷取,還有如何在布局檔案添加我們的自定義控件。這幾乎是自定義控件中必不可少的兩步,而onMeasure()、onDraw()方法如果是在我們講的TopBar這樣的隻需修改幾個屬性的控件中使用是可以不做的。onLayout()就更不必說了,它是來設定子View的位置的。是以這篇部落格我會仔細講解這幾個方法。

2、onMeasure解析

我們在TopBar中繼承的是RelativeLayout,是以已經複寫好了View類中的onMeasure方法,是以我們在用的時候不會出現寬高的問題,但是如果繼承的是View或ViewGroup的話,就必須複寫onMeasure()方法了,我們來看看View中的onMeasure()的代碼:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
           

系統幫我們測量的高度和寬度都是MATCH_PARNET,當我們設定明确的寬度和高度時,系統幫我們測量的結果就是我們設定的結果,當我們設定為WRAP_CONTENT,或者MATCH_PARENT系統幫我們測量的結果就是MATCH_PARENT的長度。

是以,當設定了WRAP_CONTENT時,我們需要自己進行測量,即重寫onMesure方法,再看看RelativeLayout的代碼,這個比較長,就用動圖示範一下,大家有興趣可以自己去看源代碼:

Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

大家也不用管代碼的功能是什麼,隻要知道RelativeLayout已經幫我們考慮了很多情況,我們不用太考慮寬高的測量。

對于onMeasure(),首先我們要了解的是widthMeasureSpec, heightMeasureSpec這兩個參數,onMeasure()由包含這個View的具體的ViewGroup調用,是以值也是從這個ViewGroup中傳入的。

子View的這兩個參數,由ViewGroup中的layout_width,layout_height和padding以及View自身的layout_margin共同決定。權值weight也是尤其需要考慮的因素,有它的存在情況可能會稍微複雜點。

了解了這兩個參數的來源,還要知道這兩個值的作用。拿heightMeasureSpec來說,這個值由高32位和低16位組成,高32位儲存的值叫specMode,可以通過MeasureSpec.getMode()擷取;低16位為specSize,同樣可以由MeasureSpec.getSize()擷取。

那麼specMode和specSize的作用有是什麼呢?要想知道這一點,我們需要知道所有的View的onMeasure()的最後一行都會調用的setMeasureDimension()方法的作用——這個函數調用中傳進去的值是View最終的視圖大小。也就是說onMeasure()中之前所作的所有工作都是為了最後這一句話服務的。

我們知道在ViewGroup中,給View配置設定的空間大小并不是确定的,有可能随着具體的變化而變化,而這個變化的條件就是傳到specMode中決定的,specMode一共有三種可能:

  • MeasureSpec.EXACTLY:父視圖希望子視圖的大小應該是specSize中指定的。一般是設定了明确的值或者是MATCH_PARENT。
  • MeasureSpec.AT_MOST:子視圖的大小最多是specSize中指定的值,也就是說不建議子視圖的大小超過specSize中給定的值。表示子布局限制在一個最大值内,一般為WARP_CONTENT。
  • MeasureSpec.UNSPECIFIED:我們可以随意指定視圖的大小,表示子布局想要多大就多大,很少使用。

我們寫個例子,列印一下log,這樣能讓大家更能了解,specMode如何取值,specSize如何計算:

public class CustomView extends View {

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

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

        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        Log.d("TAG", "---widthSize = " + widthSize + "");
        Log.d("TAG", "---heightSize = " + heightSize + "");
        if(widthMode == MeasureSpec.AT_MOST){
            Log.d("TAG", "---AT_MOST---");
        }
        if(widthMode == MeasureSpec.EXACTLY){
            Log.d("TAG", "---EXACTLY---");
        }
        if(widthMode == MeasureSpec.UNSPECIFIED){
            Log.d("TAG", "---UNSPECIFIED---");
        }
        if(heightMode == MeasureSpec.AT_MOST){
            Log.d("TAG", "---AT_MOST---");
        }
        if(heightMode == MeasureSpec.EXACTLY){
            Log.d("TAG", "---EXACTLY---");
        }
        if(heightMode == MeasureSpec.UNSPECIFIED){
            Log.d("TAG", "---UNSPECIFIED---");
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}
           
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:layout_marginTop="30dp"
        android:background="@android:color/darker_gray"
        android:paddingTop="20dp">

        <com.ht.animator.CustomView
            android:layout_width="150dp"
            android:layout_height="match_parent"
            android:layout_marginTop="15dp"
            android:background="@android:color/holo_red_light"
            android:paddingTop="10dp" />

    </LinearLayout>
</LinearLayout>
           
Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

log的輸出為:

Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

可以看到specMode的值的确如我介紹的那樣,layout_height和layout_width的值為match_parent或精确的值則對應的specMode為EXACTLY,wrap_content沒有測試但就是AT_MOST,大家可以自己試試。這個值與父視圖的layout_width是沒有關系的。

有點要注意的就是,xml中用的機關是dp,log中得到的機關是px,我所使用的虛拟機的dpi為360,是以螢幕密度為2.0,隻需要進行簡單的換算即得px = 2.0 * dp。

widthSize = 2.0 * layout_width = 300;

heightSize = layout_height(LinearLayout) * 2.0 - paddingTop(LinearLayout) * 2.0 - layout_marginTop * 2.0 = 600 - 40 - 30 = 530。

影響heightSize的因素為:父視圖的layout_height和paddingTop以及自身的layout_marginTop。但是我們不要忘記有weight時的影響。

3、TopBar測量

這篇部落格的例子同樣是自定義TopBar,不過這次我們就不繼承RelativeLayout了,這意味着我們必須重寫它的onMeasure()。不過在寫onMeasure()方法之前,我們要先了解測量都需要用到什麼樣的參數。我覺得首先肯定是各個控件的寬高,然後我們有可能為我們的控件在父布局中設定margin,父布局中也會設定padding,是以我們要計算的應該就是這幾個參數的值。對于我們這個例子,我們隻需要ViewGroup能夠支援margin即可,那麼我們直接使用系統的MarginLayoutParams。

@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
{
    return new MarginLayoutParams(getContext(), attrs);
}
           

重寫父類的該方法,傳回MarginLayoutParams的執行個體,這樣就為我們的ViewGroup指定了其LayoutParams為MarginLayoutParams。我們就可以直接使用margin了。

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

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

    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int width = ;
    int height = ;

    int cCount = getChildCount();

    int cWidth = ;
    int cHeight = ;
    MarginLayoutParams cParams = null;

    for (int i = ; i < cCount; i++) {
        View childView = getChildAt(i);
        cWidth = childView.getMeasuredWidth();
        cHeight = childView.getMeasuredHeight();
        cParams = (MarginLayoutParams) childView.getLayoutParams();

        width += cWidth + cParams.leftMargin + cParams.rightMargin;
        height = Math.max(height, cHeight + cParams.topMargin + cParams.bottomMargin);
    }

    setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? sizeWidth
                         : width, (heightMode == MeasureSpec.EXACTLY) ? sizeHeight
                         : height);
}
           

首先擷取該ViewGroup父容器為其設定的計算模式和尺寸,大多情況下,隻要不是wrap_content,父容器都能正确的計算其尺寸。是以我們自己需要計算如果設定為wrap_content時的寬和高,那就是通過其childView的寬和高來進行計算。

通過ViewGroup的measureChildren()方法為其所有的孩子設定寬和高,此行執行完成後,childView的寬和高都已經正确的計算過了。

根據childView的寬和高,以及margin,計算ViewGroup在wrap_content時的寬和高。

最後,如果寬高屬性值為wrap_content,則設定為我們計算的值,否則為其父容器傳入的寬和高。

因為我們要完成的是TopBar,是以我們都是設定為match_parent,這種寫法,僅供示範。

4、onLayout

onLayout方法是ViewGroup中子View的布局方法,用于放置子View的位置。放置子View很簡單,隻需在重寫onLayout方法,然後擷取子View的執行個體,調用子View的layout方法實作布局。在實際開發中,一般要配合onMeasure測量方法一起使用。

onLayout方法:

@Override
protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
           

該方法在ViewGroup中定義是抽象函數,繼承該類必須實作onLayout方法,而ViewGroup的onMeasure并非必須重寫的。View的放置都是根據一個矩形空間放置的,onLayout傳下來的l,t,r,b分别是放置父控件的矩形可用空間(除去margin和padding的空間)的左上角的left、top以及右下角right、bottom值。

layout:

public void layout(int l, int t, int r, int b);
           

該方法是View的放置方法,在View類實作。調用該方法需要傳入放置View的矩形空間左上角left、top值和右下角right、bottom值。這四個值是相對于父控件而言的。例如傳入的是(10, 10, 100, 100),則該View在距離父控件的左上角位置(10, 10)處顯示,顯示的大小是寬高是90(參數r,b是相對左上角的),這有點像絕對布局。我們确定了子View的位置也就是l,t,r,b四個值後就用這個方法把子View放到那去。

平常開發所用到RelativeLayout、LinearLayout、FrameLayout…這些都是繼承ViewGroup的布局。這些布局的實作都是通過都實作ViewGroup的onLayout方法,隻是實作方法不一樣而已。

在自定義View中,onLayout配合onMeasure方法一起使用,可以實作自定義View的複雜布局。自定義View首先調用onMeasure進行測量,然後調用onLayout方法,動态擷取子View和子View的測量大小,然後進行layout布局。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {

    int cCount = getChildCount();
    int cWidth = ;
    int cHeight = ;
    MarginLayoutParams cParams = null;

    int cl = , ct = , cr = , cb = ;

    for (int i = ; i < cCount; i++) {
        View childView = getChildAt(i);
        cWidth = childView.getMeasuredWidth();
        cHeight = childView.getMeasuredHeight();
        cParams = (MarginLayoutParams) childView.getLayoutParams();

        switch (i) {
            case :
                cl = cParams.leftMargin;
                ct = cParams.topMargin;
                break;
            case :
                cl = getWidth() - cWidth - cParams.rightMargin;
                ct = cParams.topMargin;
                break;
        }
        cr = cl + cWidth;
        cb = cHeight + ct;
        childView.layout(cl, ct, cr, cb);
    }
}
           

因為還要講解onDraw方法,是以這個例子我隻使用了兩個Button,打算在ViewGroup的中間去主動繪制文字。

這個邏輯是很清晰的,我們要實作的TopBar,一個Button在最左邊,另一個則在最右邊,知道位置自然很好擷取。

寫到這裡,布局已經可以出現一些東西了,讓我們運作看看結果是不是如我們所想:

Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

可以看到我們定義的兩個Button已經到指定的地方了,最後就是繪制我們的文字。

5、onDraw

onDraw方法其實反而是最常用的自定義時的方法,因為就像我在上篇部落格所說的,系統提供的控件已經能滿足大部分效果,我們大多數是不滿意它的樣子,而要改變它的樣子自然要重寫onDraw方法。

使用onDraw方法,首先要對Canvas和Paint這兩個類有所料解,它們一個是畫布,一個是畫筆,它們的使用是多種多樣的,這裡我就不提啦,有興趣的朋友可以去查閱相關資料。

我們要繪制的是比較簡單的,實作的效果就是在ViewGroup的中間繪制文字,我們需要的就是中間的那一塊矩形。

private Rect mBound;
private Paint mPaint;

public CustomTopBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context, attrs);

    setBackgroundColor();

    mPaint = new Paint();
    mPaint.setTextSize(titleTextSize);
    mBound = new Rect();
    layout();
}

@Override
protected void onDraw(Canvas canvas) {
    mPaint.getTextBounds(title, , title.length(), mBound);
    mPaint.setColor(Color.YELLOW);
    canvas.drawRect(, , getMeasuredWidth(), getMeasuredHeight(), mPaint);

    mPaint.setColor(titleTextColor);
    canvas.drawText(title, getWidth() /  - mBound.width() / , getHeight() /  + mBound.height() / , mPaint);
}
           

mBound就是我們為文字設定的矩形,我們将測量好的控件繪制顔色,在中間繪制文字相信大家都能看懂。

<?xml version="1.0" encoding="utf-8"?>
<com.ht.animator.CustomTopBar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:lht="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    lht:leftBackground="@android:color/holo_blue_bright"
    lht:leftTextColor="#FFFFFF"
    lht:leftText="Back"
    lht:rightBackground="@android:color/holo_blue_bright"
    lht:rightTextColor="#FFFFFF"
    lht:rightText="More"
    lht:title="自定義标題"
    lht:titleTextColor="#000000"
    lht:titleTextSize="20sp">

</com.ht.animator.CustomTopBar>
           

都設定好了,就看看我們運作的結果吧。

Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

6、使用者互動

一個精美的布局是我們自定義控件所要達到的基礎,但是我們可不能滿足與此,我們還需要給它添加使用者互動,否則就算界面再漂亮也隻是空殼子而已。

我們的這個例子沒有onTouch方法的處理,不過有兩個Button,我們可以為它們的點選事件設定回調方法,讓大家看看如何使用回調。

我們不可能每次要修改點選事件就去檔案中修改代碼,應該在調用這個控件的時候為裡面的Button添加事件,我們可以寫個像onClick的接口回調。

private topbarClickListener listener;

public interface topbarClickListener {
    public void leftClick();
    public void rightClick();
}

public void setOnTopbarClickListener(topbarClickListener listener) {
    this.listener = listener;
}
           

Button的setOnClickListener是觸發點選事件,真正實作點選之後内容的是new OnClickListener()這個匿名内部類,是以我們仿照它寫,定義了topbarClickListener這個接口,裡面的方法分别實作左右Button的點選邏輯,然後就是寫個對外的方法setOnTopbarClickListener()。這樣我們就實作了我們的回調方法啦。

leftButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        listener.leftClick();
    }
});

rightButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        listener.rightClick();
    }
});
           

設定好就在我們的Activity中調用:

public class TopBarActivity extends AppCompatActivity {

    private boolean flag = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test);

        final CustomTopBar topbar = (CustomTopBar) findViewById(R.id.topbar);
        topbar.setOnTopbarClickListener(new CustomTopBar.topbarClickListener() {
            @Override
            public void leftClick() {
                Toast.makeText(TopBarActivity.this, "LEFT", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void rightClick() {
                Toast.makeText(TopBarActivity.this, "RIGHT", Toast.LENGTH_SHORT).show();
            }
        });
    }
}
           
Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

這樣我們就可以在外面也可以對自定義控件中的View做操作啦,更改功能也不需要去修改自定義控件中代碼,提升了代碼複用。

同樣我們也可以在控件中提供方法,去修改那些子View的屬性,更友善我們對自定義控件的操控,這裡寫個設定可見的例子。

public void setVisible(boolean flag) {
    if (flag) {
        leftButton.setVisibility(View.VISIBLE);
    } else {
        leftButton.setVisibility(View.GONE);
    }
}
           
private boolean flag = true;
...
@Override
public void rightClick() {
    flag = !flag;
    topbar.setVisible(flag);
}
           
Android--自定義控件解析(二)1、前言2、onMeasure解析3、TopBar測量4、onLayout5、onDraw6、使用者互動

以上就是我這篇部落格的全部内容啦。

結束語:本文僅用來學習記錄,參考查閱。