1. 程式人生 > >Android之自定義View

Android之自定義View

在Android開發中,系統本身為我們提供了許多可供選擇的UI控制元件,但是在有些情況下也是需要自定義一些控制元件的。比如UI中的柱狀圖、餅圖等。而自定義View就需要明白它的原理了。

大體上分為三步onMeasure、onLayout、onDraw。大部分情況下我們只需要重寫兩個函式:onMeasure、onDraw。onMeasure負責對當前View的尺寸進行測量,onDraw負責將當前這個View繪製出來。

在自定義View時,建構函式也是必須的:

public CustomView(Context context){
        this.CustomView(context,null);
}

public CustomView(Context context,AttributeSet attr){
        this.CustomView(context,attr,0);
}

下面結合程式碼介紹以上三種方法:

1、onMeasure

思考:我在xml中已經設定好了寬、高了,在自定義View中有必要再次獲取寬、高並設定它們嗎?

答案是有必要的:比如我們在xml中指定layout_width和layout_height為wrap_content或者是match_parent。意思就是“包住內容”和“填充父佈局給我們的空間”。這兩個設定並沒有指定具體的寬和高,而我們繪製到螢幕上的View是有具體的寬和高的。所以我們有必要在onMeasure()方法中來設定它的寬和高。

比如,我們希望在螢幕中顯示一個正方形,而在XML佈局中,它是一個長方形。

onMeasure函式原型

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec)

理解:

widthMeasureSpec和heightMeasureSpec兩個引數包含寬和高的資訊,而不僅僅有寬和高,還包括測量模式。

在設定寬高時有3個選擇:wrap_contentmatch_parent固定尺寸,而測量模式也有三種:UNSPECFIED,EXACTLYAT_MOST。它們不是一一對應的關係。但測量模式無非就是這3種情況,而如果使用二進位制,我們只需要使用2個bit就可以做到,因為2個bit取值範圍是[0,3]裡面可以存放4個數足夠我們用了。那麼Google是怎麼把一個int同時放測量模式和尺寸資訊呢?我們知道int型資料佔用32個bit,而google實現的是,將int資料的前面2個bit用於區分不同的佈局模式,後面30個bit存放的是尺寸的資料。

那我們怎麼從int資料中提取測量模式和尺寸?放心,不用你每次都要寫一次移位<<和取且&操作,Android內建類MeasureSpec幫我們寫好啦~,我們只需按照下面方法:

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
 

測量模式:UNSPECFIED,EXACTLY,AT_MOST

測量模式        表示意思
UNSPECFIED     父容器沒有對當前View有任何限制,當前View可以任意取尺寸
EXACTLY        當前的尺寸就是當前View應該取的尺寸
AT_MOST        當前尺寸的當前View能取的最大尺寸

測量模式與佈局的wrap_content、match_parent和固定尺寸的對應關係:

match_parent---EXACTLY,match_parent就是利用父View給我們提供剩餘空間,如果父View空間是確定的,那麼它就是測量模式中就存放整數裡面存放的尺寸。

wrap_content---AT_MOST,我們想要將大小設定為包裹我們的view內容,尺寸大小就是父View給我們作為參考的尺寸,只要不超過這個尺寸就可以,具體尺寸可以根據我們的需求去設定。

固定尺寸---EXACTLY

重寫onMeasure函式

將當前View以正方形的形式展示,寬高設定為100dp

public int getSize(int defaultSize,int measureSpec){
    int mySize;
    int mode=MeasureSpec.getMode(measureSpec);
    int size=MeasureSpec.getSize(measureSpec);

    switch(mode){
        case MesasureSpec.UNSPECFIED:
                mySize=defaultSize;
                break;
        case MeasureSpec.AT_MOST:
                mySize=size;
                break;
        case MesaureSpec.EXACTLY:
                mySize=size;
                break;
        }
    }

    
    @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
        int width=getSize(100,widthMeasureSpec);
        int height=getSize(100,heightMeasureSpec);

        setMeasuredDimension(width,height);
    }
}


在XML佈局中新增View:

<com.example.CustomView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#ff0000"
    />

重寫onDraw函式:

假如要實現的是顯示一個圓形:

@Override
protected void onDraw(Canvas canvas){
    //呼叫父ViewonDraw函式,因為這個View類為我們實現了一些基本的繪製功能
    super.onDraw(canvas);
    int r=getMesauredWidth()/2; //圓的半徑
    int centerX=getLeft()+r;
    int centerY=getTop() +r;

    Paint paint=new Paint();//初始化最好放在外部
    paint.setColor(Color.RED);
    
    canvas.drawCircle(centerX,centerY,r,paint);
}

 

OK,基本上實現了自定義View的功能,這些只是基礎,更復雜的功能需要在它們的基礎之上進行改善,需要更深入的理解。

 

補充

自定義ViewGroup
自定義View的過程很簡單,就那幾步,可自定義ViewGroup可就沒那麼簡單啦~,因為它不僅要管好自己的,還要兼顧它的子View。我們都知道ViewGroup是個View容器,它裝納child View並且負責把child View放入指定的位置。我們假象一下,如果是讓你負責設計ViewGroup,你會怎麼去設計呢?

1.首先,我們得知道各個子View的大小吧,只有先知道子View的大小,我們才知道當前的ViewGroup該設定為多大去容納它們。

2.根據子View的大小,以及我們的ViewGroup要實現的功能,決定出ViewGroup的大小

3.ViewGroup和子View的大小算出來了之後,接下來就是去擺放了吧,具體怎麼去擺放呢?這得根據你定製的需求去擺放了,比如,你想讓子View按照垂直順序一個挨著一個放,或者是按照先後順序一個疊一個去放,這是你自己決定的。

4.已經知道怎麼去擺放還不行啊,決定了怎麼擺放就是相當於把已有的空間”分割”成大大小小的空間,每個空間對應一個子View,我們接下來就是把子View對號入座了,把它們放進它們該放的地方去。

現在就完成了ViewGroup的設計了,我們來個具體的案例:將子View按從上到下垂直順序一個挨著一個擺放,即模仿實現LinearLayout的垂直佈局。

首先重寫onMeasure,實現測量子View大小以及設定ViewGroup的大小:   

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //將所有的子View進行測量,這會觸發每個子View的onMeasure函式
        //注意要與measureChild區分,measureChild是對單個view進行測量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();

        if (childCount == 0) {//如果沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
            setMeasuredDimension(0, 0);
        } else {
            //如果寬高都是包裹內容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我們將高度設定為所有子View的高度相加,寬度設為子View中最大的寬度
                int height = getTotleHeight();
                int width = getMaxChildWidth();
                setMeasuredDimension(width, height);

            } else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹內容
                //寬度設定為ViewGroup自己的測量寬度,高度設定為所有子View的高度總和
                setMeasuredDimension(widthSize, getTotleHeight());
            } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有寬度是包裹內容
                //寬度設定為子View中寬度最大的值,高度設定為ViewGroup自己的測量值
                setMeasuredDimension(getMaxChildWidth(), heightSize);

            }
        }
    }
    /***
     * 獲取子View中寬度最大的值
     */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth)
                maxWidth = childView.getMeasuredWidth();

        }

        return maxWidth;
    }

    /***
     * 將所有子View的高度相加
     **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();

        }

        return height;
    }

程式碼中的註釋我已經寫得很詳細,不再對每一行程式碼進行講解。上面的onMeasure將子View測量好了,以及把自己的尺寸也設定好了,接下來我們去擺放子View吧~

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //記錄當前的高度位置
        int curHeight = t;
        //將子View逐個擺放
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            //擺放子View,引數分別是子View矩形區域的左、上、右、下邊
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }

我們測試一下,將我們自定義的ViewGroup裡面放3個Button ,將這3個Button的寬度設定不一樣,把我們的ViewGroup的寬高都設定為包裹內容wrap_content,為了看的效果明顯,我們給ViewGroup加個背景:

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

    <com.hc.studyview.MyViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff9900">

        <Button
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="btn" />

        <Button
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:text="btn" />

        <Button
            android:layout_width="50dp"
            android:layout_height="wrap_content"
            android:text="btn" />


    </com.hc.studyview.MyViewGroup>

</LinearLayout>

看看最後的效果吧~

 

最後附上MyViewGroup的完整原始碼:

package com.hc.studyview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

/**
 * Package com.hc.studyview
 * Created by HuaChao on 2016/6/3.
 */
public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {

        super(context, attrs);
    }

    /***
     * 獲取子View中寬度最大的值
     */
    private int getMaxChildWidth() {
        int childCount = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getMeasuredWidth() > maxWidth)
                maxWidth = childView.getMeasuredWidth();

        }

        return maxWidth;
    }

    /***
     * 將所有子View的高度相加
     **/
    private int getTotleHeight() {
        int childCount = getChildCount();
        int height = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            height += childView.getMeasuredHeight();

        }

        return height;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //將所有的子View進行測量,這會觸發每個子View的onMeasure函式
        //注意要與measureChild區分,measureChild是對單個view進行測量
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();

        if (childCount == 0) {//如果沒有子View,當前ViewGroup沒有存在的意義,不用佔用空間
            setMeasuredDimension(0, 0);
        } else {
            //如果寬高都是包裹內容
            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
                //我們將高度設定為所有子View的高度相加,寬度設為子View中最大的寬度
                int height = getTotleHeight();
                int width = getMaxChildWidth();
                setMeasuredDimension(width, height);

            } else if (heightMode == MeasureSpec.AT_MOST) {//如果只有高度是包裹內容
                //寬度設定為ViewGroup自己的測量寬度,高度設定為所有子View的高度總和
                setMeasuredDimension(widthSize, getTotleHeight());
            } else if (widthMode == MeasureSpec.AT_MOST) {//如果只有寬度是包裹內容
                //寬度設定為子View中寬度最大的值,高度設定為ViewGroup自己的測量值
                setMeasuredDimension(getMaxChildWidth(), heightSize);

            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //記錄當前的高度位置
        int curHeight = t;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }


}