首頁 > Java > java教程 > 主體

Android自訂View

高洛峰
發布: 2016-11-17 09:07:09
原創
1305 人瀏覽過

前言

Android自訂View的詳細步驟是我們每個Android開發人員都必須掌握的技能,因為在開發中總是會遇到自訂View的需求。為了提升自己的技術水平,自己就係統的去研究了一下,在這裡寫下一點心得,有不足之處希望大家及時指出。

流程

在Android中對於佈局的請求繪製是在Android framework層開始處理的。繪製是從根節點開始,對佈局樹進行measure與draw。在RootViewImpl中的performTraversals展開。它所做的就是對需要的視圖進行measure(測量視圖大小)、layout(確定視圖的位置)與draw(繪製視圖)。下面的圖能很好的展現視圖的繪製流程:  

Android自訂View

當使用者呼叫requestLayout時,只會觸發measure與layout,但係統開始呼叫時還會觸發draw

下面來詳細介紹這幾個流程。

measure

measure是View中的final型方法不可以進行重寫。它是對視圖的大小進行測量計算,但它會回調onMeasure方法,所以我們在自訂View的時候可以重寫onMeasure方法來對View進行我們所需要的測量。它有兩個參數widthMeasureSpec與heightMeasureSpec。其實這兩個參數都包含兩個部分,分別為size與mode。 size為測量的大小而mode為視圖佈局的模式

我們可以透過以下程式碼分別取得:

int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
int heightSize = MeasureSpec.getSize(heightMeasureSpec); 
int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
登入後複製

所取得的mode種類分為以下三種:

Android自訂View

setMeasuredDimension

透過以上邏輯視圖的邏輯視圖寬高,最後要呼叫setMeasuredDimension方法將測量好的寬高進行傳遞出去。其實最後是呼叫setMeasuredDimensionRaw方法對傳過來的值進行屬性賦值。呼叫super.onMeasure()的呼叫邏輯也是一樣的。

下面以自訂一個驗證碼的View為例,它的onMeasure方法如下:

@Override 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
        int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
        int heightSize = MeasureSpec.getSize(heightMeasureSpec); 
        int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
        int heightMode = MeasureSpec.getMode(heightMeasureSpec); 
        if (widthMode == MeasureSpec.EXACTLY) { 
            //直接获取精确的宽度 
            width = widthSize; 
        } else if (widthMode == MeasureSpec.AT_MOST) { 
            //计算出宽度(文本的宽度+padding的大小) 
            width = bounds.width() + getPaddingLeft() + getPaddingRight(); 
        } 
        if (heightMode == MeasureSpec.EXACTLY) { 
            //直接获取精确的高度 
            height = heightSize; 
        } else if (heightMode == MeasureSpec.AT_MOST) { 
            //计算出高度(文本的高度+padding的大小) 
            height = bounds.height() + getPaddingBottom() + getPaddingTop(); 
        } 
        //设置获取的宽高 
        setMeasuredDimension(width, height); 
    }
登入後複製

可以對自訂View的layout_width與layout_height進行設定不同的屬性,達到不同的mode類型,就可以看到不同的效果

measureChildren

如果你是對繼承ViewGroup的自訂View那麼在進行測量自身的大小時還要測量子視圖的大小。一般透過measureChildren(int widthMeasureSpec, int heightMeasureSpec)方法來測量子視圖的大小。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { 
        final int size = mChildrenCount; 
        final View[] children = mChildren; 
        for (int i = 0; i < size; ++i) { 
            final View child = children[i]; 
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { 
                measureChild(child, widthMeasureSpec, heightMeasureSpec); 
            } 
        } 
    }
登入後複製

透過上面的原始碼會發現,它其實是遍歷每一個子視圖,如果該子視圖不是隱藏的就調用measureChild方法,那麼來看下measureChild源碼:

protected void measureChild(View child, int parentWidthMeasureSpec, 
            int parentHeightMeasureSpec) { 
        final LayoutParams lp = child.getLayoutParams(); 
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
                mPaddingLeft + mPaddingRight, lp.width); 
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 
                mPaddingTop + mPaddingBottom, lp.height); 
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
    }
登入後複製

會發現它首先調用了getChildMeasureSpec方法來分別取得寬高,最後再呼叫的就是View的measure方法,而透過前面的分析我們已經知道它所做的就是對視圖大小的計算。而對於measure中的參數是透過getChildMeasureSpec獲取,再來看下其原始碼:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
        int specMode = MeasureSpec.getMode(spec); 
        int specSize = MeasureSpec.getSize(spec); 
  
        int size = Math.max(0, specSize - padding); 
  
        int resultSize = 0; 
        int resultMode = 0; 
  
        switch (specMode) { 
        // Parent has imposed an exact size on us 
        case MeasureSpec.EXACTLY: 
            if (childDimension >= 0) { 
                resultSize = childDimension; 
                resultMode = MeasureSpec.EXACTLY; 
            } else if (childDimension == LayoutParams.MATCH_PARENT) { 
                // Child wants to be our size. So be it. 
                resultSize = size; 
                resultMode = MeasureSpec.EXACTLY; 
            } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
                // Child wants to determine its own size. It can&#39;t be 
                // bigger than us. 
                resultSize = size; 
                resultMode = MeasureSpec.AT_MOST; 
            } 
            break; 
  
        // Parent has imposed a maximum size on us 
        case MeasureSpec.AT_MOST: 
            if (childDimension >= 0) { 
                // Child wants a specific size... so be it 
                resultSize = childDimension; 
                resultMode = MeasureSpec.EXACTLY; 
            } else if (childDimension == LayoutParams.MATCH_PARENT) { 
                // Child wants to be our size, but our size is not fixed. 
                // Constrain child to not be bigger than us. 
                resultSize = size; 
                resultMode = MeasureSpec.AT_MOST; 
            } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
                // Child wants to determine its own size. It can&#39;t be 
                // bigger than us. 
                resultSize = size; 
                resultMode = MeasureSpec.AT_MOST; 
            } 
            break; 
  
        // Parent asked to see how big we want to be 
        case MeasureSpec.UNSPECIFIED: 
            if (childDimension >= 0) { 
                // Child wants a specific size... let him have it 
                resultSize = childDimension; 
                resultMode = MeasureSpec.EXACTLY; 
            } else if (childDimension == LayoutParams.MATCH_PARENT) { 
                // Child wants to be our size... find out how big it should 
                // be 
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; 
                resultMode = MeasureSpec.UNSPECIFIED; 
            } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
                // Child wants to determine its own size.... find out how 
                // big it should be 
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; 
                resultMode = MeasureSpec.UNSPECIFIED; 
            } 
            break; 
        } 
        //noinspection ResourceType 
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
    }
登入後複製

是不是容易理解了點呢。它做的就是前面所說的根據mode的類型,取得對應的size。根據父視圖的mode類型與子視圖的LayoutParams類型來決定子視圖所屬的mode,最後再將取得的size與mode透過MeasureSpec.makeMeasureSpec方法整合回傳。最後傳遞到measure中,這就是前面所說的widthMeasureSpec與heightMeasureSpec中包含的兩部分的值。整個過程為measureChildren->measureChild->getChildMeasureSpec->measure->onMeasure->setMeasuredDimension,所以透過measureChildren就可以對子視圖進行測量計算。

layout

layout也是一樣的內部會回調onLayout方法,該方法是用來確定子視圖的繪製位置,但這個方法在ViewGroup中是個抽象方法,所以如果要自定義的View是繼承ViewGroup的話就必須實作該方法。但如果是繼承View的話就不需要了,View裡有一個空實作。而子視圖位置的設定是透過View的layout方法透過傳遞計算出來的left、top、right與bottom值,而這些值一般都要藉助View的寬高來計算,視圖的寬高則可以透過getMeasureWidth與getMeasureHeight方法獲取,這兩個方法所獲得的值就是上面onMeasure中setMeasuredDimension傳遞的值,即子視圖測量的寬度。

getWidth、getHeight與getMeasureWidth、getMeasureHeight是不同的,前者是在onLayout之後才能得到的值,分別為left-right與top-bottom;而後者是在onMeasure之後才能獲得到的值。只不過這兩種取得的值一般都是相同的,所以要注意呼叫的時機。

下面以定義一個把子視圖放置於父視圖的四個角的View為例:

@Override 
    protected void onLayout(boolean changed, int l, int t, int r, int b) { 
        int count = getChildCount(); 
        MarginLayoutParams params; 
         
        int cl; 
        int ct; 
        int cr; 
        int cb; 
             
        for (int i = 0; i < count; i++) { 
            View child = getChildAt(i); 
            params = (MarginLayoutParams) child.getLayoutParams(); 
                 
            if (i == 0) { 
                //左上角 
                cl = params.leftMargin; 
                ct = params.topMargin; 
            } else if (i == 1) { 
                //右上角 
                cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth(); 
                ct = params.topMargin; 
            } else if (i == 2) { 
                //左下角 
                cl = params.leftMargin; 
                ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight() 
                 - params.topMargin; 
            } else { 
                //右下角 
                cl = getMeasuredWidth() - params.rightMargin - child.getMeasuredWidth(); 
                ct = getMeasuredHeight() - params.bottomMargin - child.getMeasuredHeight() 
                 - params.topMargin; 
            } 
            cr = cl + child.getMeasuredWidth(); 
            cb = ct + child.getMeasuredHeight(); 
            //确定子视图在父视图中放置的位置 
            child.layout(cl, ct, cr, cb); 
        } 
    }
登入後複製

至於onMeasure的實現源碼我後面會給鏈接,如果要看效果圖的話,我後面也會貼出來,前面的驗證碼的也是一樣

draw

draw是由dispatchDraw发动的,dispatchDraw是ViewGroup中的方法,在View是空实现。自定义View时不需要去管理该方法。而draw方法只在View中存在,ViewGoup做的只是在dispatchDraw中调用drawChild方法,而drawChild中调用的就是View的draw方法。那么我们来看下draw的源码:

public void draw(Canvas canvas) { 
        final int privateFlags = mPrivateFlags; 
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && 
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); 
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; 
          
        /* 
         * Draw traversal performs several drawing steps which must be executed 
         * in the appropriate order: 
         * 
         *      1. Draw the background 
         *      2. If necessary, save the canvas&#39; layers to prepare for fading 
         *      3. Draw view&#39;s content 
         *      4. Draw children 
         *      5. If necessary, draw the fading edges and restore layers 
         *      6. Draw decorations (scrollbars for instance) 
         */ 
           
        // Step 1, draw the background, if needed 
        int saveCount; 
  
        if (!dirtyOpaque) { 
            drawBackground(canvas); 
        } 
          
        // skip step 2 & 5 if possible (common case) 
        final int viewFlags = mViewFlags; 
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; 
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; 
        if (!verticalEdges && !horizontalEdges) { 
            // Step 3, draw the content 
            if (!dirtyOpaque) onDraw(canvas); 
              
            // Step 4, draw the children 
            dispatchDraw(canvas); 
              
            // Overlay is part of the content and draws beneath Foreground 
            if (mOverlay != null && !mOverlay.isEmpty()) { 
                            mOverlay.getOverlayView().dispatchDraw(canvas); 
            } 
                          
            // Step 6, draw decorations (foreground, scrollbars) 
            onDrawForeground(canvas); 
                        
            // we&#39;re done... 
            return; 
        } 
        //省略2&5的情况 
        .... 
}
登入後複製

源码已经非常清晰了draw总共分为6步;

绘制背景

如果需要的话,保存layers

绘制自身文本

绘制子视图

如果需要的话,绘制fading edges

绘制scrollbars

其中 第2步与第5步不是必须的。在第3步调用了onDraw方法来绘制自身的内容,在View中是空实现,这就是我们为什么在自定义View时必须要重写该方法。而第4步调用了dispatchDraw对子视图进行绘制。还是以验证码为例:

@Override 
    protected void onDraw(Canvas canvas) { 
        //绘制背景 
        mPaint.setColor(getResources().getColor(R.color.autoCodeBg)); 
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint); 
 
        mPaint.getTextBounds(autoText, 0, autoText.length(), bounds); 
        //绘制文本 
        for (int i = 0; i < autoText.length(); i++) { 
             mPaint.setColor(getResources().getColor(colorRes[random.nextInt(6)])); 
            canvas.drawText(autoText, i, i + 1, getWidth() / 2 - bounds.width() / 2 + i * bounds.width() / autoNum 
                    , bounds.height() + random.nextInt(getHeight() - bounds.height()) 
                    , mPaint); 
        } 
  
        //绘制干扰点 
        for (int j = 0; j < 250; j++) { 
             canvas.drawPoint(random.nextInt(getWidth()), random.nextInt(getHeight()), pointPaint); 
        } 
  
        //绘制干扰线 
        for (int k = 0; k < 20; k++) { 
            int startX = random.nextInt(getWidth()); 
            int startY = random.nextInt(getHeight()); 
            int stopX = startX + random.nextInt(getWidth() - startX); 
            int stopY = startY + random.nextInt(getHeight() - startY); 
             linePaint.setColor(getResources().getColor(colorRes[random.nextInt(6)])); 
            canvas.drawLine(startX, startY, stopX, stopY, linePaint); 
        } 
    }
登入後複製

图,与源码链接

示例图

Android自訂View

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板