天天看點

走向自定義View的深坑——自定義屬性

為了讓我們的自定義看起來和官方的差不多,正經一點,對沒錯是正經一點。我們的自定義控件要做的全面一點。

BB兩句

  • 為什麼要自定義控件?
    • 為了裝逼
    • 為了滿足腦洞
    • 為了世界的發展

自定義屬性,單獨自定義屬性沒啥用,因為自定義屬性是提供給自定義View使用的,是以我們要先建立一個自定義View才能愉快的使用。

前排提示,文章略長,請耐心看完。

流程

  • 建立自定義View
  • 編寫要用到的屬性
  • 使用style給屬性指派
  • 代碼中擷取屬性的值
  • 畫出文字

建立自定義View

自定義View的第一步就是要把我們的類寫成View,怎麼寫成View呢,隻要我們繼承View這個類就可以了,一般情況下我們都是繼承View或者ViewGroup這兩個類進行View擴充。為了友善通常也是直接內建相關方面的View進行修改,如這些TextView、EditText、LinearLayout等等。

首先建立一個類,繼承View,此時應該是這樣的

public class TextView extends View {
    }
           

啊嘞,這好像啥變化都沒有啊-_-! 此時會報錯,提示需要重寫構造方法,一般情況下我們需要重寫三個構造方法,以滿足各個地方使用的需求,下面介紹使用場景

public TextView(Context context) {
        this(context,null);
    }

    public TextView(Context context, AttributeSet attrs) {
        this(context, attrs,);
    }

    public TextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
           
  • 第一個構造方法簡單來說就是在代碼中執行個體化的時候執行
  • 第二個構造方法是在XML布局檔案中使用的時候執行,并且是沒有使用Style來定義要使用的屬性
  • 第三個構造方法是在XML布局檔案中使用并且指定了style的時候執行。

注意看第一個方法和第二個方法内,我是用的this,就是調用目前類的第二個、第三個構造,依次類推,這是為了簡化代碼,直接在第三個方法中初始化一次就行了

好了,簡單結構了解了,就開始走下一步。

編寫要用到的屬性

為什麼要自定義屬性?想想你使用的TextView

<TextView
        android:text="@string/app_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
           

這些都是屬性,我們的自定義View想要有更好的體驗,我們需要進行自定義一些屬性友善在XML中直接配置。

自定義屬性需要在res/value/attrs.xml中配置,沒有的建立一個,使用declare-styleable标簽進行定義,标簽中的name屬性寫成自定義View的類名,下面看代碼。

<declare-styleable name="TextView">
        <attr name="textSize" format="dimension"/>
        <attr name="textColor" format="color"/>
        <attr name="text" format="string"/>
        <attr name="gravity" format="enum">
            <enum name="center" value="0"/>
            <enum name="top" value="1"/>
            <enum name="bottom" value="2"/>
            <enum name="left" value="3"/>
            <enum name="right" value="4"/>
            <enum name="center_horizontal" value="5"/>
            <enum name="center_vertical" value="6"/>
        </attr>
    </declare-styleable>
           

好了,現在定義完了,可以在XML使用了,在使用前,需要先給自定義View設定一個命名控件,以便在代碼中可以找到自定義的屬性。

<!--命名控件的聲明-->
    xmlns:app="http://schemas.android.com/apk/res-auto"
           
<com.github.odriver.viewdemo.view.TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:gravity="center"
        app:textSize="16sp"
        app:text="@string/app_name"
        app:textColor="@color/colorPrimary"
        />
           

好了,屬性的使用就是這些了。

使用Style給屬性指派

有時候我們在使用控件的時候為了友善,為了懶,不想寫重複代碼。會使用style達到複用效果,這個時候,就是第三個構造方法執行,因為使用了style。

<com.github.odriver.viewdemo.view.TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        style="@style/textStyle"
        />
           

上面這段代碼是使用style定義屬性的值,下面我們來看看style的定義,style也是在declare-styleable定義的。

<style name="textStyle">
        <item name="textSize">sp</item>
        <item name="textColor">#FFFFFF</item>
        <item name="text">@string/app_name</item>
        <item name="gravity">bottom</item>
    </style>
           

在代碼中擷取屬性的值

我們在代碼中使用了屬性,并且設定了屬性的值。但是現在我們隻是設定了,還沒有進行處理,也就是相當于沒個*用,是以我們還要繼續,下面我們來擷取屬性的值,并且使用上,下面來看構造方法中的代碼。

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

        // 使用context對象獲得我們自定義的屬性的值,attrs是我們xml中使用的屬性集合,其中包括屬性名和屬性值等相關資訊,後面是我們自定義的屬性,也就是我們定義的declare-styleable。
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextView);

        // 使用相應的方法擷取各個類型屬性的值,第二個參數是擷取不到屬性值預設的屬性值。
        mTextColor = typedArray.getColor(R.styleable.TextView_textColor, Color.BLACK);
        System.out.println("mTextColor = " + mTextColor +"------defColor="+Color.BLACK);

        mTextSize = typedArray.getDimension(R.styleable.TextView_textSize, );
        System.out.println("mTextSize = " + mTextSize +"-----defDimension = " + );

        mGravity = typedArray.getInt(R.styleable.TextView_gravity, );
        System.out.println("mGravity = " + mGravity +"-----defAnInt = " + );

        mText = typedArray.getString(R.styleable.TextView_text);
        System.out.println("mText = " + mText);

        // 沒啥,google推薦的,使用完及時釋放
        typedArray.recycle();

        // 建立一個畫筆,我們要在界面上畫出相應的東西,都需要靠這支筆。
        mPaint = new Paint();
        // 設定抗鋸齒
        mPaint.setAntiAlias(true);
    }
           

如果你設定給這些屬性都指派了,那麼我們在擷取值的時候是和預設值不一樣的,可以列印出來看結果。

畫出文字

自定義屬性的工作基本上已經完成了,下面就是把自定義屬性使用上了,我們現在View顯示的還是空蕩蕩的一片,現在我們要讓他顯示出我們設定的文字,首先介紹幾個方法,是我們自定義View是常用的方法。

- onDraw(Canvas canvas) 最重要的方法,用于将我們想要展示的東西繪制到螢幕上,不然看不到。。

- onMeasure(int widthMeasureSpec, int heightMeasureSpec) 測量方法有時候我們設定的是wrap_content或者match_parent又或者是具體值,我們需要在這個方法裡進行判斷,計算出我們自身能使用的大小。在自定義ViewGroup的時候該方法也是用來子View顯示的大小和位置的。

- onLayout(boolean changed, int left, int top, int right, int bottom) 用于确定子View的位置,在自定義ViewGroup使用。

- onTouchEvent(MotionEvent event) 處理觸摸事件

- on一大堆。。。

下面我們先讓我們定義的文字顯示在界面上,重寫onDraw方法。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText(mText, , , mPaint);
    }
           

Canvas的draw方法,以後再講。

現在來看螢幕,注意看白色部分那塊黑色部分,沒錯,那就是我們畫上去的文字。。。

走向自定義View的深坑——自定義屬性

直到現在,我們還沒有使用過任何自定義的屬性,下面就使用上。

畫出指定大小的文字

我們的文字太小了,我們把字型加大,在onDraw(Canvas canvas)方法中修改,還記的我們建立的那個畫筆嗎,全靠配置他來實作。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 設定我們要顯示的字型的大小
        mPaint.setTextSize(mTextSize);
        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText(mText, , , mPaint);
    }
           

現在再看界面上

走向自定義View的深坑——自定義屬性

主要就是靠這支畫筆來控制。

畫出指定顔色的文字

我們來給文字加上顔色,還是要通過這個畫筆來實作。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 設定我們要顯示的字型的大小
        mPaint.setTextSize(mTextSize);
        // 設定文字的顔色
        mPaint.setColor(mTextColor);
        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText(mText, , , mPaint);
    }
           

看,我們想要給文字加上顔色,隻要用畫筆的setColor就可以了

這個畫筆就和我們現實中的畫筆是一樣的,我們的Canvas就相當于畫布,現實中我們一般的繪畫都是畫布擺在哪裡,不做任何動作,全靠畫筆和人控制來進行繪畫,我們需要畫什麼顔色,什麼大小都是人和畫筆的動作。而Canvas(畫布)就跟他們說一句話:* 坐上來,自己動 *。畫面有點污。。。

畫出背景

看下面的代碼我們是為了設定一個背景,而背景在我們自定義View中有些需求是局部背景而不是整個控件背景,是以我們要在畫布上畫出一個背景,原理就是畫出一個矩形,使用顔色填充作為背景。

先看一個小例子來了解一下onDraw方法裡面的繪制層級

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
// 第一層
        // 設定畫筆顔色
        mPaint.setColor(Color.CYAN);
        // 畫出背景,以上面設定的顔色填充
        canvas.drawRect(, , , , mPaint);

        // 設定我們要顯示的字型的大小
        mPaint.setTextSize(mTextSize);
        // 設定文字的顔色
        mPaint.setColor(mTextColor);
        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText("第一層", , , mPaint);

//-----------------------------------------------------------//
// 第二層
        // 設定畫筆顔色
        mPaint.setColor(Color.BLUE);
        // 畫出背景,以上面設定的顔色填充
        canvas.drawRect(, , , , mPaint);
        // 設定我們要顯示的字型的大小
        mPaint.setTextSize(mTextSize);
        // 設定文字的顔色
        mPaint.setColor(Color.WHITE);
        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText("第二層", , , mPaint);
    }
           
走向自定義View的深坑——自定義屬性
從上面代碼加圖檔中的樣式我們不難看出,畫出來的第二層把第一層給覆寫掉了,我們的代碼也是由上至下執行的,最上面的代碼畫出來的東西會被我們在下面寫的代碼給壓在下面。

注意:因為我們使用的畫筆隻有一個,在畫出背景的時候我們給為了讓背景顯示特定的顔色填充整個背景,需要在畫矩形的時候給畫筆設定一個顔色,而這個畫筆又隻有一個并且現在已經設定上了一個顔色,如果下面再使用這個畫筆在畫布上畫其他東西,使用的顔色是現在設定上的顔色,這樣的話我們下面要畫出的文字的顔色就和背景是一個顔色的了,為了給我們的文字設定文字指定的顔色我們需要在沒有在畫布上繪畫的時候重新給畫筆設定一個顔色。

onMeasure方法的使用

我們先把代碼簡化到隻畫文字,此時我們還有一個需求就是設定一個背景色,而設定背景顔色這個在我們繼承的* View 這個類已經幫我們提供好了,那就是 android:background*這個屬性,下面我們先把這個屬性在布局檔案中給我們自定義的View設定上,設定為#555555,也就是灰色,然後我們來看現在顯示的效果。

走向自定義View的深坑——自定義屬性

可以看到,上圖顯示出來的,我們整個螢幕都變成了灰色,為什麼,我們設定的也是* wrap_content*為什麼還是全屏呢。

因為我們的View所在的父控件并沒有限制我們目前View可顯示的大小,預設我們的View就充滿整個螢幕了。這樣我們想要設定一個具體的值也是沒有用的,因為他現在是根據父View能給的大小來顯示。

為了我們能夠友善的管理這個View的大小,我們需要重寫onMeasure方法在父View調用我們子View的onMeasure方法詢問我們想要使用的大小的時候進行相應的計算,來合理地顯示我們的View。

下面是我們在onMeasure中的邏輯處理。

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

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

        // 指定了精确值,也就是設定了絕對大小,10dp這種
        if (widthMode == MeasureSpec.EXACTLY){
            mWidth = widthSize;
        }else{
            // 使用我們的畫筆來測量文字的大小,然後加上左邊padding和右邊padding值,來算出我們想要的寬度
            widthSize= mBounds.width() + getPaddingLeft() + getPaddingRight();

            // 等于這個模式就相當wrap_content,我們要找最小值
            if (widthMode == MeasureSpec.AT_MOST){
                // 讓預設的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
                widthSize = Math.min(mWidth, widthSize);
            }
        }

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

        // 指定了精确值,也就是設定了絕對大小,10dp這種
        if (heightMode == MeasureSpec.EXACTLY){
            mHeight = heightSize;
        }else{
            // 使用我們的畫筆來測量文字的大小,然後加上左邊padding和右邊padding值,來算出我們想要的寬度
            heightSize= mBounds.height() + getPaddingTop()+ getPaddingBottom();

            // 等于這個模式就相當wrap_content,我們要找最小值
            if (heightMode == MeasureSpec.AT_MOST){
                // 讓預設的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
                heightSize = Math.min(mHeight, heightSize);
            }
        }
        setMeasuredDimension(widthSize,heightSize);
    }
           

其中mBounds實在構造方法中,初始化畫筆的時候建立的,使用他主要是為了友善擷取文字的寬高,下面貼出初始化代碼。

// 建立一個畫筆,我們要在界面上畫出相應的東西,都需要靠這支筆。
        mPaint = new Paint();
        // 設定抗鋸齒
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        mBounds = new Rect();
        mPaint.getTextBounds(mText,,mText.length(), mBounds);
        mWidth = mBounds.width();
        mHeight = mBounds.height();
           

在初始化的時候注意,一定要在下面把預設的寬高也初始化,就是* mWidth = mBounds.width()和mHeight = mBounds.height(),從bound對象中拿到寬高,給我們預設的寬高指派,否則在我們測量的方法中設定如果是 wrap_content*的時候,我們的view在界面上是看不到的,因為沒有初始化寬高,預設是0,好了下面我們來看現在的效果圖。

走向自定義View的深坑——自定義屬性

可以看到,我們設定的背景成功的顯示了,并且是最小範圍的顯示,下面我們把寬高都設定成* match_parent*

走向自定義View的深坑——自定義屬性

可以看到,我們的文字跑到了最下面,這是為什麼呢,下面我們觀察onDraw方法裡面的代碼

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText(mText, , getMeasuredHeight(), mPaint);
    }
           

注意看我們drawText的第三個參數,這個參數我們設定的是測量後的高度,參數本身是用來定義從什麼高度的位置開始畫的,我們定義的是測量後的高度,而且我們現在是* match_parent*充滿螢幕的,是以現在就是從最底下開始畫文字,而這個文字畫的時候是文字的底部是0點,也就是我們現在文字的底部是在測量後的高度的最大值處,這時候我們想要讓文字從頂部開始畫,就用到我們剛才的Bound對象了,因為前面說了,我們的文字的底部是0點,那麼我們文字頂部就是負的高度,也就是說我們從高度為0的這個位置開始畫,我們的文字是在螢幕外面的。。。。看不見他,是以下面代碼改成這個樣子。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText(mText, , mBounds.height(), mPaint);
    }
           

來我們看看效果

走向自定義View的深坑——自定義屬性

現在我們看到,文字剛好顯示在螢幕的頂部。

Gravity屬性的創造

看看我們開始的時候定義的declare-styleable,其中還有一個屬性我們現在還沒有适配,gravity,下面我們來适配它。

在前面我們的測量方法已經寫好了,是以下面我們适配這個屬性很輕松了,

- top 按照我們上面寫好的代碼,就相當于top,不多做解釋(其實更相當于start,我想偷點懶。。。)

- bottom 還記得我們剛才那個文字跑到底下去了的那個嗎,那個就相當于bottom。

- 等等。。。。下面我們看代碼好了,有注釋的。。。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

import com.github.odriver.viewdemo.R;

/**
 * Created by odriver on 16-8-25.
 */

class TextView extends View {

    private int mTextColor;
    private float mTextSize;
    private int mGravity;
    private String mText;
    private Paint mPaint;
    private int mWidth;
    private int mHeight;
    private Rect mBounds;

    public TextView(Context context) {
        this(context,null);
    }

    public TextView(Context context, AttributeSet attrs) {
        this(context, attrs,);
    }

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

        // 使用context對象獲得我們自定義的屬性的值,attrs是我們xml中使用的屬性集合,其中包括屬性名和屬性值等相關資訊,後面是我們自定義的屬性,也就是我們定義的declare-styleable。
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TextView);

        // 使用相應的方法擷取各個類型屬性的值,第二個參數是擷取不到屬性值預設的屬性值。
        mTextColor = typedArray.getColor(R.styleable.TextView_textColor, Color.BLACK);
        mTextSize = typedArray.getDimension(R.styleable.TextView_textSize, );
        mGravity = typedArray.getInt(R.styleable.TextView_gravity, );
        mText = typedArray.getString(R.styleable.TextView_text);

        // 沒啥,google推薦的,使用完及時釋放
        typedArray.recycle();

        // 建立一個畫筆,我們要在界面上畫出相應的東西,都需要靠這支筆。
        mPaint = new Paint();
        // 設定抗鋸齒
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
        mBounds = new Rect();
        mPaint.getTextBounds(mText,,mText.length(), mBounds);
        mWidth = mBounds.width();
        mHeight = mBounds.height();
    }

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

        // top 1 || left 3
        float startx = ;
        float starty = mBounds.height();

        // center
        if (mGravity == ){
            startx = getMeasuredWidth()/-mBounds.width()/;
            starty = getMeasuredHeight()/-mBounds.height()/;
        }else if (mGravity == ){ // bottom
            starty = getMeasuredHeight();
        }else if (mGravity == ){ // right
            startx = getMeasuredWidth()-mBounds.width();
        }else if (mGravity == ){ // center_horizontal
            startx = getMeasuredWidth()/-mBounds.width()/;
        }else if (mGravity == ){ // center_vertical
            starty = getMeasuredHeight()/-mBounds.height()/;
        }

        // 使用canvas類的drawText方法将我們的文字畫到螢幕上。
        canvas.drawText(mText, startx, starty, mPaint);

    }

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

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

        // 指定了精确值,也就是設定了絕對大小,10dp這種
        if (widthMode == MeasureSpec.EXACTLY){
            mWidth = widthSize;
        }else{
            // 使用我們的畫筆來測量文字的大小,然後加上左邊padding和右邊padding值,來算出我們想要的寬度
            widthSize= mBounds.width() + getPaddingLeft() + getPaddingRight();

            // 等于這個模式就相當wrap_content,我們要找最小值
            if (widthMode == MeasureSpec.AT_MOST){
                // 讓預設的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
                widthSize = Math.min(mWidth, widthSize);
            }
        }

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

        // 指定了精确值,也就是設定了絕對大小,10dp這種
        if (heightMode == MeasureSpec.EXACTLY){
            mHeight = heightSize;
        }else{
            // 使用我們的畫筆來測量文字的大小,然後加上左邊padding和右邊padding值,來算出我們想要的寬度
            heightSize= mBounds.height() + getPaddingTop()+ getPaddingBottom();

            // 等于這個模式就相當wrap_content,我們要找最小值
            if (heightMode == MeasureSpec.AT_MOST){
                // 讓預設的寬度和計算出的寬度做對比,哪個小使用哪個,就是盡可能的小
                heightSize = Math.min(mHeight, heightSize);
            }
        }
        setMeasuredDimension(widthSize,heightSize);
    }
}
           

* 以上就是整個自定義View的相關代碼,style裡面配置的都在前面講的有*,下面我們來欣賞一下各種擺放姿勢。

走向自定義View的深坑——自定義屬性
走向自定義View的深坑——自定義屬性
走向自定義View的深坑——自定義屬性
走向自定義View的深坑——自定義屬性
走向自定義View的深坑——自定義屬性
走向自定義View的深坑——自定義屬性

* 好了簡單的自定義控件結束了,如果你有想法,那麼這麼多已經可以簡單自定義其他的控件了,比如進度條,隻需要改改onDraw方法把文字的相關操作去掉就可以了*