由Android禁止viewpager滑动,想到的安卓事件分发机制

19 篇文章 0 订阅

前言

产品有个需求是两个tab页面可以左右切换,当时立马想到我用viewPager+fragment,但是我们知道viewPager默认是可以左右滑动的,而我的需求是只可点击不可滑动,于是我就翻了一下viewpager的API发现并没有可以设置是否可以滑动的相关方法。于是我就想是否可以通过事件的分发机制去拦截它左右滑动的touch。果不其然!

public class myViewpager extends ViewPager {
    private boolean canTouch = false;//设置默认不滑动

    public myViewpager(@NonNull Context context) {
        super(context);
    }

    public myViewpager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return canTouch;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return canTouch;
    }

    public void setCanTouch(boolean canTouch) {//提供设置是否可以点击方法默认不可以
        this.canTouch = canTouch;
    }
}

可以看到这里重写了onTnterceptTouchEvent、onTouchEvent并且返回false事情就这样实现了!

事件分发机制

但是它的事件是如何传递的呢?这就得从android的事件分发机制说起:
说到它必须排上三个方法:

dispatchTouchEvent()
onInterceptTouchEvent()
onTouchEvent()

字面意思其实就是事件的分发、拦截、触摸的处理。
我们知道,安卓中的控件千千万,也就是说每一个view都有自己的事件分发,那我怎么能找到他们的规律呢?总不能以偏概全吧?是的,我们Java的三大特性是啥?封装、继承、多态我们看它的父类不难发现所有的自定义控件都是继承view或者是viewgroup,还有一个特殊的就是我们的activity。activity是一个可视化窗口(我是这么理解的)你把所有的控件摆在上面,当然它也能处理一些事件。因此我们事件的作用对象可以大致分为:

View的事件分发机制
ViewGroup的事件分发机制
Activity的事件分发机制

产生一个事件

事件是如何产生并进行分发的?

当屏幕被触摸时(view或者viewGroup),将会产生点击事件touch,touch是事件的具体实现像:UP(抬起)、DOWN(按下)、MOVE(移动)、CANCEL(取消)。

听着听着是不是感觉这个不就是咱们的MotionEvent,还就是,我们的touch细节就被封装在MotionEvent方法里面而我们的MotionEvent方法在哪调用了?

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(ev);
    }

是的就是我们前面排出的三大方法。想搞懂事件分发机制我们先看看这个方法。

MotionEvent

MotionEvent常用四种类型:

MotionEvent.ACTION_DOWN 按下View(所有事件的开始)
MotionEvent.ACTION_UP 抬起View(与DOWN对应)
MotionEvent.ACTION_MOVE 滑动View
MotionEvent.ACTION_CANCEL 结束事件

当然你打开MotionEvent会发现里面有很多touch的类型方法如:ACTION_HOVER_MOVE、ACTION_POINTER_UP
总结一下:一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事

事件分发机制流程

上面咱们看了一个重要的方法MotionEvent(处理事件的类型为事件分发做准备),到这我们看一下事件分发的流程。

事件分发的传递顺序是:Activity -> ViewGroup -> View
即:点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View

这里说一下View和ViewGroup区别:大白话说,我们去自定义控件的时候可能这个控件不像Button一样,是一个简单的绘制的一个距形框上绘制字体,比如LineaLayout,我们自带的可以包含子布局的控件,ViewGroup相比较View多一个onLayout方法,也就是设置子布局摆放位置的相关方法。但是viewGroup最终调用的还是View,所以说viewGroup也是view多一个子类,但是它是一个特殊的view因为view可以分为两类:我本身只是个view,我不会再容下其他View。而我们的特殊View(ViewGroup)我这个view还可以放其他view,我可以约束你,让你规范。(好难表达)?

事件分发过程
方法作用调用时机
dispatchTouchEvent()分发点击事件当事件传递给view时(按下屏幕时机)
onTouchevent ()处理点击事件在dispatchTouchEvent内部事件没有被父类消耗掉是调用
onInterceptTouchEvent()判断是否拦截某个点击事件,View无改方法在ViewGroup的dispatchTouchEvent内部调用

这里可以看到对于view、viewGroup、Activity他们的事件分发机制是不同的,说到这最难的时候来了,空说无凭啊,他们源码到底是如何实现的呢?

Activity的事件分发

在Activity中分别调用者三个方法(注意包名)
activity是:package android.app;
View、ViewGroup是:package android.view;

        this.dispatchTouchEvent();
        view.dispatchTouchEvent()
        viewGroup.dispatchTouchEvent()

点开this.dispatchTouchEvent();

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//方法1
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;//方法2
        }
        return onTouchEvent(ev);//方法3
    }
    方法1:
    看到的是如下的空方法:大概意思是你需要自己去实现,处理通知栏相关的,比如你下滑、上推通知栏,也就是用户实现交互的方式。
      public void onUserInteraction() {
    }
    方法2:
    getWindow().superDispatchTouchEvent(ev)
    mDecor.superDispatchTouchEvent(event)
   属于顶层View(DecorView)
      a. DecorView类是PhoneWindow类的一个内部类
      b. DecorView继承自FrameLayout,是所有界面的父类
      c. FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup
  */
    @return boolean Return true if this event was consumed
    这个是官方的注释,若getWindow().superDispatchTouchEvent(ev)的返回true表示该事件停止往下传递,被消耗掉。
    方法3:
      public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
    这个是对view的边界处理,点击事件在边界外返回true,否则返回false,而这个mWindow.shouldCloseOnTouch(this, event)内部实现的是对边界判断。
    同时我们也发现Activity是没有onInterceptTouchEvent方法的。这是因为Android里面只有可以作为双亲的视图才会有onInterceptTouchEvent,因此也只有ViewGroup才有这个方法,用来截取触摸事件

这里简单总结一下:

判断事件是否被消耗
false
true
开始
调用dispatchTouchEvent
getWindow.superDispatchTouchEvent
mDecor.superDispatchTouchEvent<实现了事件从Activity到viewGroup的传递>
Activity.dispatchTouchEvent将结果返回给onTouchEvent
Activity.OnTouchEvent到此事件结束同时判断边界问题
Activity.dispatchTouchEvnet 返回true
结束

这便是Activity的事件传递

viewGroup事件分发

嗯,乍一看完全看不懂,要比Activity的dispatchTouchEvent要复杂的多,但是,我们还是可以看懂一点的。我们知道viewgroup是有onInterceptTouchEvent这里是对事件是否拦截做的处理,源码里也有体现

  // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
 !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
                    // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
                    // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断


                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
这里是对内部view循环判断处理,找到当前点击对view
  // 条件判断的内部调用了该View的dispatchTouchEvent()
 // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)

所以有以下事件分发的流程图
在这里插入图片描述#### View的事件分发机制
view和ViewGroup是在同一个类。

public boolean dispatchTouchEvent(MotionEvent event) {  

        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        } 
        return onTouchEvent(event);  
  }
  // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent()
  //     1. mOnTouchListener != null
  //     2. (mViewFlags & ENABLED_MASK) == ENABLED
  //     3. mOnTouchListener.onTouch(this, event)
  // 下面对这3个条件逐个分析


/**
  * 条件1:mOnTouchListener != null
  * 说明:mOnTouchListener变量在View.setOnTouchListener()方法里赋值
  */
  public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  
    // 即只要我们给控件注册了Touch事件,mOnTouchListener就一定被赋值(不为空)
        
} 

/**
  * 条件2:(mViewFlags & ENABLED_MASK) == ENABLED
  * 说明:
  *     a. 该条件是判断当前点击的控件是否enable
  *     b. 由于很多View默认enable,故该条件恒定为true
  */

/**
  * 条件3:mOnTouchListener.onTouch(this, event)
  * 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)
  */
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
     
            return false;  
        }  
    });
    // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束
    // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event)

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值