自定義view(一)----自定義TextView

自定義view也算是Android的一大難點,里面涉及到很多值得學習的地方,我會在接下來寫一系列文章去介紹它,本篇文章以自定義一個TextView為例。

View的構(gòu)造方法

自定義view之前我們先了解view的四個構(gòu)造方法,自定義view無非就是新建一個類去繼承View,為了閱讀方便,我們采用Java代碼進行分析(kotlin語言可讀性較差),首先定義一個類TextView2繼承View,按照提示實現(xiàn)所有構(gòu)造方法

public class TextView2 extends View {
    public TextView2(Context context) {
        super(context);
    }

    public TextView2(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public TextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public TextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

需要實現(xiàn)它的四個構(gòu)造方法,我在下面分別給出每個構(gòu)造方法的調(diào)用時期。

  public TextView2(Context context) {
        super(context);
    }

這個構(gòu)造方法通過閱讀源碼注釋加以理解可以知道,它是在new對象時簡單調(diào)用的構(gòu)造方法,其中參數(shù)context是視圖運行的上下文,通過它可以訪問當前的主題、資源等。

public TextView2(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

上面這個構(gòu)造方法是當TextView2在layout布局中使用時調(diào)用,參數(shù):
context-----視圖運行的上下文,通過它可以訪問當前的主題、資源等
attrs-----使視圖膨脹的XML標記的屬性

  public TextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

這個構(gòu)造方法調(diào)用時期是在自定義樣式時調(diào)用,比如我們在layout視圖中使用了我們自定義的某個style,就會調(diào)用這個構(gòu)造方法,參數(shù):
context-----視圖運行的上下文,通過它可以訪問當前的主題、資源等
attrs-----使視圖膨脹的XML標記的屬性
defStyleAttr-----當前主題中的一個屬性,它包含對為視圖提供默認值的樣式資源的引用

 public TextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

最后這個方法是后面才加入的,也很少用到,它的作用是從XML執(zhí)行膨脹,并從主題屬性或樣式資源應(yīng)用特定于類的基樣式。 View的這個構(gòu)造函數(shù)允許子類在膨脹時使用自己的基樣式。當確定一個特定屬性的最終值時,有四個輸入會起作用:

  • 給定AttributeSet中的任何屬性值。
  • 在AttributeSet中指定的樣式資源(名為“style”)。
  • defStyleAttr指定的默認樣式。
  • defStyleRes指定的默認樣式。
    關(guān)于這個構(gòu)造函數(shù)在使用時發(fā)現(xiàn)會報錯,具體什么原因后面會細講,現(xiàn)在先賣個關(guān)子。
    我們一般會對前三個構(gòu)造方法進行改造,方便我們使用其中任何一個構(gòu)造方法都能遍歷所有構(gòu)造方法,比如可以在第一個構(gòu)造方法中調(diào)用第二個,在第二個中調(diào)用第三個,這樣不管使用哪個構(gòu)造方法都能實現(xiàn)我們構(gòu)造方法中的業(yè)務(wù)邏輯。改造如下
//在第一個構(gòu)造方法中調(diào)用第二個構(gòu)造方法
    public TextView2(Context context) {
        this(context, null);
    }

    //在第二個構(gòu)造方法中調(diào)用第三個構(gòu)造方法
    public TextView2(Context context, @Nullable AttributeSet attrs) {

        this(context, attrs, 0);
    }

    //通常在第三個構(gòu)造方法中獲取自定義屬性
    public TextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

自定義屬性并獲取自定義屬性

接上文,我們自定義view入門就是自定義一個Textview,系統(tǒng)提供的TextView在布局中使用時基本上如下所示:

   <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="zzp"
        android:textColor="@color/black"
        android:textSize="16sp" />

一個TextView必須具備的就是寬和高的指定,這個在在定義的下一部分會講,還有就是后面三句,對text、textColor和textSize進行了賦值,這三個就是view的屬性,那我們在自定義view時屬性也是需要自定義的,那怎么進行屬性的自定義呢,我在這里將它分為一下幾步

  • 1、創(chuàng)建自定義屬性資源文件
    在res文件夾下的value模塊創(chuàng)建一個Values Resource File取名為attrs(可以自由取名),在內(nèi)部實現(xiàn)三個自定義屬性text1、textColor1、textSize1,如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--自定義屬性的名字-->
    <declare-styleable name="TextView2">
        <!--name 屬性名稱  format 格式-->
       <!-- 常用格式:color  顏色
                    string 文字
                    dimension 寬高或者文字大小
                    reference 資源
                    -->
        <attr name="text1" format="string"/>
        <attr name="textColor1" format="color"/>
        <attr name="textSize1" format="dimension"/>
    </declare-styleable>
</resources>

這沒什么好說的,注意看代碼中注釋的常用格式?,F(xiàn)在就是已經(jīng)自定義了三個屬性text1、textColor1、textSize1,分別是string型、color型、dimension型。值得注意的是在自定義屬性時系統(tǒng)已經(jīng)有的屬性不能被重復(fù)定義,所以name不能用text、textColor和textSize。我都加了個1作區(qū)分,不然會被認定為重新定義系統(tǒng)屬性而報錯。

  • 2、在布局文件中使用
    接下來可以仿照TextView在布局文件中使用
 <com.example.kotlindemo.TextView2
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:text1="zzp"
        app:textColor1="@color/black"
        app:textSize1="16sp" />

我們使用了自定義的TextView2,并將自定義屬性賦值,但是運行還是沒有效果,因為我們還需要將自定義的屬性與原本的屬性相關(guān)聯(lián),比如自定義的text1屬性相當于TextView的text,textColor1屬性相當于TextView的textColor等

  • 3、獲取自定義屬性
    一般在第三個構(gòu)造方法中獲取屬性,整體代碼如下,請看注釋:
public class TextView2 extends View {
    private String mText;
    private int mTextColor= Color.BLACK;
    private int mTextSize;
    //在第一個構(gòu)造方法中調(diào)用第二個構(gòu)造方法
    public TextView2(Context context) {
        this(context, null);
    }

    //在第二個構(gòu)造方法中調(diào)用第三個構(gòu)造方法
    public TextView2(Context context, @Nullable AttributeSet attrs) {

        this(context, attrs, 0);
    }

    //通常在第三個構(gòu)造方法中獲取自定義屬性
    public TextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //獲取自定義布局TextView2的屬性
        TypedArray array=context.obtainStyledAttributes(attrs,R.styleable.TextView2);
        //將獲取到的屬性賦值給TextView2的成員變量
        mText=array.getString(R.styleable.TextView2_text1);
        mTextColor=array.getColor(R.styleable.TextView2_textColor1,Color.BLACK);//第二個參數(shù)為默認值
        mTextSize=array.getDimensionPixelSize(R.styleable.TextView2_textSize1,18);//注意接收時使用getDimensionPixelSize,在TextView源碼中就是使用這個方法接收,也算是個坑
        //回收
        array.recycle();
    }
}

運行起來發(fā)現(xiàn)還是沒有效果,因為還沒有指定寬高和繪畫,且往下看。

自定義View中的onMeaSure()

我們在上面實現(xiàn)的代碼運行起來依舊一片空白,我也說了是因為沒有指定寬高和進行繪畫,一步步來,先看指定寬高,怎么指定寬高呢?自定義view時需要在onMeaSure方法為view指定寬高,所以在解決這個問題前我們先來了解一下這個方法的一些基本知識。
onMeaSure()方法是自定義view中一個非常重要的方法,他具體有什么作用呢?它是自定義view的測量方法,view的寬和高都由它來指定。
我們先來看一段代碼,了解幾樣東西。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //獲取寬高的模式
        int widthmode=MeasureSpec.getMode(widthMeasureSpec);
        int heifhtmode=MeasureSpec.getMode(heightMeasureSpec);
    }

在上面這段代碼中我們實現(xiàn)了onMeaSure方法,并且在里面獲取了兩個變量。我們在布局文件中定義view時通常會給view指定寬高,指定寬高時分為三種情況,第一種是包裹內(nèi)容(wrap_content),第二種是是view充滿布局(match_parent),第三種是給view指定一個數(shù)值的寬高(比如100dp),上面代碼中的

 int widthmode=MeasureSpec.getMode(widthMeasureSpec);
 int heifhtmode=MeasureSpec.getMode(heightMeasureSpec);

可以用來判斷該自定義view使用了哪種方式指定寬高,下面我就直接用代碼說明獲取到的值分別對應(yīng)哪一種指定寬高的情況。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //獲取寬高的模式
        int widthmode=MeasureSpec.getMode(widthMeasureSpec);
        int heifhtmode=MeasureSpec.getMode(heightMeasureSpec);
        //說明三種情況對應(yīng)的值,以寬為例
        if (widthmode==MeasureSpec.AT_MOST){
            Log.e("zzp","指定寬度的方式為wrap_content");
        }else if (widthmode==MeasureSpec.EXACTLY){
            Log.e("zzp","指定寬度的方式為確切的值,match_parent,fill_parent");
        }else if (widthmode==MeasureSpec.UNSPECIFIED){
            Log.e("zzp","布局盡可能大,一般在listview,scollview會用到");
        }
    }

如上面代碼所示,getMode()能夠獲取到三個有效值,每個值對應(yīng)哪種情況,值得注意的是確切值和match_parent都是對應(yīng)MeasureSpec.EXACTLY,我看到好多博客都把match_parent寫成對應(yīng)MeasureSpec.UNSPECIFIED,這是錯誤的。
在了解三種測量模式后,我們就可以針對不同情況對view指定寬高,根據(jù)三種模式分為三種情況:

  • 1、確切的值,不需要計算,給多少就是多少
  • 2、給的是wrap_content,需要計算寬高,使用畫筆進行計算
public class TextView2 extends View {
    private String mText;
    private int mTextColor = Color.WHITE;
    private int mTextSize;

    private Paint mPaint = new Paint();

    //在第一個構(gòu)造方法中調(diào)用第二個構(gòu)造方法
    public TextView2(Context context) {
        this(context, null);
    }

    //在第二個構(gòu)造方法中調(diào)用第三個構(gòu)造方法
    public TextView2(Context context, @Nullable AttributeSet attrs) {

        this(context, attrs, 0);
    }

    //通常在第三個構(gòu)造方法中獲取自定義屬性
    public TextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //獲取自定義布局TextView2的屬性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextView2);
        mText = array.getString(R.styleable.TextView2_text1);
        mTextColor = array.getColor(R.styleable.TextView2_textColor1, Color.BLACK);//第二個參數(shù)為默認值
        mTextSize = array.getDimensionPixelSize(R.styleable.TextView2_textSize1, 18);//注意接收時使用getDimensionPixelSize,在TextView源碼中就是使用這個方法接收,也算是個坑
        //設(shè)置抗鋸齒
        mPaint.setAntiAlias(true);
        //設(shè)置字體
        mPaint.setTextSize(mTextSize);
        //設(shè)置字體顏色
        mPaint.setColor(mTextColor);
        //回收
        array.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //獲取寬高的模式
        int widthmode=MeasureSpec.getMode(widthMeasureSpec);
        int heifhtmode=MeasureSpec.getMode(heightMeasureSpec);
        //確切的值,不需要測量
        int width=MeasureSpec.getSize(widthMeasureSpec);
        //給的是wrap_content,需要計算寬高,使用畫筆進行計算
        if (widthmode==MeasureSpec.AT_MOST){
            Rect rect=new Rect();
            mPaint.getTextBounds(mText,0,mText.length(),rect);
             width=rect.width();
        }

        int height=MeasureSpec.getSize(heightMeasureSpec);
        if (heifhtmode==MeasureSpec.AT_MOST){
            Rect rect=new Rect();
            mPaint.getTextBounds(mText,0,mText.length(),rect);
            height=rect.width();
        }
        setMeasuredDimension(width,height);
    }
}

為了效果明顯,我給了一個黑色背景

<com.example.kotlindemo.TextView2
        android:background="#000"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:text1="zzp"
        app:textColor1="@color/white"
        app:textSize1="16sp" />

現(xiàn)在運行起來就有效果了,有一個黑色的小框,只是沒有顯示文字,因為還要進行繪畫,可以在布局文件中切換wrap_content和確切的值運行后查看效果。關(guān)于畫筆,有興趣的同學可以去了解一下,我都是把它當做一個測量的工具,直接死記硬背。

自定義View中的onDraw()

我們在上面實現(xiàn)計算控件寬高,但是顯示不出文字,是因為還沒進行繪畫,所以還需要實現(xiàn)它的onDraw()方法。

 protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //第二個參數(shù)--代表開始的水平位置(以控件為基準)
        //第三個參數(shù)--基線,baseline
        canvas.drawText(mText,0,getHeight()/2,mPaint);
    }

這樣就像是出了效果


image.png

但是發(fā)現(xiàn)文字顯示偏上,這是因為基線y的值不能直接賦值為高度的一半,這里涉及到多個概念,我也是查閱很多資料才弄懂。

  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       //第二個參數(shù)--代表開始的水平位置(以控件為基準)
        //第三個參數(shù)--基線,baseline
        Paint.FontMetricsInt fontMetricsInt=mPaint.getFontMetricsInt();
        int dy=(fontMetricsInt.bottom-fontMetricsInt.top)/2-fontMetricsInt.bottom;
        int baseLine=getHeight()/2+dy;
        canvas.drawText(mText,0,baseLine,mPaint);
    }

下面借助一張圖來解釋一下為什么要這樣算基線的值


image.png

如果我們想要文字豎直居中,那么基線位置如圖中BaseLine一樣,先來梳理一下現(xiàn)在的已知條件,
getHeight()/2------整個視圖高度的一半
FontMetricsInt.top ----基線到文字頂部的距離,是一個負值
FontMetricsInt.bottom--基線到文字底部的距離,是一個正值
假設(shè)基線的值y=getHeight()/2+dy
dy表示基線與高度的一半之間的距離。
dy=(fontMetricsInt.bottom-fontMetricsInt.top)/2-fontMetricsInt.bottom
這樣基線的值就可以計算出來了。
重新運行,就能看到豎直居中后的效果了。

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

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

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