通俗讲解 Android 事件分发机制 —— 责任链模式的典型应用

目录

一、家族规矩

Android的控件家族的家里有个规矩:

1.当一个 帅气的小哥哥 or 美丽的小姐姐 给Android控件家族送来一个蛋糕

---每次都是爷爷先拿到蛋糕,爷爷来发蛋糕,按照老规矩,爷爷会把这个蛋糕发给爸爸,让爸爸来发蛋糕;

---爸爸拿到蛋糕,经过妈妈的同意后,把蛋糕发给儿子,让儿子来发蛋糕;

---儿子拿到蛋糕,发给自己,然后Android控件家族开始准备吃蛋糕;

---儿子先决定吃不吃蛋糕,不吃的话就还给爸爸;

---爸爸又拿到了蛋糕,决定自己吃不吃,不吃的话就还给爷爷。

---爷爷决定自己吃掉,或者丢掉

2.这是祖祖辈辈的老规矩,如果谁想吃蛋糕,就在中间环节打断。

这个规矩是很民主的,每个人都有发蛋糕和吃蛋糕的权利,所以每个人都有机会吃到蛋糕。

在这个例子中:
爷爷是Activity
爸爸是ViewGroup
儿子是View
妈妈是onInterceptTouchEvent()
蛋糕就是点击事件
帅气的小哥哥 or 美丽的小姐姐就是屏幕前的各位。

二、事件分发机制分析

1.爷爷先拿到蛋糕,爷爷来发蛋糕,按老规矩是发给爸爸;但是他也可以选择自己直接吃掉,发蛋糕活动就结束了;爷爷还可以选择不往下面发蛋糕了,爷爷自己又不吃,就决定给自己的父辈吃,但爷爷是Android家族里最年长的,上面没有父辈,所以发蛋糕活动也结束了。这个蛋糕就被浪费了。

等同于:Activity收到一个点击事件,用dispatchTouchEvent分发这个事件,按照老规矩,也就是return super.dispatchTouchEvent(),把点击事件发给ViewGroup;Activity也可以选择自己消费掉,也就是在 dispatchTouchEvent 中 return true,点击事件分发就终止了;Activity还可以选择不往下面下发了,也就是在 dispatchTouchEvent 中 return false,这个点击事件会回到上一级,但是Activity没有上一级,所以事件分发也终止了。

2.要是爷爷按照老规矩发给了爸爸。爸爸按照老规矩是发给儿子;爸爸也可以选择自己吃掉,发蛋糕活动就结束了;爸爸还可以选择不往下面发蛋糕了,这个蛋糕就会还给父辈,也就是爷爷,给爷爷决定吃掉或者丢掉。

等同于:Activity调用了return super.dispatchTouchEvent()把点击事件给了ViewGroup,ViewGroup用dispatchTouchEvent决定怎么分发这个事件,按照老规矩,也就是return super.dispatchTouchEvent(),把点击事件发给View;ViewGroup也可以选择自己消费掉,也就是在 dispatchTouchEvent 中 return true,点击事件分发就终止了;ViewGroup还可以选择不往下面下发了,这个点击事件会回到上一级,也就是Activity,在Activity中的onTouchEvent()中消费掉或者不处理。

3.爸爸发给儿子之前会先经过妈妈的拦截。要是爸爸想要直接把蛋糕发给自己吃,需要经过妈妈的同意,如果妈妈同意了,就不再分蛋糕。爸爸直接开始决定吃不吃蛋糕。妈妈按老规矩是不同意爸爸直接吃,而是应该分给儿子。

等同于:ViewGroup下发给View的过程中会经过onInterceptTouchEvent()的拦截,如果onInterceptTouchEvent()中返回true,则不再分发事件,直接到达ViewGroup的onTouchEvent()中处理点击事件。如果onInterceptTouchEvent()中返回false或返回super.onInterceptTouchEvent(),则按照老规矩把点击事件分发给View。

4.要是爸爸按照老规矩发给了儿子,儿子按照老规矩是发给自己,然后开始吃蛋糕活动;同样,儿子可以选择直接吃掉,发蛋糕活动就结束了;儿子也可以选择不往下面发蛋糕了,这个蛋糕就会还给父辈,也就是爸爸,爸爸会决定吃掉或者给爷爷。

等同于:ViewGroup调用了return super.dispatchTouchEvent()把点击事件给了View,View按照老规矩是发给自己,也就是在dispatchTouchEvent 中 return super.dispatchTouchEvent() 的话,事件分发就结束了,然后开始决定由谁消费这个点击事件;同样,View也可以选择直接消费掉,也就是在 dispatchTouchEvent 中 return true,点击事件分发就终止了;View也可以选择不往下面下发了,这个点击事件会回到上一级,也就是ViewGroup,在ViewGroup中的onTouchEvent()中消费掉或者传到Activity。

5.要是儿子按照老规矩发给了自己,开始吃蛋糕活动。儿子决定自己吃不吃,自己吃掉的话,吃蛋糕活动结束;按照老规矩是自己不吃,拿给爸爸决定吃不吃。

等同于:View调用了return super.dispatchTouchEvent()把点击事件发给了自己。开始决定谁消费点击事件。在View的onTouchEvent()中return true或者false,如果View选择消费掉,也就是return true,事件分发结束。return super.onTouchEvent()和return false一样,都是交给ViewGroup处理。

6.爸爸又拿到了蛋糕,决定自己吃不吃,自己吃掉的话,吃蛋糕活动结束;按照老规矩是自己不吃,拿给爷爷决定吃不吃。

等同于:ViewGroup又拿到了点击事件。决定是否消费这个点击事件。如果View选择消费掉,也就是在ViewGroup的onTouchEvent()中return true,事件分发结束。return super.onTouchEvent()和return false一样,都是交给Activity处理。

7.爷爷又拿到了蛋糕,爷爷决定自己吃掉,或者丢掉。

等同于:Activity又拿到了点击事件,选择在onTouchEvent()中return true,消费掉;或者return false 或return super.onTouchEvent(),不做任何处理。

三、事件分发机制伪代码

在《Android开发艺术探索》一书中有一段伪代码,将dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent的关系表现得“淋漓尽致”,伪代码如下:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

原理和上文的解释是一样的。以上,就是Android 的View分发机制,在设计模式中属于典型的责任链模式。

整个机制的示意图:
事件分发机制

四、一个使用事件分发机制解决滑动冲突的例子

效果图

这是笔者所在公司最近一个项目的部分UI设计。我采用了一个垂直的ViewPager+ScrollView来实现。

代码实现

1.两个fragment,由于不是本次讲解的重点,所以直接上代码

上面的主页面HomeFragment:

public class HomeFragment extends Fragment {
    private View rootView;
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        rootView = inflater.inflate(R.layout.fragment_home, container, false);
        return rootView;
    }
}

布局fragment_home.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
   <TextView
       android:id="@+id/tv_temperature"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="22°C"
       android:textSize="64sp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintLeft_toLeftOf="parent"
       android:layout_margin="16dp" />
</android.support.constraint.ConstraintLayout>

下面的天气详情页面DetailFragment:

public class DetailFragment extends Fragment {
    private View rootView;
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        rootView = inflater.inflate(R.layout.fragment_detail, container, false);
        return rootView;
    }
}

布局fragment_detail.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:id="@+id/sv_detail"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <android.support.constraint.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="wrap_content">

       <!--weather 卡片-->
       <android.support.v7.widget.CardView
           android:id="@+id/card_weather"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_margin="16dp"
           app:cardBackgroundColor="#4f000000"
           app:cardCornerRadius="3dp"
           app:layout_constraintTop_toTopOf="parent">

           <android.support.constraint.ConstraintLayout
               android:layout_width="match_parent"
               android:layout_height="match_parent">

               <TextView
                   android:id="@+id/tv_weather"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:layout_marginTop="3dp"
                   android:text="WEATHER"
                   android:textColor="@android:color/white"
                   app:layout_constraintLeft_toLeftOf="parent"
                   app:layout_constraintRight_toRightOf="parent"
                   app:layout_constraintTop_toTopOf="parent" />

               <View
                   android:id="@+id/v_temp_line"
                   android:layout_width="match_parent"
                   android:layout_height="1dp"
                   android:layout_marginLeft="22dp"
                   android:layout_marginRight="22dp"
                   android:layout_marginTop="3dp"
                   android:background="@android:color/white"
                   app:layout_constraintTop_toBottomOf="@id/tv_weather" />

               <TextView
                   app:layout_constraintTop_toBottomOf="@id/v_temp_line"
                   android:layout_width="wrap_content"
                   android:layout_height="300dp" />

           </android.support.constraint.ConstraintLayout>
       </android.support.v7.widget.CardView>

       <!--detail 卡片-->
       <android.support.v7.widget.CardView
           android:id="@+id/card_detail"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_margin="16dp"
           app:cardBackgroundColor="#4f000000"
           app:cardCornerRadius="3dp"
           app:layout_constraintTop_toBottomOf="@id/card_weather">

           <android.support.constraint.ConstraintLayout
               android:layout_width="match_parent"
               android:layout_height="match_parent">

               <TextView
                   android:id="@+id/tv_detail"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:layout_marginTop="3dp"
                   android:text="DETAIL"
                   android:textColor="@android:color/white"
                   app:layout_constraintLeft_toLeftOf="parent"
                   app:layout_constraintRight_toRightOf="parent"
                   app:layout_constraintTop_toTopOf="parent" />

               <View
                   android:id="@+id/v_detail_line"
                   android:layout_width="match_parent"
                   android:layout_height="1dp"
                   android:layout_marginLeft="22dp"
                   android:layout_marginRight="22dp"
                   android:layout_marginTop="3dp"
                   android:background="@android:color/white"
                   app:layout_constraintTop_toBottomOf="@id/tv_detail" />

               <TextView
                   app:layout_constraintTop_toBottomOf="@id/v_detail_line"
                   android:layout_width="wrap_content"
                   android:layout_height="300dp" />
           </android.support.constraint.ConstraintLayout>
       </android.support.v7.widget.CardView>

       <!--climate 卡片-->
       <android.support.v7.widget.CardView
           android:id="@+id/card_climate"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_margin="16dp"
           app:cardBackgroundColor="#4f000000"
           app:cardCornerRadius="3dp"
           app:layout_constraintTop_toBottomOf="@id/card_detail">

           <android.support.constraint.ConstraintLayout
               android:layout_width="match_parent"
               android:layout_height="match_parent">

               <TextView
                   android:id="@+id/tv_climate"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:layout_marginTop="3dp"
                   android:text="CLIMATE"
                   android:textColor="@android:color/white"
                   app:layout_constraintLeft_toLeftOf="parent"
                   app:layout_constraintRight_toRightOf="parent"
                   app:layout_constraintTop_toTopOf="parent" />

               <View
                   android:id="@+id/v_climate_line"
                   android:layout_width="match_parent"
                   android:layout_height="1dp"
                   android:layout_marginLeft="22dp"
                   android:layout_marginRight="22dp"
                   android:layout_marginTop="3dp"
                   android:background="@android:color/white"
                   app:layout_constraintTop_toBottomOf="@id/tv_climate" />

               <TextView
                   app:layout_constraintTop_toBottomOf="@id/v_climate_line"
                   android:layout_width="wrap_content"
                   android:layout_height="300dp" />

           </android.support.constraint.ConstraintLayout>
       </android.support.v7.widget.CardView>
   </android.support.constraint.ConstraintLayout>
</ScrollView>

由于用到了Material Design的CardView,在Module: app的dependencied{}中引入Material Design:

implementation 'com.android.support:cardview-v7:27.1.0'

2.垂直ViewPager是采用的Github上的一个开源项目,原理是把垂直滑动的手势,转换成了横向滑动的手势,以达到垂直滑动切换ViewPager页面的效果,再给 ViewPager 设置一个垂直切换的 Transfromer,就达到了垂直ViewPager的效果。

转换了手势的VerticalViewPager代码:

public class VerticalViewPager extends ViewPager {

    public VerticalViewPager(Context context) {
        super(context);
    }

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

    private MotionEvent swapTouchEvent(MotionEvent event) {
        float width = getWidth();
        float height = getHeight();
        event.setLocation((event.getY() / height) * width, (event.getX() / width) * height);
        return event;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(swapTouchEvent(MotionEvent.obtain(ev)));
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        return super.onInterceptTouchEvent(swapTouchEvent(MotionEvent.obtain(event)));
    }
}

垂直Transformer代码:

public class DefaultTransformer implements ViewPager.PageTransformer {

    @Override
    public void transformPage(View view, float position) {
        float alpha = 0;
        if (0 <= position && position <= 1) {
            alpha = 1 - position;
        } else if (-1 < position && position < 0) {
            alpha = position + 1;
        }
        view.setAlpha(alpha);
        view.setTranslationX(view.getWidth() * -position);
        float yPosition = position * view.getHeight();
        view.setTranslationY(yPosition);
    }
}

在MainActivity中设置一下:

viewPager.setPageTransformer(true, new DefaultTransformer());
viewPager.setOverScrollMode(OVER_SCROLL_NEVER);

这样就实现了垂直ViewPager。

3.MainActivity布局:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <com.cassin.toucheventstudy.VerticalViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>

在MainActivity设置好ViewPager,完整代码:

public class MainActivity extends AppCompatActivity {
    private ArrayList<Fragment> list = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        VerticalViewPager viewPager = findViewById(R.id.view_pager);
        list.add(new HomeFragment());
        list.add(new DetailFragment());
        viewPager.setOffscreenPageLimit(2);//缓存相邻的两个页面,防止切换时重新加载fragment
        viewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
            @Override
            public Fragment getItem(int position) {
                return list.get(position);
            }

            @Override
            public int getCount() {
                return list.size();
            }
        });
        viewPager.setPageTransformer(true, new DefaultTransformer());
        viewPager.setOverScrollMode(OVER_SCROLL_NEVER);
    }
}

4.按照上面的步骤设置好后,就会发现ScrollView和ViewPager存在滑动冲突,滑动到DetailFragment顶部之后,下拉时有时候会无法返回到HomeFragment。如下图:

解决滑动冲突

在VertivalViewPager的onInterceptTouchEvent()中拦截事件:

private float yDown = 0;//记录手指按下时的y坐标值
private boolean isDown = false;//记录是否是在ScrollView顶部下拉

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    Activity activity = (Activity) getContext();
    ScrollView scrollView = activity.findViewById(R.id.sv_detail);
    if(scrollView.getScrollY() == 0){//ScrollView在顶部时判断手势
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                isDown = false;//初始化isDown
                yDown = event.getY();
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_MOVE:
                if((event.getY() - yDown) > 0){
                    isDown = true;//如果手指移动时的y坐标值大于手指按下时的坐标值,表示在下拉
                }
                break;
        }
        if(isDown){
            return true;//如果是在ScrollView顶部下拉,ViewPager拦截点击事件
        }else{
            return super.onInterceptTouchEvent(swapTouchEvent(MotionEvent.obtain(event)));
        }
    }
    return super.onInterceptTouchEvent(swapTouchEvent(MotionEvent.obtain(event)));
}

当页面是在ScrollView顶部下拉的时候,ViewPager拦截点击事件。否则的话就按照老规矩,即 return super.onInterceptTouchEvent() ,交给ViewPager的孩子ScrollView处理。

源码已上传

https://github.com/wkxjc/TouchEventStudy

参考:

图解Android事件分发机制:https://www.jianshu.com/p/e99b5e8bd67b

VerticalViewPager: https://github.com/kaelaela/VerticalViewPager

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值