前言
最近年底了,打算把自己的Android知識都整理一下。
Android技能書系列:
Android基礎知識
Android技能樹 — Android儲存路徑及IO操作小結
資料結構基礎知識
演算法基礎知識
這次是講View的事件體系。特別是不同情況下的事件分發,我會用很簡單的方式教會大家。
還是老樣子,先上腦圖,然後具體一塊塊詳細說明。
腦圖連結:View事件體系
我們通過具體案例來學習
View相關的基礎知識
比如我們現在的需求是這樣的:介面上有一個按鈕,我們的手指點選這個按鈕後滑動,這個按鈕可以跟著我們的手指一起滑動。(桌面的一些小的清理垃圾的懸浮窗的操作差不多,明白了吧)
具體實現可以看我以前寫過的文章,十分簡單: 小Demo大知識-控制Button移動來學Android座標
我們來分析,既然按鈕可以跟著我們手指滑動,我們肯定是不停告訴按鈕,當前你的位置是哪裡,既然涉及到一些基本知識點,比如View的位置引數等等。
View的位置引數
這裡我配上一張圖,更清楚的來說明這些獲取各自引數的值的說明:
(!!!!!這裡我多畫了getRawX和getRawY方法,View是沒有這二個方法的,請注意!!!!!)
(!!!!!這裡我多畫了getRawX和getRawY方法,View是沒有這二個方法的,請注意!!!!!)
(!!!!!這裡我多畫了getRawX和getRawY方法,View是沒有這二個方法的,請注意!!!!!)
看了這個圖,是不是馬上很清楚了。
注意點:
這裡要說明一個誤區,我面試一些初級水平安卓,我說ViewGroup裡面有個View,這個View的getLeft(),getTop(),getTop(),getBottom()
是什麼,讓他畫給我看下,有些人會給下面這個答案:
這是錯誤的答案,而且根據正確的描述圖,我們可以通過getLeft(),getTop(),getTop(),getBottom()
來獲取相應的View的寬高:
width = getRight() - getLeft();
height = getBottom() - getTop();
複製程式碼
View操作相關知識
MotinoEvent
MotionEvent是什麼,單獨問大家可能有點懵逼,我們來寫下我們平常經常寫的設定觸控的監聽方法:
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
複製程式碼
有沒有發現,裡面傳遞過來的引數就有MotionEvent
。
我們可以看到,MotionEvent是觸屏事件。當使用者觸控螢幕時將產生觸屏事件,事件相關細節(發生觸控的位置、時間、歷史記錄、手勢動作等)被封裝成MotionEvent物件。
具體的介紹真的很多,百度一搜一大把。要細講實在太多了。這裡不多介紹了。
特別提示!!!
很多人會把上面我們提到過的view.getX/Y()
和這裡的motionvent.getX/Y()
弄混。這裡是有差別的。我再畫個圖來明確下二者的區別。
所以區別是:
View的getX/Y()是指自己View的左上角相對於父View左上角的距離。MotionEvent的getX/Y()是指點選處離自己View的左上角的距離。
ps:所以面試官問你getX/Y()的時候,一定問清楚是問的哪個。!不然很容易回答錯誤。
TouchSlop
TouchSlop是系統所能識別出來的被認為滑動的最小的距離。如果你手指在螢幕上滑動的時候小於這個值,系統就認為你不是滑動。
VelocityTracker
滑動時候我們可能還要監聽速度,比如說我們的需求就是滑動的快和滑動慢,移動的最終距離不同等。這時候我們一定要知道當前使用者在N時間段內的速度到底是什麼。這時候我們就需要速度(Velocity)追蹤者(Tracker)。
GestureDetector
我們先來看看英文翻譯:
沒錯,既然你在螢幕上操作,你可能是劃來劃去,可能是單擊,可能是雙擊。很多情況。所以這個類就可以幫我們來監聽不同的操作。
ScaleGestureDetector
在GestureDetector前面新增了一個Scale。
那就明顯是比例的手勢監測,通俗來說就是放大縮小的手勢監測。
比如我們的需求是在檢視圖片的時候,可以二個手指放大縮小圖片,那我恩就可以用這個ScaleGestureDetector
來監測。十分方便。
附上我以前寫過的文章:圖片操作系列 —(1)手勢縮放圖片功能
View的事件分發機制
事件傳遞三個階段及事件處理的類
其實這二個算是基礎知識。
接下去我會用一個真實的例子帶你們更好的理解事件分發,如果講的不合理,可以提出來哦✧(≖ ◡ ≖✿)
舉個例子:
PS:(如果例子不適合,大家可以評論反饋。因為如果例子不適合反而誤導了讀者,反而是我的問題了。)
好比你們公司是一個軟體外包公司,現在有個客戶手點了一下滑鼠發給你們老闆一封郵件,說要開發這麼一個APP。你們老闆是不是會一層層的分發下去,老闆 ——> 主管 ——>開發人員。
額外提到點:
-
你們老闆收到了通知就是把這個任務分下去,不可能說第一反應先想想說我要不要把這個任務攔下來自己做,不要叫手下的人去做了(不然還請你們幹嘛,請了你們還要每次想著要不要自己做)。所以他沒有攔截功能,預設肯定不會去攔截,肯定第一反應就是直接給手下。
-
主管都是有權利把任務攔下來的,不給手下的人去做,可以自己處理,畢竟主管不只是就分配下任務就夠了,這麼簡單我也想去做主管,可能因為手下都有任務在做,忙不過來的時候,主管會自己去做一些開發任務。
-
最底層的開發人員,沒有攔截功能,因為任務分到你這裡了。你還能再給誰呢,攔了也是你做,不攔你又沒有下級可以給背鍋,還是你做。
所以對比下知道是不是發現跟我們的Activity,ViewGroup,View
很像:
PS:當收到觸控事件傳遞到某個層的時候,這個的dispactchEvent會被呼叫。(相當於上面接受到通知任務的時候會執行這個方法)
老闆 - Activity: 有收到通知的能力,所以會呼叫dispatchTouchEvent(),然後因為他可以去通知主管,所以是
客戶通知老闆你有專案了。老闆的dispatchEvent()會被呼叫。
老闆.dispatchTouchEvent(){
//老闆先通知主管去處理,
如果主管給的回覆是:老闆你不用管接下去的事。我們會處理的。
if(主管.dispatchTouchEvent()){
return true;//就直接結束了。
}
//手下的人說這個app開發不了,只能老闆出馬做事(跟客戶去溝通去)
return 老闆.onTouchEvent();
}
複製程式碼
所以只有dispatchEvent()
和onTouchEvent()
方法。
主管 - ViewGroup
老闆通知了主管有個app要你們部門去開發。主管的dispatchTouchEvent()會被呼叫
主管.dispatchTouchEvent(){
//主管把這個活攔下來準備自己來開發這個app
if(主管.interceptTouchEvent()){
return 主管.onTouchEvent();//主管也有做事能力
}else{
//主管不攔截,主管也可以去通知開發人員,
//如果開發人員回饋說主管你別管了。我們這個app能做好
if(開發人員.dispatchTouchEvent()){
return true; //直接就結束了。
}else{
//如果手下的開發人員也反饋給主管說搞不定。
//就只能主管自己出來做事了。
return 主管.onTouchEvent();
}
}
}
複製程式碼
所以有dispatchTouchEvent()、interceptTouchEvent()、onTouchEvent()
。
開發人員 - View
主管通知了開發人員有個app要開發。開發人員的dispatchTouchEvent()會被呼叫
開發人員.dispatchTouchEvent(){
return 開發人員.onTouchEvent();
}
複製程式碼
所以有dispatchTouchEvent()、onTouchEvent()
。
不同返回值導致不同的流程
我知道大家一定看到過類似下面的這種圖:
很多人都會死記硬背的去記下來,說return true/false/super等不同情況下不同的呼叫流程。但是這樣其實很不好記住的。很多人會問我是怎麼記住的,我就是用虛擬碼來幫忙記住,什麼事虛擬碼,上面那種表達方式就是虛擬碼。我們現在正是來看具體的虛擬碼。
Activity的真實程式碼:
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction == MotionEvent.ACTION_DOWN){
onUserInteraction();
}
/**
呼叫window的superDispatchTouchEvent方法,
然後再呼叫下面的ViewGroup(DecorView)的dispatchTouchEvent()方法。
我們就直接這麼想,這裡就Activity通知了ViewGroup的dispatchTouchEvent方法。
1.如果這裡getWindow.superDispatchTouchEvent()返回了true,
這時候就會執行return true語句。
2.如果這裡getWindow.superDispatchTouchEvent()返回了false,
這時候就會執行return onTouchEvent(ev);這句,
所以只有當上面的if語句返回false,
才有機會呼叫Activity自己的onTouchEvent()方法。
*/
if(getWindow.superDispatchTouchEvent()){
return true;
}
return onTouchEvent(ev);
}
複製程式碼
所以很多人會所你重寫Activity的dispatchTouchEvent()方法,返回true/false,都直接結束了事件。返回super才能正常分發,這個說法是不合理的。實際應該這麼描述:
預設重寫Activity的dispatchTouchEvent
方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
/**
其實是呼叫了super.dispatchTouchEvent方法,
才會呼叫上面我們貼出的Activity的dispatchTouchEvent方法,
才能繼續把事件分發下去。
*/
return super.dispatchTouchEvent(ev);
}
複製程式碼
而大家通俗上說返回true/false
就事件結束,是因為沒有呼叫了super.dispatchTouchEvent(ev);
。所以就不會分發下去,也就事件結束了。
那假如我這麼寫呢:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
super.dispatchTouchEvent(ev);
return true/false;
}
複製程式碼
沒錯,事件也是一樣會分發下去。子View的方法也會被呼叫,而不會說直接結束了。
ViewGroup
因為上面我們已經說過了getWindow.superDispatchTouchEvent()
可以直接理解為是去呼叫了ViewGroup的dispatchTouchEvent()
;
ViewGroup的虛擬碼:
public boolean dispatchTouchEvent(MotionEvent ev){
/**
如果ViewGroup做了攔截,
則直接返回了ViewGroup的onTouchEvent()事件的結果。
*/
if(onInterceptTouchEvent(ev)){
return onTouchEvent(ev);
}else{
/**
如果ViewGroup不做攔截,則先分發給child,
看他們的反應,他們都不接受,則一定會返回false,
則只能ViewGroup自己去執行自己的onTouchEvent(ev);
*/
if(child.dispatchTouchEvent(ev)){
return true;
}else{
return onTouchEvent(ev);
}
}
}
複製程式碼
View的虛擬碼:
public boolean dispatchTouchEvent(MotionEvent ev){
/**
View 就返回自己的onTouchEvent()
*/
return onTouchEvent();
}
複製程式碼
可能很多人還是說我看了這些程式碼還是不懂啊,我連起來給你看,你就理解了。
這樣,在不同情況下,返回不同的false/true,執行順序就知道了。
額外補充:
《補充1》:
當然其實還有更復雜的情況,我們知道有ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL
等,比如我們直接ViewGroup攔截Down事件,或者Down事件傳遞到了View後,我們在MOVE處再攔截,都會執行不同的:
- DOWN事件被傳遞給ViewGroup的onInterceptTouchEvent()後,該方法返回true,表示攔截該事件,說明ViewGroup自己要處理該事件(事件不再往下傳遞);呼叫自身的onTouchEvent()處理事件(DOWN事件將不再往上傳遞給Activity的onTouchEvent());該事件列的其他事件(Move、Up)將直接傳遞給ViewGroup 的onTouchEvent()。
- 若 ViewGroup 攔截了一個半路的事件(如MOVE),該事件將會被系統變成一個CANCEL事件 並且 傳遞給之前處理該事件的子View; 該事件不會再傳遞給ViewGroup 的onTouchEvent(); 只有再到來的事件才會傳遞到ViewGroup的onTouchEvent()。
《補充2》:
我們剛記不記得我們的View的虛擬碼是這樣的:public boolean dispatchTouchEvent(MotionEvent ev){
return onTouchEvent();
}
複製程式碼
其實上面是做了簡化,其實除了onTouchEvent
,還有onTouch
事件和onClick
事件,我們繼續用虛擬碼來說明規則:
public boolean dispatchTouchEvent(MotionEvent ev){
if(設定了TouchListener){
if(onTouch的返回值){
return true;
}else{
return onTouchEvent();
}
}
return onTouchEvent();
}
public boolean onTouchEvent(){
if(設定了ClickListener){
執行onClick;
}
.......
}
複製程式碼
View的滑動
既然我們學會了View的事件體系,很多人說那我學會了能怎麼樣,最明顯的就是我們可以用來解決很多滑動衝突事件。因為我們可以根據實際需求,選擇性的攔截,然後做自己的事件處理。
所以我們具體來看View的滑動有關的知識:
View的滑動的基本知識我就不特意提出來了。大家可以分別去搜尋。
主要是第二塊View的滑動衝突。我們就以最簡單的外部左右滑動,內部上下滑動為例子。
比如我們規定,滑動的角度是N度以內的時候就是說明我們在內部滑動,角度是N度以外的時候是外部滑動。
- 外部攔截法
預設父元素攔截,然後再適合的條件下,不讓父元素攔截。
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;
//必須不能攔截,否則後續的ACTION_MOME和ACTION_UP事件都會攔截。
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要當前點選事件){
intercepted=true;
}else {
intercepted=false;
}
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;
default:
break;
}
mLastXIntercept=x;
mLastXIntercept=y;
return intercepted;
}
複製程式碼
- 內部攔截法:
預設剛開始是不允許父元素做攔截,也就是子元素剛開始就呼叫requestDisallowInterceptTouchEvent(true);方法,禁止父元素做攔截,然後再適合的條件再讓父元素攔截。
子元素的dispatchTouchEvent()重寫:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要當前點選事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:{
break;
}
}
mLastXIntercept = x;
mLastYIntercept = y;
return super.dispatchTouchEvent(ev);
}
複製程式碼
同時還要修改父容器的onInterceptTouchEvent()方法,不能做攔截,因為如果剛開始DOWN就攔截了,後面的MOVE,UP都沒機會到子元素的上面的程式碼。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
複製程式碼
結語
歡迎大家檢視糾正,?。。。。讓吐槽來的更猛烈些吧。