Android TouchEvent事件传递和情景分析

一、关于View事件传递

Android 事件主要有两种,KeyEvent和TouchEvent,其中TouchEvent事件相对复杂。

  • KeyEvent传递,这种相对简单,只要找到当前的foucs View,因此,想接收事件的View必须Focusable。
  • TouchEvent传递,涉及父布局拦截、子布局拦截两种状态,处理起来相对复杂。

二、TouchEvent事件传递

【1】事件的核心点

涉及事件传递问题,一般都自定义ViewGroup的情况或者滑动ViewGroup冲突时才会有相应的需求,事件传递的核心是ViewGroup而不是子View。TouchEvent传递事件有两个过程,一个是 分发过程 ,一个是 处理过程 ,单独的说具体原理实际上很难处理,因此,我们这里通过情景分析方式来说明各种需求事件如何传递。

【2】事件的起点

ACTION_DOWN作为事件的起点,是事件分发和处理过程中不能忽视的因素,因为一旦ACTION_DOWN被某View处理(这里的“处理”是指onTouchEvent返回值为true),因为这后续的事件 直接 由该View处理,除非该View主动放弃,主动放弃需调用父布局的dispatchTouchEvent发送ACTION_CANCEL。

【3】事件转移

本文虽然是事件传递,但事件转移也是非常重要的,我们可能遇到的情景是滑动子View,当滑动到顶部时需要将事件转移给父布局,让父布局滑动。当然,Android 官方提供了NestedScrolling机制,也是一种很不错的解决方式。但我们还需要了解另一种方式,流程如下。

结合【2】,我们转移事件之前,需要调用父布局的dispatchTouchEvent发送ACTION_CANCEL,接着通过父布局的dispatchTouchEvent发送ACTION_DOWN,这样相当于创建了一个事件的起点,通过这种方式,可以让父布局去捕获事件ACTION_DOWN事件,从而完成转移。

【4】情景分析

在下面的论述中,我们用ChildView指代子View,ParentLayout指代父View。

情景一:parentLayout不需要事件,ChildView需要事件

ParentLayout不做任何处理,ChildView在onTouchEvent中处理ACTION_DOWN返回true,这样后续事件由ChildView处理

情景二:ParentLayout需要事件,ChildView不需要事件

ChildView不做任何处理,parentLayout 中的onInteceptTouchEvent和onTouchEvent中处理ACTION_DOWN返回true,后续事件由parentLayout处理

情景三:用户视觉中,谁先被点击,事件交由谁处理

父布局dispatchTouchEvent 中需要判断点击位置,如果点击位置在用户视觉中不属于它自己,在onInteceptTouchEvent中不要拦截该事件,这样传递事件给ChildView,否则走默认逻辑。

情景四:通过滑动方向转移事件

这种是最复杂的事件传递方式,需要父布局在dispatchTouchEvent中直接处理事件, 初始状态 ,接收到ACTION_DOWN,优先拦截,满足分发给ChildView,满足自己处理事件的条件时自己来拦截。最为典型的时时SlidingMenu + ScrollView的实现,作为父布局的SlidingMenu需要和ScrollView以某种方式联动,当然不联动也是可以的。

不联动方式:

父布局检测到该滑动方向不由自己处理,然后修改当前的事件为ACTION_DOWN, 记录状态 ,不让onInteceptTouchEvent拦截下一个ACTION_DOWN ,最后调用super.dispatchTouchEvent分发事件。

父布局检测到该事件可以由自己处理,然后修改当前事件为ACTION_DWON, 记录状态 ,让onInteceptTouchEvent拦截ACTION_DOWN,最后调用super.dispatchTouchEvent分发事件。

状态重置非常重要,我们需要在parentLayout的dispatchTouchEvent处理ACTION_UP或者ACTION_CANCEL时让状态值恢复到原来的状态。

联动方式:

联动方式主要是互相通过ChildView和ParentLayout互相实现某种接口进行通信,可以说增加View间的耦合,降低View的通用性,但这种方式无疑是最简单的,同样也是最好用的,Android 官方推荐的NestedScrolling这种机制,目前NestedScrollView和RecyclerView都实现了,通过这种方式让很多复杂的View实现变得很简单,这种方式也越来越多的在View组件中出现。

情景五:事件双响应

parentLayout 和childView 同时响应事件,这种情况下parentLayout 需要在onInteceptTouchEvent中拦截ACTION_DOWN,同时主动调用dispatchTransformTouchEvent分发事件给View

三、总结

  • ViewGroup拦截事件需要两次处理TouchEvent,分别是onInteceptTouchEvent和onTouchEvent

  • 子View拦截事件只需要子啊onTouchEvent进行处理

  • 事件分发的主动权由ViewGroup掌握
  • 事件一旦被一方捕获,那么后续事件只能由捕获方处理
  • 事件转移之前需要触发ACTION_CANCEL

四、使用场景案例收集

【1】EditText分为编辑态和非编辑态,我们想做到编辑态时让EditTex能正常拦截焦点,非编辑态能处理点击事件。

网上有很多方法,比如修改bufferType,设置setOnTouchFocusable等,实际上都能实现,这里我们提供另一种方案:

当EditText 为可编辑时(enable=true),父布局不拦截事件,当EditText为不可编辑态(enable=false)时,父布局拦截事件,让父布局代理点击事件

public class EditInputLayout extends FrameLayout {

    private boolean mInterceptTouchEvent = false;

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

    public EditInputLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public EditInputLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected boolean addViewInLayout(View child, int index, ViewGroup.LayoutParams params, boolean preventRequestLayout) {
        if (!(child instanceof EditText)) {
            throw new IllegalArgumentException("Child Widget must be EditText in this layout");
        }
        return super.addViewInLayout(child, index, params, preventRequestLayout);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int actionMasked = ev.getAction() & MotionEvent.ACTION_MASK;
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            View child = findClickTarget(ev);
            //处理当前事件,保留原始状态下的
            if (child != null && !child.isEnabled()  ||  super.onInterceptTouchEvent(ev)) {
                mInterceptTouchEvent = true;
                return true;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        final int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            super.onTouchEvent(event);//保证原有逻辑执行
            return mInterceptTouchEvent ;
        }
        if (action == MotionEvent.ACTION_UP
                || action == MotionEvent.ACTION_CANCEL
                || action == MotionEvent.ACTION_OUTSIDE) {
            mInterceptTouchEvent = false;
        }
        return super.onTouchEvent(event);
    }

    private View findClickTarget(MotionEvent ev) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            float x = ev.getX();
            float y = ev.getY();
            if (child.getX() < x && (child.getX() + child.getWidth()) > x) {
                if (child.getY() < y && (child.getY() + child.getHeight()) > y) {
                    return child;
                }
            }
        }
        return null;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值