关于java:Android事件分发机制四学了事件分发有什么用

4次阅读

共计 9290 个字符,预计需要花费 24 分钟才能阅读完成。

“学了事件散发,影响我 CV 大法吗?”

“影响我陪女朋友的工夫”

“…..”

前言

Android 事件散发机制曾经来到第四篇了,在前三篇中:

  • Android 事件散发机制一:事件是如何达到 activity 的?: 从 window 机制登程剖析了事件散发的整体流程,以及事件散发的真正终点
  • Android 事件散发机制二:viewGroup 与 view 对事件的解决 : 源码剖析了 viewGroup 和 view 是如何散发事件的
  • Android 事件散发机制三:事件散发工作流程 : 剖析了触摸事件在控件树中的散发流程模型

那么对于事件散发的常识在下面三篇文章也就剖析地差不多了,接下来就剖析一下学了之后该如何使使用到理论开发中,简略论述一下笔者的思考。

Android 中的 view 个别由两个重要的局部组成:绘制和触摸反馈。如何精准地针对用户的操作给出正确的反馈,是咱们学事件散发最重要的指标。

使用事件散发个别有两个场景:给 view 设置监听器和自定义 view。接下来就针对这两方面开展剖析,最初再给出笔者的一些思考与总结。

监听器

触摸事件监听器能够说是咱们接触 Android 事件体系的第一步。监听器通常有:

  • OnClickListener:单击事件监听器
  • OnLongClickListener:长按事件监听器
  • OnTouchListener:触摸事件监听器

这些是咱们应用得最频繁的监听器,他们之间的关系是:

if (mOnTouchListener!=null && mTouchListener.onTouch(event)){return true;}else{if (单击事件){mOnClickListener.onClick(view);
    }else if(长按事件){mOnLongClickListener.onLongClick(view);
    }
}

下面的伪代码能够很显著地发现:onTouchListener 是间接把 MotionEvent 对象间接接管给本人解决且会最先调用,而其余的两个监听器是 view 判断点击类型之后再别离调用

除此之外,另一个很常见的监听器是双击监听器,但这种监听器并不是 view 默认反对的,须要咱们本人去实现。双击监听器的实现思路能够参考 view 实现长按监听器的思路来实现:

当咱们接管到点击事件时,能够发送一个单击延时工作。如果在延迟时间到还没收到另一个点击事件,那么这就是一个单击事件;如果在延迟时间内收到另一个点击事件,阐明这是一个双击事件,并勾销延时工作。

咱们能够实现 view.OnClickListener 接口来实现以上逻辑,外围代码如下:

// 实现 onClickListener 接口
class MyClickListener() : View.OnClickListener{
    private var isClicking = false
    private var singleClickListener : View.OnClickListener? = null
    private var doubleClickListener : View.OnClickListener? = null
    private var delayTime = 1000L
    private var clickCallBack : Runnable? = null
    private var handler : Handler = Handler(Looper.getMainLooper())

    override fun onClick(v: View?) {
        // 创立一个单击提早工作,延迟时间到了之后触发单击事件
        clickCallBack = clickCallBack?: Runnable {singleClickListener?.onClick(v)
            isClicking = false
        }
        // 如果曾经点击过一次,在延迟时间内再次承受到点击
        // 意味着这是个双击事件
        if (isClicking){
            // 移除提早工作,回调双击监听器
            handler.removeCallbacks(clickCallBack!!)
            doubleClickListener?.onClick(v)
            isClicking = false
        }else{
            // 第一次点击,发送提早工作
            isClicking = true
            handler.postDelayed(clickCallBack!!,delayTime)
        }
    }
...
}

代码中实现了创立了一个 View.OnclickListener 接口实现类,并在类型实现单击和双击的逻辑判断。咱们能够如下应用这个类:

val c = MyClickListener()
// 设置单击监听事件
c.setSingleClickListener(View.OnClickListener {Log.d(TAG, "button: 单击事件")
})
// 设置双击监听事件
c.setDoubleClickListener(View.OnClickListener {Log.d(TAG, "button: 双击事件")
})
// 把监听器设置给按钮
button.setOnClickListener(c)

这样就实现了按钮的双击监听了。

其余类型的监听器如:三击、双击长按等等,都能够基于这种思路来实现监听器接口。

自定义 view

在自定义 view 中,咱们能够更加灵便地使用事件散发来解决理论的需要。举几个例子:

滑动嵌套问题:外层是 viewPager,里层是 recyclerView,要实现左右滑动切换 viewPager,高低滑动 recyclerView。这也就是驰名的滑动抵触问题。相似的还有外层 viewPager,里层也是能够左右滑动的 recyclerView。
实时触摸反馈问题:如设计一个按钮,要让他按下的时候放大升高高度,放开的时候复原到原来的大小和高度,而且如果在一个可滑动的容器中,按下之后滑动不会触发点击事件而是把事件交给外层可滑动容器。

咱们能够发现,基本上都是基于理论的开发需要来灵活运用事件散发。具体到代码实现,都是围绕着三个要害办法开展:dispatchTouchEventonIntercepterTouchEventonTouchEvent。这三个办法在 view 和 viewGroup 中曾经有了默认的实现,而咱们须要基于默认实现来实现咱们的需要。上面看看几种常见的场景如何实现。

实现方块按下放大

咱们先来看看具体的实现成果:

方块按下后,会放大高度变低透明度减少,开释又会复原。

这个需要能够通过联合属性动画来实现。按钮块自身有高度、有圆角,咱们能够思考继承 cardView 来实现,重写 cardView 的 dispatchTouchEvent 办法,在按下的时候,也就是接管到 down 事件的时候放大,在接管到 up 和 cancel 事件的时候复原。留神,这里可能会漠视 cancel 事件,导致按钮块的状态无奈复原,肯定要加以思考 cancel 事件。而后来看下代码实现:

public class NewCardView extends CardView {

    // 点击事件到来的时候进行判断解决
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 获取事件类型
        int actionMarked = ev.getActionMasked();
        // 依据工夫类型判断调用哪个办法来展现动画
        switch (actionMarked){
            case MotionEvent.ACTION_DOWN :{clickEvent();
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                upEvent();
                break;
            default: break;
        }
        // 最初回调默认的事件散发办法即可
        return super.dispatchTouchEvent(ev);
    }

    // 手指按下的时候触发的事件; 大小高度变小,透明度缩小
    private void clickEvent(){setCardElevation(4);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(ObjectAnimator.ofFloat(this,"scaleX",1,0.97f),
                ObjectAnimator.ofFloat(this,"scaleY",1,0.97f),
                ObjectAnimator.ofFloat(this,"alpha",1,0.9f)
        );
        set.setDuration(100).start();}

    // 手指抬起的时候触发的事件;大小高度复原,透明度复原
    private void upEvent(){setCardElevation(getCardElevation());
        AnimatorSet set = new AnimatorSet();
        set.playTogether(ObjectAnimator.ofFloat(this,"scaleX",0.97f,1),
                ObjectAnimator.ofFloat(this,"scaleY",0.97f,1),
                ObjectAnimator.ofFloat(this,"alpha",0.9f,1)
        );
        set.setDuration(100).start();}
}

动画方面的内容就不剖析了,不属于本文的领域。能够看到咱们只是给 cardView 设置了动画成果,监听事件咱们能够设置给 cardView 外部的 ImageView 或者间接设置给 CardView。须要留神的是,如果设置给 cardView 须要重写 cardView 的 intercepTouchEvent 办法永远返回 true,避免事件被子 view 生产而无奈触发监听事件。

解决滑动抵触

滑动抵触是事件散发使用最频繁的场景,也是一个重难点(敲黑板,考试要考的)。滑动抵触的根本场景有以下三种:

  • 状况一:内外 view 的滑动方向不同,例如 viewPager 嵌套 ListView
  • 状况二:内外 view 滑动方向雷同,例如 viewPager 嵌套程度滑动的 recyclerView
  • 状况三:状况一和状况二的组合

解决这类问题个别有两个步骤:确定最终实现成果、代码实现。

滑动抵触的解决须要联合具体的实现需求,而不是一套解决方案能够解决所有的滑动抵触问题,这不事实。因而在解决这类问题时,须要先确定好最终的实现成果,而后再依据这个成果去思考代码实现。这里次要探讨状况一和状况二,状况三类同。

状况一

状况一是内外滑动方向不统一。这种状况的通用解决方案就是:依据手指滑动直线与水平线的角度来判断是左右滑动还是高低滑动:

如果这个角度小于 45 度,能够认为是在左右滑动,如果大于 45 度,则认为是高低滑动。那么当初确定好解决方案,接下来就思考如何代码实现。

滑动角度能够通过两个间断的 MotionEvent 对象的坐标计算出来,之后咱们再依据角度的状况抉择把事件交给内部容器还是外部 view。这里依据事件处理的地位可分为 外部拦截法和内部拦截法

  • 内部拦截法:在 viewGroup 中判断滑动的角度,如果合乎本身滑动方向生产则拦挡事件
  • 外部拦截法:在外部 view 中判断滑动的角度,如果是合乎本身滑动方向则持续生产事件,否则申请内部 viewGroup 拦挡事件处理

从实现的复杂度来看,内部拦截法会更加优良,不须要里外 view 去配合,只须要 viewGroup 本身做好事件拦挡解决即可。两者的区别就在于主动权在谁的手上。如果 view 须要做更多的判断能够采纳外部拦截法,而个别状况下采纳内部拦截法会更加简略。

接下来思考一下这两种办法的代码实现。


内部拦截法中,重点在于是否拦挡事件,那么咱们的重心就放在了 onInterceptTouchEvent 办法中。在这个办法中计算滑动角度并判断是否要进行拦挡。这里以 ScrollView 为例子(内部是垂直滑动,外部是程度滑动),代码如下:

public class MyScrollView extends ScrollView {
    // 记录上一次事件的坐标
    float lastX = 0;
    float lastY = 0;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {int actionMasked = ev.getActionMasked();
        // 不能拦挡 down 事件,否则子 view 永远无奈获取到事件
        // 不能拦挡 up 事件,否则子 view 的点击事件无奈被触发
        if (actionMasked == MotionEvent.ACTION_DOWN || actionMasked == MotionEvent.ACTION_UP){lastX = ev.getX();
            lastY = ev.getY();
            return false;
        }   

        // 获取斜率并判断
        float x = ev.getX();
        float y = ev.getY();
        return Math.abs(lastX - x) < Math.abs(lastY - y);
    }
}

代码的实现思路很简略,记录两次触控点的地位,而后计算出斜率来判断是垂直还是程度滑动。代码中有个须要留神的点:viewGroup 不能拦挡 up 事件和 down 事件。如果拦挡了 down 事件那么子 view 将永远接管不到事件信息;如果拦挡了 up 事件那么子 view 将永远无奈触发点击事件。

下面的代码是事件散发的外围代码,更加具体的代码还须要依据理论需要去欠缺细节,但整体的逻辑是不变的。


外部拦截法的思路和内部拦挡的思路很像,只是判断的地位放到了外部 view 中。外部拦截法意味着外部 view 必须要有管制事件流走向的能力,能力对事件进行解决。这里就使用到了外部 view 一个重要的办法:requestDisallowInterceptTouchEvent

这个办法能够强制外层 viewGroup 不拦挡事件。因而,咱们能够让 viewGroup 默认拦挡除了 down 事件以外的所有事件。当子 view 须要处理事件时,只须要调用此办法即可获取事件;而当想要把事件交给 viewGroup 解决时,那么只须要勾销这个标记,外层 viewGroup 就会拦挡所有事件。从而达到外部 view 管制事件流走向的目标。

代码实现须要分两步走,首先是设置内部 viewGroup 拦挡除了 down 事件以外的所有事件(这里用 viewPager 和 ListView 来进行代码演示):

public class MyViewPager extends ViewPager {public boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.getActionMasked()==MotionEvent.ACTION_DOWN){return false;}
        return true;
    }
}

接下来须要重写外部 view 的 dispatchTouchEvent 办法:

public class MyListView extends ListView {
    float lastX = 0;
    float lastY = 0;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {int actionMarked = ev.getActionMasked();
        switch (actionMarked){
            // down 事件,必须申请不拦挡,否则拿不到 move 事件无奈进行判断
            case MotionEvent.ACTION_DOWN:{requestDisallowInterceptTouchEvent(true);
                break;
            }
            // move 事件,进行判断是否处理事件
            case MotionEvent.ACTION_MOVE:{float x = ev.getX();
                float y = ev.getY();
                // 如果滑动角度大于 90 度本人处理事件
                if (Math.abs(lastY-y)<Math.abs(lastX-x)){requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            default:break;
        }
        // 保留本次触控点的坐标
        lastX = ev.getX();
        lastY = ev.getY();
        // 调用 ListView 的 dispatchTouchEvent 办法来处理事件
        return super.dispatchTouchEvent(ev);
    }
}

两种办法的代码思路基本一致,然而外部拦截法会更加简单一点,所以在个别的状况下,还是应用内部拦截法较好。

到这里曾经解决了状况一的滑动抵触解决方案,接下来看看状况二的滑动抵触如何解决。

状况二

第二种状况是里外容器的滑动方向是统一的,这种状况的支流解决办法有两种,一种是外容器先滑动,外容器滑动到边界之后再滑动外部 view,例如京东 app(留神向下滑动时的状况):

第二种状况的外部 view 先滑动,等外部 view 滑动到边界之后再滑动内部 viewGroup, 例如饿了么 app(留神向下滑动时的状况):

这两种计划没有孰好孰坏,而是须要依据具体的业务需要来确定具体的解决方案。上面就上述的第二种计划开展剖析,第一种计划类同。

首先剖析一下具体的成果:外层 viewGroup 与内层 view 的滑动方向是统一的,都是垂直滑动或程度滑动;向上滑动时,先滑动 viewGroup 到顶部,再滑动外部 view;向下滑动时,先滑动外部 view 到顶部后再滑动外层 viewGroup。

这里咱们采纳内部拦截法来实现。首先咱们先确定好咱们的布局:

最外层是一个 ScrollView,外部首先是一个 LinearLayout,因为 ScrollView 只能有一个 view。外部顶部是一个 LinearLayout 能够搁置头部布局,上面是一个 ListView。当初须要确定 ScrollView 的拦挡规定:

  1. 当 ScrollView 没有滑动到底部时,间接给 ScrollView 解决
  2. 当 ScrollView 滑动到底部时:

    • 如果 LinearLayout 没有滑动到顶部,则交给 ListView 解决
    • 如果 LinearLayout 滑动到顶部:

      • 如果是向上滑动则交给 listView 解决
      • 如果是向下滑动则交给 ScrollView 解决

接下来就能够确定咱们的代码了:

public class MyScrollView extends ScrollView {
    ...
    float lastY = 0;
    boolean isScrollToBottom = false;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        int actionMarked = ev.getActionMasked();
        switch (actionMarked){
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:{
                // 这三种事件默认不拦挡,必须给子 view 解决
                break;
            }
            case MotionEvent.ACTION_MOVE:{LinearLayout layout = (LinearLayout) getChildAt(0);
                ListView listView = (ListView)layout.getChildAt(1);
                // 如果没有滑动到底部,由 ScrollView 解决,进行拦挡
                if (!isScrollToBottom){
                    intercept = true;
                    // 如果滑动到底部且 listView 还没滑动到顶部,不拦挡
                }else if (!ifTop(listView)){intercept = false;}else{
                    // 否则判断是否是向下滑
                    intercept = ev.getY() > lastY;}
                break;
            }
            default:break;
        }
        // 最初记录地位信息
        lastY = ev.getY();
        // 调用父类的拦挡办法,ScrollView 须要做一些解决,不然可能会造成无奈滑动
        super.onInterceptTouchEvent(ev);
        return intercept;
    }
    ...
}

代码中我还减少了如果 listView 上面有 view 的状况,判断是否滑动到底部。判断 listView 滑动状况和 scrollView 滑动状况的代码如下:

{if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        // 设置滑动监听
        setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {ViewGroup viewGroup = (ViewGroup)v;
            isScrollToBottom = v.getHeight() + scrollY >= viewGroup.getChildAt(0).getHeight();});
    }
}
// 判断 listView 是否达到顶部
private boolean ifTop(ListView listView){if (listView.getFirstVisiblePosition()==0){View view = listView.getChildAt(0);
        return view != null && view.getTop() >= 0;}
    return false;
}

最终的实现成果如下图:

这样就简略地解决一个滑动抵触了。然而要留神的是,在理论问题中,往往有更加简单的细节须要解决。而上述只是把解决滑动抵触的一个思维剖析了一下,具体到业务上,还须要去仔细打磨代码才行。有趣味能够去看看 NeatedScrollView 是如何解决滑动抵触的源码。

最初

事件散发作为 Android 的基础知识储备堪称是十分重要。不能说学了事件散发,就能够间接一飞冲天。而是把握了事件散发之后,面对一些具体的需要,就有了肯定的思路去解决。或者在理解一些框架的源码的时候,懂得他这些代码是什么意思。

学习事件散发的过程中,深入研究了很多的源码,有一些小伙伴感觉没必要。理论开发中也就用到那三个次要的办法,理解一个次要的流程就足够了。我想说:的确是这样;但没有钻研背地的原理,就只能知其然而不知其所以然。当遇到一些异样的状况时,就无奈从源码的角度去剖析后果的 bug。学习源码的过程中,也是与设计 android 零碎的作者的一种交换。假使当初没有事件散发机制,那么我该如何去解决触摸信息的散发问题?学习的过程就是在思考 android 零碎作者给出的解决方案。而把握原理之后,对于事件散发的问题,稍加思考和剖析,也就手到擒来了。正所谓:

只有战胜 10 级的敌人,能力掌控 9 级的敌人。

心愿文章对你有帮忙。

要不留下个小小的点赞激励一下作者?

全文到此,原创不易,感觉有帮忙能够点赞珍藏评论转发。
笔者满腹经纶,有任何想法欢送评论区交换斧正。
如需转载请评论区或私信交换。

另外欢迎光临笔者的集体博客:传送门

正文完
 0