
一、View事件體系
1.什么是 View 和 View的位置坐標(biāo)
- View是什么:
View 是一種界面層的控件的一種抽象,一組 View 則稱為 ViewGroup,同時(shí) ViewGroup 繼承了 View。意味著 View 可以是單個(gè)控件也可以是多個(gè)控件組成的組控件,通過這種關(guān)系形成了 View 樹的結(jié)構(gòu)。
- View的位置坐標(biāo)
- Android坐標(biāo)系:以屏幕的左上角為坐標(biāo)原點(diǎn),向右為x軸增大方向,向下為y軸增大方向。
- View的寬高和坐標(biāo)關(guān)系:width = right - left,height = bottom - top。
- 從android3.0開始,View增加了額外幾個(gè)參數(shù):x,y,translationX、translationY。其中x和y是View左上角的坐標(biāo),translationX和translationY是新View左上角相對(duì)于父容器的偏移量,它們默認(rèn)值是0。
- 存在關(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ū)別同理。
2.MotionEvent和TouchTop
-
MotionEvent:在手指接觸屏幕后所產(chǎn)生的一系列事件,任何事件列都是以DOWN事件開始,UP事件結(jié)束,中間有無數(shù)的MOVE事件。
- ACTION_DOWM:手指剛接觸屏幕。
- ACTION_MOVE:手指在屏幕上移動(dòng)。
- ACTION_UP:手指在屏幕上松開的一瞬間。

通過MotionEvent 對(duì)象可以得到觸摸位置的x、y坐標(biāo)。其中通過getX()、getY()可獲取相對(duì)于當(dāng)前view左上角的x、y坐標(biāo);通過getRawX()、getRawY()可獲取相對(duì)于手機(jī)屏幕左上角的x,y坐標(biāo)。
- TouchTop: 系統(tǒng)所能識(shí)別的被認(rèn)為是滑動(dòng)的最小距離。即當(dāng)手指在屏幕上滑動(dòng)時(shí),如果兩次滑動(dòng)之間的距離小于這個(gè)常量,那么系統(tǒng)就不認(rèn)為你是在進(jìn)行滑動(dòng)操作。獲取這個(gè)常量:
該常量和設(shè)備有關(guān),可用它來判斷用戶的滑動(dòng)是否達(dá)到閾值,獲取方法:
ViewConfiguration.get(getContext()).getScaledTouchSlop()。
3.VelocityTracker 和 GestureDetector
- VelocityTracker:速度追蹤,用于追蹤手指在滑動(dòng)過程中的速度,包括水平和豎直方向的速度。
A、首先在view的onTouchEvent方法中追蹤當(dāng)前單擊事件的速度:
//實(shí)例化一個(gè)VelocityTracker 對(duì)象
VelocityTracker velocityTracker = VelocityTracker.obtain();
//添加追蹤事件
velocityTracker.addMovement(event);
B、接著在 ACTION_UP 事件中獲取當(dāng)前的速度。注意這里計(jì)算的是1000ms時(shí)間間隔移動(dòng)的像素值,假設(shè)像素是100,即速度是每秒100像素。另外,手指逆著坐標(biāo)系的正方向滑動(dòng),所產(chǎn)生的速度為負(fù)值 ,順著正反向滑動(dòng),所產(chǎn)生的速度為正值。
//獲取速度前先計(jì)算速度,這里計(jì)算的是在1000ms內(nèi)
velocityTracker .computeCurrentVelocity(1000);
//得到的是1000ms內(nèi)手指在水平方向從左向右滑過的像素?cái)?shù),即水平速度
float xVelocity = velocityTracker .getXVelocity();
//得到的是1000ms內(nèi)手指在水平方向從上向下滑過的像素?cái)?shù),垂直速度
float yVelocity = velocityTracker .getYVelocity();
C、最后,當(dāng)不需要使用它的時(shí)候,需要調(diào)用clear方法來重置并回收內(nèi)存:
velocityTracker.clear();
velocityTracker.recycle();
- GestureDetector:手勢(shì)檢測,用于輔助檢測用戶的單擊、滑動(dòng)、長按、雙擊等行為。
A、使用過程:創(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)象
B、接著,接管目標(biāo)view的onTouchEvent方法,在待監(jiān)聽view的onTouchEvent方法中添加如下實(shí)現(xiàn):
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
C、然后,就可以有選擇的實(shí)現(xiàn)OnGestureListener和OnDoubleTapListener中的方法了。
建議:如果只是監(jiān)聽滑動(dòng)操作,建議在onTouchEvent中實(shí)現(xiàn);如果要監(jiān)聽雙擊這種行為,則使用GestureDetector 。
二、View的滑動(dòng)
滑動(dòng)方式1:通過View本身提供的scrollTo/scrollBy方法
- 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ù)值。
滑動(dòng)方式2:通過動(dòng)畫給View施加平移效果
- View動(dòng)畫:layout文件 < set > 標(biāo)簽下的 < translate fromXDelta和 toXDelta > 來平移。
- 屬性動(dòng)畫:ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
滑動(dòng)方式3:通過改變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
三種方法比較
- scrollTo/scrollBy:操作簡單,適合對(duì)view內(nèi)容滑動(dòng),非平滑。
- 動(dòng)畫:操作簡單,主要適用于沒有交互的view和實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果。
- 改變LayoutParams:操作稍微復(fù)雜,適用于有交互的view,非平滑。
三、彈性滑動(dòng)
彈性滑動(dòng)方式1:使用 Scroller
- 與scrollTo/scrollBy不同:scrollTo/scrollBy過程是瞬間完成的,非平滑;而Scroller則有過渡滑動(dòng)的效果。
- 注意:Scoller本身無法讓View彈性滑動(dòng),它需要和 View 的 computerScroller 方法配合使用。
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方法
}
}
具體實(shí)現(xiàn):在 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)。
彈性滑動(dòng)方式2:通過動(dòng)畫
動(dòng)畫本身就是一種漸近的過程,故可通過動(dòng)畫來實(shí)現(xiàn)彈性滑動(dòng)。
//在100ms內(nèi)使得View從原始位置向右平移100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
彈性滑動(dòng)方式3:使用延時(shí)策略
通過發(fā)送一系列延時(shí)信息從而達(dá)到一種漸近式的效果,具體可以通過Handler和View的postDelayed方法,也可使用線程的sleep方法。
四、View的事件分發(fā)機(jī)制
事件分發(fā)的本質(zhì):其實(shí)就是對(duì) MotionEvent 事件分發(fā)的過程。即當(dāng)一個(gè) MotionEvent 產(chǎn)生了以后,系統(tǒng)需要將這個(gè)點(diǎn)擊事件傳遞到一個(gè)具體的View上。
點(diǎn)擊事件的傳遞順序:Activity(Window) -> ViewGroup -> View
-
其中三個(gè)主要方法:
- dispatchTouchEvent:進(jìn)行事件的分發(fā)(傳遞)。
- onInterceptTouchEvent:對(duì)事件進(jìn)行攔截。
- onTouchEvent:進(jìn)行事件處理。

- 事件分發(fā)是逐級(jí)下發(fā)的,目的是將事件傳遞給一個(gè)View。當(dāng)最后一個(gè)View 沒有消費(fèi)事件,這個(gè)事件會(huì)依次返轉(zhuǎn),最后回到最高位的Activity,如果這樣都沒消費(fèi)的話才拋棄。(責(zé)任鏈)
- 在ViewGroup事件分發(fā)中,View本身是不存在分發(fā),所以也沒有攔截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中進(jìn)行處理消費(fèi)或者不消費(fèi)。
- 當(dāng)一個(gè) View 需要處理事件時(shí),如果設(shè)置了 OnTouchListener,那么 OnTouchListener 的 onTouch 方法會(huì)回調(diào),如果返回 true,那么 onTouchEvent 方法將不會(huì)調(diào)用(同時(shí)onClick事件是在onTouchEvent中調(diào)用)所以三者優(yōu)先級(jí)是onTouch->onTouchEvent->onClick(onClickListener)。
View的事件分發(fā)機(jī)制
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK)
== ENABLED && mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
- ①、在View的dispatchTouchEvent方法中首先會(huì)執(zhí)行onTouch這個(gè)回調(diào)函數(shù),但是執(zhí)行onTouch這個(gè)回調(diào)函數(shù)有兩個(gè)前提條件、第一個(gè)提前提條件是該控件注冊(cè)了觸摸監(jiān)聽,第二個(gè)條件是該控件是的狀態(tài)是Enable的。
- ②、onTouch回調(diào)函數(shù)會(huì)有一個(gè)返回值,如果返回為true的話就代表本次觸摸事件被消耗掉了,執(zhí)行接觸;返回值為false的話會(huì)繼續(xù)執(zhí)行onTouchEvent方法。
- ③、onTouchEvent方法就是真正執(zhí)行點(diǎn)擊事件的地方,也就是我們重寫的onClick方法。
- ④、onTouchEvent方法中能夠捕獲一次點(diǎn)擊事件當(dāng)中ACTION_DOWN、ACTION_ MOVE、ACTION_UP這三個(gè)動(dòng)作,當(dāng)觸摸動(dòng)作為ACTION_UP會(huì)調(diào)用onClick方法。
ViewGroup的事件分發(fā)機(jī)制
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}
- ①、首先會(huì)去調(diào)用ViewGroup的dispatchTouchEvent方法,其中會(huì)有onInterceptTouchEvent方法對(duì)事件傳遞進(jìn)行攔截,如果返回值為true的話就表示事件不往子View中傳遞,如果為false的話就表示不對(duì)事件傳遞進(jìn)行攔截,事件會(huì)往子View中傳遞。
五、View滑動(dòng)沖突
1.常見的滑動(dòng)沖突場景
- 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向不一致(左右滑動(dòng):Fragment,上下滑動(dòng): ListView)。
- 外部滑動(dòng)方向與內(nèi)部滑動(dòng)方向一致,(ScrollView 中包含 ListView );
- 上面兩種情況的嵌套;
2.滑動(dòng)沖突的處理規(guī)則
- 滑動(dòng)路徑和水平方向所形成的夾角。
- 水平方向和豎直方向上的距離差。
- 水平和豎直方向的速度差。
- 從業(yè)務(wù)的需求上得出相應(yīng)的處理規(guī)則。
3.滑動(dòng)沖突的解決方式
第一種:外部攔截法
含義:指點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。
方法:需要重寫父容器的 onInterceptTouchEvent 方法,在內(nèi)部做出相應(yīng)的攔截。
過程:在 onInterceptTouchEvent 方法中,首先在 ACTION_DOWN 這個(gè)事件中,父容器必須返回 false,即不攔截 ACTION_DOWN 事件,因?yàn)橐坏└溉萜鲾r截了 ACTION_DOWN,那么后續(xù)的 ACTION_MOVE / ACTION_UP 都會(huì)直接交給父容器處理;其次是 ACTION_MOVE,根據(jù)需求來決定是否要攔截;最后 ACTION_UP 事件,這里必須要返回 false,一旦攔截子View的onClick事件將不會(huì)觸發(fā)。
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:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要當(dāng)前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default : break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
第二種:內(nèi)部攔截法
含義:指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進(jìn)行處理。
方法:需要配合requestDisallowInterceptTouchEvent方法。
過程:父元素需要默認(rèn)攔截除 ACTION_DOWN 以外的事,這樣子元素調(diào)用 parent.requestDisallowInterceptTouchEvent(false) 方法時(shí),父元素才能繼續(xù)攔截需要的事件。(ACTION_DOWN 事件不受 requestDisallowInterceptTouchEvent 方法影響,所以一旦父元素?cái)r截 ACTION_DOWN 事件,那么所有元素都無法傳遞到子元素去)。
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);
}
六、View工作原理
1.View的繪制流程
- 整個(gè) 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 樹的遍歷。
- performLayout 和 performDraw 的傳遞流程也是類似,唯一不同的是 performDraw 都傳遞過程是用 draw 方法中的 dispatchDraw 來實(shí)現(xiàn)。
- measure 過程決定了View的寬高,Measure完成以后,可以通過getMeasuredWidth和getMeasureHeight方法來獲取到View的寬/高。
- Layout 過程決定了View的四個(gè)頂點(diǎn)的坐標(biāo)和View的實(shí)際寬高,完成以后通過getTop、getBottom、getLeft、getRight來拿到四個(gè)頂點(diǎn)的位置,并通過getWidth和getHeight方法來拿到View的最終寬高。
- Draw 過程決定了View的顯示,只有draw方法完成后View的內(nèi)容才能呈現(xiàn)在屏幕上。

2.measure方法
2.1 MeasureSpec
作用:通過寬測量值widthMeasureSpec和高測量值heightMeasureSpec 決定View的大小。
組成:32 位的 int 值,高 2 位 代表 SpecMode測量模式,低 30 位代表 SpecSize 規(guī)格大小。
-
三種測量模式:
- UNSPECIFIED:不確定,父容器不對(duì) View 進(jìn)行任何限制,要多大給多大,一般用于系統(tǒng)內(nèi)部。
- EXACTLY:父容器檢測到 View 所需要的精確大小,這時(shí)候 View 的最終大小就是 SpecSize 所指定的值,對(duì)應(yīng) LayoutParams 中的“match_parent”和“具體數(shù)值”這兩種模式。
- AT_MOST:父容器指定了最大的尺寸 SpecSize,View 的大小不能大于這個(gè)值,對(duì)應(yīng) LayoutParams 中的 wrap_content。
決定因素:值由子View的布局參數(shù)LayoutParams和父容器的MeasureSpec值共同決定。
LayoutParams布局參數(shù):具體數(shù)值、match_parent和wrap_content。
2.2 measure測量過程圖
- ①View的measure:View的measure由measure方法來完成,而它是一個(gè)final類型的方法,意味這不能重寫,在View的measure中去調(diào)用View的onMeasure方法,因此只需要在onMeasure中實(shí)現(xiàn)即可。

從getDefaultSize()中可以看出,直接繼承View的自定義View需要重寫onMeasure()并設(shè)置wrap_content時(shí)的自身大小,否則效果相當(dāng)于macth_parent。
-
②ViewGroup的measure:
- measure():基本測量邏輯的判斷,調(diào)用onMeasure進(jìn)行下一步測量;
- onMeasure()(需要重寫):計(jì)算View寬/高,并存儲(chǔ)。
- measureChildren():遍歷子View并調(diào)用measureChild()進(jìn)行子View下一步測量。
- measureChild():計(jì)算單個(gè)子View的MeasureSpec;調(diào)用每個(gè)子View的measure()進(jìn)行下一步測量。
- getChildMeasureSpec():計(jì)算子View的MeasureSpec參數(shù)。
- ......遍歷子View測量(View的measure過程)........
- setMeasureDimension:存儲(chǔ)測量后的子View寬高。


3. 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)椴煌?ViewGroup 對(duì)其子 View 的布局是不相同的。


4. draw方法
- 繪制順序:
- 繪制背景:background.draw(canvas)
- 繪制自己:onDraw(canvas)
- 繪制children:dispatchDraw(canvas)
- 繪制裝飾:onDrawScrollBars(canvas)

ViewGroup 通常情況下不需要繪制,因?yàn)楸旧頉]有需要繪制的東西,如果不是指定了ViewGroup的背景顏色,那么連 ViewGroup 都不會(huì)調(diào)用。ViewGroup 會(huì)使用dispatchDraw方法來繪制其子View。
七、自定義View
1. 自定義 View 的分類
- 繼承 View 重寫 onDraw 方法:主要實(shí)現(xiàn)一些不規(guī)則的效果,重寫onDraw方法,需要支持wrap_content并且需要自己處理padding。(重寫view實(shí)現(xiàn)新的控件)
- 繼承 ViewGroup 派生特殊的 Layout:實(shí)現(xiàn)自定義布局,除了LinearLayout、RelativeLayout等這幾種系統(tǒng)之外的布局。稍微復(fù)雜需要合適地處理ViewGroup的測量、布局這兩個(gè)過程,并同時(shí)處理子元素的測量和布局的過程。
- 繼承特定的 View,比如 TextView:擴(kuò)展某種已有View的功能,比如TextView。容易實(shí)現(xiàn),不需要自己支持wrap_content和padding。(對(duì)現(xiàn)有的控件進(jìn)行拓展)
- 繼承特定的 ViewGroup(比如 LinearLayout):與第二種的區(qū)別在于不用自己處理ViewGroup的測量和布局,第二種更接近底層。(通過組合來創(chuàng)建復(fù)合控件)
2. 自定義 View 須知
- 讓 View 支持 wrap_content;
- 如果有必要,讓你的 View 支持 padding;
- 盡量不要在 View 中使用 Handler,沒有必要,View 內(nèi)部提供了 post 系列的方法;
- View 中如果有線程或者動(dòng)畫,需要繼續(xù)停止,參考View#onDetachedFromWindow;
- View 帶有滑動(dòng)嵌套情形時(shí),需要處理好滑動(dòng)沖突;
3. 自定義 View 的思想
- 首先要掌握基本功,比如 View 的彈性滑動(dòng)、滑動(dòng)沖突、繪制原理等,這些都是自定義 View 所必須的。
- 熟練掌握基本功后,在面對(duì)新的自定義 View 時(shí),要能夠?qū)ζ浞诸惒⑦x擇合適的實(shí)現(xiàn)思路。
- 另外平時(shí)還要多積累一些自定義 View 相關(guān)的經(jīng)驗(yàn),并逐漸做到融會(huì)貫通。
4. 為什么自定義控件
- 特定的顯示風(fēng)格。
- 處理特有的用戶風(fēng)格
- 優(yōu)化布局
- 封裝
5.如何自定義控件
- 自定義屬性的聲明與獲取。
- 測量onMeasure。
- 布局onLayout(viewgroup)
- 繪制onDraw
- onTouchEvent。
- onInterceptTouchEvent(ViewGroup)。
6. 自定義屬性聲明與獲取
- 分析需要的自定義屬性。
- 在res/values/attrs.xml定義聲明
- 在layout.xml方法中進(jìn)行使用。
- 在View的構(gòu)造方法中進(jìn)行獲取。

7. 自定義View的三要素
- Canvas:畫布,用于繪制View所要顯示的內(nèi)容,一般來自onDraw()函數(shù)的傳入。
- Paint :畫筆,用于繪制View所需要繪制的內(nèi)容,相當(dāng)于筆,在其內(nèi)部可以設(shè)置顏色,粗細(xì),是否實(shí)心等信息,一般通過new的方式獲取該類對(duì)象;
- Point:點(diǎn),用于確定View所需要繪制的內(nèi)容大小及位置等相關(guān)信息,當(dāng)然這里的Point不具有實(shí)際意義,也有可能是線段,矩形等,只不過大多數(shù)是通過點(diǎn)的組合關(guān)系確定而已;
問題1:onTouch()、onTouchEvent()和onClick()關(guān)系?
優(yōu)先度onTouch()>onTouchEvent()>onClick()。因此onTouchListener的onTouch()方法會(huì)先觸發(fā);如果onTouch()返回false才會(huì)接著觸發(fā)onTouchEvent(),同樣的,內(nèi)置諸如onClick()事件的實(shí)現(xiàn)等等都基于onTouchEvent();如果onTouch()返回true,這些事件將不會(huì)被觸發(fā)。
問題2:SurfaceView和View的區(qū)別?
SurfaceView是從View基類中派生出來的顯示類,它和View的區(qū)別有:
- View需要在UI線程對(duì)畫面進(jìn)行刷新,而SurfaceView可在子線程進(jìn)行頁面的刷新
- View適用于主動(dòng)更新的情況,而SurfaceView適用于被動(dòng)更新,如頻繁刷新,這是因?yàn)槿绻褂肰iew頻繁刷新會(huì)阻塞主線程,導(dǎo)致界面卡頓
- SurfaceView在底層已實(shí)現(xiàn)雙緩沖機(jī)制,而View沒有,因此SurfaceView更適用于需要頻繁刷新、刷新時(shí)數(shù)據(jù)處理量很大的頁面
問題3:invalidate()和postInvalidate()的區(qū)別?
invalidate()與postInvalidate()都用于刷新View,主要區(qū)別是invalidate()在主線程中調(diào)用,若在子線程中需要配合handler使用;而postInvalidate()可在子線程中直接調(diào)用。
問題4: requestLayout()和invalidate()的區(qū)別?
調(diào)用invalidate()只會(huì)執(zhí)行onDraw方法;調(diào)用requestLayout()只會(huì)執(zhí)行onMeasure方法和onLayout方法,并不會(huì)執(zhí)行onDraw方法。
所以當(dāng)我們進(jìn)行View更新時(shí),若僅View的顯示內(nèi)容發(fā)生改變,則只需調(diào)用invalidate方法;若View寬高和位置發(fā)生改變,則調(diào)用requestLayout方法;若兩者均發(fā)生改變,則需先調(diào)用requestLayout()再調(diào)用invalidate()。

問題5:Android中真實(shí)寬高getWidth和getMeasuredWidth的區(qū)別:哪個(gè)計(jì)算的是真實(shí)的寬?
getWidth():得到的是View在父Layout中布局好后的寬度值,如果沒有父布局,那么默認(rèn)的父布局就是整個(gè)屏幕。
getMeasuredWidth():得到的是最近一次調(diào)用measure()方法測量后得到的是View的寬度,它僅僅用在測量和Layout的計(jì)算中。所以此方法得到的是View的內(nèi)容占據(jù)的實(shí)際寬度。
總結(jié):
getWidth(): View在設(shè)定好布局后整個(gè)View的寬度。
getMeasuredWidth(): 對(duì)View上的內(nèi)容進(jìn)行測量后得到的View內(nèi)容占據(jù)的寬度,前提是你必須在父布局的onLayout()方法或者此View的onDraw()方法里調(diào)用measure(0,0);否則你得到的結(jié)果和getWidth()得到的結(jié)果是一樣的。
問題6:為什么Viewgroup的Measure過程和View的過程不一樣,還要自己重寫onMeasure()方法?
因?yàn)椴煌腣iewGroup子類(LinearLayout、RelativeLayout / 自定義ViewGroup子類等)具備不同的布局特性,這導(dǎo)致他們子View的測量方法各有不同。
總結(jié)一句話:View的measure過程的onMeasure()具有統(tǒng)一實(shí)現(xiàn),而ViewGroup則沒有。
本文參考資料:
- 《Android開發(fā)藝術(shù)探索》
- 《第一行代碼》
- http://www.itdecent.cn/p/718aa3c1a70b
- http://www.itdecent.cn/p/6ec82716a3ac