androidlayoutview
① Android UI繪制之View繪制的工作原理
這是AndroidUI繪制流程分析的第二篇文章,主要分析界面中View是如何繪制到界面上的具體過程。
ViewRoot 對應於 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View的三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢後,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 建立關聯。
measure 過程決定了 View 的寬/高, Measure 完成以後,可以通過 getMeasuredWidth 和 getMeasuredHeight 方法來獲取 View 測量後的寬/高,在幾乎所有的情況下,它等同於View的最終的寬/高,但是特殊情況除外。 Layout 過程決定了 View 的四個頂點的坐標和實際的寬/高,完成以後,可以通過 getTop、getBottom、getLeft 和 getRight 來拿到View的四個頂點的位置,可以通過 getWidth 和 getHeight 方法拿到View的最終寬/高。 Draw 過程決定了 View 的顯示,只有 draw 方法完成後 View 的內容才能呈現在屏幕上。
DecorView 作為頂級 View ,一般情況下,它內部會包含一個豎直方向的 LinearLayout ,在這個 LinearLayout 裡面有上下兩個部分,上面是標題欄,下面是內容欄。在Activity中,我們通過 setContentView 所設置的布局文件其實就是被加到內容欄中的,而內容欄id為 content 。可以通過下面方法得到 content:ViewGroup content = findViewById(R.android.id.content) 。通過 content.getChildAt(0) 可以得到設置的 view 。 DecorView 其實是一個 FrameLayout , View 層的事件都先經過 DecorView ,然後才傳遞給我們的 View 。
MeasureSpec 代表一個32位的int值,高2位代表 SpecMode ,低30位代表 SpecSize , SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。
SpecMode 有三類,如下所示:
UNSPECIFIED
EXACTLY
AT_MOST
LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬/高。
對於頂級View,即DecorView和普通View來說,MeasureSpec的轉換過程略有不同。對於DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定;
對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同決定;
MeasureSpec一旦確定,onMeasure就可以確定View的測量寬/高。
小結一下
當子 View 的寬高採用 wrap_content 時,不管父容器的模式是精確模式還是最大模式,子 View 的模式總是最大模式+父容器的剩餘空間。
View 的工作流程主要是指 measure 、 layout 、 draw 三大流程,即測量、布局、繪制。其中 measure 確定 View 的測量寬/高, layout 確定 view 的最終寬/高和四個頂點的位置,而 draw 則將 View 繪制在屏幕上。
measure 過程要分情況,如果只是一個原始的 view ,則通過 measure 方法就完成了其測量過程,如果是一個 ViewGroup ,除了完成自己的測量過程外,還會遍歷調用所有子元素的 measure 方法,各個子元素再遞歸去執行這個流程。
如果是一個原始的 View,那麼通過 measure 方法就完成了測量過程,在 measure 方法中會去調用 View 的 onMeasure 方法,View 類裡面定義了 onMeasure 方法的默認實現:
先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源碼:
可以看到, getMinimumWidth 方法獲取的是 Drawable 的原始寬度。如果存在原始寬度(即滿足 intrinsicWidth > 0),那麼直接返回原始寬度即可;如果不存在原始寬度(即不滿足 intrinsicWidth > 0),那麼就返回 0。
接著看最重要的 getDefaultSize 方法:
如果 specMode 為 MeasureSpec.UNSPECIFIED 即未指定模式,那麼返回由方法參數傳遞過來的尺寸作為 View 的測量寬度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精確模式,那麼返回從 measureSpec 中取出的 specSize 作為 View 測量後的寬度和高度。
看一下剛才的表格:
當 specMode 為 EXACTLY 或者 AT_MOST 時,View 的布局參數為 wrap_content 或者 match_parent 時,給 View 的 specSize 都是 parentSize 。這會比建議的最小寬高要大。這是不符合我們的預期的。因為我們給 View 設置 wrap_content 是希望View的大小剛好可以包裹它的內容。
因此:
如果是一個 ViewGroup,除了完成自己的 measure 過程以外,還會遍歷去調用所有子元素的 measure 方法,各個子元素再遞歸去執行 measure 過程。
ViewGroup 並沒有重寫 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 這幾個方法專門用於測量子元素。
如果是 View 的話,那麼在它的 layout 方法中就確定了自身的位置(具體來說是通過 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft , mRight , mTop , mBottom 這四個值), layout 過程就結束了。
如果是 ViewGroup 的話,那麼在它的 layout 方法中只是確定了 ViewGroup 自身的位置,要確定子元素的位置,就需要重寫 onLayout 方法;在 onLayout 方法中,會調用子元素的 layout 方法,子元素在它的 layout 方法中確定自己的位置,這樣一層一層地傳遞下去完成整個 View 樹的 layout 過程。
layout 方法的作用是確定 View 本身的位置,即設定 View 的四個頂點的位置,這樣就確定了 View 在父容器中的位置;
onLayout 方法的作用是父容器確定子元素的位置,這個方法在 View 中是空實現,因為 View 沒有子元素了,在 ViewGroup 中則進行抽象化,它的子類必須實現這個方法。
1.繪制背景( background.draw(canvas); );
2.繪制自己( onDraw );
3.繪制 children( dispatchDraw(canvas) );
4.繪制裝飾( onDrawScrollBars )。
dispatchDraw 方法的調用是在 onDraw 方法之後,也就是說,總是先繪制自己再繪制子 View 。
對於 View 類來說, dispatchDraw 方法是空實現的,對於 ViewGroup 類來說, dispatchDraw 方法是有具體實現的。
通過 dispatchDraw 來傳遞的。 dispatchDraw 會遍歷調用子元素的 draw 方法,如此 draw 事件就一層一層傳遞了下去。dispatchDraw 在 View 類中是空實現的,在 ViewGroup 類中是真正實現的。
如果一個 View 不需要繪制任何內容,那麼就設置這個標記為 true,系統會進行進一步的優化。
當創建的自定義控制項繼承於 ViewGroup 並且不具備繪制功能時,就可以開啟這個標記,便於系統進行後續的優化;當明確知道一個 ViewGroup 需要通過 onDraw 繪制內容時,需要關閉這個標記。
參考:《Android開發藝術探索》
② Android 自定義控制項 layout
Android 繪制流程
View :View主要執行layout方法,使用 serFrame 方法來設置本身 View 的四個頂點的位置,確定View本身的位置。
ViewGroup :ViewGroup主要執行onLayout方法,遞歸遍歷所有子View,確定子View的位置。
我們來看ViewRootImpl中的 performLayout() 方法
看到這里,那host.getMeasuredWidth() / host.getMeasuredHeight()是什麼?它是直接調用View中的方法,其實就是經過measure後的DecorView的測量寬度和高度。在 Android 自定義控制項 measure 中有說明。
2.3.2.1 我們先來看ViewGroup中的 layout() 方法
ViewGroup裡面的layout最終會調入到父類View中的layout,View的layout後面講解。這里可以先告訴大家,最終會調用View的onLayout方法,而ViewGroup的onLayout是抽象方法,所以它的子類LinearLayout必須要實現。
2.3.2.2 我們再來看LinearLayout中的 onLayout() 方法。
2.3.2.3 挑一個縱向的吧,我們再來看LinearLayout中的 layoutVertical() 方法。
2.3.2.4 我們再來看LinearLayout中的 setChildFrame() 方法。
又一次回到了View的layout方法,接下來就看View分發的layout。
我們先來看View中的 layout() 方法。
我們先來看View中的 onLayout() 方法。
空空如也,其實View的布局由父容器決定,所以空實現是正常的,當然也可以在自定義View中進行更改。
《Android 視圖模塊 全家桶》
Android開發之自定義控制項(二)---onLayout詳解
自定義View Layout過程 - 最易懂的自定義View原理系列(3)
③ Android開發之layout_gravity屬性
Android開發之layout_gravity屬性是用來設置視圖(View)在其父容器(Container)中的對齊方式和位置的屬性。
layout_gravity屬性主要應用在LinearLayout、FrameLayout和RelativeLayout等布局容器中,用於控制子視圖(child views)在這些布局容器中的對齊和定位方式。這個屬性接受一系列預定義的常量值,例如"top"、"bottom"、"left"、"right"、"center_vertical"、"center_horizontal"和"center"等,用於指定視圖在容器中的對齊方式。
例如,在一個垂直方向的LinearLayout中,如果你將一個Button的layout_gravity設置為"bottom",那麼這個Button將會垂直對齊到LinearLayout的底部。同樣,如果你在一個水平的LinearLayout中將一個TextView的layout_gravity設置為"center_vertical",那麼這個TextView將會在垂直方向上居中對齊。
另外,需要注意的是,layout_gravity屬性對於絕對布局(AbsoluteLayout)和網格布局(GridLayout)是無效的,因為這些布局方式有自己特定的布局規則。在絕對布局中,視圖的位置是通過x和y坐標來直接指定的,而在網格布局中,視圖的位置則是通過行和列的索引來確定的。
總的來說,layout_gravity屬性是Android開發中非常重要的一個屬性,它允許開發者靈活地控制視圖在其父容器中的對齊和定位方式,從而實現復雜且富有創意的用戶界面設計。