如何自定義View

首先奉上AndroodDeveloper的教程

假設我們以自定義一個View,實現圓形的按鈕功能。

說一下簡單的流程:

  • 繼承View
  • 重寫構造函數
  • 重寫OnMeasure()方法
  • 重寫OnDraw()方法
  • 配置XML
    我把配置XML放到最后不是因為需要最后去處理,而是它相對來說比較獨立。

繼承View,重寫構造函數

首先重寫View的構造函數

private CustomView (Context context){
    this(context,null);
 }
private CustomView(Context context, AttributeSet attrs){
    this(context,attrs,0);
}
private CustomView(Contxt context, AttributeSet attrs, int defStyleAttr){
    super(context,attrs,defStyleAttr);
 }

重寫OnMeasure()函數

這一塊著重的講一下,之前我這里也不是特別的理解。我覺得在XML文件中其實已經將View 的尺寸寬高已經固定好了,何必在View中再次測量并設置么。同理可以再往上層分析下,若Google在父View中直接獲取XML里面的尺寸豈不更好。

在我們布局XML的時候,有兩個屬性wrap_contentmatch_parent。可以看到這兩個屬性并沒有去告訴系統,我要多少尺寸的大小,而是描述了一種關系,即內容包裹填充父空間,因此在我們繪制到屏幕的過程中,就必須知道View的具體寬高,所以我們必須去處理尺寸。當View默認處理,無法滿足我們需求的時候,就需要重寫OnMeasure()函數了。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = measure(widthMeasureSpec, 100);
        int height = measure(heightMeasureSpec, 100);
        
        if (width < height){
            height = width;
        }else{
            width = height;
        }
    
        setMeasuredDimension(width, height);
    }

在這個函數中傳了兩個參數,這里需要注意的是參數是int型的,卻包含了兩個重要的信息:測量的模式以及測量的大小。Google將int數據的前兩個bit用于區分不同的布局模式,后面三十個bit存放的是尺寸的數據。一般我們需要通過移位操作來獲取數據,Android中的MeasureSpec中有兩個函數getMode()getSize()就可以很方便的獲取測量的模式和大小。

這樣恐怕你會有疑問,既然已經獲取了View的Size了,那要Mode有何用?其實這里的Size只是父級View提供的參考大小而已。Mode分為下面三種:

| 測量模式 | 英文 | 中文 |
| UNSPECIFIED | The parent has not imposed any constraint on the child. It can be whatever size it wants | 父容器對當前View沒有任何限制,當前View可以取任意尺寸
| EXACTLY |   The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be | 父容器給子容器了確定的大小,無論子容器想要多大,它只能接收父容器給的大小
| AT_MOST | The child can be as large as it wants up to the specified size | 子容器可以獲得它想要的尺寸大小

簡單點來說,和warp_contentmatch_parten做下對比不難發現。

match_parent--->EXACTLY。match_parent就是要利用父View給我們提供的所有剩余空間,而父View剩余空間是確定的,即Size。

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

固定尺寸(如100dp)--->EXACTLY。用戶自己指定了尺寸大小,我們就不用再去干涉了,當然是以指定的大小為主啦。

    private int measure(int measureSpec, int defaultSize) {
        int result = defaultSize;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        
        switch(mode){
            case MeasureSpec.EXACTLY:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = size;
                break;     
            case MeasureSpec.UNSPECIFIED:
                result = defaultSize;
                break;
            default:
                break;
           }
        return result;
    }

假設我們在XML中設置該控件的長寬屬性都是match_parent,則效果如下

重寫OnDraw()方法

上面我們通過OnMeasure()來設定了View的大小,接下來需要通過OnDraw()來繪制這個View的樣子。

這里需要注意下Canvas和Paint的區別,下面一段話說明Canvas確定了你在屏幕中所能展現的形狀,而Paint用來定義具體的顏色,樣式,字體等。

Simply put, Canvasdefines shapes that you can draw on the screen, while Paint defines the color, style, font, and so forth of each shape you draw.

OK,假設我們去繪制一個原諒色的原型,代碼如下:

@override
protected void OnDraw(Canvas canvas){
    Super.onDraw(canvas);
    int r = getMeasureWidth() / 2;
    int x = getLeft() + r;
    int y = getTop() + r;
    
    Paint paint = new Paint();
    paint.setColor(Color.Green);
    canvas.drawCircle(x, y, r, paint);
}

效果如下:

設置監聽事件

自定義XML屬性

如果我們需要給用戶一些更加靈活的設置,就需設置一些屬性。首先我們在res/values/styles.xml中聲明我們自己的屬性:

<resources>
    <declare-styleable name="CostomView">
        <attr name="default_size" format="dimension"/>
    </declare-styleable>
</resources>

接著在布局文件中使用我們的聲明的屬性:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="domon.cn.coustomerview.MainActivity">

    <domon.cn.coustomerview.View.RectView
        android:id="@+id/my_rv"
        android:layout_width="match_parent"
        android:background="#f2e"
        app:default_size="100dp"
        android:layout_height="100dp" />
    </LinearLayout>

在引用自己的屬性的時候,需要注意一下命名空間的問題,基本上我們的自定義View就已經好了。

案例分析

下面我代碼家的一個自定義控件NumberProgressBar來簡單分析一下。

  • 構造函數
public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        default_reached_bar_height = dp2px(1.5f);
        default_unreached_bar_height = dp2px(1.0f);
        default_text_size = sp2px(10);
        default_progress_text_offset = dp2px(3.0f);

        //load styled attributes.
        final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NumberProgressBar,
                defStyleAttr, 0);

        mReachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_reached_color, default_reached_color);
        mUnreachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_unreached_color, default_unreached_color);
        mTextColor = attributes.getColor(R.styleable.NumberProgressBar_progress_text_color, default_text_color);
        mTextSize = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_size, default_text_size);

        mReachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_reached_bar_height, default_reached_bar_height);
        mUnreachedBarHeight = attributes.getDimension(R.styleable.NumberProgressBar_progress_unreached_bar_height, default_unreached_bar_height);
        mOffset = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_offset, default_progress_text_offset);

        int textVisible = attributes.getInt(R.styleable.NumberProgressBar_progress_text_visibility, PROGRESS_TEXT_VISIBLE);
        if (textVisible != PROGRESS_TEXT_VISIBLE) {
            mIfDrawText = false;
        }

        setProgress(attributes.getInt(R.styleable.NumberProgressBar_progress_current, 0));
        setMax(attributes.getInt(R.styleable.NumberProgressBar_progress_max, 100));

        attributes.recycle();
        initializePainters();
    }

通過在構造函數中,獲取在XML中設置的屬性。

  • 重寫OnMeasure()方法
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
    }

    private int measure(int measureSpec, boolean isWidth) {
        int result;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
            result += padding;
            if (mode == MeasureSpec.AT_MOST) {
                if (isWidth) {
                    result = Math.max(result, size);
                } else {
                    result = Math.min(result, size);
                }
            }
        }
        return result;
    }

根據不同模式測量不同的尺寸

  • 重寫OnDraw()方法
@Override
    protected void onDraw(Canvas canvas) {
        if (mIfDrawText) {
            calculateDrawRectF();
        } else {
            calculateDrawRectFWithoutProgressText();
        }

        if (mDrawReachedBar) {
            canvas.drawRect(mReachedRectF, mReachedBarPaint);
        }

        if (mDrawUnreachedBar) {
            canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint);
        }

        if (mIfDrawText)
            canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint);
    }

在OnDraw()中根據條件判斷不同的繪制對象。我們就來看看calculateDrawRectFWithoutProgressText()這個方法。

private void calculateDrawRectFWithoutProgressText() {
        mReachedRectF.left = getPaddingLeft();
        mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f;
        mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() + getPaddingLeft();
        mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f;

        mUnreachedRectF.left = mReachedRectF.right;
        mUnreachedRectF.right = getWidth() - getPaddingRight();
        mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f;
        mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f;
    }

ReachedRectf是已經完成部分的矩形,UnReachedRectF是未完成的矩形。



因此在繪制的時候,我們需要通過上下左右的位置坐標,將這個View畫出來。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容