Android 座標系及動畫移動座標相關集合
Android座標系
說到Android座標系其實就是一個三維座標,Z軸向上,X軸向右,Y軸向下。這三維座標的點處理就能構成Android豐富的介面或者動畫等效果,所以Android座標系在整個Android介面中算是蓋樓房的尺寸草圖,下面我們就來看看這些相關的概念。
2-1 Android螢幕區域劃分
我們先看一副圖來了解一下Android螢幕的區域劃分如下:
Android螢幕的區域劃分
通過上圖我們可以很直觀的看到Android對於螢幕的劃分定義。下面我們就給出這些區域裡常用區域的一些座標或者度量方式。如下:
- //獲取螢幕區域的寬高等尺寸獲取
DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(metrics); int widthPixels = metrics.widthPixels; int heightPixels = metrics.heightPixels;
- //應用程式App區域寬高等尺寸獲取
Rect rect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
- //獲取狀態列高度
Rect rect= new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rectangle.top;
- //View佈局區域寬高等尺寸獲取
Rect rect = new Rect(); getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
特別注意:上面這些方法最好在Activity的onWindowFocusChanged ()方法或者之後調運,因為只有這時候才是真正的顯示OK,不懂的可以看我之前關於setContentView相關的部落格。
2-2 Android View絕對相對座標系
上面我們分析了Android螢幕的劃分,可以發現我們平時開發的重點其實都在關注View佈局區域,那麼下面我們就來細說一下View區域相關的各種座標系。先看下面這幅圖:
View區域相關的各種座標系
通過上圖我們可以很直觀的給出View一些座標相關的方法解釋,不過必須要明確的是上面這些方法必須要在layout之後才有效,如下:
View的靜態座標方法 | 解釋 |
---|---|
getLeft() | 返回View自身左邊到父佈局左邊的距離 |
getTop() | 返回View自身頂邊到父佈局頂邊的距離 |
getRight() | 返回View自身右邊到父佈局左邊的距離 |
getBottom() | 返回View自身底邊到父佈局頂邊的距離 |
getX() | 返回值為getLeft()+getTranslationX(),當setTranslationX()時getLeft()不變,getX()變。 |
getY() | 返回值為getTop()+getTranslationY(),當setTranslationY()時getTop()不變,getY()變。 |
同時也可以看見上圖中給出了手指觸控式螢幕幕時MotionEvent提供的一些方法解釋,如下:
MotionEvent座標方法 | 解釋 |
---|---|
getX() | 當前觸控事件距離當前View左邊的距離 |
getY() | 當前觸控事件距離當前View頂邊的距離 |
getRawX() | 當前觸控事件距離整個螢幕左邊的距離 |
getRawY() | 當前觸控事件距離整個螢幕頂邊的距離 |
上面就解釋了你在很多程式碼中看見各種getXXX方法進行數學邏輯運算判斷的含義。不過上面只是說了一些相對靜止的Android座標點關係,下面我們來看看幾個和上面方法緊密相關的View方法。如下:
View寬高方法 | 解釋 |
---|---|
getWidth() | layout後有效,返回值是mRight-mLeft,一般會參考measure的寬度(measure可能沒用),但不是必須的。 |
getHeight() | layout後有效,返回值是mBottom-mTop,一般會參考measure的高度(measure可能沒用),但不是必須的。 |
getMeasuredWidth() | 返回measure過程得到的mMeasuredWidth值,供layout參考,或許沒用。 |
getMeasuredHeight() | 返回measure過程得到的mMeasuredHeight值,供layout參考,或許沒用。 |
上面解釋了自定義View時各種獲取寬高的一些含義,下面我們再來看看關於View獲取螢幕中位置的一些方法,不過這些方法需要在Activity的onWindowFocusChanged ()方法之後才能使用。如下圖:
這裡寫圖片描述
下面我們就給出上面這幅圖涉及的View的一些座標方法的結果(結果採用使用方法返回的實際座標,不依賴上面實際絕對座標轉換,上面絕對座標只是為了說明例子中的位置而已),如下:
View的方法 | 上圖View1結果 | 上圖View2結果 | 結論描述 |
---|---|---|---|
getLocalVisibleRect() | (0,0 - 410, 100) | (0, 0 - 410, 470) | 獲取View自身可見的座標區域,座標以自己的左上角為原點(0,0),另一點為可見區域右下角相對自己(0,0)點的座標,其實View2當前height為550,可見height為470。 |
getGlobalVisibleRect() | (30, 100 - 440, 200) | (30, 250 - 440, 720) | 獲取View在螢幕絕對座標系中的可視區域,座標以螢幕左上角為原點(0,0),另一個點為可見區域右下角相對螢幕原點(0,0)點的座標。 |
getLocationOnScreen() | (30, 100) | (30, 250) | 座標是相對整個螢幕而言,Y座標為View左上角到螢幕頂部的距離。 |
getLocationInWindow() | (30, 100) | (30, 250) | 如果為普通Activity則Y座標為View左上角到螢幕頂部(此時Window與螢幕一樣大);如果為對話方塊式的Activity則Y座標為當前Dialog模式Activity的標題欄頂部到View左上角的距離。 |
到此常用的相關View的靜態座標獲取處理的方法和含義都已經敘述完了,下面我們看看動態的一些解釋(所謂動靜只是我個人稱呼而已)。
2-3 Android View動畫相關座標系
其實在我們使用動畫時,尤其是補間動畫時,你會發現其中涉及很多座標引數,一會兒為相對的,一會兒為絕對的,你可能會各種蒙圈。那麼不妨看下《Android應用開發之所有動畫使用詳解 》這篇部落格,這裡面詳細介紹了關於Android動畫相關的座標系統,這裡不再累贅敘述。
2-4 Android View滑動相關座標系
關於View提供的與座標息息相關的另一組常用的重要方法就是滾動或者滑動相關的,下面我們給出相關的解釋(特別注意:View的scrollTo()和scrollBy()是用於滑動View中的內容,而不是改變View的位置;改變View在螢幕中的位置可以使用offsetLeftAndRight()和offsetTopAndBottom()方法,他會導致getLeft()等值改變。),如下:
View的滑動方法| 效果及描述
-:---|:---
offsetLeftAndRight(int offset)|水平方向挪動View,offset為正則x軸正向移動,移動的是整個View,getLeft()會變的,自定義View很有用。
offsetTopAndBottom(int offset)|垂直方向挪動View,offset為正則y軸正向移動,移動的是整個View,getTop()會變的,自定義View很有用。
scrollTo(int x, int y)|將View中內容(不是整個View)滑動到相應的位置,參考座標原點為ParentView左上角,x,y為正則向xy軸反方向移動,反之同理。
scrollBy(int x, int y)|在scrollTo()的基礎上繼續滑動xy。
setScrollX(int value)|實質為scrollTo(),只是只改變Y軸滑動。
setScrollY(int value)|實質為scrollTo(),只是只改變X軸滑動。
getScrollX()/getScrollY()|獲取當前滑動位置偏移量。
關於Android View的scrollBy()和scrollTo()引數傳遞正數卻向座標系負方向移動的特性可能很多人都有疑惑,甚至是死記結論,這裡我們簡單給出產生這種特性的真實原因—-原始碼分析,如下:
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
View的該方法註釋裡明確說明了調運他會觸發onScrollChanged()和invalidated()方法,那我們就將矛頭轉向invalidated()方法觸發的draw()過程,draw()過程中最終其實會觸發下面的invalidate()方法,如下:
public void invalidate(int l, int t, int r, int b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
//scroller時為何引數和座標反向的真實原因
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}
核心就在這裡,相信不用我解釋大家也知道咋回事了,自行腦補。
scrollTo()和scrollBy()方法特別注意:如果你給一個ViewGroup呼叫scrollTo()方法滾動的是ViewGroup裡面的內容,如果想滾動一個ViewGroup則再給他巢狀一個外層,滾動外層即可。
3 View中還有一些其他與座標獲取相關的方法
關於view獲取自身座標的方法和點選事件中座標的獲取,網上也有一些部落格,寫的不是很完整,現在系統的來講一下。
其實只要把下面這張圖看明白就沒問題了。
View中還有一些其他與座標獲取相關的方法
涉及到的方法一共有下面幾個:
- view獲取自身座標:getLeft(),getTop(),getRight(),getBottom()
- view獲取自身寬高:getHeight(),getWidth()
- motionEvent獲取座標:getX(),getY(),getRawX(),getRawY()
首先是view的幾個方法,
獲取自身的寬高的這兩個方法很清楚,不用多說,獲取座標的這幾個就有點混亂了。
根據上面的圖應該會比較容易明白,圖中螢幕上放了一個ViewGroup佈局,裡面有個View控制元件
getTop:獲取到的,是view自身的頂邊到其父佈局頂邊的距離
getLeft:獲取到的,是view自身的左邊到其父佈局左邊的距離
getRight:獲取到的,是view自身的右邊到其父佈局左邊的距離
getBottom:獲取到的,是view自身的底邊到其父佈局頂邊的距離
這些方法獲取到的資料可以用在什麼地方呢?比如要實現一個自定義的特殊佈局,像http://blog.csdn.net/singwhatiwanna/article/details/42614953 這裡要實現的是一個水波紋特效佈局,該佈局內的任何控制元件點選後都會出現波紋效果
那麼在點選了佈局內的一個控制元件之後,就要通過不斷重新整理佈局,去更新這個控制元件上面的波紋半徑,為了節省資源,每次重新整理佈局都時候不會整個佈局都重新整理,而只是通過
postInvalidateDelayed(INVALIDATE_DURATION, left, top, right, bottom);
在佈局的畫布上每次只去更新點選事件所點選的對應的控制元件的位置,那麼這裡就可以用view的那四個方法,分別獲取自身的四條邊對應的座標.從而讓佈局去重新整理重繪。
當然部落格中是使用絕對座標去計算的,因為這裡實現的是一個佈局,可能裡面還會巢狀另外的佈局,經過多次巢狀之後所獲取到的值,是相對於控制元件直接對應的父佈局(這個佈局有可能已經是我們重寫的佈局的子佈局了)的距離,這樣去重新整理的區域肯定是不準確的,所以部落格裡面使用相對螢幕的絕對座標計算需要重新整理的控制元件區域。
如果這裡自定義的不是佈局,而只是一個控制元件的話,就可以通過以上方法獲取到座標,然後要求自己所在的佈局去重繪這一區域就可以了。當然這只是一種思路,其實沒必要去要求佈局重繪,完全可以直接view自身重繪就可以了。
然後是motionEvent的方法:
getX():獲取點選事件相對控制元件左邊的x軸座標,即點選事件距離控制元件左邊的距離
getY():獲取點選事件相對控制元件頂邊的y軸座標,即點選事件距離控制元件頂邊的距離
getRawX():獲取點選事件相對整個螢幕左邊的x軸座標,即點選事件距離整個螢幕左邊的距離
getRawY():獲取點選事件相對整個螢幕頂邊的y軸座標,即點選事件距離整個螢幕頂邊的距離
這些方法可以用在什麼地方呢?
getRawX和getRawY在之前那篇部落格裡廣泛使用了,可以去那裡看用法,getX()和getY()這兩個方法在對view進行自定義的時候可能用的會比較多。
scrollTo()和scrollBy()方法特別注意:如果你給一個ViewGroup呼叫scrollTo()方法滾動的是ViewGroup裡面的內容,如果想滾動一個ViewGroup則再給他巢狀一個外層,滾動外層即可。
1.view.getTranslationX計算的是該view的偏移量。初始值為0,向左偏移值為負,向右偏移值為正。
2.view.getX相當於該view距離父容器左邊緣的距離,等於getLeft+getTranslationX。
android view的多種移動方式
目前為止大致有這幾種方法可以移動view:
**1、setTranslationX/Y
2、scrollTo/scrollBy
3、offsetTopAndBottom/offsetLeftAndRight
4、平移動畫
5、設定margin
**
主要是驗證一些屬性:
1、getX()、getY()
2、getScrollX() 、getScrollY()
3、getTranslationX() 、getTranslationY()
4、getLeft()、 getTop()、 getRight()、 getBottom()(座標位置是否改變)
5、點選事件觸發區域是否改變
6、是否會影響同層級的其他view的位置
7、超過父View是否繪製
現在主要把他們用一張表列出來:
稍微整理一下他們各自特點:
setTranslationX/Y
- getX getY 會變
- getTranslationXY會變
- 點選事件的位置也變了但是不會超過父佈局
- 會超過邊界到同級View的區域去(被覆蓋或者覆蓋別人)
- 這個方法的底層實現主要是通過metrix矩陣變換來的,座標位置沒有改變(跟offset不同,它是通過座標位置改變)
scrollTo/scrollBy
- getScrollXY 會變
- 點選事件還是在原位置 (跟動畫類似)
- 但是內容區域變了(如果超出自己的區域 就顯示不出來)
- 它只是內容區域的移動,本身view是不移動的
- scrollBy的x y 是相對移動的值
- scrollTo的x y 是絕對移動的值
offsetTopAndBottom/offsetLeftAndRight
- 上下左右座標會變 (主要是通過座標位置的改變產生移動效果)
- getXY會變
- 點選事件的位置也變了
- 會超過邊界到別人的區域去(被覆蓋或者覆蓋別人)
- 它的offY是相對移動的值
平移動畫
- 點選事件還是在原位置
- 如果setFillAfter位置保留 但是其他任何座標位置沒有改變 再次點選從原位置重新開始移動
設定margin
- 如果父View為wrap的話,設定margin可以移動,但是可能會對同級view造成影響(比如在linear中或者relative中有關聯關係)
下面是驗證過程:(前方高能,多圖預警!!!!!最重要的東西都羅列在前面了,沒時間不用往下看了)
預設情況log
setTranslationXY:
指定了父佈局
不能超過父佈局,會顯示不出來
會超過邊界到同層view的區域去(被覆蓋或者覆蓋別人)
- getX getY 會變
- getTranslationXY會變
- 點選事件的位置也變了但是不會超過父佈局
- 會超過邊界到同層view的區域去(被覆蓋或者覆蓋別人--取決於先後順序)
scrollBy:
offsetTopAndBottom offsetLeftAndRight:
動畫+setFillAfter(true):
margin:
組合
比如先多點幾次 offset ,然後再margin會立馬回到(原位置+margin)後的狀態
說明:margin的平移效果是以view在父View中的位置和margin值決定的,是父View通過計算margin值之後,重新給你排的位置,實現的移動。當我們設定margin之後,會觸發requestLayout,所以父VIew又重新給它排了位置。
如果,我先offset幾次,然後再點選動畫,動畫會在原來的基礎上,繼續平移。
說明:動畫不是根據位置來移動的,可能是根據一個metrix的矩陣變換來實現平移的(請指正)
如果,先scrollBy,然後再動畫、offset和其他移動方法,
說明:其他的平移方法,都是對於view本身在做移動,而不像scrollBy只是對其內容進行平移
先理解一個概念:
事件:在android中,點選螢幕是時,產生的長按,點選,滑動,雙擊,多指操作等,構成了android中的事件響應。
如:ACTION_DOWN
ACTION_MOVE
ACTION_UP
所有的操作事件首先必須執行的是按下操作(ACTION_DOWN),之後所有的操作都是以按下操作作為前提,當按下操作完成後,接下來可能是一段移動(ACTION_MOVE)然後擡起(ACTION_UP),或者是按下操作執行完成後沒有移動就直接擡起。這一系列的動作在Android中都可以進行控制。
檢視:android中的所有檢視都繼承於View,ViewGroup也繼承View。如圖:
先附上自己的理解到的事件分發概要框架有些細節木有寫出來,(先不要看,跳過這個私人思維導圖。建議看完下面全文再來看這個不夠清晰的私人思維導圖):
給出一個清晰的思維導圖大神部落格:清晰的事件攔截圖
下面是事件分發的總結:
參考自:大神部落格
1、ViewGroup的事件分發機制
從上層來分析(不去搞Linux核心,硬體),與使用者直接進行互動的是activity,所以,當用戶點選螢幕時,發生了啥?
看了大神部落格,是執行分發事件的方法,dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果是按下狀態就呼叫onUserInteraction()方法,onUserInteraction()方法
//是個空的方法, 我們直接跳過這裡看下面的實現
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//getWindow().superDispatchTouchEvent(ev)返回false,這個事件就交給Activity
//來處理, Activity的onTouchEvent()方法直接返回了false
return onTouchEvent(ev);
}
這個方法中我們還是比較關心getWindow()的superDispatchTouchEvent()方法,getWindow()返回當前Activity的頂層視窗Window物件,我們直接看Window API的superDispatchTouchEvent()方法
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
這個是個抽象方法,所以我們直接找到其子類來看看superDispatchTouchEvent()方法的具體邏輯實現,Window的唯一子類是PhoneWindow,我們就看看PhoneWindow的superDispatchTouchEvent()方法
public boolean superDispatchTouchEvent(KeyEvent event) {
return mDecor.superDispatcTouchEvent(event);
}
裡面直接呼叫DecorView類的superDispatchTouchEvent()方法,或許很多人不瞭解DecorView這個類,DecorView是PhoneWindow的一個final的內部類並且繼承FrameLayout的,也是Window介面的最頂層的View物件。耐心看下去
先新建一個專案,取名AndroidTouchEvent,然後直接用模擬器執行專案, MainActivity的佈局檔案為
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:text="@string/hello_world" />
</RelativeLayout>
利用hierarchyviewer工具來檢視下MainActivity的View的層次結構,如下圖
我們看到最頂層就是PhoneWindow$DecorView,接著DecorView下面有一個LinearLayout, LinearLayout下面有兩個FrameLayout
上面那個FrameLayout是用來顯示標題欄的,這個Demo中是一個TextView,當然我們還可以定製我們的標題欄,利用getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,R.layout.XXX); xxx就是我們自定義標題欄的佈局XML檔案
下面的FrameLayout是用來裝載ContentView的,也就是我們在Activity中利用setContentView()方法設定的View,現在我們知道了,原來我們利用setContentView()設定Activity的View的外面還嵌套了這麼多的東西
我們來理清下思路,Activity的最頂層窗體是PhoneWindow,而PhoneWindow的最頂層View是DecorView,接下來我們就看DecorView類的superDispatchTouchEvent()方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
在裡面呼叫了父類FrameLayout的dispatchTouchEvent()方法,而FrameLayout(繼承於ViewGroup)中並沒有dispatchTouchEvent()方法,所以我們直接看ViewGroup的dispatchTouchEvent()方法
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
//這個值預設是false, 然後我們可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法
//來改變disallowIntercept的值
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//這裡是ACTION_DOWN的處理邏輯
if (action == MotionEvent.ACTION_DOWN) {
//清除mMotionTarget, 每次ACTION_DOWN都很設定mMotionTarget為null
if (mMotionTarget != null) {
mMotionTarget = null;
}
//disallowIntercept預設是false, 就看ViewGroup的onInterceptTouchEvent()方法
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
ev.setAction(MotionEvent.ACTION_DOWN);
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
//遍歷其子View
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
//如果該子View是VISIBLE或者該子View正在執行動畫, 表示該View才
//可以接受到Touch事件
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
//獲取子View的位置範圍
child.getHitRect(frame);
//如Touch到螢幕上的點在該子View上面
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
//呼叫該子View的dispatchTouchEvent()方法
if (child.dispatchTouchEvent(ev)) {
// 如果child.dispatchTouchEvent(ev)返回true表示
//該事件被消費了,設定mMotionTarget為該子View
mMotionTarget = child;
//直接返回true
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
//判斷是否為ACTION_UP或者ACTION_CANCEL
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
//如果是ACTION_UP或者ACTION_CANCEL, 將disallowIntercept設定為預設的false
//假如我們呼叫了requestDisallowInterceptTouchEvent()方法來設定disallowIntercept為true
//當我們擡起手指或者取消Touch事件的時候要將disallowIntercept重置為false
//所以說上面的disallowIntercept預設在我們每次ACTION_DOWN的時候都是false
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
//mMotionTarget為null意味著沒有找到消費Touch事件的View, 所以我們需要呼叫ViewGroup父類的
//dispatchTouchEvent()方法,也就是View的dispatchTouchEvent()方法
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
//這個if裡面的程式碼ACTION_DOWN不會執行,只有ACTION_MOVE
//ACTION_UP才會走到這裡, 假如在ACTION_MOVE或者ACTION_UP攔截的
//Touch事件, 將ACTION_CANCEL派發給target,然後直接返回true
//表示消費了此Touch事件
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
//如果沒有攔截ACTION_MOVE, ACTION_DOWN的話,直接將Touch事件派發給target
return target.dispatchTouchEvent(ev);
}
這個方法相對來說還是蠻長,不過所有的邏輯都寫在一起,看起來比較方便,接下來我們就具體來分析一下
我們點選螢幕上面的TextView來看看Touch是如何分發的,先看看ACTION_DOWN
在DecorView這一層會直接呼叫ViewGroup的dispatchTouchEvent(), 先看18行,每次ACTION_DOWN都會將mMotionTarget設定為null, mMotionTarget是什麼?我們先不管,繼續看程式碼,走到25行, disallowIntercept預設為false,我們再看ViewGroup的onInterceptTouchEvent()方法
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
直接返回false, 繼續往下看,迴圈遍歷DecorView裡面的Child,從上面的MainActivity的層次結構圖我們可以看出,DecorView裡面只有一個Child那就是LinearLayout, 第43行判斷Touch的位置在不在LinnearLayout上面,這是毫無疑問的,所以直接跳到51行, 呼叫LinearLayout的dispatchTouchEvent()方法,LinearLayout也沒有dispatchTouchEvent()這個方法,所以也是呼叫ViewGroup的dispatchTouchEvent()方法,所以這個方法卡在51行沒有繼續下去,而是去先執行LinearLayout的dispatchTouchEvent()
LinearLayout呼叫dispatchTouchEvent()的邏輯跟DecorView是一樣的,所以也是遍歷LinearLayout的兩個FrameLayout,判斷Touch的是哪個FrameLayout,很明顯是下面那個,呼叫下面那個FrameLayout的dispatchTouchEvent(), 所以LinearLayout的dispatchTouchEvent()卡在51也沒繼續下去
繼續呼叫FrameLayout的dispatchTouchEvent()方法,和上面一樣的邏輯,下面的FrameLayout也只有一個Child,就是RelativeLayout,FrameLayout的dispatchTouchEvent()繼續卡在51行,先執行RelativeLayout的dispatchTouchEvent()方法
執行RelativeLayout的dispatchTouchEvent()方法邏輯還是一樣的,迴圈遍歷 RelativeLayout裡面的孩子,裡面只有一個TextView, 所以這裡就呼叫TextView的dispatchTouchEvent(), TextView並沒有dispatchTouchEvent()這個方法,於是找TextView的父類View,在看View的dispatchTouchEvent()的方法之前,我們先理清下上面這些ViewGroup執行dispatchTouchEvent()的思路,我畫了一張圖幫大家理清下(這裡沒有畫出onInterceptTouchEvent()方法)
這就是ViewGroup的事件分發機制,會一層一層地把事件一直分發下去,直到目標View。當然,我們也可以在中間的ViewGroup把事件消費掉(重寫某些方法就可以,比如onTouchListener,onTouchEvent。返回ture即可。看完下面即可理解),這樣事件的分發就會終止。
2、View的Touch事件分發機制
參考自:大神部落格
郭神部落格
我們都知道,按鈕的點選事件的設定。如下
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});
如果想給這個按鈕再新增一個touch事件,只需要呼叫:
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " + event.getAction());
return false;
}
});
onTouch方法裡能做的事情比onClick要多一些,比如判斷手指按下、擡起、移動等事件。那麼如果我兩個事件都註冊了,哪一個會先執行呢?我們來試一下就知道了,執行程式點選按鈕,列印結果如下:
可以看到,onTouch是優先於onClick執行的,並且onTouch執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(你還可能會有多次ACTION_MOVE的執行,如果你手抖了一下)。因此事件傳遞的順序是先經過onTouch,再傳遞到onClick。
細心的朋友應該可以注意到,onTouch方法是有返回值的,這裡我們返回的是false,如果我們嘗試把onTouch方法裡的返回值改成true,再執行一次,結果如下:
我們發現,onClick方法不再執行了!為什麼會這樣呢?你可以先理解成onTouch方法返回true就認為這個事件被onTouch消費掉了,因而不會再繼續向下傳遞。
我們發現,onClick方法不再執行了!為什麼會這樣呢?你可以先理解成onTouch方法返回true就認為這個事件被onTouch消費掉了,因而不會再繼續向下傳遞。
然後我們來看一下View中dispatchTouchEvent方法的原始碼:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
在這個方法裡面,先進行了一個判斷
第一個條件mOnTouchListener就是我們呼叫View的setTouchListener()方法設定的
第二個條件是判斷View是否為enabled的, View一般都是enabled,除非你手動設定為disabled
第三個條件就是OnTouchListener介面的onTouch()方法的返回值了,如果呼叫了setTouchListener()設定OnTouchListener,並且onTouch()方法返回true,View的dispatchTouchEvent()方法就直接返回true,否則就執行View的onTouchEvent() 並返回View的onTouchEvent()的值
現在你瞭解了View的onTouchEvent()方法和onTouch()的關係了吧,為什麼Android提供了處理Touch事件onTouchEvent()方法還要增加一個OnTouchListener介面呢?我覺得OnTouchListener介面是對處理Touch事件的遮蔽和擴充套件作用吧,遮蔽作用就是事件必須先到達onTouch,由是否在onTouch中消費掉事件(即返回true或者false來決定是否執行onTouchEvent),我就說下擴充套件吧,比如我們要列印View的Touch的點的座標,我們可以自定義一個View如下
public class CustomView extends View {
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("tag", "X的座標 = " + event.getX() + " Y的座標 = " + event.getY());
return super.onTouchEvent(event);
}
}
也可以直接對View設定OnTouchListener介面,在return的時候呼叫下v.onTouchEvent()
view.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("tag", "X的座標 = " + event.getX() + " Y的座標 = " + event.getY());
return v.onTouchEvent(event);
}
});
我們再看View的onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
//如果設定了Touch代理,就交給代理來處理,mTouchDelegate預設是null
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//如果View是clickable或者longClickable的onTouchEvent就返回true, 否則返回false
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
//當手指在View上面滑動超過View的邊界,
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
這個方法也是比較長的,我們先看第4行,如果一個View是disabled, 並且該View是Clickable或者longClickable, onTouchEvent()就不執行下面的程式碼邏輯直接返回true, 表示該View就一直消費Touch事件,如果一個enabled的View,並且是clickable或者longClickable的,onTouchEvent()會執行下面的程式碼邏輯並返回true,綜上,一個clickable或者longclickable的View是一直消費Touch事件的,而一般的View既不是clickable也不是longclickable的(即不會消費Touch事件,只會執行ACTION_DOWN而不會執行ACTION_MOVE和ACTION_UP) Button是clickable的,可以消費Touch事件,但是我們可以通過setClickable()和setLongClickable()來設定View是否為clickable和longClickable。當然還可以通過重寫View的onTouchEvent()方法來控制Touch事件的消費與否
我們在看57行的ACTION_DOWN, 新建一個CheckForTap,我們看看CheckForTap是什麼
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
&nb