在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
簡(jiǎn)介:在Android的世界中View是所有控件的基類,其中也包括ViewGroup在內(nèi)鹦聪,ViewGroup是代表著控件的集合账阻,其中可以包含多個(gè)View控件。
從某種角度上來(lái)講Android中的控件可以分為兩大類:View與ViewGroup泽本。通過(guò)ViewGroup淘太,整個(gè)界面的控件形成了一個(gè)樹形結(jié)構(gòu),上層的控件要負(fù)責(zé)測(cè)量與繪制下層的控件规丽,并傳遞交互事件蒲牧。
在每棵控件樹的頂部都存在著一個(gè)ViewParent對(duì)象,它是整棵控件樹的核心所在赌莺,所有的交互管理事件都由它來(lái)統(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()方法里無(wú)法獲取到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è)過(guò)程產(chǎn)生的一系列事件
任何事件列都是以DOWN事件開始辐益,UP事件結(jié)束断傲,中間有無(wú)數(shù)的MOVE事件。如圖:
通過(guò)MotionEvent 對(duì)象可以得到觸摸事件的x智政、y坐標(biāo)认罩。其中通過(guò)getX()、getY()可獲取相對(duì)于當(dāng)前view左上角的x续捂、y坐標(biāo)垦垂;通過(guò)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),可用它來(lái)判斷用戶的滑動(dòng)是否達(dá)到閾值酒繁,獲取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()滓彰。
c.VelocityTracker:速度追蹤,用于追蹤手指在滑動(dòng)過(guò)程中的速度州袒,包括水平和豎直方向的速度揭绑。
使用過(guò)程:首先在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)手指在水平方向從左向右滑過(guò)的像素?cái)?shù)抬闯,即水平速度
float yVelocity = velocityTracker .getYVelocity();//得到的是1000ms內(nèi)手指在水平方向從上向下滑過(guò)的像素?cái)?shù)井辆,垂直速度
最后,當(dāng)不需要使用它的時(shí)候溶握,需要調(diào)用clear方法來(lái)重置并回收內(nèi)存:
velocityTracker.clear();
velocityTracker.recycle();
推薦閱讀:Android常用觸控類分析:MotionEvent 杯缺、 ViewConfiguration、VelocityTracker
d.GestureDetector:手勢(shì)檢測(cè)睡榆,用于輔助檢測(cè)用戶的單擊萍肆、滑動(dòng)、長(zhǎng)按胀屿、雙擊等行為塘揣。
使用過(guò)程:創(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);// 解決長(zhǎng)按屏幕后無(wú)法拖動(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)三種辦法:
①通過(guò)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ù)值。(更直觀感受:查看下一張照片或者查看長(zhǎng)圖時(shí)手指滑動(dòng)方向?yàn)檎?/p>
②通過(guò)動(dòng)畫給View施加平移效果:主要通過(guò)改變View的translationX和translationY參數(shù)來(lái)實(shí)現(xiàn)敦姻。可用view動(dòng)畫歧杏,也可以采用屬性動(dòng)畫镰惦,如果使用屬性動(dòng)畫的話,為了能夠兼容3.0以下版本犬绒,需要采用開源動(dòng)畫庫(kù)nineoldandroids旺入。注意View動(dòng)畫的View移動(dòng)只是位置移動(dòng),并不能真正的改變view的位置凯力,而屬性動(dòng)畫可以茵瘾。
③通過(guò)改變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:操作簡(jiǎn)單慷暂,適合對(duì)view內(nèi)容滑動(dòng)。非平滑
- 動(dòng)畫:操作簡(jiǎn)單晨雳,主要適用于沒(méi)有交互的view和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果
- 改變LayoutParams:操作稍微復(fù)雜行瑞,適用于有交互的view。非平滑
b.實(shí)現(xiàn)View彈性滑動(dòng)三種方法:
①使用Scroller:
- 與scrollTo/scrollBy不同:scrollTo/scrollBy過(guò)程是瞬間完成的餐禁,非平滑血久;而Scroller則有過(guò)渡滑動(dòng)的效果。
- 注意:Scoller本身無(wú)法讓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();//通過(guò)不斷的重繪不斷的調(diào)用computeScroll方法
}
}
其中startScroll源碼如下,可見它并沒(méi)有進(jìn)行實(shí)際的滑動(dòng)操作筑舅,而是通過(guò)后續(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;
}
- 具體過(guò)程:在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位置变泄,再通過(guò)scrollTo繼續(xù)滑動(dòng)。即把一次滑動(dòng)拆分成無(wú)數(shù)次小距離滑動(dòng)從而實(shí)現(xiàn)彈性滑動(dòng)熙卡。
推薦閱讀: 站在源碼的肩膀上全解Scroller工作機(jī)制
②通過(guò)動(dòng)畫:動(dòng)畫本身就是一種漸近的過(guò)程杖刷,故可通過(guò)動(dòng)畫來(lái)實(shí)現(xiàn)彈性滑動(dòng)。方法是:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms內(nèi)使得View從原始位置向右平移100像素
③使用延時(shí)策略:通過(guò)發(fā)送一系列延時(shí)信息從而達(dá)到一種漸近式的效果驳癌,具體可以通過(guò)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ā)的過(guò)程。即當(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)是沒(méi)有的。一旦攔截泡嘴,則執(zhí)行ViewGroup的onTouchEvent甫恩,在ViewGroup中處理事件,而不接著分發(fā)給View酌予。且只調(diào)用一次磺箕,所以后面的事件都會(huì)交給ViewGroup處理奖慌。
onTouchEvent:進(jìn)行事件處理。
事件分發(fā)過(guò)程圖:
- 事件分發(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í)際這么用沒(méi)問(wèn)題,因?yàn)閂iewPager內(nèi)部已處理過(guò))脸哀。
- 外部滑動(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)的方向判斷誰(shuí)來(lái)攔截事件盲镶。關(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)過(guò)父容器的攔截處理颁湖,如果父容器需要此事件就攔截宣蠕,否則就不攔截。
- 方法:需要重寫父容器的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事件到來(lái)捧挺,該標(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è)方法捕发。
具體過(guò)程:
- 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過(guò)程,這樣就完成了一次measure過(guò)程帝蒿;接著子元素會(huì)重復(fù)父容器的measure過(guò)程荐糜,如此反復(fù)至完成整個(gè)View樹的遍歷。layout和draw同理陵叽。過(guò)程圖如下:
補(bǔ)充閱讀:了解ViewRoot和DecorView
a.measure過(guò)程:確定測(cè)量寬高
先來(lái)理解MeasureSpec:
- 作用:通過(guò)寬測(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過(guò)程:
①View的measure:只有一個(gè)原始的View,通過(guò)measure()即可完成測(cè)量招狸。過(guò)程圖見下:
從getDefaultSize()中可以看出敬拓,直接繼承View的自定義View需要重寫onMeasure()并設(shè)置wrap_content時(shí)的自身大小邻薯,否則效果相當(dāng)于macth_parent。解決上述問(wèn)題的典型代碼:
@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ù)不同的模式來(lái)設(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中沒(méi)有重寫onMeasure()营勤,而是提供measureChildren()灵嫌。
圖片來(lái)源:自定義View Measure過(guò)程
b.layout過(guò)程:確定View的最終寬高和四個(gè)頂點(diǎn)的位置
- 大致流程:從頂級(jí)View開始依次調(diào)用layout(),其中子View的layout()會(huì)調(diào)用setFrame()來(lái)設(shè)定自己的四個(gè)頂點(diǎn)(mLeft葛作、mRight寿羞、mTop、mBottom)进鸠,接著調(diào)用onLayout()來(lái)確定其坐標(biāo)稠曼,注意該方法是空方法,因?yàn)椴煌腣iewGroup對(duì)其子View的布局是不相同的客年。
圖片來(lái)源:自定義View Layout過(guò)程
c.draw過(guò)程:繪制到屏幕
繪制順序:
- 繪制背景: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是沒(méi)有這個(gè)優(yōu)化標(biāo)志的(設(shè)為true)司恳。
圖片來(lái)源:自定義View Draw過(guò)程
2.自定義View
a.自定義View的類型:
b.特別提醒:
最后,因?yàn)樽远xView內(nèi)容非常多绍傲,這里不再展開扔傅。最重要的是實(shí)踐,就是現(xiàn)在帶著理論基礎(chǔ)開始實(shí)戰(zhàn)吧~
希望這篇文章對(duì)你有幫助~