在Android中的任何一個(gè)布局、任何一個(gè)控件其實(shí)都是直接或間接繼承自View的,因此View是一個(gè)很重要的概念。本篇將深入學(xué)習(xí)View,內(nèi)容如下:
- View事件體系
- View位置參數(shù)
- View的觸控
- View的滑動(dòng)
- View事件分發(fā)機(jī)制
- View滑動(dòng)沖突
- View工作原理
- View工作流程
- 自定義View
簡介:在Android的世界中View是所有控件的基類,其中也包括ViewGroup在內(nèi),ViewGroup是代表著控件的集合,其中可以包含多個(gè)View控件。
從某種角度上來講Android中的控件可以分為兩大類:View與ViewGroup。通過ViewGroup,整個(gè)界面的控件形成了一個(gè)樹形結(jié)構(gòu),上層的控件要負(fù)責(zé)測(cè)量與繪制下層的控件,并傳遞交互事件。
在每棵控件樹的頂部都存在著一個(gè)ViewParent對(duì)象,它是整棵控件樹的核心所在,所有的交互管理事件都由它來統(tǒng)一調(diào)度和分配,從而對(duì)整個(gè)視圖進(jìn)行整體控制。
一.View事件體系
1.View的位置參數(shù)
a.Android坐標(biāo)系:以屏幕的左上角為坐標(biāo)原點(diǎn),向右為x軸增大方向,向下為y軸增大方向。
b.View的位置由它的四個(gè)頂點(diǎn)決定,分別對(duì)應(yīng)View的四個(gè)屬性:top、left、right、bottom。其中l(wèi)eft是左上角的橫坐標(biāo),right是右下角的橫坐標(biāo),top是左上角的縱坐標(biāo),bottom是右下角的縱坐標(biāo)。注意這些坐標(biāo)是相對(duì)于view父容器而言,是一種相對(duì)的坐標(biāo)。具體關(guān)系見下圖:
因此,View的寬高和坐標(biāo)關(guān)系:width = right - left,height = bottom - top。
可利用View的get方法獲取上述屬性,如:
- left = getLeft();
- right = getRight();
- top = getTop();
- bottom = getBottom();
- width=getWidth();
- height=getHeight();
c.從android3.0開始,View增加了額外幾個(gè)參數(shù):x,y,translationX、translationY。其中x和y是View左上角的坐標(biāo),translationX和translationY是View左上角相對(duì)于父容器的偏移量,它們默認(rèn)值是0。這些參數(shù)也是相對(duì)于View父容器。具體關(guān)系見下圖:
- 存在關(guān)系:x = left + translationX,y = top + translationY
- 由此可見,x和left不同體現(xiàn)在:left是View的初始坐標(biāo),在繪制完畢后就不會(huì)再改變;而x是View偏移后的實(shí)時(shí)坐標(biāo),是實(shí)際坐標(biāo)。y和top的區(qū)別同理。
類似地,安卓也提供了相應(yīng)的get/set方法。需要注意的是,在onCreate()方法里無法獲取到View的坐標(biāo)參數(shù),這是因?yàn)榇藭r(shí)View還未開始繪制,全部坐標(biāo)參數(shù)將都是0。
推薦閱讀:Android應(yīng)用坐標(biāo)系統(tǒng)全面詳解
2.觸控系列
a.MotionEvent:是手指觸摸屏幕所產(chǎn)生的一系列事件。典型事件有:
- ACTION_DOWN:手指剛接觸屏幕
- ACTION_MOVE:手指在屏幕上滑動(dòng)
- ACTION_UP:手指在屏幕上松開的一瞬間
事件列:從手指接觸屏幕至手指離開屏幕,這個(gè)過程產(chǎn)生的一系列事件
任何事件列都是以DOWN事件開始,UP事件結(jié)束,中間有無數(shù)的MOVE事件。如圖:
通過MotionEvent 對(duì)象可以得到觸摸事件的x、y坐標(biāo)。其中通過getX()、getY()可獲取相對(duì)于當(dāng)前view左上角的x、y坐標(biāo);通過getRawX()、getRawY()可獲取相對(duì)于手機(jī)屏幕左上角的x,y坐標(biāo)。具體關(guān)系見下圖:
b.TouchSlop:系統(tǒng)所能識(shí)別的被認(rèn)為是滑動(dòng)的最小距離。即當(dāng)手指在屏幕上滑動(dòng)時(shí),如果兩次滑動(dòng)之間的距離小于這個(gè)常量,那么系統(tǒng)就不認(rèn)為你是在進(jìn)行滑動(dòng)操作。
該常量和設(shè)備有關(guān),可用它來判斷用戶的滑動(dòng)是否達(dá)到閾值,獲取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()。
c.VelocityTracker:速度追蹤,用于追蹤手指在滑動(dòng)過程中的速度,包括水平和豎直方向的速度。
使用過程:首先在view的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();//實(shí)例化一個(gè)VelocityTracker 對(duì)象
velocityTracker.addMovement(event);//添加追蹤事件
接著在ACTION_UP事件中獲取當(dāng)前的速度。注意這里計(jì)算的是1000ms時(shí)間間隔移動(dòng)的像素值,假設(shè)像素是100,即速度是每秒100像素。另外,手指逆著坐標(biāo)系的正方向滑動(dòng),所產(chǎn)生的速度為負(fù)值,順著正反向滑動(dòng),所產(chǎn)生的速度為正值。
velocityTracker .computeCurrentVelocity(1000);//獲取速度前先計(jì)算速度,這里計(jì)算的是在1000ms內(nèi)
float xVelocity = velocityTracker .getXVelocity();//得到的是1000ms內(nèi)手指在水平方向從左向右滑過的像素?cái)?shù),即水平速度
float yVelocity = velocityTracker .getYVelocity();//得到的是1000ms內(nèi)手指在水平方向從上向下滑過的像素?cái)?shù),垂直速度
最后,當(dāng)不需要使用它的時(shí)候,需要調(diào)用clear方法來重置并回收內(nèi)存:
velocityTracker.clear();
velocityTracker.recycle();
推薦閱讀:Android常用觸控類分析:MotionEvent 、 ViewConfiguration、VelocityTracker
d.GestureDetector:手勢(shì)檢測(cè),用于輔助檢測(cè)用戶的單擊、滑動(dòng)、長按、雙擊等行為。
使用過程:創(chuàng)建一個(gè)GestureDetecor對(duì)象并實(shí)現(xiàn)OnGestureListener接口,根據(jù)需要實(shí)現(xiàn)單擊等方法:
GestureDetector mGestureDetector = new GestureDetector(this);//實(shí)例化一個(gè)GestureDetector對(duì)象
mGestureDetector.setIsLongpressEnabled(false);// 解決長按屏幕后無法拖動(dòng)的現(xiàn)象
接著,接管目標(biāo)view的onTouchEvent方法,在待監(jiān)聽view的onTouchEvent方法中添加如下實(shí)現(xiàn):
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然后,就可以有選擇的實(shí)現(xiàn)OnGestureListener和OnDoubleTapListener中的方法了。
建議:如果只是監(jiān)聽滑動(dòng)操作,建議在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽雙擊這種行為,則使用GestureDetector 。
推薦閱讀:Android手勢(shì)檢測(cè)——GestureDetector全面分析
3.滑動(dòng)系列
a.實(shí)現(xiàn)View滑動(dòng)三種辦法:
①通過View本身提供的scrollTo/scrollBy方法
- 兩者區(qū)別:scrollBy是內(nèi)部調(diào)用了scrollTo的,它是基于當(dāng)前位置的相對(duì)滑動(dòng);而scrollTo是絕對(duì)滑動(dòng),因此如果利用相同輸入?yún)?shù)多次調(diào)用scrollTo()方法,由于View初始位置是不變只會(huì)出現(xiàn)一次View滾動(dòng)的效果而不是多次。
- 注意:兩者都只能對(duì)view內(nèi)容進(jìn)行滑動(dòng),而不能使view本身滑動(dòng)。
mScrollX和mScrollY分別表示View在X、Y方向的滾動(dòng)距離。mScrollX:View的左邊緣減去View的內(nèi)容的左邊緣;mScrollY:View的上邊緣減去View的內(nèi)容的上邊緣。從右向左滑動(dòng),mScrollX為正值,反之為負(fù)值;從下往上滑動(dòng),mScrollY為正值,反之為負(fù)值。(更直觀感受:查看下一張照片或者查看長圖時(shí)手指滑動(dòng)方向?yàn)檎?/p>
②通過動(dòng)畫給View施加平移效果:主要通過改變View的translationX和translationY參數(shù)來實(shí)現(xiàn)??捎胿iew動(dòng)畫,也可以采用屬性動(dòng)畫,如果使用屬性動(dòng)畫的話,為了能夠兼容3.0以下版本,需要采用開源動(dòng)畫庫nineoldandroids。注意View動(dòng)畫的View移動(dòng)只是位置移動(dòng),并不能真正的改變view的位置,而屬性動(dòng)畫可以。
③通過改變View的LayoutParams使得View重新布局:比如將一個(gè)View向右移動(dòng)100像素,向右,只需要把它的marginLeft參數(shù)增大即可,代碼見下:
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 請(qǐng)求重新對(duì)View進(jìn)行measure、layout
三種方式對(duì)比:
- scrollTo/scrollBy:操作簡單,適合對(duì)view內(nèi)容滑動(dòng)。非平滑
- 動(dòng)畫:操作簡單,主要適用于沒有交互的view和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果
- 改變LayoutParams:操作稍微復(fù)雜,適用于有交互的view。非平滑
b.實(shí)現(xiàn)View彈性滑動(dòng)三種方法:
①使用Scroller:
- 與scrollTo/scrollBy不同:scrollTo/scrollBy過程是瞬間完成的,非平滑;而Scroller則有過渡滑動(dòng)的效果。
- 注意:Scoller本身無法讓View彈性滑動(dòng),它需要和View的computerScroller方法配合使用。
Scroller慣用代碼:
Scroller scroller = new Scroller(mContext); //實(shí)例化一個(gè)Scroller對(duì)象
private void smoothScrollTo(int dstX, int dstY) {
int scrollX = getScrollX();//View的左邊緣到其內(nèi)容左邊緣的距離
int scrollY = getScrollY();//View的上邊緣到其內(nèi)容上邊緣的距離
int deltaX = dstX - scrollX;//x方向滑動(dòng)的位移量
int deltaY = dstY - scrollY;//y方向滑動(dòng)的位移量
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //開始滑動(dòng)
invalidate(); //刷新界面
}
@Override//計(jì)算一段時(shí)間間隔內(nèi)偏移的距離,并返回是否滾動(dòng)結(jié)束的標(biāo)記
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurY());
postInvalidate();//通過不斷的重繪不斷的調(diào)用computeScroll方法
}
}
其中startScroll源碼如下,可見它并沒有進(jìn)行實(shí)際的滑動(dòng)操作,而是通過后續(xù)invalidate()方法去做滑動(dòng)動(dòng)作。
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;//滑動(dòng)時(shí)間
mStartTime = AnimationUtils.currentAminationTimeMills();//開始時(shí)間
mStartX = startX;//滑動(dòng)起點(diǎn)
mStartY = startY;//滑動(dòng)起點(diǎn)
mFinalX = startX + dx;//滑動(dòng)終點(diǎn)
mFinalY = startY + dy;//滑動(dòng)終點(diǎn)
mDeltaX = dx;//滑動(dòng)距離
mDeltaY = dy;//滑動(dòng)距離
mDurationReciprocal = 1.0f / (float)mDuration;
}
- 具體過程:在MotionEvent.ACTION_UP事件觸發(fā)時(shí)調(diào)用startScroll方法->馬上調(diào)用invalidate/postInvalidate方法->會(huì)請(qǐng)求View重繪,導(dǎo)致View.draw方法被執(zhí)行->會(huì)調(diào)用View.computeScroll方法,此方法是空實(shí)現(xiàn),需要自己處理邏輯。具體邏輯是:先判斷computeScrollOffset,若為true(表示滾動(dòng)未結(jié)束),則執(zhí)行scrollTo方法,它會(huì)再次調(diào)用postInvalidate,如此反復(fù)執(zhí)行,直到返回值為false。如圖所示:
- 原理:Scroll的computeScrollOffset()根據(jù)時(shí)間的流逝動(dòng)態(tài)計(jì)算一小段時(shí)間里View滑動(dòng)的距離,并得到當(dāng)前View位置,再通過scrollTo繼續(xù)滑動(dòng)。即把一次滑動(dòng)拆分成無數(shù)次小距離滑動(dòng)從而實(shí)現(xiàn)彈性滑動(dòng)。
推薦閱讀: 站在源碼的肩膀上全解Scroller工作機(jī)制
②通過動(dòng)畫:動(dòng)畫本身就是一種漸近的過程,故可通過動(dòng)畫來實(shí)現(xiàn)彈性滑動(dòng)。方法是:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms內(nèi)使得View從原始位置向右平移100像素
③使用延時(shí)策略:通過發(fā)送一系列延時(shí)信息從而達(dá)到一種漸近式的效果,具體可以通過Handler和View的postDelayed方法,也可使用線程的sleep方法。
對(duì)彈性滑動(dòng)完成總時(shí)間有精確要求的使用場(chǎng)景下,使用延時(shí)策略是一個(gè)不太合適的選擇。
推薦閱讀:View滑動(dòng)與實(shí)現(xiàn)滑動(dòng)的幾種方法
4.View事件分發(fā)機(jī)制
a.事件分發(fā)本質(zhì):就是對(duì)MotionEvent事件分發(fā)的過程。即當(dāng)一個(gè)MotionEvent產(chǎn)生了以后,系統(tǒng)需要將這個(gè)點(diǎn)擊事件傳遞到一個(gè)具體的View上。(關(guān)于MotionEvent介紹見本篇2.a)
b.點(diǎn)擊事件的傳遞順序:Activity(Window) -> ViewGroup -> View
補(bǔ)充閱讀:對(duì)Activity、View、Window的理解
c.需要的三個(gè)主要方法:
dispatchTouchEvent:進(jìn)行事件的分發(fā)(傳遞)。返回值是 boolean 類型,受當(dāng)前onTouchEvent和下級(jí)view的dispatchTouchEvent影響
onInterceptTouchEvent:對(duì)事件進(jìn)行攔截。該方法只在ViewGroup中有,View(不包含 ViewGroup)是沒有的。一旦攔截,則執(zhí)行ViewGroup的onTouchEvent,在ViewGroup中處理事件,而不接著分發(fā)給View。且只調(diào)用一次,所以后面的事件都會(huì)交給ViewGroup處理。
onTouchEvent:進(jìn)行事件處理。
事件分發(fā)過程圖:
- 事件分發(fā)是逐級(jí)下發(fā)的,目的是將事件傳遞給一個(gè)View。
- ViewGroup一旦攔截事件,就不往下分發(fā),同時(shí)調(diào)用onTouchEvent處理事件。
推薦閱讀:Android事件分發(fā)機(jī)制詳解(源碼)
5.View滑動(dòng)沖突
a.產(chǎn)生原因:
- 一般情況下,在一個(gè)界面里存在內(nèi)外兩層可同時(shí)滑動(dòng)的情況時(shí),會(huì)出現(xiàn)滑動(dòng)沖突現(xiàn)象。
b.可能場(chǎng)景:
- 外部滑動(dòng)和內(nèi)部滑動(dòng)方向不一致:如ViewPager嵌套ListView(實(shí)際這么用沒問題,因?yàn)閂iewPager內(nèi)部已處理過)。
- 外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向一致:如ScrollView嵌套ListView(實(shí)際上也已被解決)。
- 上面兩種情況的嵌套
c.處理規(guī)則:
- 對(duì)場(chǎng)景一:當(dāng)用戶左右/上下滑動(dòng)時(shí)讓外部View攔截點(diǎn)擊事件,當(dāng)用戶上下/左右滑動(dòng)時(shí)讓內(nèi)部View攔截點(diǎn)擊事件。即根據(jù)滑動(dòng)的方向判斷誰來攔截事件。關(guān)于判斷是上下滑動(dòng)還是左右滑動(dòng),可根據(jù)滑動(dòng)的距離或者滑動(dòng)的角度去判斷。
- 對(duì)場(chǎng)景二:一般從業(yè)務(wù)上找突破點(diǎn)。即根據(jù)業(yè)務(wù)需求,規(guī)定何時(shí)讓外部View攔截事件何時(shí)由內(nèi)部View攔截事件。
- 對(duì)場(chǎng)景三:相對(duì)復(fù)雜,可同樣根據(jù)需求在業(yè)務(wù)上找到突破點(diǎn)。
d.解決方式:
- 法一:外部攔截法
- 含義:指點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。
- 方法:需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做出相應(yīng)的攔截。以下是偽代碼:
//重寫父容器的攔截方法
public boolean onInterceptTouchEvent (MotionEvent event){
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://對(duì)于ACTION_DOWN事件必須返回false,一旦攔截后續(xù)事件將不能傳遞給子View
intercepted = false;
break;
case MotionEvent.ACTION_MOVE://對(duì)于ACTION_MOVE事件根據(jù)需要決定是否攔截
if (父容器需要當(dāng)前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP://對(duì)于ACTION_UP事件必須返回false,一旦攔截子View的onClick事件將不會(huì)觸發(fā)
intercepted = false;
break;
default : break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
- 法二:內(nèi)部攔截法:
- 含義:指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進(jìn)行處理。
- 方法:需要配合requestDisallowInterceptTouchEvent方法。以下是子View的dispatchTouchEvent方法的偽代碼:
public boolean dispatchTouchEvent ( MotionEvent event ) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);//為true表示禁止父容器攔截
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此類點(diǎn)擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除子容器需要做處理外,父容器也要默認(rèn)攔截除了ACTION_DOWN以外的其他事件,這樣當(dāng)子容器調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時(shí),父元素才能繼續(xù)攔截所需的事件。因此,父View需要重寫onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
內(nèi)部攔截法要求父容器不能攔截ACTION_DOWN的原因:由于該事件并不受FLAG_DISALLOW_INTERCEPT(由requestDisallowInterceptTouchEvent方法設(shè)置)標(biāo)記位控制,一旦ACTION_DOWN事件到來,該標(biāo)記位會(huì)被重置。所以一旦父容器攔截了該事件,那么所有的事件都不會(huì)傳遞給子View,內(nèi)部攔截法也就失效了。
推薦閱讀:一文解決Android View滑動(dòng)沖突
二.View工作原理
1.View工作流程:measure測(cè)量->layout布局->draw繪制
- measure確定View的測(cè)量寬高
- layout確定View的最終寬高和四個(gè)頂點(diǎn)的位置
- draw將View 繪制到屏幕上
- 對(duì)應(yīng)onMeasure()、onLayout()、onDraw()三個(gè)方法。
具體過程:
- ViewRoot對(duì)應(yīng)于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶。
- View的繪制流程是從ViewRoot和performTraversals開始。
- performTraversals()依次調(diào)用performMeasure()、performLayout()和performDraw()三個(gè)方法,分別完成頂級(jí) View的繪制。
- 其中,performMeasure()會(huì)調(diào)用measure(),measure()中又調(diào)用onMeasure(),實(shí)現(xiàn)對(duì)其所有子元素的measure過程,這樣就完成了一次measure過程;接著子元素會(huì)重復(fù)父容器的measure過程,如此反復(fù)至完成整個(gè)View樹的遍歷。layout和draw同理。過程圖如下:
補(bǔ)充閱讀:了解ViewRoot和DecorView
a.measure過程:確定測(cè)量寬高
先來理解MeasureSpec:
- 作用:通過寬測(cè)量值widthMeasureSpec和高測(cè)量值heightMeasureSpec決定View的大小
- 組成:一個(gè)32位int值,高2位代表SpecMode(測(cè)量模式),低30位代表SpecSize( 某種測(cè)量模式下的規(guī)格大小)。
- 三種模式:
- UNSPECIFIED:父容器不對(duì)View有任何限制,要多大有多大。常用于系統(tǒng)內(nèi)部。
- EXACTLY(精確模式):父視圖為子視圖指定一個(gè)確切的尺寸SpecSize。對(duì)應(yīng)LyaoutParams中的match_parent或具體數(shù)值。
- AT_MOST(最大模式):父容器為子視圖指定一個(gè)最大尺寸SpecSize,View的大小不能大于這個(gè)值。對(duì)應(yīng)LayoutParams中的wrap_content。
- 決定因素:值由子View的布局參數(shù)LayoutParams和父容器的MeasureSpec值共同決定。具體規(guī)則見下圖:
現(xiàn)在,分別討論兩種measure過程:
①View的measure:只有一個(gè)原始的View,通過measure()即可完成測(cè)量。過程圖見下:
從getDefaultSize()中可以看出,直接繼承View的自定義View需要重寫onMeasure()并設(shè)置wrap_content時(shí)的自身大小,否則效果相當(dāng)于macth_parent。解決上述問題的典型代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//分析模式,根據(jù)不同的模式來設(shè)置
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
}
補(bǔ)充閱讀:為什么你的自定義View wrap_content不起作用
②ViewGroup的measure:除了完成ViewGroup自身的測(cè)量外,還會(huì)遍歷去調(diào)用所有子元素的measure方法。
ViewGroup中沒有重寫onMeasure(),而是提供measureChildren()。
圖片來源:自定義View Measure過程
b.layout過程:確定View的最終寬高和四個(gè)頂點(diǎn)的位置
- 大致流程:從頂級(jí)View開始依次調(diào)用layout(),其中子View的layout()會(huì)調(diào)用setFrame()來設(shè)定自己的四個(gè)頂點(diǎn)(mLeft、mRight、mTop、mBottom),接著調(diào)用onLayout()來確定其坐標(biāo),注意該方法是空方法,因?yàn)椴煌腣iewGroup對(duì)其子View的布局是不相同的。
圖片來源:自定義View Layout過程
c.draw過程:繪制到屏幕
繪制順序:
- 繪制背景:background.draw(canvas)
- 繪制自己:onDraw(canvas)
- 繪制children:dispatchDraw(canvas)
- 繪制裝飾:onDrawScrollBars(canvas)
注意:Vew有一個(gè)特殊的方法setWillNotDraw(),該方法用于設(shè)置 WILL_NOT_DRAW 標(biāo)記位(其作用是當(dāng)一個(gè)View不需要繪制內(nèi)容時(shí),系統(tǒng)可進(jìn)行相應(yīng)優(yōu)化)。默認(rèn)情況下View是沒有這個(gè)優(yōu)化標(biāo)志的(設(shè)為true)。
圖片來源:自定義View Draw過程
2.自定義View
a.自定義View的類型:
b.特別提醒:
最后,因?yàn)樽远xView內(nèi)容非常多,這里不再展開。最重要的是實(shí)踐,就是現(xiàn)在帶著理論基礎(chǔ)開始實(shí)戰(zhàn)吧~
希望這篇文章對(duì)你有幫助~