自定義View的繪製流程基礎分析

anAngryAnt發表於2018-02-25

一個基本的自定義View應該做的事情:

  1. 繪製角度來說,一共有三點 測量擺放繪畫他們本身以及子views(針對於ViewGroup而言)。
  2. 儲存UI狀態
  3. 處理觸控事件

今天先從繪製流程開始學習吧,繪製流程:constructor()->onMeasure()->onLayout()->onDraw()

在開始之前,我們先來看看Android 預設檢視的層級:

自定義View的繪製流程基礎分析


1. 通過onMeasure()方法,根據父容器的尺寸大小和約束,能知道一個View要佔多大的地方。這是一個自下而上執行的方法,也就是說,先從最下層的RootView開始執行測量,然後分發測量viewGroup中眾多的子Views。

  • 要搞清楚onMeasure(),我們先來看一下方法原型吧。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {....}
    複製程式碼

    剛剛讀原始碼的童鞋可能並不知道widthMeasureSpecheightMeasureSpec這兩個引數的作用是什麼。這兩個引數實際上可以通過MeasureSpec.getSize()MeasureSpec.getMode()這兩個方法來判斷該view被測量後的尺寸。那麼,為什麼還要通過MeasureSpec.getMode()呢?這就是接下來要說明的重點。

    以下是MeasureSpec中剔除部分程式碼之後的getMode()實現方式。

    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    //我們通過位運算可以知道,MODE_MASK實際上等於11(二進位制)左移30位的二進位制,在Java中int佔32位,所以這裡就表示的是int中最高位為11,其餘30位為0的二進位制數。
    
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    
     public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    ....
    
    public static int getMode(int measureSpec) {
        //measureSpec就是我們傳進來的 widthMeasureSpec 或者 heightMeasureSpec
        //它與上measureSpec,低30位就全部都清零了。
        //將返回值與UNSPECIFIED、EXACTLY、AT_MOST比較就能得到measureSpec的mode了。
        
        return (measureSpec & MODE_MASK);
    }
        
    複製程式碼

    通過以上的註釋style分析,我們知道了MeasureSpec是如何計算mode了,那麼為什麼要計算這些mode呢?與size又有什麼關係呢? View的MeasureSpec Mode有三種,分別為:UNSPECIFIEDEXACTLYAT_MOST,他們的規則如下(要注意一點的是,在子view中計算的mode,是parent view告訴它的):

    • UNSPECIFIED:parent view對子view沒有任何的約束,子view可以任一尺寸,但必須要有。發生場景: 對於一個ScorllView來說,它通常不會去約束子view高度的,也就是說,子view的高度加起來有多高,那麼ScrollView就有多高。在這種情況下,子view的mode就會為UNSPECIFIED
    • EXACTLY:parent view已經有了確切的size,子view的大小不能夠超過parent的大小。發生場景: 在parent view指定大小或者為match_parent的時候,子view會得到這個mode。
    • AT_MOST:在這種模式下,子view能夠儘可能大的達到設定的尺寸或者原尺寸。發生場景: 在parent view指定大小或者match_parent的情況下,子view為wrap_content,那麼mode將會為AT_MOST

    通過以上對mode的規則分析,我們就能夠知道,在onMeasure中要搞到尺寸,是需要根據mode來搞的。

    於是我們可以根據規則寫出如下程式碼:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
    
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;//如果不指定size,那麼我們就使用預設的size
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;//這兩種mode都允許我們直接使用測量出來的size
            break;
        }
        return result;
    }
    複製程式碼

2. 通過onLayout()方法知道這個控制元件應該放在哪個位置。*同樣,是一個自下而上的方法。*一般我們只有在重寫ViewGroup的時候需要自己處理onLayout()方法,因為該方法主要是ViewGroup用於擺放子view位置的(如:水平擺放或者垂直襬放,在這裡同學們可以參考一下LinearLayoutonLayout()方法的實現),一般我們只繼承View來定製我們的自定義View的時候,都不需要重寫該方法。不過需要注意的一點是,子view的margin屬性是否生效就要看parent是否在自身的onLayout方法進行處理,而view得padding屬性是在onDraw方法中生效的。

以下是重頭戲!!!

3. 通過onDraw(Canvas canvas)方法將這個控制元件繪畫出來。主要是通過Paint和引數中的canvas,還有各種Animation以及invalidate()postInvalidate()這兩個方法去進行檢視的重繪,實現動態效果。canvas中畫各種圖形的方法,比如說:rect(矩形),circle(圓形) 等等。當然你可以使用Path,PathMeasure去完成更加細膩的動畫。下面結合一個很的效果例項原始碼,來說明動態自定義View。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //根據模式來賦值with 和 height
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if( widthMode == MeasureSpec.EXACTLY )
            width = MeasureSpec.getSize(widthMeasureSpec);
        else width = ViewGroup.LayoutParams.MATCH_PARENT;


        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if( heightMode == MeasureSpec.EXACTLY )
            height = MeasureSpec.getSize(heightMeasureSpec);
        else height = ViewGroup.LayoutParams.MATCH_PARENT;
        
        //左端點的X
        leftX = margin;
        //右端點的X
        rightX = width - margin;

        y = height/2.0f;
        
        //線段的長度
        distance = width - ( 2 * margin + 2 * radius  );

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //繪製左端點
        canvas.drawCircle( leftX, y , radius * factor , mPaint);
        //繪製右端點
        canvas.drawCircle( rightX , y , radius * ( 1 - factor ), mPaint);
        mPaint.setStrokeWidth(5);
        //繪製線段
        canvas.drawLine(margin , y , margin + radius+ ((radius + distance)*(1-factor)) , y, mPaint);
    }
    
     public void startAnimation(){
        mAnimator = ValueAnimator.ofFloat(1, 0);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                factor = (float) animation.getAnimatedValue();
                postInvalidate();
            }
        });
        mAnimator.setDuration(1500);
        // 重複次數 無限迴圈
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        // 重複模式, RESTART: 重新開始 REVERSE:恢復初始狀態再開始
        mAnimator.setRepeatMode(ValueAnimator.REVERSE);
        mAnimator.start();
    }
    
複製程式碼

效果圖

自定義View的繪製流程基礎分析
沒錯!就是這麼皮!!╭(╯^╰)╮


requestLayout()、postInvalidate()、invalidate()的區別

  1. 實際上,後兩者的作用是一樣的,只不過postInvalidate內部會將重繪操作放入子執行緒中,而invalidate則是在呼叫執行緒中重繪view。
  2. requestLayout在什麼時候用呢?當view本身的測量屬性改變了的時候,就可以呼叫該方法去讓parent view去重新呼叫view的onMeasureonLayout方法,去重新評估view的大小和所在位置。

  • Enjoy Android :) 如果有誤,輕噴,歡迎指正。

相關文章