本文基本不說原理,只說流程、公式、套路與“安全措施”。
Step 1
建構函式只需要用到兩個,其餘兩個百分之95的人與需求不會用得到。
public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
複製程式碼
很簡單,程式碼new用到第一個建構函式,xml則會用到第二個建構函式。
Step 2
獲取xml配置的屬性,以及使用自定義的屬性
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomView, 0, 0);
float attr_1 = ta.getDimension(R.styleable.CustomView_attr_1, 0f);
ta.recycle();
}
複製程式碼
在第二個建構函式使用TypeArray
獲取xml
中設定的attrs
。
注意使用完typearray
記得呼叫recycle()
再看看自定義屬性是怎麼寫
於values
-> attrs
<declare-styleable name="CustomView">
<attr name="attr_1" format="dimension" />
</declare-styleable>
複製程式碼
注意了,這裡和上面獲取屬性,名稱是拼接起來的。如上面程式碼,name
是CustomView
,而attr
的name
是attr_1
,那麼通過typearray
獲取attr_1
屬性的值是使用R.styleable.CustomView_attr_1
。
接著看看xml佈局怎麼使用自定義屬性
Step 3
測量
- 單一view的測量
measure()
為final方法,子類不可複寫,最後會呼叫````onMeasure()。
onMeasure()這裡進行你的需求來measure->
setMeasureDimension()儲存測量後的子view的寬高。如果在使用自定義view時,用了
wrap_content。那麼在
onMeasure中就要呼叫
setMeasuredDimension,來指定view的寬高。如果使用的matchl_parent或者一個具體的dp值。那麼直接使用
super.onMeasure``即可。 - viewgroup的測量
measure()->onMeasure()->測量子view->setMeasureDimension()
在這裡我們需要在onMeasure()測量子view。測量子view的方法一般有四種
2.1 getChildAt(int index).可以拿到index上的子view。
通過getChildCount得到子view的數目,再迴圈遍歷出子view。
接著,subView.measure(int wSpec, int hSpec); //使用子view自身的測量方法
2.2 或者呼叫viewGroup的測量子view的方法:
//某一個子view,多寬,多高, 內部加上了viewGroup的padding值
measureChild(subView, int wSpec, int hSpec);
2.3 //所有子view 都是 多寬,多高, 內部呼叫了measureChild方法
measureChildren(int wSpec, int hSpec);
2.4 //某一個子view,多寬,多高, 內部加上了viewGroup的padding值、margin值和傳入的寬高wUsed、hUsed
measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed);
Step 4
佈局
view 的佈局兩個步驟layout
->onLayout
layout:計算自身位置呼叫setFrame()
onLayout:看下面解析
- 自定義View
1.1 在單一的View中,只會用到
layout
,而onLayout
是個空實現。 - 自定義ViewGroup,呼叫
layout()
計算自身的位置,呼叫onLayout()
遍歷子View並呼叫子Viewlayout()
確定自身子View的位置。 那麼自定義ViewGroup一般套路就是
// 虛擬碼,會省略方法的一些引數
onLayout(){
// 迴圈所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 計算當前子View的四個位置值
// 計算的邏輯需要自己實現,也是自定義View的關鍵
...
// 對計算後的位置值進行賦值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom
// 呼叫子view的layout()並傳遞計算過的引數
// 從而計算出子View的位置
child.layout(mLeft, mTop, mRight, mBottom);
}
}
複製程式碼
然後到一個注意點了
- getWidth() / getHeight() = View最終的寬 / 高
- getMeasuredWidth() / getMeasuredHeight() = View的測量的寬 / 高
在哪裡賦值 | 賦值方法 | 何處可用 | |
---|---|---|---|
getMeasuredWidth() | onMeasure() | setDimension() | onMeasure()後 |
getWidth() | layout() | setFrame() | 在layout()呼叫後 |
那何種情況下兩者會值不同 just one
@Override
public void layout( int l , int t, int r , int b){
super.layout(l,t,r+10,b+200)
}
複製程式碼
Step 5
draw繪製 view的繪製流程為: 繪製過程如下:
- 繪製view背景
- 繪製view內容
- 繪製子View
- 繪製裝飾(漸變框,滑動條等等)
程式碼流程為draw()
->1
->2 onDraw()
->3 dispatchDraw()
->4
其中自定義View沒有子view,故不需要第三步(空實現),一般只需要完成第二步即可,在onDraw
繪製我們需要的內容。
其次自定義ViewGroup
有子view,故需要第三步,但一般我們也無需理會(系統已經為我們實現了該方法),故是在onDraw
繪製我們需要的內容,但但但但是,一般viewgroup也無需繪製什麼東西
又到注意事項:
上面說到viewgroup
也無需繪製什麼東西,故viewgroup
需要在onDraw()
繪製東西的時候,需要呼叫setWillNotDraw(boolean willNotDraw)
,設定為false
。這樣viewgroup才會執行onDraw()
,否則不執行。
Step 6
重新整理
Android中實現view的更新有兩組方法,一組是invalidate
,另一組是postInvalidate
,其中前者是在UI執行緒自身中使用,而後者在非UI執行緒中使用。
那麼套路就是handler+invalidate
或postInvalidate
或onDraw根據條件+invalidate