一個基本的自定義View應該做的事情:
- 從繪製角度來說,一共有三點 測量,擺放,繪畫他們本身以及子views(針對於ViewGroup而言)。
- 儲存UI狀態。
- 處理觸控事件。
今天先從繪製流程開始學習吧,繪製流程:constructor()->onMeasure()->onLayout()->onDraw()
在開始之前,我們先來看看Android 預設檢視的層級:
1. 通過onMeasure()
方法,根據父容器的尺寸大小和約束,能知道一個View要佔多大的地方。這是一個自下而上執行的方法,也就是說,先從最下層的RootView開始執行測量,然後分發測量viewGroup中眾多的子Views。
-
要搞清楚
onMeasure()
,我們先來看一下方法原型吧。@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {....} 複製程式碼
剛剛讀原始碼的童鞋可能並不知道
widthMeasureSpec
,heightMeasureSpec
這兩個引數的作用是什麼。這兩個引數實際上可以通過MeasureSpec.getSize()
,MeasureSpec.getMode()
這兩個方法來判斷該view被測量後的尺寸。那麼,為什麼還要通過MeasureSpec.getMode()
呢?這就是接下來要說明的重點。以下是MeasureSpec中剔除部分程式碼之後的getMode()實現方式。
private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; //我們通過位運算可以知道,MODE_MASK實際上等於11(二進位制)左移30位的二進位制,在Java中int佔32位,所以這裡就表示的是int中最高位為11,其餘30位為0的二進位制數。 public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; .... public static int getMode(int measureSpec) { //measureSpec就是我們傳進來的 widthMeasureSpec 或者 heightMeasureSpec //它與上measureSpec,低30位就全部都清零了。 //將返回值與UNSPECIFIED、EXACTLY、AT_MOST比較就能得到measureSpec的mode了。 return (measureSpec & MODE_MASK); } 複製程式碼
通過以上的註釋style分析,我們知道了
MeasureSpec
是如何計算mode了,那麼為什麼要計算這些mode呢?與size又有什麼關係呢? View的MeasureSpec Mode有三種,分別為:UNSPECIFIED
,EXACTLY
,AT_MOST
,他們的規則如下(要注意一點的是,在子view中計算的mode,是parent view告訴它的):UNSPECIFIED
:parent view對子view沒有任何的約束,子view可以任一尺寸,但必須要有。發生場景: 對於一個ScorllView
來說,它通常不會去約束子view高度的,也就是說,子view的高度加起來有多高,那麼ScrollView
就有多高。在這種情況下,子view的mode就會為UNSPECIFIED
。EXACTLY
:parent view已經有了確切的size,子view的大小不能夠超過parent的大小。發生場景: 在parent view指定大小或者為match_parent
的時候,子view會得到這個mode。AT_MOST
:在這種模式下,子view能夠儘可能大的達到設定的尺寸或者原尺寸。發生場景: 在parent view指定大小或者match_parent
的情況下,子view為wrap_content
,那麼mode將會為AT_MOST
。
通過以上對mode的規則分析,我們就能夠知道,在
onMeasure
中要搞到尺寸,是需要根據mode來搞的。於是我們可以根據規則寫出如下程式碼:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size;//如果不指定size,那麼我們就使用預設的size break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize;//這兩種mode都允許我們直接使用測量出來的size break; } return result; } 複製程式碼
2. 通過onLayout()
方法知道這個控制元件應該放在哪個位置。*同樣,是一個自下而上的方法。*一般我們只有在重寫ViewGroup
的時候需要自己處理onLayout()
方法,因為該方法主要是ViewGroup
用於擺放子view位置的(如:水平擺放或者垂直襬放,在這裡同學們可以參考一下LinearLayout
的onLayout()
方法的實現),一般我們只繼承View來定製我們的自定義View的時候,都不需要重寫該方法。不過需要注意的一點是,子view的margin屬性是否生效就要看parent是否在自身的onLayout方法進行處理,而view得padding屬性是在onDraw方法中生效的。
以下是重頭戲!!!
3. 通過onDraw(Canvas canvas)
方法將這個控制元件繪畫出來。主要是通過Paint
和引數中的canvas
,還有各種Animation
以及invalidate()
、postInvalidate()
這兩個方法去進行檢視的重繪,實現動態效果。canvas
中畫各種圖形的方法,比如說:rect(矩形),circle(圓形) 等等。當然你可以使用Path
,PathMeasure
去完成更加細膩的動畫。下面結合一個很的效果例項原始碼,來說明動態自定義View。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//根據模式來賦值with 和 height
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if( widthMode == MeasureSpec.EXACTLY )
width = MeasureSpec.getSize(widthMeasureSpec);
else width = ViewGroup.LayoutParams.MATCH_PARENT;
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if( heightMode == MeasureSpec.EXACTLY )
height = MeasureSpec.getSize(heightMeasureSpec);
else height = ViewGroup.LayoutParams.MATCH_PARENT;
//左端點的X
leftX = margin;
//右端點的X
rightX = width - margin;
y = height/2.0f;
//線段的長度
distance = width - ( 2 * margin + 2 * radius );
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
//繪製左端點
canvas.drawCircle( leftX, y , radius * factor , mPaint);
//繪製右端點
canvas.drawCircle( rightX , y , radius * ( 1 - factor ), mPaint);
mPaint.setStrokeWidth(5);
//繪製線段
canvas.drawLine(margin , y , margin + radius+ ((radius + distance)*(1-factor)) , y, mPaint);
}
public void startAnimation(){
mAnimator = ValueAnimator.ofFloat(1, 0);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
factor = (float) animation.getAnimatedValue();
postInvalidate();
}
});
mAnimator.setDuration(1500);
// 重複次數 無限迴圈
mAnimator.setRepeatCount(ValueAnimator.INFINITE);
// 重複模式, RESTART: 重新開始 REVERSE:恢復初始狀態再開始
mAnimator.setRepeatMode(ValueAnimator.REVERSE);
mAnimator.start();
}
複製程式碼
效果圖
沒錯!就是這麼皮!!╭(╯^╰)╮requestLayout()、postInvalidate()、invalidate()的區別
- 實際上,後兩者的作用是一樣的,只不過
postInvalidate
內部會將重繪操作放入子執行緒中,而invalidate
則是在呼叫執行緒中重繪view。 requestLayout
在什麼時候用呢?當view本身的測量屬性改變了的時候,就可以呼叫該方法去讓parent view去重新呼叫view的onMeasure
,onLayout
方法,去重新評估view的大小和所在位置。
- Enjoy Android :) 如果有誤,輕噴,歡迎指正。