当前位置: 首页 > news >正文

Android View的事件分发机制

文章目录

    • 1. 使View滑动
      • 1.1 View基本知识
      • 1.2 使用scrollTo/scrollBy
      • 1.3 使用动画来移动View
      • 1.4 修改layoutParams来移动View
    • 2. View的弹性滑动
      • 2.1 Scroller
      • 2.2使用动画
      • 2. 3 使用延时策略
    • View的事件分发机制
      • 事件分发源码解析
      • 1.Activty事件分发的过程
      • 2.ViewGroup事件分发
      • 3.View的事件分发

1. 使View滑动

1.1 View基本知识

在这里插入图片描述
x并不是等于left,x与left的关系如下

  1. x = left + translationX
  2. y = top + translationY

1.2 使用scrollTo/scrollBy

此方法移动的是VIew的内容,并非VIew的实际位置

   protected int mScrollX;protected int mScrollY;

将view像左移动100像素,向上移动30像素,scrollTo的移动方向和我们熟知的x轴和y轴方向是相反的

 mTextView.scrollTo(100, 30)

scrollBy本质来说也调用的是scrollTo,scrollTo是绝对移动,scrollBy是相对移动
在这里插入图片描述

1.3 使用动画来移动View

这种方式改变的是View的以下属性
translationX 和 translationY

   public float getTranslationX() {return mRenderNode.getTranslationX();}

使用ObjectAnimator来移动View

   ObjectAnimator.ofFloat(animaTextView, "translationX", a, 100f).apply {duration = 100start()}

1.4 修改layoutParams来移动View

val marginLayoutParams = (layoutView.layoutParams as MarginLayoutParams)
marginLayoutParams.marginStart = 300  
layoutView.layoutParams = marginLayoutParams

2. View的弹性滑动

如上1.View的滑动所示,这样移动VIew,有点像闪现,没有移动的过程,在现实使用中并不好看,所以为了有更好的移动效果,需要使用弹性滑动。
实现弹性滑动的方式也有三种,Scroller, 属性动画, 使用延时策略

2.1 Scroller

比如使用Scroller去弹性滑动移动一个TestView

    private lateinit var scrollView: TextViewprivate lateinit var mScroller: Scrolleroverride fun onCreate(savedInstanceState: Bundle?){val scrollBtn: Button = findViewById(R.id.smooth_scroller_btn);scrollView = findViewById(R.id.ScrollerText)scrollBtn.setOnClickListener {scrollView.setScroller(mScroller)smoothScrollerTo(-200, 0)}Log.d(TAG, "btn.scrollX: " + btn.scrollX + " btn.scrollY: " +  btn.scrollY)}private fun smoothScrollerTo(destX: Int, destY: Int) {val curX = scrollView.scrollXval deltaX = destX - curXmScroller.startScroll(curX, 0, deltaX, 0, 2000)scrollView.invalidate()}

Scroller弹性滑动原理

  1. 首先看看Scroller.startScroll方法, 这个方法并不是开始弹性滑动,仅仅是对进行弹性滑动所需要的参数进行赋值
  public void startScroll(int startX, int startY, int dx, int dy, int duration) {mMode = SCROLL_MODE;mFinished = false;mDuration = duration;mStartTime = AnimationUtils.currentAnimationTimeMillis();mStartX = startX;mStartY = startY;mFinalX = startX + dx;mFinalY = startY + dy;mDeltaX = dx;mDeltaY = dy;mDurationReciprocal = 1.0f / (float) mDuration;}

2.真正开始弹性滑动的方法为scrollView.invalidate
在这里插入图片描述
其中最终要的是computeScroll方法, 以TextView的为例

    @Overridepublic void computeScroll() {if (mScroller != null) {if (mScroller.computeScrollOffset()) {mScrollX = mScroller.getCurrX();mScrollY = mScroller.getCurrY();invalidateParentCaches();postInvalidate();  // 再一次重绘View}}}

大致逻辑就是不断地改变mScrollX和mScroll,然后重新drawView来实现弹性滑动,可以间接理解成不断的去调用scrollTo方法
那不断地drawView什么时候停止?
这就需要看mScroller.computeScrollOffset()方法,这个方法主要做了如下几个事情

 public boolean computeScrollOffset() {//如果动画已经完成,return false,从而中断computeScroller方法的调用if (mFinished) {return false;}//计算这个动画经过的时间int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);//如果这个动画已经经过您设定的时间if (timePassed < mDuration) {switch (mMode) {//这个模式是默认模式,就是普通的滑动case SCROLL_MODE:final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);mCurrX = mStartX + Math.round(x * mDeltaX);mCurrY = mStartY + Math.round(x * mDeltaY);break;//这种模式滑动后,会会滑翔一小段再停止case FLING_MODE:final float t = (float) timePassed / mDuration;final int index = (int) (NB_SAMPLES * t);float distanceCoef = 1.f;float velocityCoef = 0.f;if (index < NB_SAMPLES) {final float t_inf = (float) index / NB_SAMPLES;final float t_sup = (float) (index + 1) / NB_SAMPLES;final float d_inf = SPLINE_POSITION[index];final float d_sup = SPLINE_POSITION[index + 1];velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);distanceCoef = d_inf + (t - t_inf) * velocityCoef;}mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));// Pin to mMinX <= mCurrX <= mMaxXmCurrX = Math.min(mCurrX, mMaxX);mCurrX = Math.max(mCurrX, mMinX);mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));// Pin to mMinY <= mCurrY <= mMaxYmCurrY = Math.min(mCurrY, mMaxY);mCurrY = Math.max(mCurrY, mMinY);if (mCurrX == mFinalX && mCurrY == mFinalY) {mFinished = true;}break;}}else {//动画完成mCurrX = mFinalX;mCurrY = mFinalY;mFinished = true;}return true;}

2.2使用动画

像上面有使用ObjectAnimator去实现完全是可以的,这里我们换种方式,使用ValueAnimator去实现

        val valueAnimatorBtn: Button = findViewById(R.id.smooth_animter_btn);val valueAnimatorTextView: TextView = findViewById(R.id.smooth_text_btn)val startX = 0val deltaX = 400valueAnimatorBtn.setOnClickListener {ValueAnimator.ofInt(0, 1).apply {duration = 2000addUpdateListener {val fraction = it.animatedFractionvalueAnimatorTextView.scrollTo(-(startX + (deltaX * fraction).toInt()), 0)}start()}}

2. 3 使用延时策略

           /**  假设需要1000ms 移动 300px, 每次移动30像素*  总共要移动 300 / 30 = 10次  delay时间 1000 / 10 = 100ms* */private var count = 0private val mHandler = object : Handler(Looper.getMainLooper()) {override fun handleMessage(msg: Message) {count ++when (msg.what) {1 -> {count++if (count <= 10) {handleView.scrollBy(-30, 0)sendEmptyMessageDelayed(1, 100)}}else -> {}}}}

View的事件分发机制

MotionEvent是对应点击事件的封装类,常见的点击事件是down、up、move,分别对应按下,抬起,移动
如何把一个点击事件分发给某一个特定的VIew,这就需要了解Android的事件分发机制。
点击事件分发机制由3个重要方法组成

public boolean dispatchTouchEvent(MotionEvent ev)
//onInterceptTouchEvent方法存在于ViewGroup中,View没有
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent event)

这三个方法的关系是
在这里插入图片描述
通过dispatchTouchEvent方法去给VIew分发事件,VIew收到事件后再判断是否拦截该事件,如果要拦截,这个事件才真正的被这个View所消费,否则就将这个事件分发给子View

除了上述三个方法外,还有一个比较重要的Listener,OnTouchListener,可以通过View的setOnTouchListener设置

    public interface OnTouchListener {boolean onTouch(View v, MotionEvent event);}

OnTouchListener的优先级比onTouchEvent高,如果onTouch的方法返回true。代表OnTouchListenr消费这个事件,即该View的onTouchEvent不会被调用,因为onClickListener再OnTouchEvent中被调用,所以该View的点击事件也会失效
最终的事件分发的总体框架如下图所示
在这里插入图片描述
关于View的事件分发有如下几个结论

  1. 从手指按压屏幕,移动,再到抬起,被称作一个事件序列
  2. 正常情况下,一个事件序列只能被同一个View所消费
  3. 一个ViewGroup一但决定拦截(onInterceptTouchEvent返回ture),那么这一个事件序列都有它来处理
  4. 某个VIew如果不消耗down事件,那么一个事件序列中的其他事件也不会让他来处理
  5. ViewGroup默认不消耗任何事件,因为它的onInterceptTouchEvent方法默认返回false
  6. View没有onInterceptTouchEvent方法,默认都消耗事件,除非View不可点击(clickable, longClickable同时为false)
  7. View的enbale不会改变onTouchEvent的返回值

事件分发源码解析

1.Activty事件分发的过程

当一个事件出现时,会以以下的顺序进行传递,Activity–> window ->顶层View(DecorView)
在这里插入图片描述
decorView是顶层View,我们Activity通过setContentView方法设置的VIew最终会设置到DecorView的子元素上(android.R.id.content),DecorVIew的大致结构如下
在这里插入图片描述
事件最终流传到了ViewGroup的dispatchTouchEvent方法中,接下来重点看看这个方法

2.ViewGroup事件分发

先看ViewGrop如何判断是否拦截某个事件

     // Handle an initial down.if (actionMasked == MotionEvent.ACTION_DOWN) {// Throw away all previous state when starting a new touch gesture.// The framework may have dropped the up or cancel event for the previous gesture// due to an app switch, ANR, or some other state change.cancelAndClearTouchTargets(ev);resetTouchState();}final boolean intercepted;//如果是down事件 或者 有子元素处理事件时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;}

一个ViewGroup是否需要通过onInterceptTouchEvent方法来判断是否拦截事件,取决于两个因素,并且必须都满足

  1. 是否是down事件 || 或者有子View接受事件,mFirstTouchTarget 是接受事件的子View的引用
 actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
  1. FLAG_DISALLOW_INTERCEPT标记位,如果没有这个标记位,才会调用onInterceptTouchEvent方法
    这个标记位可通过requestDisallowInterceptTouchEvent方法却设置,并且绝对不会影响down事件,因为resetTouchState方法会把这个标记位抹除
  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

结论1:如果VIewGroup拦截down事件,那么其他事件也会被VIewGroup拦截,并且onInterceptTouchEvent方法只会再down事件判断一次,其他事件序列将不用onInterceptTouchEvent再次判断 原因如下:
在这里插入图片描述
接下来看看如果ViewGroup不拦截事件,事件是如何分发到子VIew的

if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//此处省略一些源码for (int i = childrenCount - 1; i >= 0; i--) {//此处省略一些源码if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {ev.setTargetAccessibilityFocus(false);continue;}//此处省略一些源码                  }}

首先如上源码所示,在down事件时,遍历所有的子元素,判断子View是否有资格被分发事件,判断条件有2条,并且条件是或的关系

  1. 通过canReceivePointerEvents方法去判断 View是VISIBLE,或者正在播放动画
    /*** Returns whether this view can receive pointer events.* @return {@code true} if this view can receive pointer events.* @hide*/protected boolean canReceivePointerEvents() {return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;}
  1. 通过isTransformedTouchPointInView方法去判断 点击的是否是这个View的区域
    这个方法就不详细讲了,大致实现方式就是通过的点击的x和y坐标结合View的长宽和位置去判断

判断完子View是否有资格被分发事件后就会走到下面的逻辑

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 indexfor (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;
}

如上源码所示其中最关键就是dispatchTransformedTouchEvent方法,在这个方法中回去调用子View的dispatchTouchEvent方法,大致伪代码如下所示

//如果子View为null,就会调用父类(View)的dispatchTouchEvent方法if (child == null) {handled = super.dispatchTouchEvent(event);} else {//将事件分发给子Viewhandled = child.dispatchTouchEvent(event);}

再刚开始分析源码时提到了mFirstTouchTarget成员,他会影响ViewGroup是否拦截某个事件的判断,该成员的赋值时机就在addTouchTarget方法中

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);target.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;}

根据衣裳代码会发现mFirstTouchTarget 本质是一个链表头,连接着每个被分发事件的子VIew,并采用头插法插入新元素,所以之前才会说mFirstTouchTarget != null代表着已经有事件成功分发给的子元素。当一个事件序列被分发完mFirstTouchTarget 链表就会被清空

  if (canceled|| actionMasked == MotionEvent.ACTION_UP|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//该方法会便利链表全部置空resetTouchState();

当然也会有这样一种情况,如果将事件分发给子元素,但是子元素也不消耗这个事件,即onTouchEvent方法返回false,如果时这种情况mFirstTouchTarget还是为null的状态,就会走到如下的逻辑中

 if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);}

dispatchTransformedTouchEvent方法的child参数为空,上面也分析过这个方法,如果child==null,就会调用super.dispatchTouchEvent方法,该方法最终又会调用ViewGroup的onTouchEvent方法
结论:如果一个子VIew不消费分过来的事件,即onTouchEvent返回false,最终事件会交给ViewGroup的onTouchEvent来处理, 即子元素不处理事件,父亲来处理
down事件时才会遍历子元素,才有机会给mFirstTouchTarget赋值,所以这就得出一个结论:
如果一个VIew不接受down事件,一个事件序列中后面的事件也一定不会给它了

3.View的事件分发

上面说到了如果子View不接受ViewGroup分发的事件,就会调用super.dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,我们接下来分析下这个方法

    if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}if (!result && onTouchEvent(event)) {result = true;}

上面的源码也印证了上面之前说的一个结论:mOnTouchListener的优先级别比onTouchEvent高,如果mOnTouchListener的onTouch方法的返回值时true,那onTouchEvent就不是执行

接下来好好看看onTouchEvent方法

        if ((viewFlags & ENABLED_MASK) == DISABLED&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;// A disabled view that is clickable still consumes the touch// events, it just doesn't respond to them.return clickable;}

从上面源码中,我们又印证了一个结论一个View是否消费事件与是否enable无关,只与是否click有关

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {.................省略switch (action) {case MotionEvent.ACTION_UP:performClickInternal();.....................省略                    }.................省略}.................省略
}

如果VIew的可点击的,会在up事件时通过performClickInternal方法去调用onClickListener的onClick方法

onTouchEvent主要是View怎么处理事件的逻辑,这里不再细讲

总结

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Conditional Flow Matching: Simulation-Free Dynamic Optimal Transport论文阅读笔记
  • Paxos算法概述:从Basic Paxos到Fast Paxos及在Zookeeper中的应用
  • 计算机毕业设计Python+Flask弹幕情感分析 B站视频数据可视化 B站爬虫 机器学习 深度学习 人工智能 NLP文本分类 数据可视化 大数据毕业设计
  • 76. 最小覆盖子串【 力扣(LeetCode) 】
  • Redis清空缓存
  • 分布式ID-一窥雪花算法的原生实现问题与解决方案(CosId)
  • C语言——预处理
  • 【速览】设计模式(更新中)
  • 【Linux】软硬链接
  • 0.91寸OLED迷你音频频谱
  • 乘积小于 K 的子数组(LeetCode)
  • 软件工程(2)面向对象方法:Booch方法与开发实例
  • 机器学习笔记六-朴素贝叶斯
  • 【机器学习】探索机器学习在旅游业的革新之旅
  • BI分析实操案例分享:零售企业如何利用BI工具对销售数据进行分析?
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • bearychat的java client
  • Bytom交易说明(账户管理模式)
  • HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP
  • javascript 总结(常用工具类的封装)
  • JS专题之继承
  • Linux快速复制或删除大量小文件
  • 安卓应用性能调试和优化经验分享
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 从PHP迁移至Golang - 基础篇
  • 浅谈Golang中select的用法
  • 如何打造100亿SDK累计覆盖量的大数据系统
  • 时间复杂度与空间复杂度分析
  • 使用putty远程连接linux
  • 微服务核心架构梳理
  • 一起参Ember.js讨论、问答社区。
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • 你学不懂C语言,是因为不懂编写C程序的7个步骤 ...
  • 曾刷新两项世界纪录,腾讯优图人脸检测算法 DSFD 正式开源 ...
  • ​ ​Redis(五)主从复制:主从模式介绍、配置、拓扑(一主一从结构、一主多从结构、树形主从结构)、原理(复制过程、​​​​​​​数据同步psync)、总结
  • ​TypeScript都不会用,也敢说会前端?
  • # Python csv、xlsx、json、二进制(MP3) 文件读写基本使用
  • # 计算机视觉入门
  • #### golang中【堆】的使用及底层 ####
  • #Datawhale AI夏令营第4期#多模态大模型复盘
  • #我与Java虚拟机的故事#连载18:JAVA成长之路
  • (13):Silverlight 2 数据与通信之WebRequest
  • (C#)if (this == null)?你在逗我,this 怎么可能为 null!用 IL 编译和反编译看穿一切
  • (zhuan) 一些RL的文献(及笔记)
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (六) ES6 新特性 —— 迭代器(iterator)
  • (牛客腾讯思维编程题)编码编码分组打印下标题目分析
  • (十)Flink Table API 和 SQL 基本概念
  • (心得)获取一个数二进制序列中所有的偶数位和奇数位, 分别输出二进制序列。
  • (一)Thymeleaf用法——Thymeleaf简介
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • ./include/caffe/util/cudnn.hpp: In function ‘const char* cudnnGetErrorString(cudnnStatus_t)’: ./incl
  • .bat批处理(八):各种形式的变量%0、%i、%%i、var、%var%、!var!的含义和区别
  • .Net 8.0 新的变化
  • .Net core 6.0 升8.0