Android View繪制流程
如上圖,Activity的window組成,Activity內部有個Window成員,它的實例為PhoneWindow,PhoneWindow有個內部類是DecorView,這個DecorView就是存放布局文件的,里面有TitleActionBar和我們setContentView傳入進去的layout布局文件
Window類時一個抽象類,提供繪制窗口的API
PhoneWindow是繼承Window的一個具體的類,該類內部包含了一個DecorView對象,該DectorView對象是所有應用窗口(Activity界面)的根View
DecorView繼承FrameLayout,里面id=content的就是我們傳入的布局視圖
依據面向對象從抽象到具體我們可以類比上面關系就像如下:
Window是一塊電子屏,PhoneWindow是一塊手機電子屏,DecorView就是電子屏要顯示的內容,Activity就是手機電子屏安裝位置
View 繪制中主要流程分為measure,layout, draw 三個階段。
measure :根據父 view 傳遞的 MeasureSpec 進行計算大小。
layout :根據 measure 子 View 所得到的布局大小和布局參數,將子View放在合適的位置上。
draw :把 View 對象繪制到屏幕上。
一、Measure
1、MeasureSpec
MeasureSpec 封裝了從父級傳遞到子級的布局要求。每個 MeasureSpec 代表對寬度或高度的要求。MeasureSpec 由大小和模式組成。
MeasureSpec 一個32位二進制的整數型,前面2位代表的是mode,后面30位代表的是size。mode 主要分為3類,分別是
EXACTLY:父容器已經測量出子View的大小。對應是 View 的LayoutParams的match_parent 或者精確數值。
AT_MOST:父容器已經限制子view的大小,View 最終大小不可超過這個值。對應是 View 的LayoutParams的wrap_content
UNSPECIFIED:父容器不對View有任何限制,要多大給多大,這種情況一般用於系統內部,表示一種測量的狀態。(這種不怎么常用)
View 的 MeasureSpec 並不是父 View 獨自決定,它是根據父 view 的MeasureSpec加上子 View 的自己的 LayoutParams,通過相應的規則轉化。
2、流程
在measure 方法,核心就是調用onMeasure( ) 進行View的測量。在onMeasure( )里面,獲取到最小建議值,如果父類傳遞過來的模式是MeasureSpec.UNSPECIFIED,也就是父View大小未定的情況下,使用最小建議值,如果是AT_MOST或者EXACTLY模式,則設置父類傳遞過來的大小。
然后調用setMeasuredDimension 方法進行存儲大小。
二、Layout
確定 View 在父 View 的位置進行排版布局
三、draw
draw 過程中一共分成7步,其中兩步我們直接直接跳過不分析了。
第一步:drawBackground(canvas): 作用就是繪制 View 的背景。
第三步:onDraw(canvas) :繪制 View 的內容。View 的內容是根據自己需求自己繪制的,所以方法是一個空方法,View的繼承類自己復寫實現繪制內容。
第三步:dispatchDraw(canvas):遍歷子View進行繪制內容。在 View 里面是一個空實現,ViewGroup 里面才會有實現。在自定義 ViewGroup 一般不用復寫這個方法,因為它在里面的實現幫我們實現了子 View 的繪制過程,基本滿足需求。
第四步:onDrawForeground(canvas):對前景色跟滾動條進行繪制。
第五步:drawDefaultFocusHighlight(canvas):繪制默認焦點高亮
四、自定義視圖類
1、步驟
1、自定義View的屬性
2、在View的構造方法中獲得我們自定義的屬性
3、重寫onMeasure
4、重寫onDraw
2、定義自定義屬性
自定義View的屬性,首先在res/values/ 下建立一個attrs.xml , 在里面定義我們的屬性和聲明我們的整個樣式。
如需在自定義視圖中啟用此行為,您必須:
- 在
<declare-styleable>
資源元素中定義視圖的自定義屬性 - 在 XML 布局中指定屬性值
- 在運行時檢索屬性值
- 將檢索到的屬性值應用到視圖
<resources>
<declare-styleable name="CustomView">
<attr name="titleText" format="string"/>
<attr name="titleTextColor" format="color"/>
<attr name="titleTextSize" format="dimension"/>
</declare-styleable>
</resources>
定義了自定義屬性后,便可像內置屬性一樣在布局 XML 文件中使用它們。唯一的區別是自定義屬性屬於另一個命名空間。它們不屬於 http://schemas.android.com/apk/res/android
命名空間,而是屬於 http://schemas.android.com/apk/res/[your package name]
。
當然在我的Android Studio中我改用xmlns:custom="http://schemas.android.com/apk/res-auto"
,因為當前工程是做為lib使用,那么你如上所寫 ,會出現找不到自定義屬性的錯誤 。這時候你就可以寫成xmlns:custom="http://schemas.android.com/apk/res-auto"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.uidesign.CustomView
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_centerInParent="true"
custom:titleText="liming"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp"/>
</RelativeLayout>
3、應用自定義屬性
通過 XML 布局創建視圖時,XML 標記中的所有屬性都會從資源包讀取,並作為 AttributeSet
傳遞到視圖的構造函數中。雖然可以直接從 AttributeSet
讀取值,但這樣做有一些弊端:
- 不解析屬性值中的資源引用
- 不應用樣式
請改為將 AttributeSet
傳遞給 obtainStyledAttributes()
。此方法會傳回一個 TypedArray
數組,其中包含已解除引用並設置了樣式的值。
Android 資源編譯器做了大量工作,以便您更輕松地調用 obtainStyledAttributes()
。對於 res 目錄中的各個 <declare-styleable>
資源,生成的 R.java 定義一個由屬性 ID 組成的數組,同時定義一組常量,用於定義該數組中各屬性的索引。您可以使用預定義的常量從 TypedArray
讀取屬性。
public class CustomView extends View {
private String mTitleText;
private int mTitleTextColor;
private int mTitleTextSize;
private Rect rect;
private Paint paint;
public CustomView(Context context, AttributeSet attrs) {
super(context,attrs);
//為自定義View添加事件
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mTitleText=randomText();
postInvalidate();
}
});
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CustomView,
0, 0);
try {
mTitleText = a.getString(R.styleable.CustomView_titleText);
mTitleTextColor=a.getColor(R.styleable.CustomView_titleTextColor,Color.BLACK);
mTitleTextSize=a.getDimensionPixelSize(R.styleable.CustomView_titleTextSize,(int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
} finally {
a.recycle();
}
}
//重寫onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, 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
{
paint.setTextSize(mTitleTextSize);
paint.getTextBounds(mTitleText, 0, mTitleText.length(), rect);
float textWidth = rect.width();
int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
width = desired;
}
if (heightMode == MeasureSpec.EXACTLY)
{
height = heightSize;
} else
{
paint.setTextSize(mTitleTextSize);
paint.getTextBounds(mTitleText, 0, mTitleText.length(), rect);
float textHeight = rect.height();
int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
height = desired;
}
setMeasuredDimension(width, height);
}
//重寫onDraw
@Override
protected void onDraw(Canvas canvas) {
paint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
paint.setColor(mTitleTextColor);
canvas.drawText(mTitleText, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);
}
}
TypedArray
對象是共享資源,必須在使用后回收。
由於系統幫我們測量的高度和寬度都是MATCH_PARNET,當我們設置明確的寬度和高度時,系統幫我們測量的結果就是我們設置的結果,當我們設置為WRAP_CONTENT,或者MATCH_PARENT系統幫我們測量的結果就是MATCH_PARENT的長度。
所以,當設置了WRAP_CONTENT時,我們需要自己進行測量,即重寫onMesure方法。重寫之前先了解MeasureSpec的specMode,一共三種類型:
EXACTLY:一般是設置了明確的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一個最大值內,一般為WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用