android 觸摸事件詳解(完結)



沖突的原因
    電腦本來設計好了最簡單的規則,down事件碰到哪個控件,哪個控件就接收全部事件,是原始的以人為本!
       但人偏偏喜歡打破規則。或者是偷懶,便捷緣故,如scrollView.不需要設計旁邊下拉條。人就想往中間拉,管你碰到中間什么控件,我就要滑動的事件。
    為什么電腦端沒有這么多沖突?因為電腦端時代,是鍵盤,和鼠標,人還算守規則,要滑動,用滾動條啊,有了觸摸后。拉什么拉,我要流的滑。
    所以出現了截斷,截斷就像是潘多拉之盒,雖然有了截斷的便利,也帶來了世界的混亂。
因為事件的處理是從控件樹的上層到下層,截斷后,上下之間順序優先獨占,雖然對於scorllview,大部分是ok的。
但有時候這又和我們人類感覺的不匹配。為什么下滑里面的不讓下滑,因為下滑不知道里面還有下滑啊,下滑必須掌控下滑動作。不行,下滑碰到下滑,要給里面下滑。 等等。
所以才會產生控件間事件該如何分配的問題。
    所以總結
電腦的設計(無沖突,有時候不那么方便):down碰到誰給誰,鐵一般的紀律。要滑動,用滾動條。

    人類的感覺(全部截斷):0.有時候,手碰到了子控件,還是要把事件給上層,所以就產生了截斷,打開了混亂的源頭。如scorllview:在子控件上滑動,事件還要歸scrollview。
    人類的感覺(外部截斷)
1.有時候,上層截斷后,需要把上層不需要的動作,分配給下層。 所以需要上層,精確控制自己的截取,放行自己不需要的。

人類的感覺(內部截斷)2.有時候,上層截斷后,需要把下層需要的動作, 分配給下層。所以需要上層,先放行down, 讓下層執行getparent().disallowinterxxxxx. 這樣讓下層掌握控制權。

我的感覺:優先碰到誰給誰,如果一定要截斷,那么只截取自己的。如果子和我要搶同一個事件,那么優先看看是否可以避免這種設計。所以盡量不用getparent().disallowinter.
盡量用外部截斷法來分配滑動,因為簡潔,容易理解和定位bug,只有一種情況是必須使用內部截斷法的。就是內外需要分配的事件是同一個,從業務上無法區分的動作,而且此動作應該給子控件。那么就必須內部截斷法。

所以本來,從下往上一個listener.touch就可以工作。一切的起源都是上層想要截斷事件。所以才有onintercetp+ontouch. 為了解決截斷的特殊情況又出現了disallowflag. click感覺是一個動作語法糖而已。

 

 

個人名詞修正

滑動沖突,因為修改為滑動分配。這樣更容易理解本質。
因為本質上就是如何分配事件。不管和外部截斷和內部截斷。
截斷的目的就是分配。

diapatchEvent:個人感覺應該翻譯為下發,而不是分發。
有3個蘋果,都給一個小朋友,是下發。給3個才叫分發。很明顯,事件最終是一個人處理。只是看看給誰而已。
直譯很多情況下,都會發生意思偏差。

 

 

觸摸設計的推導假設

從直接觸碰的控件往上傳播所有事件,包括down和move,up。
這樣同一個枝的控件都可以知道所有事件。設置一個listener就可以工作。
設置一個字段,isHandle,是否掌控。一但為真,那么就不再往上傳。
這個設計很簡單,從下往上符合人的感知和經驗。
隨時可以觸發自己的動作。觸發了自己的,設置下ishandle.
為什么這么簡單的流程不用。要搞的這么復雜?
因為有特例要要上層截斷,所以才有截斷判斷,還可以設置截斷條件。
截斷又搭配一個ontouch,放在listener.touch之后,比較符合常理。
又想優化下每次事件的傳播效率,才有down作為判斷消費者的設計。不必要每次都傳到最底層。
截斷后,又想要特例,所有又有了 disallow.

 

觸摸事件的偽代碼
首先<<android 開發藝術探索>>和網上的偽代碼是一樣的,估計大家都是抄書的,但是個人感覺有非常明顯的失誤。都是一抄全錯。

書上的偽代碼,看來是down的偽代碼,但是尾遞歸之后又少一個很重要的,兜底處理。

自己理解的偽代碼,分為down和其他事件。因為差別挺大,分為2個部分更容易理解。

down 偽代碼

down event
public boolean dispatchTouchEvent(MotionEvent ev) 
{
    boolean consume = false;
    if (onInterceptTouchEvent(ev))
    {
        consume = TouchListener.onTouch(ev)->this.onTouchEvent(ev)->ClickListener.onClick(ev);
    } 
    else 
    {
        consume = child.dispatchTouchEvent (ev) ;
        if(consume==false)
        {
            consume = TouchListener.onTouch(ev)->this.onTouchEvent(ev)->ClickListener.onClick(ev); }
    }
    return consume;
}

 

move:偽代碼

public boolean dispatchTouchEvent(MotionEvent ev) 
{
    boolean consume = false;
    if (target==null)//沒有下發目標,自己處理.  有2種情況  1.最早截斷過down. 2.上次截斷過move
    {
        consume = TouchListener.onTouch(ev)->this.onTouchEvent(ev)->ClickListener.onClick(ev);
    } 
    else 
    {
        if (onInterceptTouchEvent(ev))
        {
            ev=cancel;
            consume = child.dispatchTouchEvent (ev) ;
            target=null;
        }
        else
        {
            consume = child.dispatchTouchEvent (ev) ;
        }
    }
    return consume;
}

 

 

 

詳細流程圖,

分為down事件和非down事件。

down 事件

 

非down事件

 

 

 

典型事件圖


   

 

 


 

 

 

分析過程

一。自己的總結。
從大的說,其實就是一個遞歸。
1.down的目的就是找到誰來處理事件,循環所有子控件,一直往下(dispatchTouchEvent)問(onintercepevent),只要有控件截斷,那么之后所有事件的終點站就是它了。
都不處理,那么遞歸出來時再沿往回問(touchListener + clickListener)。這樣,通過down事件找到了誰來處理,
2.那么其他事件就不需要循環所有子控件了,直接走處理鏈的那一條路
(dispatchTouchEvent),一直到目標,調用它的touchListener + clickListener
中途,有截斷的話,那么就把處理者由原處理者更改為截斷者。特殊情況down的時候發現沒有處理的view,那么交給activity處理。
3.調用touchListener + clickListener,一般說成Listener.Ontouch 和 onTouchEvent. 因為onTouchEvent的基類實現就是調用view.onclick.我們重寫onTouchEvent就是覆蓋view.onclick
細致點就是先 touchlistener,如果返回flase,再onclickListener.
 
         
 
         
網上都是任務下派來作為比喻,很好。只不過大部分沒有詳細點明一些細節。自己詳細比喻下。
假如某公司有多級部門。總公司中心處是activity。activity
總公司中心處 不記住任何東西。只派發任務,並處理大家都不處理的任務。而group會存儲是否有我的分部門處理這件事。而分部分又會記載分分部分。直到分分分分分記錄了某個人。
從總公司中心處,派發任務,當派發了某個任務,這里就比喻為點擊了某處。那么就把這個任務給相關部門,此部門,一層一層的下放到最小的部門的某個人。 當然如果是好差事,中間會有截取。
如果下放到某個人,或者被中間某人截取,但是他后來才發現他沒有能力處理(也就是某個控件,觸摸事件點到它了,但是它沒有消費down)。那么就一層一層沿來路往上,看看誰能處理。
這里就比喻 down下發時候的ontouchEvent都返回false的回歸邏輯。最終有人處理,或者真的無人處理。這里
ontouchEvent包括我們的click和自定義的ontoucheventlister。down返回了true。那么和截斷一樣。就確定了處理人。下次會逐層傳遞到這里為止,也就是還是會從上往下詢問是否需要中斷,但是不會再像down一樣往回問處不處理。因為已經有人處理了。
之后如果有這個任務的后續處理事件,就比喻為move,up事件。 那么還是從總公司中心處,一層一層過來(很多文章都是說交給某人處理,沒有強調是從上往下一層一層的),直到交給處理這個任務的人,就不再往下了。
這里就是比喻其他事件,也是
遞歸進去,並比較是否是當初存儲的那個處理的view。是,就停止遞歸。
當然中間也可以再截取這個任務,然后再一層一層的通知原來處理這個事情的人,這個任務作廢了。也就是比喻為中間截取了move或up信號,並一層一層發送cancel事件到down處理者。只發送一次cancel。以后截取的view就成為了新的處理者,截斷所有事件。
如果當初是無人處理。那么后續事項,總公司中心處,還是需要先發給大部門,大部知道無法處理。就直接說無法處理。總公司才自己處理。Activity是不存儲誰處理事情的,只有group才存儲。所以就算沒有處理。activity還是要先問下頂級group。
 
        

 

 

最佳實踐

1.最方便是只寫 listener.
2.如果需要上層覆蓋下層。那么最好是只用外部截斷法。套用固定套路。
2.1 down,up,cancel 都放行。
2.2 對於move,只截斷自己需要的,盡量吧范圍縮小。
3.實在是無法區分上下事件,無法區分也就是無法下放下層事件,那么就用內部截斷法。也是固定套路。
3.1 ondispatchEvent中。down事件,就告訴上級不要截斷事件。
3.2 必要的話,可以放棄通過down事件獲得的事件接收權。

4.要注意分辨,onIntercept和listener消費的區別和含義。
4.1 onIntercept的目的是截取我要的動作。獲得控制權。 所以一般對於down是要放行,以便讓down走到最接近人觸摸點的位置,以便符合人的感覺。 而對於move動作,需要就必須截斷。以符合人的最早的動作意圖就是我的本意的習慣。
4.2 listener+touch的目的是是否消費這個動作。有2種情況,進入此函數。
1.沒有任何子空間消費down,那么down會進入此函數問我是否消費。
2.如果截斷了事件。那么進入此函數會問我是否消費。 所以listener必須覆蓋這2中情況。這2中情況的余集,就是對於情況1對於down的處理。
所以一般listen是必須消費down和up.正常處理move。

 

 

 

固定套路

外部截斷法。

public MotionEvent mDownEvent=null;//down 動作。 因為down是不會被截斷的。所以不會進入listener+touch。所以最好保存下,給listener+ontouch使用。
private MotionEvent mLastInterceptEvent=null;//最新的move動作。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
{
if(ev.getAction()==MotionEvent.ACTION_DOWN)
{
mDownEvent=MotionEvent.obtain(ev);//必須copy。因為ev是一個被所有事件共同使用的變量,隨時會被更新,而不是new。
return false;
}
else if(ev.getAction()==MotionEvent.ACTION_MOVE)
{
boolean res=false;
if(需要)//只截斷左右滑動。
{
res=true;
}
mLastInterceptEvent=MotionEvent.obtain(ev);
return res;
}
else if(ev.getAction()==MotionEvent.ACTION_UP)
{
return false;
}
else//cancel 應該只有下級的cancel才會經過這里。如果是自己cancel。是會直接進入listener+ontouch.所以必須放行。
{
return false;
}
}




內部截斷法
內部截斷法,對於我看來。就是外部截斷法的補充。所以內部截斷法中的上層的代碼包括外部截斷法的ontercept.

@Override
public boolean dispatchTouchEvent(MotionEvent ev)
{
if(getParent()!=null && ev.getAction()==MotionEvent.ACTION_DOWN)
{
getParent().requestDisallowInterceptTouchEvent(true);
}
if(getParent()!=null&& ev.getAction()==MotionEvent.ACTION_MOVE && 上層需要)
{
getParent().requestDisallowInterceptTouchEvent(false);
}
return super.dispatchTouchEvent(ev);
}

 

 

 一個實際例子

內部控件

public class MyHorizontalScrollViewEx extends HorizontalScrollView
{
    public MyHorizontalScrollViewEx(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev)
    {
        if(getParent()!=null && ev.getAction()==MotionEvent.ACTION_DOWN)
        {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.dispatchTouchEvent(ev);
    }
}

外部控件

public class MyConstrainLayoutEx extends ConstraintLayout
{
    public MotionEvent mDownEvent=null;//down 動作。 因為down是不會被截斷的。所以不會進入listener+touch。所以最好保存下,給listener+ontouch使用。
    private MotionEvent mLastInterceptEvent=null;//最新的move動作。

    private PointF mdisInterceptStart=null;
    private PointF mdisInterceptEnd=null;

    public MyConstrainLayoutEx(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    //v1.分配事件。放行click,一旦有move,那么之后就全部要。要注意,up放行。前提是沒有觸發move,move觸發后,表示截斷,那么onInterceptTouchEvent是不會再執行的。之后的move和up是會直接給listener+ontouch
    //v2.改動就在於截斷move的時候加了一個條件判斷。其他基本沒動。
    //v3.如果同向,可以提供一個方法,用於告訴group,再那個區域的不要截斷。好像這樣和內部截斷的功效一樣,內部也是告訴group。別截斷,但是本質是不一樣的。內部法是內部從此掌握了所有事件。
    //如果上層還想要。必須內部放行。而我們畫蛇添足的加入一個方法讓外部調用。本質上還是上層控制主動。好處是耦合低,如果內部法有一個方法,可以讓上層重新掌握主動。而不是靠內部來判讀,那才算是耦合度合理。
    //但是不可能有,因為外部法,就是由於無法通過已有的方法,分辨出何時該放。何時該收。但是google為什么不多提供一個接口呢,而不是只能用內部這種不完美的方案。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev)
    {
        if(ev.getAction()==MotionEvent.ACTION_DOWN)
        {
            mDownEvent=MotionEvent.obtain(ev);//必須copy。因為ev是一個被所有事件共同使用的變量,隨時會被更新,而不是new。
            return false;
        }
        else if(ev.getAction()==MotionEvent.ACTION_MOVE)
        {
            boolean res=false;
            if(mDownEvent!=null && ev!=null)//只截斷左右滑動。
            {
                LSTouch.scrollDirection direction=LSTouch.getscrollDirection(mDownEvent, ev);
                if(direction==LSTouch.scrollDirection.LEFT || direction==LSTouch.scrollDirection.RIGHT)
                {
//                    if(ev.getRawX()>=0 && ev.getRawY()>=200)//這里做一個假設,可以提供一個方法,傳遞某個控件的位置,這樣當觸摸點在這個位置,那么不能截斷。也是可以的。
//                    {
//                        res=false;
//                    }
//                    else
//                    {
//                        res = true;
//                    }
                    res=true;
                }
            }
            mLastInterceptEvent=MotionEvent.obtain(ev);
            return res;
        }
        else if(ev.getAction()==MotionEvent.ACTION_UP)
        {
            return false;
        }
        else//cancel 應該只有下級的cancel才會經過這里。如果是自己cancel。是會直接進入listener+ontouch.所以必須放行。
        {
            return false;
        }
    }

 

 

未解決的疑點

1.當有匹配的事件發生,只給下面說你的事件取消了,但是不告訴自己去觸發事件? 這樣不是浪費了一個事件了不?雖然很多情況下是無關緊要,但是邏輯上還是錯誤啊。萬一下一個事件就是up事件呢?所以截取一定不能截取up?否則不會觸發自己的touch事件!!!
解決:en .可以在onintercept,設置一個變量,來告訴事情已經發生了。如果最后一個是up。那么就直接觸發動作。不需要touch事件。否則,根據定義好的變量,在touch中直接做動作,后面的事件直接消費就好了,不作為事件是否發生的標志。
2.如果截斷后產生了新的事件消費者控件,事件都已經觸發了,假設它上層某個控件有個事件,又匹配上了用戶的后續動作呢?,又要截斷? 那要觸發2個動作。不符合人的常識啊。
解決:可以在截斷后,設置 disallow為true。這樣保證上層不會再截止動作了。只有我們自己一個動作執行者。

 

 

補充 activity ,window, dector的處理分析

C:\android\sdk\sources\android-28\android\app\activity.java
/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    else
     {
return onTouchEvent(ev);
      }
}
private Window mWindow;
public Window getWindow() {
    return mWindow;
}
mWindow = new PhoneWindow(this, window, activityConfigCallback);


C:\android\sdk\sources\android-28\android\view\window.java
/**
 * 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);




C:\android\sdk\sources\android-28\com\android\internal\policy\PhoneWindow.java
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

mDecor = (DecorView) preservedWindow.getDecorView();
mDecor = generateDecor(-1);

DecorView就是Window的頂級View,它派生於FrameLayout,而FrameLayout又派生於groupview。所以我們可以最后追到ViewGroup.java
所以最終看ViewGroup.java的dispatchTouchEvent就可以。但是需要配合下面這幅圖。其中contentViews是我們的布局xml文件的內容。


 

 
         

 




C:\android\sdk\sources\android-28\com\android\internal\policy\DecorView.java

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM