4.1 View的事件體系
一、View的基礎(chǔ)知識
1、View的位置參數(shù)
1.1买鸽、兩種坐標系
Android坐標系:以屏幕左上角點作為坐標系原點。
View坐標系:以View的左上角點作為坐標系原點贯被。
1.2眼五、View的位置屬性
View的位置主要由四個屬性決定:top妆艘、left、right弹砚、bottom双仍。從Android3.0開始,還增加了x桌吃、y朱沃、translationX、translationY茅诱。這幾個參數(shù)都是相對于父容器坐標系而言逗物。
width = right - left
height = bottom - top
x = left + translationX //left不會變
y = top + translationY //top不會變
x、y是View的左上角坐標
translationX瑟俭、translationY是View的左上角相對于父容器的偏移量翎卓,默認值是0
2、MotionEvent
典型的事件類型
- ACTION_DOWN 手指剛接觸屏幕
- ACTION_MOVE 手指在屏幕上移動
- ACTION_UP 手指從屏幕上松開
MotionEvent的getX()
和getY()
是相對于發(fā)生事件的View本身坐標系而言的摆寄,getRawX()
和getRawY()
是相對于Android坐標系而言的失暴。
若在View處按下,View接收到了MotionEvent對象微饥,移到View上方時逗扒,getY()返回負數(shù),移到View下方時欠橘,getY()將返回的值大于getHeight()矩肩,getX()也是類似的。
3肃续、TouchSlop
系統(tǒng)所能識別出的被認為是滑動的最小距離黍檩,這是一個常量,與設(shè)備有關(guān)始锚,可通過以下方法獲得
ViewConfiguration.get(getContext()).getScaledTouchSloup()
當我們處理滑動時刽酱,比如滑動距離小于這個值,我們就可以過濾這個事件(系統(tǒng)會默認過濾)瞧捌,從而有更好的用戶體驗肛跌。
4、VelocityTracker
速度追蹤察郁,用于追蹤手指在滑動過程中的速度,包括水平放向速度和豎直方向速度转唉。使用方法:
- 在View的onTouchEvent方法中追蹤當前事件的速度
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
- 計算速度皮钠,獲得水平速度和豎直速度
velocityTracker.computeCurrentVelocity(1000);//計算速度。獲取速度之前赠法,必須調(diào)用麦轰。
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
這里的速度是指一段時間內(nèi)手指滑過的像素數(shù)乔夯,1000指的是1000ms,得到的是1000ms內(nèi)滑過的像素數(shù)款侵。速度可正可負:速度 = ( 終點位置 - 起點位置) / 時間段
- 當不需要使用的時候末荐,需要調(diào)用clear()方法重置并回收內(nèi)存
velocityTracker.clear();
velocityTracker.recycle();
5、GestureDetector
手勢檢測新锈,用于輔助檢測用戶的單擊甲脏、滑動、長按妹笆、雙擊等行為块请。
使用過程
- 創(chuàng)建一個GestureDetector對象并實現(xiàn)OnGestureListener(或OnDoubleTapListener)接口:
GestureDetector mGestureDetector = new GestureDetector(this);
//解決長按屏幕后無法拖動的現(xiàn)象
mGestureDetector.setIsLongpressEnabled(false);
2.接管目標View的onTouchEvent方法
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
OnGestureListener和OnDoubleTapListener接口中的方法:
其中常用的方法有:onSingleTapUp
(單擊)、onFling
(快速滑動)拳缠、onScroll
(拖動)墩新、onLongPress
(長按)和onDoubleTap
( 雙擊)。建議:如果只是監(jiān)聽滑動相關(guān)的窟坐,可以自己在onTouchEvent中實現(xiàn)海渊,如果要監(jiān)聽雙擊這種行為,那么就使用GestureDetector哲鸳。
2臣疑、View的滑動
三種方式實現(xiàn)滑動:①通過View本身提供的scrollTo/scrollBy方法。②通過動畫對View施加平移效果帕胆。③通過改變View的LayoutParams使得View重新布局來實現(xiàn)滑動朝捆。
2.1、使用scrollTo/scrollBy
View的兩個屬性:mScrollX和mScollY懒豹。
mScrollX = View布局x(左邊緣) - View內(nèi)容x(內(nèi)容左邊緣)可能為負數(shù)芙盘。
scrollTo/scrollBy 只能改變View內(nèi)容的位置而不能改變View在布局中的位置。
View內(nèi)容:若View是一個ViewGroup脸秽,指的就是其子元素儒老。若View如Buttom,那么指的就是text值记餐。
scrollTo(int x, int y)
scrollBy(int x, int y)
getScrollX()
getScrollY()
2.2驮樊、使用動畫
使用動畫移動View,主要是操作View的translationX和translationY屬性片酝,既可以采用傳統(tǒng)的View動畫囚衔,也可以采用屬性動畫,如果使用屬性動畫雕沿,為了能夠兼容3.0以下的版本练湿,需要采用開源動畫庫nineolddandroids。
2.3审轮、改變參數(shù)布局
LinearLayout.MarginLayoutParams params //取決于button的父容器是什么布局
= (LinearLayout.MarginLayoutParams) button.getLayoutParams();
params.width = 100;
params.height = 200;
params.leftMargin = 100;
button.requestLayout();//或者 button.setLayoutParams(params)
ViewParent
View需要與其父ViewGroup進行交互時的API肥哎,基本所有的View都實現(xiàn)了這個接口
重要方法:
View的getParent() ViewParent
ViewParent的requestLayout()
requeLayout()
: 子View調(diào)用requestLayout方法辽俗,會標記當前View及父容器,同時逐層向上提交篡诽,直到ViewRootImpl處理該事件崖飘,ViewRootImpl會調(diào)用三大流程,從measure開始杈女,對于每一個含有標記位的view及其子View都會進行測量朱浴、布局、繪制碧信。
invalidate()
:當子View調(diào)用了invalidate方法后赊琳,會為該View添加一個標記位,同時不斷向父容器請求刷新砰碴,父容器通過計算得出自身需要重繪的區(qū)域躏筏,直到傳遞到ViewRootImpl中,最終觸發(fā)performTraversals方法呈枉,進行開始View樹重繪流程(只繪制需要重繪的視圖)趁尼。
postInvalidate()
:這個方法與invalidate方法的作用是一樣的,都是使View樹重繪猖辫,但兩者的使用條件不同酥泞,postInvalidate是在非UI線程中調(diào)用,invalidate則是在UI線程中調(diào)用啃憎。
layout()
:對控件進行重新定位執(zhí)行onLayout()這個方法芝囤,比如要做一個可回彈的ScrollView,思路就是隨著手勢的滑動子控件滑動辛萍,那么我們可以將ScrollView的子控件調(diào)用layout(l,t,r,b)這個方法就行了悯姊。
Android View 深度分析requestLayout、invalidate與postInvalidate
2.4贩毕、各種滑動方式的對比
- scrollTo/scrollBy:操作簡單悯许,適合對View內(nèi)容的滑動;
- 動畫:操作簡單辉阶,主要適用于沒有交互的View和實現(xiàn)復雜的動畫效果先壕;
- 改變布局參數(shù):操作稍微復雜,適用于有交互的View谆甜。
3垃僚、彈性滑動
共同思想:將一次大的滑動分成若干次小的滑動,并在一定時間段內(nèi)完成规辱。
3.1冈在、使用Scroller
使用Scroller實現(xiàn)彈性滑動的典型使用方法如下
Scroller scroller = new Scroller(mContext);
//緩慢移動到指定位置
private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms內(nèi)滑向destX,效果就是緩慢滑動
mScroller.startSscroll(scrollX,0,deltaX,0,1000);//僅僅保存了傳遞的參數(shù)按摘,并不會滑動
invalidate();//View會進行重繪
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
invalidate()
導致View重繪包券,在View的draw方法中調(diào)用了computeScroll()
,computeScroll()
在View中是一個空的實現(xiàn)炫贤,需要我們自己去實現(xiàn)溅固。computeScrollOffset()
會根據(jù)時間流逝去計算當前的mScrollX和mScrollY,并調(diào)用scrollTo
方法實現(xiàn)滑動兰珍,接著又調(diào)用postInvalidate()
進行第二次重繪侍郭。如此反復,直到繪制結(jié)束掠河。
Scroller方法:
startScroll(int startX, int startY, int dx, int dy, int duration)
-
boolean computeScrollOffset()
//返回true亮元,代表滑動未結(jié)束 -
int getCurrX()
//當前時刻應該所處的位置
3.2、通過動畫
方法一
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
方法二
//當然唠摹,我們也可以利用動畫來模仿Scroller實現(xiàn)View彈性滑動的過程:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatedFraction();
mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
}
});
animator.start();
3.3爆捞、使用延時策略
延時策略的核心思想是通過發(fā)送一系列延時信息從而達到一種漸近式的效果,具體可以通過Hander和View的postDelayed方法勾拉,也可以使用線程的sleep方法煮甥。 下面以Handler為例:
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGE_SCROLL_TO:
mCount ++ ;
if (mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
}
break;
default : break;
}
}
}
四、事件的分發(fā)機制
1藕赞、基礎(chǔ)認知
當用戶觸摸屏幕時將產(chǎn)生MotionEvent對象
典型的事件類型:
MotionEvent.ACTION_DOWN:按下View(所有事件的開始)
MotionEvent.ACTION_MOVE:滑動View
MotionEvent.ACTION_CANCEL:非人為原因結(jié)束本次事件
MotionEvent.ACTION_UP:抬起View(與DOWN對應)
事件分發(fā)的本質(zhì):即當一個點擊事件發(fā)生后成肘,系統(tǒng)需要將這個事件傳遞給一個具體的View去處理。這個事件傳遞的過程就是分發(fā)過程斧蜕。由三個重要方法來共同完成双霍。
boolean dispatchTouchEvent(MotionEvent event)
用來進行事件的分發(fā)
boolean onInterceptTouchEvent(MotionEvent ev)
用來判斷是否攔截事件
boolean onTouchEvent(MotionEvent event)
用來處理事件
他們之間的關(guān)系,可以用如下偽代碼表示:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
}
return consume;
}
事件分發(fā)機制的重要結(jié)論:
- 同一個事件序列以down事件開始批销,中間包含數(shù)量不定的move事件洒闸,最終以up事件結(jié)束。
- 正常情況下风钻,一個事件序列只能由一個View攔截并消耗顷蟀。
- 某個View攔截了事件后,該事件序列只能由它去處理骡技,并且它的onInterceptTouchEvent不會再被調(diào)用鸣个。
- 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) 布朦,那么同一事件序列中的其他事件都不會交給他處理囤萤,并且事件將重新交由他的父元素去處理,即父元素的onTouchEvent被調(diào)用是趴。好比一個程序員涛舍,如果這件事沒有處理好,短期內(nèi)上級不會再把事情交給他處理唆途。
- 如果View不消耗ACTION_DOWN以外的其他事件富雅,那么這個事件將會消失掸驱,此時父元素的onTouchEvent并不會被調(diào)用,并且當前View可以持續(xù)收到后續(xù)的事件没佑,最終消失的點擊事件會傳遞給Activity去處理毕贼。
- ViewGroup默認不攔截任何事件。
- View沒有onInterceptTouchEvent方法蛤奢。一旦事件傳遞給它鬼癣,它的onTouchEvent方法會被調(diào)用。
- View的onTouchEvent默認消耗事件啤贩,除非他是不可點擊的( clickable和longClickable同時為false) 待秃。View的longClickable屬性默認false,clickable默認屬性分情況(如TextView為false痹屹,button為true)章郁。
- View的enable屬性不影響onTouchEvent的默認返回值。
- onClick會發(fā)生的前提是當前View是可點擊的痢掠,并且收到了down和up事件驱犹。
- 事件傳遞過程總是由外向內(nèi)的,即事件總是先傳遞給父元素足画,然后由父元素分發(fā)給子View雄驹,通過
requestDisallowInterceptTouchEvent
方法可以在子元素中干預父元素的分發(fā)過程,但是ACTION_DOWN事件除外淹辞。 -
onTouch
(dispatchTouchEvent中調(diào)用)優(yōu)先于onTouchEvent
執(zhí)行医舆,onClick
優(yōu)先級最低。onTouch能夠得到執(zhí)行需要兩個前提條件象缀,第一mOnTouchListener的值不能為空蔬将,第二當前點擊的控件必須是enable的。因此如果你有一個控件是非enable的央星,那么給它注冊onTouch事件將永遠得不到執(zhí)行霞怀。對于這一類控件,如果我們想要監(jiān)聽它的touch事件莉给,就必須通過在該控件中重寫onTouchEvent方法來實現(xiàn)毙石。
參考文獻:
OnFling和onSingleTapUp不執(zhí)行的問題的一種解決方法
4.2 View的工作原理
一、解析Activity的構(gòu)成
1颓遏、DecorView的創(chuàng)建
當我們調(diào)用startActivity
方法時徐矩,最終調(diào)用ActivityThread#handleLaunchActivity
,該方法中會首先會調(diào)用Activity的onCreate
方法叁幢。在onCreate
方法中滤灯,會調(diào)用Activity#setContentView
,setContentView
內(nèi)部會調(diào)用Activity的成員變量mWindow的(Window是抽象類,其實現(xiàn)類是PhoneWindow鳞骤,mWindow是PhoneWindow的一個實例)setContentView窒百。其setContentView方法中,首先new一個DecorView對象豫尽,然后DecorView對象會根據(jù)不同的情況(主題贝咙,Window的feature等)加載不同的布局資源。DecorView是Activity中的根View拂募,繼承了FrameLayout。至此DecorView創(chuàng)建完成窟她。
2陈症、添加DecorView到Window
完成DecorView的創(chuàng)建之后,接著調(diào)用ActivityThread#handleResumeActivity
方法震糖。在handleResumeActivity方法中录肯,首先調(diào)用Activity#onResume方法,handleResumeActivity方法接著會得到一個DecorView對象和一個WindowManager對象(接口吊说,實現(xiàn)類是WindowManagerImpl)论咏,然后調(diào)用WindowManagerImpl#addView方法,DecorView對象作為入?yún)魅氚渚T赪indowManager#addView中厅贪,創(chuàng)建了一個ViewRootImpl對象(ViewRoot的實現(xiàn)類),并調(diào)用了ViewRootImpl#setView雅宾,DecorView對象作為入?yún)⒀獭T赩iewRootImpl#setView方法內(nèi)部,會通過跨進程的方式向WMS(WindowManagerService)發(fā)起一個調(diào)用眉抬,從而將DecorView最終添加到Window上贯吓,才能真正顯示出來。在這個過程中蜀变,ViewRootImpl悄谐、DecorView和WMS會彼此關(guān)聯(lián),最后通過WMS調(diào)用ViewRootImpl#performTraverals方法開始View的測量库北、布局爬舰、繪制流程。
Window是一個抽象類贤惯,具體是實現(xiàn)是PhoneWindow洼专,Activity、Dialog等的視圖都需要附加到Window上來呈現(xiàn)孵构。
WindowManager是外界訪問Window的入口屁商,實現(xiàn)類是WindowManagerImpl,Window的具體實現(xiàn)是在WindowManagerService中,WindowManager和WindowManagerService的交互是一個IPC過程蜡镶。雾袱。
DecorView是頂級View,是一個FrameLayout布局官还,代表了整個應用的界面芹橡。內(nèi)部有titlebar和contentParent兩個子元素,contentParent的id是content望伦,而我們設(shè)置的main.xml布局則是contentParent里面的一個子元素林说。
ViewRoot的實現(xiàn)類是ViewRootImpl,在WindowManager中創(chuàng)建屯伞,用于將DecorView添加到Window中腿箩。
二、理解MeasureSpec
MeasureSpec代表一個32位int值劣摇,高2位代表SpecMode(測量模式)珠移,低30位代表SpecSize(某種測量模式下的規(guī)格大小)。
//主要理解 & ~ | 位運算的作用末融,體會這樣設(shè)計的妙處
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;//11000000 0000...000
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 makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec通過將SpecSize和SpecMode打包成了一個int值來避免過多對象的內(nèi)存分配钧惧。
SpecMode有三類:
UNSPECIFIED :父容器不對View進行任何限制,要多大給多大勾习,一般用于系統(tǒng)內(nèi)部浓瞪。
EXACTLY:父容器檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值语卤,對應LayoutParams中的match_parent和具體數(shù)值這兩種模式(也不一定追逮,還受父容器影響,詳見下面的表格)粹舵。
AT_MOST:父容器指定了一個可用大小即SpecSize钮孵,View的大小不能大于這個值,對LayoutParams中的wrap_content眼滤。
說明:上面描述的是理論上應該有的邏輯巴席。
對于頂級DecorView,其MeasureSpec是由窗口尺寸和自身的LayoutParams共同確定诅需。對于普通的View漾唉,其MeasureSpec由父容器和自身的LayoutParams共同確定。一旦MeasureSpec確定堰塌,onMeasure中就可以確定View的測量寬/高赵刑。
三、View的工作流程
主要指measure场刑、layout般此、draw這三大流程。measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置铐懊,而draw則將View繪制到屏幕上邀桑。
ViewRootImpl#performTraversals
會依次調(diào)用performMeasure
、performLayout
和performDraw
三個方法科乎,這三個方法分別開啟頂級View的measure壁畸、layout和draw這三大流程。
其中performMeasure
中會調(diào)用頂級View#measure 方法茅茂,measure調(diào)用onMeasure
捏萍,在onMeasure 方法中則會測量自身并調(diào)用所有子元素measure方法,這樣就完成了一次measure過程空闲;子元素會重復父容器的measure過程照弥,如此反復完成了整個View樹的遍歷。另外兩個過程同理进副。
1、ViewGroup的Measure流程
對于ViewGroup既要測量自身悔常,也要遍歷子元素的measure方法(通過實現(xiàn)onMeasure方法)影斑。
在performMeasure方法中,調(diào)用了DecorView#measure(繼承自View机打,其實調(diào)用的是View#measure)矫户,measure會調(diào)用onMeasure方法。ViewGroup并沒有定義onMeasure残邀,這個方法需要子類去實現(xiàn)皆辽,主要需要實現(xiàn)兩個功能:①測量自身②測量子View。
ViewGroup提供了measureChildWithMargins
和measureChildren
方法芥挣。
1.1驱闷、measureChildWithMargins方法
protectedvoidmeasureChildWithMargins(Viewchild,
intparentWidthMeasureSpec,intwidthUsed,
intparentHeightMeasureSpec,intheightUsed){
finalMarginLayoutParamslp=(MarginLayoutParams)child.getLayoutParams();
//入?yún)ⅲ焊溉萜鞯腗easureSpec;父的padding和自身的margin(剩下為子元素可用空間)空免;自身的寬度空另。
finalintchildWidthMeasureSpec=getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft+mPaddingRight+lp.leftMargin+lp.rightMargin
+widthUsed,lp.width);
finalintchildHeightMeasureSpec=getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop+mPaddingBottom+lp.topMargin+lp.bottomMargin
+heightUsed,lp.height);
//注意:此時的入?yún)⑹亲陨淼腗easureSpec。measure又會調(diào)用child#onMeasure方法
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}
從上面的方法可以看出蹋砚,View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定扼菠,MeasureSpec一旦確定,onMeasure中就可以確定View的測量寬/高坝咐。getChildMeasureSpec(int spec, int padding, int childDimension)
方法的邏輯整理出如下表格:
表中的parentSize是指父容器目前可以使用的大小循榆,即父容器的specSize減去入?yún)adding。
ViewGroup并沒有定義onMeasure墨坚,需要其子類去實現(xiàn)秧饮,為什么ViewGroup不像View一樣對其onMeasure做統(tǒng)一呢?因為不同的ViewGroup子類有不同的布局特征,導致測量細節(jié)各不相同浦楣,無法統(tǒng)一袖肥。
根據(jù)上面的表格,我們發(fā)現(xiàn)父容器的MeasureSpec屬性為AT_MOST振劳,子元素的LayoutParams為WRAP_CONTENT的時候炭玫,子元素的測量模式為AT_MOST味抖,它的SpecSize為父容器的SpecSize減去padding(入?yún)ⅲ簿褪钦f子元素WRAP_CONTENT和MATCH_PARENT一樣的。為了解決這個問題红竭,需要在WRAP_CONTENT時指定一下默認的寬高。
1.2融求、measureChildren方法
measureChildren
中會循環(huán)調(diào)用measureChild方法陡鹃,在measureChild中,首先會調(diào)用getChildMeasureSpec方法吮旅,入?yún)⒑蜕厦骖愃葡荆瑓^(qū)別在于padding入?yún)H僅為自身的padding,然后會調(diào)用子元素的measure方法(和measureChildWithMargins
非常類似)庇勃。
2檬嘀、View的Measure過程
View的measure方法是一個final方法,會調(diào)用onMeasure方法责嚷,因此只需要關(guān)注onMeasure方法鸳兽,入?yún)樽约旱膍easureSpec
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension
用于設(shè)置測量的寬高,測量好之后罕拂,必須調(diào)用揍异。
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;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
簡單理解,getDefaultSize返回的就是measureSpec中的specSize爆班,這就是View測量后的大小衷掷。在AT_MOST和EXACTLY模式下,都返回了specSize柿菩。也就是說對于一個直接繼承View的自定義View棍鳖,它的wrap_content和match_parent效果一樣,因此如果要實現(xiàn)自定義View的wrap_content碗旅,則要重寫onMeasure方法渡处。解決問題:
@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);
// 在 MeasureSpec.AT_MOST 模式下,給定一個默認值mWidth,mHeight祟辟。默認寬高靈活指定
//參考TextView医瘫、ImageView的處理方式
//其他情況下沿用系統(tǒng)測量規(guī)則即可
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
getSuggestedMinimumWidth()
方法就是:如果View沒有設(shè)置背景,就返回minWidth屬性值(可以為0)旧困;如果設(shè)置了背景醇份,就返回minWidth和背景的最小寬度之間的最大值稼锅。
View的measure過程是三大流程中最復雜的一個,measure完成以后僚纷,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高矩距。在某些情況下,系統(tǒng)可能需要多次measure才能確定最終的測量寬/高怖竭,所以在onMeasure中拿到的寬/高很可能不是準確的锥债。一個較好的習慣是在onLayout方法中,去獲取View測量寬高或最終寬高痊臭。
3哮肚、如何正確獲得寬高
如果我們想要在Activity啟動的時候就獲取一個View的寬高,怎么操作呢广匙?因為View的measure過程和Activity的生命周期并不是同步執(zhí)行允趟,無法保證在Activity的 onCreate、onStart鸦致、onResume 時某個View就已經(jīng)測量完畢潮剪。所以有以下四種方式來獲取View的寬高:
3.1、Activity/View#onWindowFocusChanged
onWindowFocusChanged這個方法的含義是:VieW已經(jīng)初始化完畢了分唾,寬高已經(jīng)準備好了鲁纠,需要注意:它會被調(diào)用多次,當Activity的窗口得到焦點和失去焦點均會被調(diào)用鳍寂。
3.2、view.post(runnable)
通過post將一個runnable投遞到消息隊列的尾部情龄,當Looper調(diào)用此runnable的時候迄汛,View也初始化好了。
3.3骤视、ViewTreeObserver
使用 ViewTreeObserver 的眾多回調(diào)可以完成這個功能鞍爱,比如OnGlobalLayoutListener 這個接口,當View樹的狀態(tài)發(fā)送改變或View樹內(nèi)部的View的可見性發(fā)生改變時专酗,onGlobalLayout 方法會被回調(diào)睹逃,這是獲取View寬高的好時機。需要注意的是祷肯,伴隨著View樹狀態(tài)的改變沉填, onGlobalLayout 會被回調(diào)多次。
3.4佑笋、view.measure(int widthMeasureSpec,intheightMeasureSpec)
手動對view進行measure翼闹。需要根據(jù)View的layoutParams分情況處理:
- match_parent:直接放棄。根據(jù)上表所示蒋纬,需要知道parentSize猎荠,即父容器剩余空間坚弱,而此時無法知道這個值。
- 具體的數(shù)值( dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
- wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二進制表示关摇,最大值30個1荒叶,在AT_MOST模式下,我們用View理論上能支持的最大值去構(gòu)造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
四输虱、layout過程
layout
方法確定View本身的位置些楣,會調(diào)用onLayout
方法。onLayout
確定所有子元素的位置悼瓮,通過遍歷所有的子View并調(diào)用其layout
方法戈毒。
View#layout中,setFrame
確定View的四個頂點位置横堡,即初始化mLeft埋市,mRight,mTop命贴,mBottom這四個值(確定了最終的寬高)道宅,也就確定了View在父容器中的位置。接著調(diào)用onLayout方法胸蛛,確定所有子View的位置污茵,和onMeasure一樣,onLayout的具體實現(xiàn)和布局有關(guān)葬项,因此View和ViewGroup均沒有真正實現(xiàn)onLayout方法泞当。
View的測量寬高和最終寬高的區(qū)別:
在View的默認實現(xiàn)中,View的測量寬高和最終寬高相等民珍,只不過測量寬高形成于measure過程襟士,最終寬高形成于layout過程。即便View需要多次測量才能確定自己的測量寬高嚷量,但最終來說陋桂,測量寬高和最終寬高還是一致。
五蝶溶、draw過程
View的繪制過程遵循如下幾步:
- 繪制背景 drawBackground(canvas)
- 繪制自己 onDraw
- 繪制children dispatchDraw 遍歷所有子View的 draw 方法
- 繪制裝飾 onDrawScrollBars
View#setWillNotDraw嗜历,如果一個View不需要繪制任何內(nèi)容,那么置為ture抖所,系統(tǒng)會進行相應的優(yōu)化梨州。默認情況下,View為false田轧,ViewGroup為true摊唇。所以自定義ViewGroup需要通過onDraw來繪制內(nèi)容時,必須顯式的關(guān)閉 WILL_NOT_DRAW 這個優(yōu)化標記位涯鲁,即調(diào)用 setWillNotDraw(false)巷查。