View的繼承關(guān)系
- Android中所有控件,都是View或View的子類,比如開發(fā)中最常寫的代碼 findViewById() , 返回值就是 View ,然后我們會(huì)將它強(qiáng)轉(zhuǎn)為 TextView , ImageView ...
一張圖描述 View 的繼承關(guān)系:
Paste_Image.png
View的繪制流程
-
Android界面布局是以一棵樹的結(jié)構(gòu)形式展現(xiàn)的,而繪制出整個(gè)界面肯定是要遍歷整個(gè)View樹,從根節(jié)點(diǎn)開始,對(duì)這棵樹的所有節(jié)點(diǎn)分別進(jìn)行測(cè)量,布局和繪制.類似我們寫的布局文件,一個(gè)控件下面會(huì)有一個(gè)或多個(gè)子控件,子控件下可能又有子控件...
Paste_Image.png - 之前分析過View的生命周期,知道它的生命周期所對(duì)應(yīng)的方法有很多,并且根據(jù)顯示狀態(tài)的不同,也會(huì)有所區(qū)別.實(shí)對(duì)于自定義控件,通常我們只需要關(guān)注一下三個(gè)方法:
onMeasure()、onLayout()、onDraw() , 可以聯(lián)想一下畫畫的步驟:
畫多大 --> 考慮怎么畫 --> 開始動(dòng)手畫
自定義控件的分類
- 先說(shuō)下為什么要自定義控件.
原生控件滿足不了產(chǎn)品的需求, 比如圓角圖片,加密文本之類. - 自定義控件的分類(個(gè)人的分法)
* 按實(shí)現(xiàn)方式分:
1.擴(kuò)展控件(繼承某個(gè)具體的控件,增強(qiáng)它的功能,比如圓角圖片)
2.組合控件(將多個(gè)控件組合在一起,作為整體使用,提高復(fù)用性)
3.完全自定義控件(直接繼承View 或ViewGroup 自己定義規(guī)則)
* 按類型分:
1.自定義View(單個(gè)控件,沒有子控件)
2.自定義ViewGroup(布局容器)
自定義控件的具體實(shí)現(xiàn)步驟
控件的初始化
-
View的構(gòu)造方法有以上幾種重載形式
public View(Context context) {}
public View(Context context, AttributeSet attrs) {}
public View(Context context, AttributeSet attrs, int defStyleAttr) {}
還有一個(gè)帶4個(gè)參數(shù)的構(gòu)造方法,是API21新增的,沒什么用(其實(shí)還沒研究過),簡(jiǎn)單說(shuō)下上面三個(gè)方法的調(diào)用時(shí)機(jī):- 一個(gè)參數(shù)
代碼中初始化 比如: ImageView img = new ImageView(context);
- 兩個(gè)參數(shù)
加載布局文件中的控件(不設(shè)置樣式)
- 三個(gè)參數(shù)
加載布局文件中的控件(設(shè)置了樣式---style)
- 一個(gè)參數(shù)
-
列舉view的構(gòu)造方法的調(diào)用時(shí)機(jī),是因?yàn)槲覀冃枰谡_的方法中,初始化一些數(shù)據(jù),比如獲取 自定義屬性 的初始值.
在資源文件 res -> values -> xxxattrs.xml 中定義自定義屬性
<declare-styleable name="Myview">
<attr name="textSize" format="dimension"></attr>
<attr name="abc" format="reference"></attr>
<attr name="textColor" format="color"></attr>
</declare-styleable>
自定義屬性還有其它的類型,不一一列舉在構(gòu)造方法中獲取自定義屬性的值
// 根據(jù)字面意思理解(類型的數(shù)組),就是獲取含有對(duì)應(yīng)自定義屬性的對(duì)象
TypedArray t = context.obtainStyledAttributes(attrs,R.styleable.Myview);
int textColor = a.getColor(R.styleable.Myview_textColor, 0xFFFFFFFF);
...
// 獲取完所有屬性后,必須調(diào)用一次,釋放資源
t.recycle();-
簡(jiǎn)單分析下上面獲取自定義屬性的方法
public TypedArray obtainStyledAttributes(AttributeSet set,
int[] attrs, int defStyleAttr, int defStyleRes) {}- defStyleRes : 字面意思理解 --- 默認(rèn)的樣式資源,當(dāng)我們?cè)趕tyle.xml里定義一個(gè)樣式,并設(shè)置一些屬性的值,如果在獲取屬性的時(shí)候,傳入了這個(gè)樣式,那么對(duì)于缺省的值,會(huì)從該樣式中去取;
- defStyleAttr : (下面這段話比較繞口... 99%沒什么用,寫在這里湊字?jǐn)?shù))
1.自定義屬性里指定一條引用類型的屬性
2.給application的主題添加該引用,指定某個(gè)樣式
3.如果傳入了這個(gè)引用,那么,指定的樣式會(huì)作為缺省值 - 優(yōu)先級(jí) : defStyleAttr > defStyleRes
View的測(cè)量 -measure
-
可以想象,view沒有子控件,所以只管測(cè)量自己,而viewgroup還需要考慮子控件.面也說(shuō)過,view是所有控件的基類,所以,考慮這個(gè)問題最好的切入點(diǎn)就是 view 的測(cè)量方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
public final,該方法不能被重寫,可以外部調(diào)用,簡(jiǎn)單的追蹤下源碼,發(fā)現(xiàn)是在 viewgroup 中被調(diào)用 :
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);
}... protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
兩個(gè)方法的作用一樣,都是用于測(cè)量子控件,一個(gè)考慮子控件的外邊距,一個(gè)沒有.從其中一個(gè)著手簡(jiǎn)單分析.
view 的 measure 所需的參數(shù)通過 getChildMeasureSpec 獲得
/**
*
* 目標(biāo)是將父控件的測(cè)量規(guī)格和child view的布局參數(shù)LayoutParams相結(jié)合,得到一個(gè)
* 最可能符合條件的child view的測(cè)量規(guī)格。* @param spec 父控件的測(cè)量規(guī)格 * @param padding 父控件里已經(jīng)占用的大小 * @param childDimension child view布局LayoutParams里的尺寸 * @return child view 的測(cè)量規(guī)格 */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); //父控件的測(cè)量模式 int specSize = MeasureSpec.getSize(spec); //父控件的測(cè)量大小 int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // 當(dāng)父控件的測(cè)量模式 是 精確模式,也就是有精確的尺寸了 case MeasureSpec.EXACTLY: //如果child的布局參數(shù)有固定值,比如"layout_width" = "100dp" //那么顯然child的測(cè)量規(guī)格也可以確定下來(lái)了,測(cè)量大小就是100dp,測(cè)量模式也是EXACTLY if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } //如果child的布局參數(shù)是"match_parent",也就是想要占滿父控件 //而此時(shí)父控件是精確模式,也就是能確定自己的尺寸了,那child也能確定自己大小了 else if (childDimension == LayoutParams.MATCH_PARENT) {//-1 resultSize = size; // 父控件尺寸減去內(nèi)邊距 resultMode = MeasureSpec.EXACTLY; } //如果child的布局參數(shù)是"wrap_content",也就是想要根據(jù)自己的邏輯決定自己大小, //比如TextView根據(jù)設(shè)置的字符串大小來(lái)決定自己的大小 //那就自己決定唄,不過你的大小肯定不能大于父控件的大小嘛 //所以測(cè)量模式就是AT_MOST,測(cè)量大小就是父控件的size else if (childDimension == LayoutParams.WRAP_CONTENT) {//-2 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 當(dāng)父控件的測(cè)量模式 是 最大模式,也就是說(shuō)父控件自己還不知道自己的尺寸,但是大小不能超過size case MeasureSpec.AT_MOST: //同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也優(yōu)先滿足孩子的需求 if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } //child想要和父控件一樣大,但父控件自己也不確定自己大小,所以child也無(wú)法確定自己大小 //但同樣的,child的尺寸上限也是父控件的尺寸上限size else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } //child想要根據(jù)自己邏輯決定大小,那就自己決定唄 else if (childDimension == LayoutParams.WRAP_CONTENT) { 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 = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
- 通過前面的源碼分析,可以知道 viewgroup在調(diào)用 view的 measure方法時(shí),傳入的參數(shù)不僅僅是單純的寬度和高度,而是 柔和了尺寸和模式的綜合值.
MeasureSpec通常翻譯為”測(cè)量規(guī)格”,它是一個(gè)32位的int數(shù)據(jù).
其中高2位代表SpecMode即某種測(cè)量模式,低30位為SpecSize代表在該模式下的規(guī)格大小可以通過如下方式分別獲取這兩個(gè)值:
獲取SpecSize
int specSize = MeasureSpec.getSize(measureSpec)
獲取specMode
int specMode = MeasureSpec.getMode(measureSpec)三種測(cè)量模式
1.AT_MOST ---> wrap_content
2.EXACTLY ---> 具體的值/math_parent
3.UNSPECIFIED ---> listview ,scrollview 等控件才可能,不需要關(guān)心
前面說(shuō)了那么多,其實(shí)值說(shuō)明了一個(gè)問題:View的測(cè)量規(guī)格是由父控件的測(cè)量規(guī)格和自身的LayoutParams共同決定的
-
回歸主題 onMeasure
// 設(shè)置測(cè)量的寬高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
...
// 獲取最小寬度
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
...
// 計(jì)算尺寸
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就是上面getSuggestedMinimumWidth/height的返回值 break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize;//測(cè)量規(guī)格里的尺寸 break; } return result; }
在計(jì)算子控件的尺寸時(shí),不管父View的specMode是MeasureSpec.AT_MOST還是MeasureSpec.EXACTLY對(duì)于子View而言系統(tǒng)給它設(shè)置的specMode都是MeasureSpec.AT_MOST,并且其大小都是parentLeftSize即父View目前剩余的可用空間。這時(shí)wrap_content就失去了原本的意義,變成了match_parent一樣了.
所以自定義控件默認(rèn)不支持 wrap_content
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec , heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
int heightSpceSize=MeasureSpec.getSize(heightMeasureSpec);
int wrapWidth,wrapHight;//根據(jù)邏輯計(jì)算自己的尺寸
if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
}else if(widthSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, heightSpceSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpceSize, mHeight);
}
}
ViewGroup 的測(cè)量
- 可以分為兩步
- 測(cè)量所有子控件 (調(diào)用API即可)
- 測(cè)量自己. 不同的控件有不同的測(cè)量規(guī)則,很顯然,LinearLayout與RelativeLayout肯定就不一樣,所以,這個(gè)就需要根據(jù)實(shí)際情況自己去處理了
View的 layout
//l, t, r, b分別表示子View相對(duì)于父View的左、上、右、下的坐標(biāo)
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this,l,t,r,b,oldL,oldT,oldR,oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
...代碼沒看懂,引用一段網(wǎng)上的解釋
確定該View在其父View中的位置,把l,t, r, b分別與之前的mLeft,mTop,mRight,mBottom一一作比較,假若其中任意一個(gè)值發(fā)生了變化,那么就判定該View的位置發(fā)生了變化 ,若View的位置發(fā)生了變化則調(diào)用onLayout()方法
前面說(shuō)過 onLayout 才是自定義控件時(shí)需要關(guān)注的方法
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
這是個(gè)空方法(為什么是空方法?)
Called from layout when this view should assign a size and position to each of its children.
在layout方法中調(diào)用該方法,用于指定子View的大小和位置。
我們知道,只有ViewGroup才有子控件,也就是說(shuō), 自定義View是不需要考慮該方法的.再看看 該方法在ViewGroup中的實(shí)現(xiàn)
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
是個(gè)抽象方法,其實(shí)很好理解,因?yàn)槊糠N布局都有其自己的邏輯,比如:FrameLayou,LinearLayout,RelativeLayout等對(duì) onLayout的實(shí)現(xiàn)肯定就不一樣.進(jìn)一步說(shuō),自定義ViewGroup時(shí),系統(tǒng)肯定是不知道我們想要什么樣的邏輯,必須要自己根據(jù)需求去實(shí)現(xiàn).
簡(jiǎn)單的模仿一個(gè)垂直方向線性布局的需求
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount=getChildCount();
int height = 0;
if(childCount>0){
View child=getChildAt(i);
int childHeight=child.getMeasuredHeight();
if(child.getVisibility != View.GONE){
child.layout(l,height,r,height += childHeight);
}
}
}
大部分時(shí)候,如果可能,盡量避免直接繼承ViewGroup,而是繼承LinearLayout,RelativeLayout等系統(tǒng)已有的布局來(lái)簡(jiǎn)化這些步驟。
View 的繪制 -draw
draw()的源碼就不看了(實(shí)在太長(zhǎng)了),我們真正關(guān)注的是onDraw()方法
/**
* Implement this to do your drawing.
* 需要由具體的子View去實(shí)現(xiàn)各自不同的需求
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {}
結(jié)論:
一般來(lái)說(shuō),自定義ViewGroup不需要實(shí)現(xiàn)該方法(布局容器,更關(guān)注的應(yīng)該是子控件的測(cè)量和擺放);
自定義View 則需要根據(jù)具體的需求實(shí)現(xiàn)該方法.
-
我們看到 onDraw()方法有一個(gè)參數(shù) Canvas ,顧名思義,就是畫布的意思.像我們平時(shí)畫圖一樣,需要兩個(gè)工具,紙和筆。有了畫布,當(dāng)然還要有畫筆才行.
//創(chuàng)建畫筆
Paint paint=new Paint();
paint.setAntiAlias(true);//抗鋸齒功能
paint.setColor(Color.RED); //設(shè)置畫筆顏色
paint.setStyle(Style.FILL);//設(shè)置填充樣 Style.FILL/Style.FILL_AND_STROKE/Style.STROKE
paint.setStrokeWidth(5);//設(shè)置畫筆寬度
paint.setShadowLayer(10, 15, 15, Color.GREEN);//設(shè)置陰影
paint.setTextAlign(Align.CENTER);//設(shè)置文字對(duì)齊方式,取值:align.CENTER、align.LEFT或align.RIGHT
paint.setTextSize(12);//設(shè)置文字大小//樣式設(shè)置 paint.setFakeBoldText(true);//設(shè)置是否為粗體文字 paint.setUnderlineText(true);//設(shè)置下劃線 paint.setTextSkewX((float) -0.25);//設(shè)置字體水平傾斜度,普通斜體字是-0.25 paint.setStrikeThruText(true);//設(shè)置帶有刪除線效果
關(guān)于使用畫布和畫筆繪制圖形,API很多,不一一列舉,只介紹一些常用的
// 畫背景色
canvas.drawColor(Color.BLUE);
canvas.drawRGB(255, 255, 0);
// 畫點(diǎn)
void drawPoint (float x, float y, Paint paint)
void drawPoints (float[] pts, Paint paint)
void drawPoints (float[] pts, int offset, int count, Paint paint)
// 畫線
void drawLine (float startX, float startY, float stopX, float stopY, Paint paint)
void drawLines (float[] pts, Paint paint)
void drawLines (float[] pts, int offset, int count, Paint paint)
// 畫矩形
void drawRect (float left, float top, float right, float bottom, Paint paint)
void drawRect (RectF rect, Paint paint)
void drawRect (Rect r, Paint paint)
// 圓角矩形
void drawRoundRect (RectF rect, float rx, float ry, Paint paint)
// 圓形
void drawCircle (float cx, float cy, float radius, Paint paint)
// 橢圓
void drawOval (RectF oval, Paint paint)
// 文字
void drawText (String text, float x, float y, Paint paint)
void drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
void drawText (String text, int start, int end, float x, float y, Paint paint)
void drawText (char[] text, int index, int count, float x, float y, Paint paint)
關(guān)于控件的繪制,這里只列舉了一些簡(jiǎn)單的,常用的API,實(shí)際上控件的繪制非常復(fù)雜,涉及到的知識(shí)點(diǎn)也很多,推薦一個(gè)大神的博客,非常值得學(xué)習(xí).
http://blog.csdn.net/harvic880925/article/details/50995268
關(guān)于自定義控件的主要流程總結(jié)下來(lái)就是 :
初始化 --> onMeasure --> onLayout --> onDraw
關(guān)于自定義控件還有更多的東西需要學(xué)習(xí):控件的滑動(dòng),事件的分發(fā)...