搜尋
首頁Javajava教程Android自訂View

Android自訂View

Nov 17, 2016 am 09:07 AM

前言

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

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。