Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android自定義View(一)

Android自定義View(一)

編輯:關於Android編程

對於很多Android入門程序猿來說自定義View,都是比較恐懼的,但是這又是高手進階的必經之路。先總結下自定義View的步驟:
1、自定義View的屬性
2、在View的構造方法中獲得我們自定義的屬性
[ 3、重寫onMesure ]
4、重寫onDraw
我把3用[]標出了,所以說3不一定是必須的,當然了大部分情況下還是需要重寫的。
其中第1,第2點在前面的文章已經有詳細的介紹Android自定義屬性,不了解的童鞋可以去看看參考下,本文著重介紹第3和第4點。

onMeasure()
onMeasure()方法顧名思義就是用於測量視圖的大小的。它接收兩個參數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於確定視圖的寬度和高度的規格和大小。
MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型,如下所示:

EXACTLY

表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意願設置成任意的大小。(一般是在布局中設置了明確的值或者是MATCH_PARENT)

AT_MOST

表示子視圖最多只能是specSize中指定的大小,開發人員應該盡可能小得去設置這個視圖,並且保證不會超過specSize。系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意願設置成任意的大小。(一般為WARP_CONTENT)

UNSPECIFIED

表示開發人員可以將視圖按照自己的意願設置成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
那麼你可能會有疑問了,widthMeasureSpec和heightMeasureSpec這兩個值又是從哪裡得到的呢?通常情況下,這兩個值都是由父視圖經過計算後傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小。
當然,onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統默認的測量方式,可以按照自己的意願進行定制,比如:

public class MyView extends View {  

    ......  

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        setMeasuredDimension(200, 200);  
    }    
} 

這樣的話就把View默認的測量流程覆蓋掉了,不管在布局文件中定義MyView這個視圖的大小是多少,最終在界面上顯示的大小都將會是200*200。
需要注意的是,在setMeasuredDimension()方法調用之後,我們才能使用getMeasuredWidth()和getMeasuredHeight()來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0(注意區別於而getWidth()和getHeight(),這兩個方法要在ViewGroup的onLayout()過程結束後才能獲取到)。
由此可見,視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML文件中指定視圖的大小,然後視圖本身會對最終的大小進行拍板。

onDraw()
measure和layout的過程都結束後,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這裡才真正地開始對視圖進行繪制。

好了,原理性東西我們講過了,下面通過幾個簡單的例子來驗證一下,同樣按上面四步寫法:

1

自定義View的屬性,首先在res/values/ 下建立一個attrs.xml , 在裡面定義我們的屬性和聲明我們的整個樣式。

  
     
      
          
          
          
       
  

然後在布局中聲明我們的自定義View,一定要引入我們的命名空間xmlns:custom=”http://schemas.android.com/apk/res/com.hx.test”

  

        
  
2

在View的構造方法中,獲得我們的自定義的屬性,我們重寫了3個構造方法,默認的布局文件調用的是兩個參數的構造方法,所以記得讓所有的構造調用我們的三個參數的構造,我們在三個參數的構造中獲得自定義屬性。

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

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

    /** 
     * 獲得我自定義的樣式屬性 
     *  
     * @param context 
     * @param attrs 
     * @param defStyle 
     */  
    public CustomTextView(Context context, AttributeSet attrs, int defStyle)  
    {  
        super(context, attrs, defStyle);  
        /** 
         * 獲得我們所定義的自定義樣式屬性 
         */  
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTextView, defStyle, 0);  
        int n = a.getIndexCount();  
        for (int i = 0; i < n; i++)  
        {  
            int attr = a.getIndex(i);  
            switch (attr)  
            {  
            case R.styleable.CustomTextView_titleText:  
                mTitleText = a.getString(attr);  
                break;  
            case R.styleable.CustomTextView_titleTextColor:  
                mTitleTextColor = a.getColor(attr, Color.BLACK);  
                break;  
            case R.styleable.CustomTextView_titleTextSize:  
                // 默認設置為16sp,TypeValue也可以把sp轉化為px  
                mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(  
                        TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));  
                break;  

            }  

        }  
        a.recycle();  

        /** 
         * 獲得繪制文本的寬和高 
         */  
        mPaint = new Paint();  
        mPaint.setTextSize(mTitleTextSize);  
        mPaint.setColor(mTitleTextColor);  
        mBound = new Rect();  
        mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);  
    }  
3

我們重寫onDraw,onMesure調用系統提供的:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        mPaint.setColor(mTitleTextColor);
        canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2,
                getHeight() / 2 + mBound.height() / 2, mPaint);
    }

運行效果圖:
這裡寫圖片描述

這已經基本已經實現了自定義View。但是此時如果我們把布局文件的寬和高寫成wrap_content,會發現效果並不是我們的預期:
這裡寫圖片描述
這是因為MATCH_PARENT 時系統返回的specMode的值為AT_MOST,而specSize為全屏,子視圖默認占據了specSize中指定大小的范圍。這時需要我們重寫onDraw來實現自己的邏輯。

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
    {  
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
        int width;  
        int height ;  
        if (widthMode == MeasureSpec.EXACTLY) {  
            width = widthSize;  
        } else {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);   
            float textWidth = mBound.width();  
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());  
            width = desired;  
        }       
        if (heightMode == MeasureSpec.EXACTLY) {  
            height = heightSize;  
        } else {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);  
            float textHeight = mBound.height();  
            int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());  
            height = desired;  
        }  
        setMeasuredDimension(width, height);  
    } 

我們再來修改下布局文件

  

        

效果如下:
這裡寫圖片描述

完全復合我們的預期,現在我們可以對高度、寬度進行隨便的設置了,基本可以滿足我們的需求。我們還可以在構造函數中對它的點擊事件做一些處理:

this.setOnClickListener(new OnClickListener() {   
      @Override  
      public void onClick(View v) {  
            mTitleText = randomText();  
            postInvalidate();
            //invalidate(); 
      } 
});  
    private String randomText() {  
        Random random = new Random();  
        int randomInt = random.nextInt(5);
        return Str[randomInt];
    }

其中

private String[] Str = {"Text0", "Text1", "Text2", "Text3", "Text4"};

這裡寫圖片描述
可以看到點擊事件已經生效了,每次當我們點擊自定義View時顯示的文字內容都發生變化。調用postInvalidate()就是讓View重新執行onDraw,其實這裡也可以使用invalidate(),都能生效。他們區別在於:
invalidate()得在UI線程中被調動,在工作者線程中可以通過Handler來通知UI線程進行界面更新。而postInvalidate()可以在工作者線程中被調用。

但是還有一個問題,我們看到點擊後自定義View的文字變短了,但自定義View的寬卻沒有相應變窄,這不符合我們寫的WRAP_CONTENT的思想。其實這是由於postInvalidate()只是強制View重新走onDraw,而View的寬高是在onMesure中定義的,所以文字改變後需要強制自定義View也要走onMesure,重新測量,繼續修改代碼:

this.setOnClickListener(new OnClickListener() {   
        @Override  
        public void onClick(View v){  
            mTitleText = randomText();
            requestLayout();  
         } 
  }); 

這裡寫圖片描述
完美實現我們的構想。總結一下RequestLayout用法:
RequestLayout:
當view確定自身已經不再適合現有的區域時,該view本身調用這個方法要求parent view重新調用他的onMeasure onLayout來對重新設置自己位置。
特別的當view的layoutparameter發生改變,並且它的值還沒能應用到view上,這時候適合調用這個方法。也就是當通過getLayoutParrms().width = XXX的時候,我們需要重新調用RequestLayout。
invalidate:
View類調用迫使view重畫(onDraw)。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved