欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android View - 事件分发,拦截,处理机制

程序员文章站 2024-03-20 15:25:16
...

当我们触碰手机屏幕,便会产生一个触碰事件。由于View体系是以一种树状结构存在的(参考
在我的博文Android View - 控件架构),那么哪个View或者ViewGroup会响应这个事件呢?Android系统提供了一套完善的事件分发,拦截,处理机制,帮助开发者完成准确的事件分发和处理。

在《Android群英传》中有这么一个例子,可以帮助我们理解事件分发,拦截,处理机制。

假设有一家公司的员工分级如下:
总经理,级别最高。
软件开发部部长,级别次于总经理。
软件开发者,级别最低。
总经理从董事会接收到一个开发任务(添加xx功能),那么总经理就会把任务分给软件开发部部长,部长又把任务安排给软件开发者。开发者完成任务后,报告部长完成任务,部长觉得任务完成得不错,便报告总经理,总经理看了,也觉得不错,就签了名,交给董事会。那么这个任务也就算是完成了。
其实上面例子就是View分发处理机制的原理。

我们先理解3个与事件分发,拦截,处理机制相关方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。

dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event)

用于事件的分发。如果事件能传递到View(A),那么View(A)的dispatchTouchEvent一定会被调用。

onInterceptTouchEvent

public boolean onInterceptTouchEvent(MotionEvent event)

用于事件的拦截。如果返回true,表示拦截事件,事件就不再往子View传递。如果返回false,则相反。

onTouchEvent

public boolean onTouchEvent(MotionEvent event)

用于事件的处理。如果返回true,表示不用父View处理事件,也就是事件不再回到父View,那么事件到此结束。父View的onTouchEvent方法就不会被调用。如果返回false,则相反。

稍微了解了这3个方法,我们再代码理解上面“总经理-部长-开发者”的例子:

总经理 - ViewGroupA
部长 - ViewGroupB
开发者 - MyView

ViewGroupA

package com.johan.testviewevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class ViewGroupA extends FrameLayout {
    public ViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public ViewGroupA(Context context) {
        super(context);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(getClass().getName(), "ViewGroupA dispatchTouchEvent ---");
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getName(), "ViewGroupA onTouchEvent ---");
        return super.onTouchEvent(event);
    }
}

ViewGroupB

package com.johan.testviewevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class ViewGroupB extends FrameLayout {
    public ViewGroupB(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public ViewGroupB(Context context) {
        super(context);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(getClass().getName(), "ViewGroupB dispatchTouchEvent ---");
        return super.dispatchTouchEvent(ev);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getName(), "ViewGroupB onTouchEvent ---");
        return super.onTouchEvent(event);
    }
}

MyView

package com.johan.testviewevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
public class MyView extends View {
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyView(Context context) {
        super(context);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(getClass().getName(), "MyView dispatchTouchEvent ---");
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getName(), "MyView onTouchEvent ---");
        return super.onTouchEvent(event);
    }
}

发现View比View少重写一个方法:onInterceptTouchEvent,因为这个方法是判断是否拦截事件,View是不包含其他View的,不用拦截事件,只处理就好。

xml布局文件

<com.johan.testviewevent.ViewGroupA 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="com.johan.testviewevent.MainActivity" >
    <com.johan.testviewevent.ViewGroupB
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:background="@android:color/holo_blue_bright"
        >
        <com.johan.testviewevent.MyView
            android:id="@+id/my_view"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@android:color/holo_green_light"
            />
    </com.johan.testviewevent.ViewGroupB>
</com.johan.testviewevent.ViewGroupA>

此时,View的结构是这样的:

Android View - 事件分发,拦截,处理机制

activity

package com.johan.testviewevent;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

MainActivity只是设置了布局文件。

我们打开应用,触碰一下MyView绿色色块。打印结果如下:

Android View - 事件分发,拦截,处理机制

从打印结果我们可以知道事件的传递顺序和处理顺序。

事件传递顺序
ViewGroupA(总经理)-> ViewGroupB(部长)-> MyView(开发者)

事件处理顺序
MyView(开发者)-> ViewGroupB(部长)-> ViewGroupA(总经理)

从打印结果我们还知道,ViewGroup接收到事件(调用dispatchTouchEvent)后,然后会判断是否拦截事件(调用onInterceptTouchEvent),默认是不拦截事件的。

董事会再分配了一个任务,但是这个任务只能由总经理处理,此时总经理需要任务拦下来,不用分配给部长,更不用安排给开发者,自己处理。

我们从代码中实现,只要重写ViewGroupA的onInterceptTouchEvent返回true就可:

// ViewGroupA类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");
    return true;
}

打印结果:

Android View - 事件分发,拦截,处理机制

只有ViewGroupA(总经理)处理了事件。

董事会又有一个任务来了,如需要确认某些需求可以做。总经理接收到任务后,因为此事需要软件开发部部长来判断,所以把任务安排到了部长,身为部长,肯定有一定的资历,所以觉得这个任务自己就可以搞定,并不用开发者来做。所以部长就把任务给拦下来了,自己处理。处理完后,需要报告总经理,然后由总经理向董事会说明。

代码实现:
恢复ViewGroupA的代码,ViewGroupA(总经理)不能拦截事件,否则ViewGroupB(部长)不能接收到事件:

// ViewGroupA类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e(getClass().getName(), "ViewGroupA onInterceptTouchEvent ---");
    return super.onInterceptTouchEvent(ev);
}
// ViewGroupB类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
    return true;
}

打印结果:

Android View - 事件分发,拦截,处理机制

董事会又来了一个小开发任务,不太care的小任务。同样,总经理接收到任务后,安排给部长,部长自然把任务分到开发者手中。开发者接收到任务后,可能公司给得待遇不能满足开发者的要求,开发者想跳槽了,就随便做,报告部长完成任务。当然部长觉得开发者完成的不行,不敢报告总经理,而且这个任务不太重要,所以决定不上报,任务到此结束。

我们用代码模拟这个场景:
我们只需要改ViewGroupB的代码,不再拦截事件,但是要处理事件,不在返回给父View(ViewGroupA)。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
    return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.e(getClass().getName(), "ViewGroupB onTouchEvent ---");
    return true;
}

打印结果:

Android View - 事件分发,拦截,处理机制

事件回到ViewGroupB就结束了,因为ViewGroupB的onTouchEvent方法返回true。

其实不止打印了上面的结果,还有下面的结果:

Android View - 事件分发,拦截,处理机制

我们待会再分析。

董事会觉得上次的任务没有收到报告,虽然不是什么大事,但是还是觉得有必要做一下。总经理接收到任务,安排到了部长,部长看到了,还是这个任务,分给开发者,并督促一定要做好,否则滚蛋。此时,开发者已经找到心仪的公司了,做人要有始有终嘛,便完成了任务,但是不打算报告部长,任务到此结束。

我们在用代码模拟一下:

// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.e(getClass().getName(), "MyView onTouchEvent ---");
    return true;
}

打印结果:

Android View - 事件分发,拦截,处理机制

这里也一样,还打印了其他结果:

Android View - 事件分发,拦截,处理机制

为什么onTouchEvent返回true时,会重复执行多次呢??

因为onTouchEvent返回true,表示要处理触碰事件。一个完整的事件,应该包括 按下手指-移动(或许没有)- 抬起手指,毕竟要处理事件嘛,当然想要获取所有用户手指触碰的事件,所以会分发每个触碰事件。

我们从代码去理解,改一下MyView的onTouchEvent方法的代码:

// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.e(getClass().getName(), "MyView onTouchEvent --- ");
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");
            break;
        case MotionEvent.ACTION_MOVE :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");
            break;
        case MotionEvent.ACTION_UP :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");
            break;
        default:
            break;
    }
    return true;
}

看看打印结果你就知道了:

Android View - 事件分发,拦截,处理机制

这你应该清楚了吧。我们再来看看如果返回false,是什么事件呢:

// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.e(getClass().getName(), "MyView onTouchEvent --- ");
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");
            break;
        case MotionEvent.ACTION_MOVE :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");
            break;
        case MotionEvent.ACTION_UP :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");
            break;
        default:
            break;
    }
    return false;
}

打印结果:

Android View - 事件分发,拦截,处理机制

接收的是一个down事件,由于我们不处理这个事件,返回给上级处理,所以也就不会执行多次了。

requestDisallowInterceptTouchEvent

我们在开发中可能会遇到这种情况:在ViewPager中嵌套了一个横滑列表,在拖动横滑列表时同样可能导致ViewPager的tab切换。
now,我们可以在横滑列表中使用这个方法,来阻止ViewPager拦截滑动事件。

我们先看一下ViewGroup的dispatchTouchEvent方法:

Android View - 事件分发,拦截,处理机制

注意,代码中有一个标志:FLAG_DISALLOW_INTERCEPT,这个标志是决定ViewGroup是否要执行onInterceptTouchEvent方法。子View可以通诺getParent方法获取到父View,调用父View的requestDisallowInterceptTouchEvent设置这个标志,从而阻止父View拦截事件,子View才有机会响应事件。但是ViewGroup在分发ACTION_DOWN事件就已经重置了FLAG_DISALLOW_INTERCEPT,也就是说,如果父View重写onInterceptTouchEvent事件时,ACTION_DOWN事件返回true,也就是拦截了ACTION_DOWN事件,即使子类调用了父View的requestDisallowInterceptTouchEvent(true)也不能阻止父View拦截事件。

我们代码测试一下:

// ViewGroupB类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
    return true;
}
// MyView类
@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.e(getClass().getName(), "MyView onTouchEvent --- ");
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_DOWN --- ");
            break;
        case MotionEvent.ACTION_MOVE :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_MOVE --- ");
            // 阻止ViewGroupB拦截事件
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_UP :
            Log.e(getClass().getName(), "MyView onTouchEvent ACTION_UP --- ");
            // 允许ViewGroupB拦截事件
            getParent().requestDisallowInterceptTouchEvent(false);
            break;
        default:
            break;
    }
    return true;
}

打印结果为:

Android View - 事件分发,拦截,处理机制

结果显示,MyView并没有能阻止ViewGroupB拦截事件。我们改一下代码,让ViewGroupB拦截ACTION_DOWN事件:

// ViewGroupB类
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e(getClass().getName(), "ViewGroupB onInterceptTouchEvent ---");
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;
    }
    return true;
}

打印结果:

Android View - 事件分发,拦截,处理机制

MyView响应了事件,成功阻止ViewGroupB拦截除ACTION_DOWN以为的事件。

我们处理在ViewPager中嵌套了一个横滑列表,在拖动横滑列表时同样可能导致ViewPager的tab切换这种情况也是这么处理(因为没找到ViewPager源码,估计ViewPager也是没有拦截ACTION_DOWN事件吧,暂时还不知道)。

点击事件

如果MyView设置了点击事件(setOnClickListener)和触碰事件(setOnTouchListener),流程会怎么样呢?

代码实现:(ViewGroupA,ViewGroupB,MyView代码回到原始,请看上面原始代码)

package com.johan.testviewevent;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.my_view).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(getClass().getName(), "MyView setOnClickListener --");
            }
        });
        findViewById(R.id.my_view).setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.e(getClass().getName(), "MyView setOnTouchListener --");
                return false;
            }
        });
    }
}

我们看看结果:

Android View - 事件分发,拦截,处理机制

省略重复一段log:

Android View - 事件分发,拦截,处理机制

我们可以看到,MyView先响应我们设置的触碰事件(setOnTouchListener),然后再响应MyView重写的onTouchEvent,最后才响应我们设置的点击事件(setOnClickListener),这样方便开发者在外界设置响应事件。

从流程可以知道,如果我们在触碰事件(setOnTouchListener)中返回true,那么在MyView重写的onTouchEvent方法不会响应,设置的点击事件(setOnClickListener)也不会响应。如果在MyView重写的onTouchEvent方法中返回true,设置的点击事件(setOnClickListener)不会响应。setOnLongClickListener和setOnClickListener同理。

如果MyView设置点击事件或者触碰事件,都代表MyView已经处理了事件,事件到此结束,ViewGroupB的onTouchEvent就不再执行。

有兴趣的可以阅读《Android开发艺术探索》的View事件体系一章。