深入聊聊Android事件分發(fā)機制

在Android開發(fā)的過程中泪电,自定義控件一直是我們繞不開的話題拄丰。而在這個話題中事件分發(fā)機制也是其中的重點和疑點,特別是當我們處理控件嵌套滑動事件時捅暴,正確的處理各個控件間事件分發(fā)攔截狀態(tài)婉陷,可以實現(xiàn)更炫酷的控件動畫效果帚称。

一、事件分發(fā)機制介紹

關于Android事件分發(fā)秽澳,我們主要分ViewGroup和View兩個事件處理部分進行介紹闯睹,主要研究在處理事件過程中關注最多的三個方法dispatchTouchEventonInterceptTouchEvent担神、onTouchEvent楼吃,在ViewGroup和View對三個方法的支持如下圖所示:

事件種類 ViewGroup View
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent

在Android中,當用戶觸摸界面時系統(tǒng)會把產(chǎn)生一系列的MotionEvent妄讯,通過ViewGroup 的dispatchTouchEvent方法開始向下分發(fā)事件孩锡,在dispatchTouchEvent方法中,會調(diào)用onInterceptTouchEvent方法亥贸,如果該方法返回true躬窜,表明當前控件攔截了該事件,此后事件交由該控件處理并不再調(diào)用該控件的onInterceptTouchEvent方法炕置。最后交由該控件的onTouchEvent方法對事件進行處理斩披。如果當前控件在onInterceptTouchEvent方法中返回false,表示不攔截該控件讹俊,之后交由其子控件進行判斷是否對事件進行攔截處理』褪悖可以用如下偽代碼來對其進行處理:

public boolean dispatchTouchEvent(MotionEvent event) {
        boolean consume = false;
        if (onInterceptTouchEvent(event)) {
            consume = onTouchEvent(event);
        } else {
            consume = child.dispatchTouchEvent(event);
        }
        return consume;
    }
事件分發(fā)

先說結(jié)論再細分析:

  1. 事件是由其父視圖向子視圖傳遞仍劈,如圖為A->B->C
  2. 如果當前控件需要攔截該事件,則在onInterceptTouchEvent方法中返回true寡壮,但真正決定是否處理事件是在onTouchEvent方法中贩疙,也就是說如果此時onTouchEvent方法返回了false讹弯,則此控件也表示不處理該事件,交由父控件的onTouchEvent方法來判斷處理这溅。如圖:當事件由A分發(fā)至B组民,B在其onInterceptTouchEvent方法中返回true表示要攔截該事件,此時事件將不會再傳給C悲靴,但在B的onTouchEvent方法中返回了false臭胜,表示不處理該事件,則事件以此向上傳遞交由A控件的onTouchEvent方法處理癞尚。即onInterceptTouchEvent負責對事件進行攔截耸三,攔截成功后交給最先遇到onTouchEvent返回true的那個view進行處理。
  3. 一旦控件確定處理該事件浇揩,則后續(xù)事件序列也會交由該控件處理仪壮,同時該控件的onInterceptTouchEvent方法將不再調(diào)用。
  4. 由于View沒有onInterceptTouchEvent方法胳徽,在其dispatchTouchEvent方法中調(diào)用onTouchEvent方法處理事件积锅,如果返回false則表示事件不作處理。同時其ACTION_MOVE养盗、ACTION_UP不會得到響應缚陷。
  5. View的OnTouchListener優(yōu)先于onTouchEvent方法執(zhí)行,如果OnTouchListener方法返回true爪瓜,那么View的dispatchTouchEvent方法就返回true蹬跃。而后則onTouchEvent方法得不到執(zhí)行,同時因為onClick方法在onTouchEvent方法的ACTION_UP中調(diào)用铆铆,onClick方法也得不到執(zhí)行蝶缀。

情況一、A\B\C onInterceptTouchEvent onTouchEvent均返回false

事件種類 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false false
onTouchEvent false false false
事件處理

當A薄货、B翁都、C同時返回false時,事件傳遞為A(onInterceptTouchEvent) -->B(onInterceptTouchEvent) -->C(onTouchEvent)-->B(onTouchEvent) -->A(onTouchEvent)谅猾,也就是事件從A傳至C時柄慰,都沒有攔截和處理事件,則事件再次向上傳遞調(diào)用B和A的onTouchEvent方法税娜。

看下打印的結(jié)果:

事件分發(fā)

情況二坐搔、B onInterceptTouchEvent 方法返回true

事件種類 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false true
onTouchEvent false false false

當BonInterceptTouchEvent返回true時表示攔截了事件,C控件就無法響應該事件敬矩。

事件分發(fā)
打印結(jié)果

情況三概行、B onInterceptTouchEventonTouchEvent方法返回true

事件種類 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false true
onTouchEvent false true false

當BonInterceptTouchEvent弧岳、onTouchEvent返回true時表示攔截處理了事件凳忙,C控件就無法響應該事件业踏,同時事件在B的onTouchEvent之后將不再向上傳遞,隨后事件將不再調(diào)用其onInterceptTouchEvent方法涧卵。

事件分發(fā)
打印結(jié)果

情況四勤家、C onTouchEvent方法返回true

事件種類 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent false false
onTouchEvent false false true

當ConTouchEvent返回true時表示處理了該事件,之后事件就交由C控件處理柳恐,同時事件在C的onTouchEvent之后將不再向上傳遞伐脖。

事件分發(fā)
打印結(jié)果

情況五、A onInterceptTouchEvent方法返回true

事件種類 A(ViewGroup) B(ViewGroup) C(View)
onInterceptTouchEvent true false
onTouchEvent false false false

當AonInterceptTouchEvent返回true時表示攔截了事件胎撤,之后事件就交由A的onTouchEvent方法處理晓殊,B、C就無法響應該事件伤提。如果AonTouchEvent方法返回false巫俺,其ACTION_MOVE、ACTION_UP事件不會得到響應肿男。

事件分發(fā)
@Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "A --- onTouchEvent");
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "A --- onTouchEvent :ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "A --- onTouchEvent :ACTION_UP");
                break;
        }
        return false;//super.onTouchEvent(event);
    }
打印結(jié)果

二介汹、實現(xiàn)側(cè)滑刪除效果

運用上面的知識學習,我們來實現(xiàn)一下簡單的側(cè)滑刪除效果吧~

側(cè)滑刪除效果

其核心代碼主要在于對事件的攔截和處理上:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
//        boolean intercepter = false;
        Log.e("TAG", "onInterceptTouchEvent: "+ev.getAction());

        boolean intercepter = false;
        if (isMoving)
            intercepter = true;
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                downX = (int) ev.getX();
                downY = (int) ev.getY();

                if (mVelocityTracker == null)
                    mVelocityTracker = VelocityTracker.obtain();

                mVelocityTracker.clear();
                break;
            case MotionEvent.ACTION_MOVE:

                moveX = (int) ev.getX();
                moveY = (int) ev.getY();


                Log.e("TAG", "getScrollX: "+getScrollX() );
                if (Math.abs(moveX - downX) > 0){
                    intercepter = true;

                    //Log.e("TAG","onInterceptTouchEvent: ");
                    //scrollBy(moveX - downX,0);

                }else {
                    intercepter = false;
                }

                downX = moveX;
                downY = moveY;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:

                intercepter = false;

                break;
        }

        //scrollBy(45,0);
        return intercepter;//
        //super.onInterceptTouchEvent(ev);

    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        Log.e("TAG", "onTouchEvent: "+ev.getAction() );

        mVelocityTracker.addMovement(ev);
        switch (ev.getAction()){
            
            case MotionEvent.ACTION_MOVE:

                moveX = (int) ev.getX();
                moveY = (int) ev.getY();

                mVelocityTracker.computeCurrentVelocity(1000);
                Log.e("TAG", "getScrollX: "+getScrollX() );

                if (getScrollX()+downX - moveX>=0 && getScrollX()+downX - moveX <= view1.getMeasuredWidth()){

                    scrollBy(downX - moveX,0);
                 }

                isMoving = true;
                downX = moveX;
                downY = moveY;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:

                Log.e("TAG1", "getXVelocity: "+mVelocityTracker.getXVelocity() );
                Log.e("TAG1", "getYVelocity: "+mVelocityTracker.getYVelocity() );
                //
                if (getScrollX()>=view1.getMeasuredWidth()/2 || mVelocityTracker.getXVelocity() < -ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity()){
                    //scrollTo(view1.getMeasuredWidth(),0);
                    open();
                }else {
                    //scrollTo(0,0);
                   close();
                }

                mVelocityTracker.clear();
                mVelocityTracker.recycle();
                mVelocityTracker = null;
                break;
        }
        return true;//super.onTouchEvent(ev);
    }

這里整個父布局繼承自ViewGroup舶沛,在onMeasure中測量子控件大朽诔小:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
    }

onFinishInflate方法中獲取各個子控件:

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();
         view = getChildAt(0);
         view1 = getChildAt(1);
        if (mScroller == null)
            mScroller = new Scroller(getContext());

        view.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View mViewm, MotionEvent mMotionEventm) {
                if (mMotionEventm.getAction() == MotionEvent.ACTION_UP
                        && isOpen){
                    close();
                }
                if (mMotionEventm.getAction() == MotionEvent.ACTION_DOWN){
                    if (mOnChangeMenuListener!=null){
                        mOnChangeMenuListener.onStartTouch();
                    }
                }
                return false;
            }
        });
    }

并在onLayout方法中布局子控件:

@Override
    protected void onLayout(boolean mBm, int mIm, int mIm1, int mIm2, int mIm3) {
        if (getChildCount()!=2){
            throw new IllegalArgumentException("必須包含兩個子控件");
        }
        Log.e("TAG", "onLayout:getWidth "+view.getWidth() );
            view.layout(0,0,view.getMeasuredWidth(),view.getMeasuredHeight());
            view1.layout(view.getMeasuredWidth(),0,view.getMeasuredWidth()+view1.getMeasuredWidth(),view1.getMeasuredHeight());

    }

重點在對onInterceptTouchEventonTouchEvent方法的處理膀斋,我們在onInterceptTouchEvent中處理是否攔截該事件体斩。如果手指是向左滑動起便,則表示用戶在進行側(cè)滑刪除操作辙喂,則攔截該事件,需要注意的是操刀,一旦攔截了該事件乒验,之后事件將不調(diào)用該控件的onInterceptTouchEvent方法俄认,所以我們將具體的處理邏輯放在onTouchEvent方法中往毡,該方法返回true表示處理該事件蒙揣,此后事件都由dispatchTouchEvent方法交由onTouchEvent方法處理。在onTouchEvent方法中調(diào)用scrollBy方法實現(xiàn)控件左右滑動开瞭,從而實現(xiàn)類似側(cè)滑刪除效果懒震。

@Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }else {
            isMoving = false;
        }
    }

為使滑動效果更自然,用Scroller在手指抬起的時候控制控件打開或者閉合嗤详,Scroller的使用也很簡單个扰,抬起時調(diào)用其startScroll方法并刷新界面,在控件computeScroll方法中判斷是否滑動完畢并刷新界面葱色,在invalidate方法中會調(diào)用computeScroll從而直到滑動結(jié)束锨匆。

好了,總的實現(xiàn)就這么多,希望可以加深對事件分發(fā)機制的理解~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恐锣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子舞痰,更是在濱河造成了極大的恐慌土榴,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件响牛,死亡現(xiàn)場離奇詭異玷禽,居然都是意外死亡,警方通過查閱死者的電腦和手機呀打,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門矢赁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贬丛,你說我怎么就攤上這事撩银。” “怎么了豺憔?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵额获,是天一觀的道長。 經(jīng)常有香客問我恭应,道長抄邀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任昼榛,我火速辦了婚禮境肾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘胆屿。我一直安慰自己奥喻,他們只是感情好,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布莺掠。 她就那樣靜靜地躺著衫嵌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪彻秆。 梳的紋絲不亂的頭發(fā)上楔绞,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機與錄音唇兑,去河邊找鬼酒朵。 笑死,一個胖子當著我的面吹牛扎附,可吹牛的內(nèi)容都是我干的蔫耽。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼匙铡!你這毒婦竟也來了图甜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤鳖眼,失蹤者是張志新(化名)和其女友劉穎黑毅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钦讳,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡矿瘦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了愿卒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缚去。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖琼开,靈堂內(nèi)的尸體忽然破棺而出易结,到底是詐尸還是另有隱情,我是刑警寧澤稠通,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布衬衬,位于F島的核電站,受9級特大地震影響改橘,放射性物質(zhì)發(fā)生泄漏滋尉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一飞主、第九天 我趴在偏房一處隱蔽的房頂上張望狮惜。 院中可真熱鬧,春花似錦碌识、人聲如沸碾篡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽开泽。三九已至,卻和暖如春魁瞪,著一層夾襖步出監(jiān)牢的瞬間穆律,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工导俘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留峦耘,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓旅薄,卻偏偏與公主長得像辅髓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內(nèi)容