簡介:在Android的世界中View是全部控件的基類,其中也包括ViewGroup在內,ViewGroup是表明着控件的集合,其中能夠包含多個View控件。 從某種角度上來說Android中的控件能夠分爲兩大類:View與ViewGroup。經過ViewGroup,整個界面的控件造成了一個樹形結構,上層的控件要負責測量與繪製下層的控件,並傳遞交互事件。 在每棵控件樹的頂部都存在着一個ViewParent對象,它是整棵控件樹的核心所在,全部的交互管理事件都由它來統一調度和分配,從而對整個視圖進行總體控制。android
一.View事件體系canvas
1.View的位置參數bash
a.Android座標系:以屏幕的左上角爲座標原點,向右爲x軸增大方向,向下爲y軸增大方向。ide
b.View的位置由它的四個頂點決定,分別對應View的四個屬性:top、left、right、bottom。其中left是左上角的橫座標,right是右下角的橫座標,top是左上角的縱座標,bottom是右下角的縱座標。注意這些座標是相對於view父容器而言,是一種相對的座標。具體關係見下圖:佈局
所以,View的寬高和座標關係:width = right - left,height = top - bottom。post
可利用View的get方法獲取上述屬性,如:學習
c.從android3.0開始,View增長了額外幾個參數:x,y,translationX、translationY。其中x和y是View左上角的座標,translationX和translationY是View左上角相對於父容器的偏移量,它們默認值是0。這些參數也是相對於View父容器。具體關係見下圖:優化
- 存在關係:x = left + translationX,y = top + translationY
- 因而可知,x和left不一樣體如今:left是View的初始座標,在繪製完畢後就不會再改變;而x是View偏移後的實時座標,是實際座標。y和top的區別同理。
相似地,安卓也提供了相應的get/set方法。須要注意的是,在onCreate()方法裏沒法獲取到View的座標參數,這是由於此時View還未開始繪製,所有座標參數將都是0。動畫
推薦閱讀:Android應用座標系統全面詳解ui
2.觸控系列
a.MotionEvent:是手指觸摸屏幕鎖產生的一系列事件。典型事件有:
事件列:從手指接觸屏幕至手指離開屏幕,這個過程產生的一系列事件 任何事件列都是以DOWN事件開始,UP事件結束,中間有無數的MOVE事件。如圖:
經過MotionEvent 對象能夠獲得觸摸事件的x、y座標。其中經過getX()、getY()可獲取相對於當前view左上角的x、y座標;經過getRawX()、getRawY()可獲取相對於手機屏幕左上角的x,y座標。具體關係見下圖:
b.TouchSlop:系統所能識別的被認爲是滑動的最小距離。即當手指在屏幕上滑動時,若是兩次滑動之間的距離小於這個常量,那麼系統就不認爲你是在進行滑動操做。
該常量和設備有關,可用它來判斷用戶的滑動是否達到閾值,獲取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()。
c.VelocityTracker:速度追蹤,用於追蹤手指在滑動過程當中的速度,包括水平和豎直方向的速度。
使用過程:首先在view的onTouchEvent方法中追蹤當前單擊事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();//實例化一個VelocityTracker 對象
velocityTracker.addMovement(event);//添加追蹤事件
複製代碼
接着在ACTION_UP事件中獲取當前的速度。注意這裏計算的是1000ms時間間隔移動的像素值,假設像素是100,即速度是每秒100像素。另外,手指逆着座標系的正方向滑動,所產生的速度爲負值,順着正反向滑動,所產生的速度爲正值。
velocityTracker .computeCurrentVelocity(1000);//獲取速度前先計算速度,這裏計算的是在1000ms內
float xVelocity = velocityTracker .getXVelocity();//獲得的是1000ms內手指在水平方向從左向右滑過的像素數,即水平速度
float yVelocity = velocityTracker .getYVelocity();//獲得的是1000ms內手指在水平方向從上向下滑過的像素數,垂直速度
複製代碼
最後,當不須要使用它的時候,須要調用clear方法來重置並回收內存:
velocityTracker.clear();
velocityTracker.recycle();
複製代碼
推薦閱讀:Android經常使用觸控類分析:MotionEvent 、 ViewConfiguration、VelocityTracker
d.GestureDetector:手勢檢測,用於輔助檢測用戶的單擊、滑動、長按、雙擊等行爲。
使用過程:建立一個GestureDetecor對象並實現OnGestureListener接口,根據須要實現單擊等方法:
GestureDetector mGestureDetector = new GestureDetector(this);//實例化一個GestureDetector對象
mGestureDetector.setIsLongpressEnabled(false);// 解決長按屏幕後沒法拖動的現象
複製代碼
接着,接管目標view的onTouchEvent方法,在待監聽view的onTouchEvent方法中添加以下實現:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
複製代碼
而後,就能夠有選擇的實現OnGestureListener和OnDoubleTapListener中的方法了。
建議:若是隻是監聽滑動操做,建議在onTouchEvent中實現;若是要監聽雙擊這種行爲,則使用GestureDetector 。
推薦閱讀:Android手勢檢測——GestureDetector全面分析
3.滑動系列
a.實現View滑動三種辦法:
①經過View自己提供的scrollTo/scrollBy方法
- 二者區別:scrollBy是內部調用了scrollTo的,它是基於當前位置的相對滑動;而scrollTo是絕對滑動,所以若是利用相同輸入參數屢次調用scrollTo()方法,因爲View初始位置是不變只會出現一次View滾動的效果而不是屢次。
- 注意:二者都只能對view內容進行滑動,而不能使view自己滑動。
mScrollX和mScrollY分別表示View在X、Y方向的滾動距離。mScrollX:View的左邊緣減去View的內容的左邊緣;mScrollY:View的上邊緣減去View的內容的上邊緣。從右向左滑動,mScrollX爲正值,反之爲負值;從下往上滑動,mScrollY爲正值,反之爲負值。(更直觀感覺:查看下一張照片或者查看長圖時手指滑動方向爲正)
②經過動畫給View施加平移效果:主要經過改變View的translationX和translationY參數來實現。可用view動畫,也能夠採用屬性動畫,若是使用屬性動畫的話,爲了可以兼容3.0如下版本,須要採用開源動畫庫nineoldandroids。注意View動畫的View移動只是位置移動,並不能真正的改變view的位置,而屬性動畫能夠。
③經過改變View的LayoutParams使得View從新佈局:好比將一個View向右移動100像素,向右,只須要把它的marginLeft參數增大便可,代碼見下:
MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 請求從新對View進行measure、layout
複製代碼
三種方式對比:
- scrollTo/scrollBy:操做簡單,適合對view內容滑動。非平滑
- 動畫:操做簡單,主要適用於沒有交互的view和實現複雜的動畫效果
- 改變LayoutParams:操做稍微複雜,適用於有交互的view。非平滑
b.實現View彈性滑動三種方法:
①使用Scroller:
- 與scrollTo/scrollBy不一樣:scrollTo/scrollBy過程是瞬間完成的,非平滑;而Scroller則有過渡滑動的效果。
- 注意:Scoller自己沒法讓View彈性滑動,它須要和View的computerScroller方法配合使用。
Scroller慣用代碼:
Scroller scroller = new Scroller(mContext); //實例化一個Scroller對象
private void smoothScrollTo(int dstX, int dstY) {
int scrollX = getScrollX();//View的左邊緣到其內容左邊緣的距離
int scrollY = getScrollY();//View的上邊緣到其內容上邊緣的距離
int deltaX = dstX - scrollX;//x方向滑動的位移量
int deltaY = dstY - scrollY;//y方向滑動的位移量
scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //開始滑動
invalidate(); //刷新界面
}
@Override//計算一段時間間隔內偏移的距離,並返回是否滾動結束的標記
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurY());
postInvalidate();//經過不斷的重繪不斷的調用computeScroll方法
}
}
複製代碼
其中startScroll源碼以下,可見它並無進行實際的滑動操做,而是經過後續invalidate()方法去作滑動動做。
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;//滑動時間
mStartTime = AnimationUtils.currentAminationTimeMills();//開始時間
mStartX = startX;//滑動起點
mStartY = startY;//滑動起點
mFinalX = startX + dx;//滑動終點
mFinalY = startY + dy;//滑動終點
mDeltaX = dx;//滑動距離
mDeltaY = dy;//滑動距離
mDurationReciprocal = 1.0f / (float)mDuration;
}
複製代碼
- 具體過程:在MotionEvent.ACTION_UP事件觸發時調用startScroll方法->立刻調用invalidate/postInvalidate方法->會請求View重繪,致使View.draw方法被執行->會調用View.computeScroll方法,此方法是空實現,須要本身處理邏輯。具體邏輯是:先判斷computeScrollOffset,若爲true(表示滾動未結束),則執行scrollTo方法,它會再次調用postInvalidate,如此反覆執行,直到返回值爲false。如圖所示:
- 原理:Scroll的computeScrollOffset()根據時間的流逝動態計算一小段時間裏View滑動的距離,並獲得當前View位置,再經過scrollTo繼續滑動。即把一次滑動拆分紅無數次小距離滑動從而實現彈性滑動。
推薦閱讀: 站在源碼的肩膀上全解Scroller工做機制
②經過動畫:動畫自己就是一種漸近的過程,故可經過動畫來實現彈性滑動。方法是:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();//在100ms內使得View從原始位置向右平移100像素
複製代碼
③使用延時策略:經過發送一系列延時信息從而達到一種漸近式的效果,具體能夠經過Handler和View的postDelayed方法,也可以使用線程的sleep方法。
對彈性滑動完成總時間有精確要求的使用場景下,使用延時策略是一個不太合適的選擇。
推薦閱讀:View滑動與實現滑動的幾種方法
4.View事件分發機制
a.事件分發本質:就是對MotionEvent事件分發的過程。即當一個MotionEvent產生了之後,系統須要將這個點擊事件傳遞到一個具體的View上。(關於MotionEvent介紹見本篇2.a)
b.點擊事件的傳遞順序:Activity(Window) -> ViewGroup -> View
c.須要的三個主要方法:
dispatchTouchEvent:進行事件的分發(傳遞)。返回值是 boolean 類型,受當前onTouchEvent和下級view的dispatchTouchEvent影響
onInterceptTouchEvent:對事件進行攔截。該方法只在ViewGroup中有,View(不包含 ViewGroup)是沒有的。一旦攔截,則執行ViewGroup的onTouchEvent,在ViewGroup中處理事件,而不接着分發給View。且只調用一次,因此後面的事件都會交給ViewGroup處理。
onTouchEvent:進行事件處理。
事件分發過程圖:
- 事件分發是逐級下發的,目的是將事件傳遞給一個View。
- ViewGroup一旦攔截事件,就不往下分發,同時調用onTouchEvent處理事件。
推薦閱讀:Android事件分發機制詳解(源碼)
5.View滑動衝突
a.產生緣由:
b.可能場景:
c.處理規則:
d.解決方式:
//重寫父容器的攔截方法
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://對於ACTION_DOWN事件必須返回false,一旦攔截後續事件將不能傳遞給子View
intercepted = false;
break;
case MotionEvent.ACTION_MOVE://對於ACTION_MOVE事件根據須要決定是否攔截
if (父容器須要當前事件) {
intercepted = true;
} else {
intercepted = flase;
}
break;
}
case MotionEvent.ACTION_UP://對於ACTION_UP事件必須返回false,一旦攔截子View的onClick事件將不會觸發
intercepted = false;
break;
default : break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
複製代碼
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 (父容器須要此類點擊事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default :
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
複製代碼
除子容器須要作處理外,父容器也要默認攔截除了ACTION_DOWN之外的其餘事件,這樣當子容器調用parent.requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續攔截所需的事件。所以,父View須要重寫onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
複製代碼
內部攔截法要求父容器不能攔截ACTION_DOWN的緣由:因爲該事件並不受FLAG_DISALLOW_INTERCEPT(由requestDisallowInterceptTouchEvent方法設置)標記位控制,一旦ACTION_DOWN事件到來,該標記位會被重置。因此一旦父容器攔截了該事件,那麼全部的事件都不會傳遞給子View,內部攔截法也就失效了。
推薦閱讀:一文解決Android View滑動衝突
二.View工做原理
1.View工做流程:measure測量->layout佈局->draw繪製
具體過程:
- ViewRoot對應於ViewRootImpl類,它是鏈接WindowManager和DecorView的紐帶。
- View的繪製流程是從ViewRoot和performTraversals開始。
- performTraversals()依次調用performMeasure()、performLayout()和performDraw()三個方法,分別完成頂級 View的繪製。
- 其中,performMeasure()會調用measure(),measure()中又調用onMeasure(),實現對其全部子元素的measure過程,這樣就完成了一次measure過程;接着子元素會重複父容器的measure過程,如此反覆至完成整個View樹的遍歷。layout和draw同理。過程圖以下:
補充閱讀:瞭解ViewRoot和DecorView
a.measure過程:肯定測量寬高
先來理解MeasureSpec:
- 做用:經過寬測量值widthMeasureSpec和高測量值heightMeasureSpec決定View的大小
- 組成:一個32位int值,高2位表明SpecMode(測量模式),低30位表明SpecSize( 某種測量模式下的規格大小)。
- 三種模式:
- UNSPECIFIED:父容器不對View有任何限制,要多大有多大。經常使用於系統內部。
- EXACTLY(精確模式):父視圖爲子視圖指定一個確切的尺寸SpecSize。對應LyaoutParams中的match_parent或具體數值。
- AT_MOST(最大模式):父容器爲子視圖指定一個最大尺寸SpecSize,View的大小不能大於這個值。對應LayoutParams中的wrap_content。
- 決定因素:值由子View的佈局參數LayoutParams和父容器的MeasureSpec值共同決定。具體規則見下圖:
如今,分別討論兩種measure過程:
①View的measure:只有一個原始的View,經過measure()便可完成測量。過程圖見下:
從getDefaultSize()中能夠看出,直接繼承View的自定義View須要重寫onMeasure()並設置wrap_content時的自身大小,不然效果至關於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);
//分析模式,根據不一樣的模式來設置
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);
}
}
複製代碼
補充閱讀:爲何你的自定義View wrap_content不起做用
②ViewGroup的measure:除了完成ViewGroup自身的測量外,還會遍歷去調用全部子元素的measure方法。
ViewGroup中沒有重寫onMeasure(),而是提供measureChildren()。
圖片來源:自定義View Measure過程
b.layout過程:肯定View的最終寬高和四個頂點的位置
- 大體流程:從頂級View開始依次調用layout(),其中子View的layout()會調用setFrame()來設定本身的四個頂點(mLeft、mRight、mTop、mBottom),接着調用onLayout()來肯定其座標,注意該方法是空方法,由於不一樣的ViewGroup對其子View的佈局是不相同的。
圖片來源:自定義View Layout過程
c.draw過程:繪製到屏幕
繪製順序:
注意:Vew有一個特殊的方法setWillNotDraw(),該方法用於設置 WILL_NOT_DRAW 標記位(其做用是當一個View不須要繪製內容時,系統可進行相應優化)。默認狀況下View是沒有這個優化標誌的(設爲true)。
圖片來源:自定義View Draw過程
推薦閱讀:對View工做流程的理解(源碼)
2.自定義View
a.自定義View的類型:
b.特別提醒:
最後,由於自定義View內容很是多,這裏再也不展開。最重要的是實踐,就是如今帶着理論基礎開始實戰吧~
但願這篇文章對你有幫助~