前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >View,ViewGroup的Touch事件的分发机制

View,ViewGroup的Touch事件的分发机制

作者头像
xiangzhihong
发布于 2018-01-30 09:05:06
发布于 2018-01-30 09:05:06
1.2K0
举报
文章被收录于专栏:向治洪向治洪

ViewGroup的事件分发机制

我们用手指去触摸Android手机屏幕,就会产生一个触摸事件,但是这个触摸事件在底层是怎么分发的呢?这个我还真不知道,这里涉及到操作硬件(手机屏幕)方面的知识,也就是Linux内核方面的知识,我也没有了解过这方面的东西,所以我们可能就往上层来分析分析,我们知道Android中负责与用户交互,与用户操作紧密相关的四大组件之一是Activity, 所以我们有理由相信Activity中存在分发事件的方法,这个方法就是dispatchTouchEvent(),我们先看其源码吧

代码语言:js
AI代码解释
复制
 public boolean dispatchTouchEvent(MotionEvent ev) {  
  
  //如果是按下状态就调用onUserInteraction()方法,onUserInteraction()方法 
  //是个空的方法, 我们直接跳过这里看下面的实现 
  if (ev.getAction() == MotionEvent.ACTION_DOWN) {  
             onUserInteraction();  
         }  
  
  if (getWindow().superDispatchTouchEvent(ev)) {  
  return true;  
         }  
  
  //getWindow().superDispatchTouchEvent(ev)返回false,这个事件就交给Activity 
  //来处理, Activity的onTouchEvent()方法直接返回了false 
  return onTouchEvent(ev);  
     }  

这个方法中我们还是比较关心getWindow()的superDispatchTouchEvent()方法,getWindow()返回当前Activity的顶层窗口Window对象,我们直接看Window API的superDispatchTouchEvent()方法

代码语言:js
AI代码解释
复制
 /** 
      * Used by custom windows, such as Dialog, to pass the touch screen event 
      * further down the view hierarchy. Application developers should 
      * not need to implement or call this. 
      * 
      */ 
  public abstract boolean superDispatchTouchEvent(MotionEvent event);  

这个是个抽象方法,所以我们直接找到其子类来看看superDispatchTouchEvent()方法的具体逻辑实现,Window的唯一子类是PhoneWindow,我们就看看PhoneWindow的superDispatchTouchEvent()方法

代码语言:js
AI代码解释
复制
 public boolean superDispatchTouchEvent(KeyEvent event) {  
  return mDecor.superDispatcTouchEvent(event);  
     }  

里面直接调用DecorView类的superDispatchTouchEvent()方法,或许很多人不了解DecorView这个类,DecorView是PhoneWindow的一个final的内部类并且继承FrameLayout的,也是Window界面的最顶层的View对象,这是什么意思呢?别着急,我们接着往下看

我们先新建一个项目,取名AndroidTouchEvent,然后直接用模拟器运行项目, MainActivity的布局文件为

代码语言:html
AI代码解释
复制
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
  xmlns:tools="http://schemas.android.com/tools" 
  android:layout_width="match_parent" 
  android:layout_height="match_parent" 
  tools:context=".MainActivity" > 
  
  <TextView 
  android:layout_width="wrap_content" 
  android:layout_height="wrap_content" 
  android:layout_centerHorizontal="true" 
  android:layout_centerVertical="true" 
  android:text="@string/hello_world" /> 
  
 </RelativeLayout> 

利用hierarchyviewer工具来查看下MainActivity的View的层次结构,如下图

我们看到最顶层就是PhoneWindow$DecorView,接着DecorView下面有一个LinearLayout, LinearLayout下面有两个FrameLayout

上面那个FrameLayout是用来显示标题栏的,这个Demo中是一个TextView,当然我们还可以定制我们的标题栏,利用getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,R.layout.XXX); xxx就是我们自定义标题栏的布局XML文件 下面的FrameLayout是用来装载ContentView的,也就是我们在Activity中利用setContentView()方法设置的View,现在我们知道了,原来我们利用setContentView()设置Activity的View的外面还嵌套了这么多的东西

我们来理清下思路,Activity的最顶层窗体是PhoneWindow,而PhoneWindow的最顶层View是DecorView,接下来我们就看DecorView类的superDispatchTouchEvent()方法

代码语言:java
AI代码解释
复制
 public boolean superDispatchTouchEvent(MotionEvent event) {  
  return super.dispatchTouchEvent(event);  
         }  

在里面调用了父类FrameLayout的dispatchTouchEvent()方法,而FrameLayout中并没有dispatchTouchEvent()方法,所以我们直接看ViewGroup的dispatchTouchEvent()方法

代码语言:java
AI代码解释
复制
 /** 
     * {@inheritDoc} 
     */ 
  @Override 
  public boolean dispatchTouchEvent(MotionEvent ev) {  
  final int action = ev.getAction();  
  final float xf = ev.getX();  
  final float yf = ev.getY();  
  final float scrolledXFloat = xf + mScrollX;  
  final float scrolledYFloat = yf + mScrollY;  
  final Rect frame = mTempRect;  
  
  //这个值默认是false, 然后我们可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法 
  //来改变disallowIntercept的值 
  boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
  
  //这里是ACTION_DOWN的处理逻辑 
  if (action == MotionEvent.ACTION_DOWN) {  
  //清除mMotionTarget, 每次ACTION_DOWN都很设置mMotionTarget为null 
  if (mMotionTarget != null) {  
                mMotionTarget = null;  
            }  
  
  //disallowIntercept默认是false, 就看ViewGroup的onInterceptTouchEvent()方法 
  if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
                ev.setAction(MotionEvent.ACTION_DOWN);  
  final int scrolledXInt = (int) scrolledXFloat;  
  final int scrolledYInt = (int) scrolledYFloat;  
  final View[] children = mChildren;  
  final int count = mChildrenCount;  
  //遍历其子View 
  for (int i = count - 1; i >= 0; i--) {  
  final View child = children[i];  
  
  //如果该子View是VISIBLE或者该子View正在执行动画, 表示该View才 
  //可以接受到Touch事件 
  if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                            || child.getAnimation() != null) {  
  //获取子View的位置范围 
                        child.getHitRect(frame);  
  
  //如Touch到屏幕上的点在该子View上面 
  if (frame.contains(scrolledXInt, scrolledYInt)) {  
  // offset the event to the view's coordinate system 
  final float xc = scrolledXFloat - child.mLeft;  
  final float yc = scrolledYFloat - child.mTop;  
                            ev.setLocation(xc, yc);  
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
  
  //调用该子View的dispatchTouchEvent()方法 
  if (child.dispatchTouchEvent(ev))  {  
  // 如果child.dispatchTouchEvent(ev)返回true表示 
  //该事件被消费了,设置mMotionTarget为该子View 
                                mMotionTarget = child;  
  //直接返回true 
  return true;  
                            }  
  // The event didn't get handled, try the next view. 
  // Don't reset the event's location, it's not 
  // necessary here. 
                        }  
                    }  
                }  
            }  
        }  
  
  //判断是否为ACTION_UP或者ACTION_CANCEL 
  boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                (action == MotionEvent.ACTION_CANCEL);  
  
  if (isUpOrCancel) {  
  //如果是ACTION_UP或者ACTION_CANCEL, 将disallowIntercept设置为默认的false 
  //假如我们调用了requestDisallowInterceptTouchEvent()方法来设置disallowIntercept为true 
  //当我们抬起手指或者取消Touch事件的时候要将disallowIntercept重置为false 
  //所以说上面的disallowIntercept默认在我们每次ACTION_DOWN的时候都是false 
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
        }  
  
  // The event wasn't an ACTION_DOWN, dispatch it to our target if 
  // we have one. 
  final View target = mMotionTarget;  
  //mMotionTarget为null意味着没有找到消费Touch事件的View, 所以我们需要调用ViewGroup父类的 
  //dispatchTouchEvent()方法,也就是View的dispatchTouchEvent()方法 
  if (target == null) {  
  // We don't have a target, this means we're handling the 
  // event as a regular view. 
            ev.setLocation(xf, yf);  
  if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                ev.setAction(MotionEvent.ACTION_CANCEL);  
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            }  
  return super.dispatchTouchEvent(ev);  
        }  
  
  //这个if里面的代码ACTION_DOWN不会执行,只有ACTION_MOVE 
  //ACTION_UP才会走到这里, 假如在ACTION_MOVE或者ACTION_UP拦截的 
  //Touch事件, 将ACTION_CANCEL派发给target,然后直接返回true 
  //表示消费了此Touch事件 
  if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
  final float xc = scrolledXFloat - (float) target.mLeft;  
  final float yc = scrolledYFloat - (float) target.mTop;  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            ev.setLocation(xc, yc);  
  
  if (!target.dispatchTouchEvent(ev)) {  
            }  
  // clear the target 
            mMotionTarget = null;  
  // Don't dispatch this event to our own view, because we already 
  // saw it when intercepting; we just want to give the following 
  // event to the normal onTouchEvent(). 
  return true;  
        }  
  
  if (isUpOrCancel) {  
            mMotionTarget = null;  
        }  
  
  // finally offset the event to the target's coordinate system and 
  // dispatch the event. 
  final float xc = scrolledXFloat - (float) target.mLeft;  
  final float yc = scrolledYFloat - (float) target.mTop;  
        ev.setLocation(xc, yc);  
  
  if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
            mMotionTarget = null;  
        }  
  
  //如果没有拦截ACTION_MOVE, ACTION_DOWN的话,直接将Touch事件派发给target 
  return target.dispatchTouchEvent(ev);  
    }  

这个方法相对来说还是蛮长,不过所有的逻辑都写在一起,看起来比较方便,接下来我们就具体来分析一下

我们点击屏幕上面的TextView来看看Touch是如何分发的,先看看ACTION_DOWN

在DecorView这一层会直接调用ViewGroup的dispatchTouchEvent(), 先看18行,每次ACTION_DOWN都会将mMotionTarget设置为null, mMotionTarget是什么?我们先不管,继续看代码,走到25行,  disallowIntercept默认为false,我们再看ViewGroup的onInterceptTouchEvent()方法

代码语言:java
AI代码解释
复制
 public boolean onInterceptTouchEvent(MotionEvent ev) {  
  return false;  
   }  

直接返回false, 继续往下看,循环遍历DecorView里面的Child,从上面的MainActivity的层次结构图我们可以看出,DecorView里面只有一个Child那就是LinearLayout, 第43行判断Touch的位置在不在LinnearLayout上面,这是毫无疑问的,所以直接跳到51行, 调用LinearLayout的dispatchTouchEvent()方法,LinearLayout也没有dispatchTouchEvent()这个方法,所以也是调用ViewGroup的dispatchTouchEvent()方法,所以这个方法卡在51行没有继续下去,而是去先执行LinearLayout的dispatchTouchEvent()

LinearLayout调用dispatchTouchEvent()的逻辑跟DecorView是一样的,所以也是遍历LinearLayout的两个FrameLayout,判断Touch的是哪个FrameLayout,很明显是下面那个,调用下面那个FrameLayout的dispatchTouchEvent(),  所以LinearLayout的dispatchTouchEvent()卡在51也没继续下去

继续调用FrameLayout的dispatchTouchEvent()方法,和上面一样的逻辑,下面的FrameLayout也只有一个Child,就是RelativeLayout,FrameLayout的dispatchTouchEvent()继续卡在51行,先执行RelativeLayout的dispatchTouchEvent()方法

执行RelativeLayout的dispatchTouchEvent()方法逻辑还是一样的,循环遍历 RelativeLayout里面的孩子,里面只有一个TextView, 所以这里就调用TextView的dispatchTouchEvent(), TextView并没有dispatchTouchEvent()这个方法,于是找TextView的父类View,在看View的dispatchTouchEvent()的方法之前,我们先理清下上面这些ViewGroup执行dispatchTouchEvent()的思路,我画了一张图帮大家理清下(这里没有画出onInterceptTouchEvent()方法)

上面的ViewGroup的Touch事件分发就告一段落先,因为这里要调用TextView(也就是View)的dispatchTouchEvent()方法,所以我们先分析View的dispatchTouchEvent()方法在将上面的继续下去

View的Touch事件分发机制

我们还是先看View的dispatchTouchEvent()方法的源码

代码语言:java
AI代码解释
复制
 public boolean dispatchTouchEvent(MotionEvent event) {  
  if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                 mOnTouchListener.onTouch(this, event)) {  
  return true;  
         }  
  return onTouchEvent(event);  
     }  

在这个方法里面,先进行了一个判断

第一个条件mOnTouchListener就是我们调用View的setTouchListener()方法设置的

第二个条件是判断View是否为enabled的, View一般都是enabled,除非你手动设置为disabled

第三个条件就是OnTouchListener接口的onTouch()方法的返回值了,如果调用了setTouchListener()设置OnTouchListener,并且onTouch()方法返回true,View的dispatchTouchEvent()方法就直接返回true,否则就执行View的onTouchEvent() 并返回View的onTouchEvent()的值 现在你了解了View的onTouchEvent()方法和onTouch()的关系了吧,为什么Android提供了处理Touch事件onTouchEvent()方法还要增加一个OnTouchListener接口呢?我觉得OnTouchListener接口是对处理Touch事件的屏蔽和扩展作用吧,屏蔽作用我就不举例介绍了,看上面的源码就知道了,我就说下扩展吧,比如我们要打印View的Touch的点的坐标,我们可以自定义一个View如下

代码语言:java
AI代码解释
复制
 public class CustomView extends View {  
  
  public CustomView(Context context, AttributeSet attrs) {  
  super(context, attrs);  
     }  
  
  public CustomView(Context context, AttributeSet attrs, int defStyle) {  
  super(context, attrs, defStyle);  
     }  
  
  @Override 
  public boolean onTouchEvent(MotionEvent event) {  
  
         Log.i("tag", "X的坐标 = " + event.getX() + " Y的坐标 = " + event.getY());  
  
  return super.onTouchEvent(event);  
     }  
  
 }  

也可以直接对View设置OnTouchListener接口,在return的时候调用下v.onTouchEvent()

代码语言:java
AI代码解释
复制
 view.setOnTouchListener(new OnTouchListener() {  
  
  @Override 
  public boolean onTouch(View v, MotionEvent event) {  
  
                 Log.i("tag", "X的坐标 = " + event.getX() + " Y的坐标 = " + event.getY());  
  
  return v.onTouchEvent(event);  
             }  
         });  

这样子也实现了我们所需要的功能,所以我认为OnTouchListener是对onTouchEvent()方法的一个屏蔽和扩展作用,假如你有不一样的理解,你也可以告诉我下,这里就不纠结这个了。

我们再看View的onTouchEvent()方法

代码语言:java
AI代码解释
复制
 public boolean onTouchEvent(MotionEvent event) {  
  final int viewFlags = mViewFlags;  
  
  if ((viewFlags & ENABLED_MASK) == DISABLED) {  
  return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                   (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
       }  
  
  //如果设置了Touch代理,就交给代理来处理,mTouchDelegate默认是null 
  if (mTouchDelegate != null) {  
  if (mTouchDelegate.onTouchEvent(event)) {  
  return true;  
           }  
       }  
  
  //如果View是clickable或者longClickable的onTouchEvent就返回true, 否则返回false 
  if (((viewFlags & CLICKABLE) == CLICKABLE ||  
               (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
  switch (event.getAction()) {  
  case MotionEvent.ACTION_UP:  
  boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
  if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
  boolean focusTaken = false;  
  if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                           focusTaken = requestFocus();  
                       }  
  
  if (!mHasPerformedLongPress) {  
                           removeLongPressCallback();  
  
  if (!focusTaken) {  
  if (mPerformClick == null) {  
                                   mPerformClick = new PerformClick();  
                               }  
  if (!post(mPerformClick)) {  
                                   performClick();  
                               }  
                           }  
                       }  
  
  if (mUnsetPressedState == null) {  
                           mUnsetPressedState = new UnsetPressedState();  
                       }  
  
  if (prepressed) {  
                           mPrivateFlags |= PRESSED;  
                           refreshDrawableState();  
                           postDelayed(mUnsetPressedState,  
                                   ViewConfiguration.getPressedStateDuration());  
                       } else if (!post(mUnsetPressedState)) {  
                           mUnsetPressedState.run();  
                       }  
                       removeTapCallback();  
                   }  
  break;  
  
  case MotionEvent.ACTION_DOWN:  
  if (mPendingCheckForTap == null) {  
                       mPendingCheckForTap = new CheckForTap();  
                   }  
                   mPrivateFlags |= PREPRESSED;  
                   mHasPerformedLongPress = false;  
                   postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
  break;  
  
  case MotionEvent.ACTION_CANCEL:  
                   mPrivateFlags &= ~PRESSED;  
                   refreshDrawableState();  
                   removeTapCallback();  
  break;  
  
  case MotionEvent.ACTION_MOVE:  
  final int x = (int) event.getX();  
  final int y = (int) event.getY();  
  
  //当手指在View上面滑动超过View的边界, 
  int slop = mTouchSlop;  
  if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                           (y < 0 - slop) || (y >= getHeight() + slop)) {  
  // Outside button 
                       removeTapCallback();  
  if ((mPrivateFlags & PRESSED) != 0) {  
                           removeLongPressCallback();  
  
                           mPrivateFlags &= ~PRESSED;  
                           refreshDrawableState();  
                       }  
                   }  
  break;  
           }  
  return true;  
       }  
  
  return false;  
   }  

这个方法也是比较长的,我们先看第4行,如果一个View是disabled, 并且该View是Clickable或者longClickable, onTouchEvent()就不执行下面的代码逻辑直接返回true, 表示该View就一直消费Touch事件,如果一个enabled的View,并且是clickable或者longClickable的,onTouchEvent()会执行下面的代码逻辑并返回true,综上,一个clickable或者longclickable的View是一直消费Touch事件的,而一般的View既不是clickable也不是longclickable的(即不会消费Touch事件,只会执行ACTION_DOWN而不会执行ACTION_MOVE和ACTION_UP) Button是clickable的,可以消费Touch事件,但是我们可以通过setClickable()和setLongClickable()来设置View是否为clickable和longClickable。当然还可以通过重写View的onTouchEvent()方法来控制Touch事件的消费与否

我们在看57行的ACTION_DOWN, 新建一个CheckForTap,我们看看CheckForTap是什么

代码语言:java
AI代码解释
复制
 private final class CheckForTap implements Runnable {  
  public void run() {  
            mPrivateFlags &= ~PREPRESSED;  
            mPrivateFlags |= PRESSED;  
            refreshDrawableState();  
  if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {  
                postCheckForLongClick(ViewConfiguration.getTapTimeout());  
            }  
        }  
    }  

原来是个Runnable对象,然后使用Handler的post方法延时ViewConfiguration.getTapTimeout()执行CheckForTap的run()方法,在run方法中先判断view是否longClickable的,一般的View都是false, postCheckForLongClick(ViewConfiguration.getTapTimeout())这段代码就是执行长按的逻辑的代码,只有当我们设置为longClickble才会去执行postCheckForLongClick(ViewConfiguration.getTapTimeout()),这里我就不介绍了

由于考虑到文章篇幅的问题,我就不继续分析View的长按事件和点击事件了,在这里我直接得出结论吧

长按事件是在ACTION_DOWN中执行,点击事件是在ACTION_UP中执行,要想执行长按事件,这个View必须是longclickable的, 也许你会纳闷,一般的View不是longClickable为什么也会执行长按事件呢?我们要执行长按事件必须要调用setOnLongClickListener()设置OnLongClickListener接口,我们看看这个方法的源码

代码语言:java
AI代码解释
复制
 public void setOnLongClickListener(OnLongClickListener l) {  
  if (!isLongClickable()) {  
          setLongClickable(true);  
      }  
      mOnLongClickListener = l;  
  }  

看到没有,如果这个View不是longClickable的,我们就调用setLongClickable(true)方法设置为longClickable的,所以才会去执行长按方法onLongClick();

要想执行点击事件,这个View就必须要消费ACTION_DOWN和ACTION_MOVE事件,并且没有设置OnLongClickListener的情况下,如果设置了OnLongClickListener的情况下,需要onLongClick()返回false才能执行到onClick()方法,也许你又会纳闷,一般的View默认是不消费touch事件的,这不是和你上面说的相违背嘛,我们要向执行点击事件必须要调用setOnClickListener()来设置OnClickListener接口,我们看看这个方法的源码就知道了

代码语言:java
AI代码解释
复制
 public void setOnClickListener(OnClickListener l) {  
  if (!isClickable()) {  
          setClickable(true);  
      }  
      mOnClickListener = l;  
  }  

所以说一个enable的并且是clickable的View是一直消费touch事件的,所以才会执行到onClick()方法

对于View的Touch事件的分发机制算是告一段落了,从上面我们可以得出TextView的dispatchTouchEvent()的返回false的,即不消费Touch事件。我们就要往上看RelativeLayout的dispatchTouchEvent()方法的51行,由于TextView.dispatchTouchEvent()为false, 导致mMotionTarget没有被赋值,还是null, 继续往下走执行RelativeLayout的dispatchTouchEvent()方法, 来到第84行, 判断target是否为null,这个target就是mMotionTarget,满足条件,执行92行的 super.dispatchTouchEvent(ev)代码并返回, 这里调用的是RelativeLayout父类View的dispatchTouchEvent()方法,由于RelativeLayout没有设置onTouchListener, 所以这里直接调用RelativeLayout(其实就是View, 因为RelativeLayout没有重写onTouchEvent())的onTouchEvent()方法 由于RelativeLayout既不是clickable的也是longClickable的,所以其onTouchEvent()方法false, RelativeLayout的dispatchTouchEvent()也是返回false,这里就执行完了RelativeLayout的dispatchTouchEvent()方法

继续执行FrameLayout的dispatchTouchEvent()的第51行,由于RelativeLayout.dispatchTouchEvent()返回的是false, 跟上面的逻辑是一样的, 也是执行到92行的super.dispatchTouchEvent(ev)代码并返回,然后执行FrameLayout的onTouchEvent()方法,而FrameLayout的onTouchEvent()也是返回false,所以FrameLayout的dispatchTouchEvent()方法返回false,执行完毕FrameLayout的dispatchTouchEvent()方法

在上面的我就不分析了,大家自行分析一下,跟上面的逻辑是一样的,我直接画了个图来帮大家理解下(这里没有画出onInterceptTouchEvent()方法)

所以我们点击屏幕上面的TextView的事件分发流程是上图那个样子的,表示Activity的View都不消费ACTION_DOWN事件,所以就不能在触发ACTION_MOVE, ACTION_UP等事件了,具体是为什么?我还不太清楚,毕竟从Activity到TextView这一层是分析不出来的,估计是在底层实现的。

但如果将TextView换成Button,流程是不是还是这个样子呢?答案不是,我们来分析分析一下,如果是Button , Button是一个clickable的View,onTouchEvent()返回true, 表示他一直消费Touch事件,所以Button的dispatchTouchEvent()方法返回true, 回到RelativeLayout的dispatchTouchEvent()方法的51行,满足条件,进入到if方法体,设置mMotionTarget为Button,然后直接返回true, RelativeLayout的dispatchTouchEvent()方法执行完毕, 不会调用到RelativeLayout的onTouchEvent()方法

然后到FrameLayout的dispatchTouchEvent()方法的51行,由于RelativeLayout.dispatchTouchEvent()返回true, 满足条件,进入if方法体,设置mMotionTarget为RelativeLayout,注意下,这里的mMotionTarget跟RelativeLayout的dispatchTouchEvent()方法的mMotionTarget不是同一个哦,因为他们是不同的方法中的,然后返回true

同理FrameLayout的dispatchTouchEvent()也是返回true, DecorView的dispatchTouchEvent()方法也返回true, 还是画一个流程图(这里没有画出onInterceptTouchEvent()方法)给大家理清下

从上面的流程图得出一个结论,Touch事件是从顶层的View一直往下分发到手指按下的最里面的View,如果这个View的onTouchEvent()返回false,即不消费Touch事件,这个Touch事件就会向上找父布局调用其父布局的onTouchEvent()处理,如果这个View返回true,表示消费了Touch事件,就不调用父布局的onTouchEvent()

接下来我们用一个自定义的ViewGroup来替换RelativeLayout,自定义ViewGroup代码如下

代码语言:java
AI代码解释
复制
 package com.example.androidtouchevent;  
  
 import android.content.Context;  
 import android.util.AttributeSet;  
 import android.view.MotionEvent;  
 import android.widget.RelativeLayout;  
  
 public class CustomLayout extends RelativeLayout {  
  
  public CustomLayout(Context context, AttributeSet attrs) {  
  super(context, attrs, 0);  
     }  
  
  public CustomLayout(Context context, AttributeSet attrs, int defStyle) {  
  super(context, attrs, defStyle);  
     }  
  
  @Override 
  public boolean onTouchEvent(MotionEvent event) {  
  return super.onTouchEvent(event);  
     }  
  
  @Override 
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
  return true;  
     }  
  
  
 }  

我们就重写了onInterceptTouchEvent(),返回true, RelativeLayout默认是返回false, 然后再CustomLayout布局中加一个Button ,如下图

我们这次不从DecorView的dispatchTouchEvent()分析了,直接从CustomLayout的dispatchTouchEvent()分析

我们先看ACTION_DOWN 来到25行,由于我们重写了onInterceptTouchEvent()返回true, 所以不走这个if里面,直接往下看代码,来到84行, target为null,所以进入if方法里面,直接调用super.dispatchTouchEvent()方法, 也就是View的dispatchTouchEvent()方法,而在View的dispatchTouchEvent()方法中是直接调用View的onTouchEvent()方法,但是CustomLayout重写了onTouchEvent(),所以这里还是调用CustomLayout的onTouchEvent(), 这个方法返回false, 不消费Touch事件,所以不会在触发ACTION_MOVE,ACTION_UP等事件了,这里我再画一个流程图吧(含有onInterceptTouchEvent()方法的)

好了,就分析到这里吧,差不多分析完了,还有一种情况没有分析到,例如我将CustomLayout的代码改成下面的情形,Touch事件又是怎么分发的呢?我这里就不带大家分析了

代码语言:java
AI代码解释
复制
 package com.example.androidtouchevent;  
  
 import android.content.Context;  
 import android.util.AttributeSet;  
 import android.view.MotionEvent;  
 import android.widget.RelativeLayout;  
  
 public class CustomLayout extends RelativeLayout {  
  
  public CustomLayout(Context context, AttributeSet attrs) {  
  super(context, attrs, 0);  
     }  
  
  public CustomLayout(Context context, AttributeSet attrs, int defStyle) {  
  super(context, attrs, defStyle);  
     }  
  
  @Override 
  public boolean onTouchEvent(MotionEvent event) {  
  return super.onTouchEvent(event);  
     }  
  
  @Override 
  public boolean onInterceptTouchEvent(MotionEvent ev) {  
  if(ev.getAction() == MotionEvent.ACTION_MOVE){  
  return true;  
         }  
  return super.onInterceptTouchEvent(ev);  
     }  
  
  
 }  

这篇文章的篇幅有点长,如果你想了解Touch事件的分发机制,你一定要认真看完,下面来总结一下吧

1.Activity的最顶层Window是PhoneWindow,PhoneWindow的最顶层View是DecorView

2.一个clickable或者longClickable的View会永远消费Touch事件,不管他是enabled还是disabled的

3.View的长按事件是在ACTION_DOWN中执行,要想执行长按事件该View必须是longClickable的,并且不能产生ACTION_MOVE

4.View的点击事件是在ACTION_UP中执行,想要执行点击事件的前提是消费了ACTION_DOWN和ACTION_MOVE,并且没有设置OnLongClickListener的情况下,如设置了OnLongClickListener的情况,则必须使onLongClick()返回false

5.如果View设置了onTouchListener了,并且onTouch()方法返回true,则不执行View的onTouchEvent()方法,也表示View消费了Touch事件,返回false则继续执行onTouchEvent()

6.Touch事件是从最顶层的View一直分发到手指touch的最里层的View,如果最里层View消费了ACTION_DOWN事件(设置onTouchListener,并且onTouch()返回true 或者onTouchEvent()方法返回true)才会触发ACTION_MOVE,ACTION_UP的发生,如果某个ViewGroup拦截了Touch事件,则Touch事件交给ViewGroup处理

7.Touch事件的分发过程中,如果消费了ACTION_DOWN,而在分发ACTION_MOVE的时候,某个ViewGroup拦截了Touch事件,就像上面那个自定义CustomLayout,则会将ACTION_CANCEL分发给该ViewGroup下面的Touch到的View,然后将Touch事件交给ViewGroup处理,并返回true

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2015-06-23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
MIT_6.S081_xv6.Information 6:File System
xv6的文件系统由7层组成,首先就是最下面的硬件层,cache层在上面通过缓存硬件块来实现操作系统同步地访问硬盘块(这样可以降低操作系统访问硬盘块的时间),并且可以进行简单的同步管理,这样子可以保证只有一个进程同时访问一个硬盘块.记录层让更高层次的程序在在一次处理中能够处理多个硬件块,保证这些硬件块是同步处理的(要么都不处理,要么都处理).inode层负责描述文件,其中一个文件对应着一个inode,这个inode存储着许多文件的信息.其中inode层负责存储文件的控制信息,其中有一个索引负责带领文件找到索引本身.文件目录层负责实现具体的文件目录.路经名层负责完善文件树.这样可以根据文件的路径取访问文件了.文件描述器层负责完善许多UNIX抽象文件接口,负责给用户程序提供文件系统相关的系统调用.
用户7267083
2022/12/08
5970
MIT 6.S081 Lab Nine --- file system
在本作业中,您将增加xv6文件的最大大小。目前,xv6文件限制为268个块或268*BSIZE字节(在xv6中BSIZE为1024)。此限制来自以下事实:一个xv6 inode包含12个“直接”块号和一个“间接”块号,“一级间接”块指一个最多可容纳256个块号的块,总共12+256=268个块。
大忽悠爱学习
2023/10/11
4480
MIT 6.S081 Lab Nine --- file system
[mit6.s081] 笔记 Lab9: File System | 文件系统
与 FAT 文件系统类似,xv6 文件系统中的每一个 inode 结构体中,采用了混合索引的方式记录数据的所在具体盘块号。每个文件所占用的前 12 个盘块的盘块号是直接记录在 inode 中的(每个盘块 1024 字节),所以对于任何文件的前 12 KB 数据,都可以通过访问 inode 直接得到盘块号。这一部分称为直接记录盘块。
Miigon
2022/10/27
1.1K0
[mit6.s081] 笔记 Lab9: File System | 文件系统
xv6(12) 文件系统:Inode&Directory&Path
本文继续来看 $xv6$ 的文件系统部分,$xv6$ 将文件系统的设计分为 7 层:$磁盘 \rightarrow 缓存区 \rightarrow 日志 \rightarrow inode \rightarrow 目录 \rightarrow 路径 \rightarrow 文件描述符$ ,磁盘、缓存区、日志三个部分已说,本文来讲述 inode、目录、路径 三个层次
rand_cs
2023/12/06
3360
MIT_6.s081_Lab9:Xv6 and File System
在这个实验中,你会拓展文件系统中文件的最大大小,其中一开始文件是12个直接连接块,1个一级索引块,一共16*16+12=268个块,这个时候我们要修改一下改成11个直接相连,1个一级索引块,1个二级索引块,一共256*256+256+11=65536+256+11个块.
用户7267083
2022/12/08
3920
MIT_6.s081_Lab9:Xv6 and File System
MIT_6.S081_xv6.Information 集合
当x86系列的PC机启动的时候,首先会执行BIOS程序,BIOS程序一般会存放在固定的ROM中,一般在磁盘固定扇区中.BIOS 的作用是在启动时进行硬件的准备工作,接着BIOS程序会把控制权递交给操作系统.具体来说,BIOS会把控制权递交给从引导扇区中的固定的代码中(BIOS会把引导扇区存储的代码加载到内存0x7c00处),接着引导程序会把操作系统内核载入到内存中,控制权递交给内核,程序是M态的.
用户7267083
2023/03/20
2K0
MIT_6.S081_xv6.Information 集合
xv6(13) 文件系统:文件描述符&系统调用
本文需要接着系统调用,也是接着 $xv6$ 文件系统的最后一层,讲述各种具体的文件系统调用是怎么实现的,文件描述符,$inode$,文件之间到底有什么关系,创建打开关闭删除文件到底是何意义,文件删除之后数据就不存在了吗,链接又作何解释等等问题,看完本文相信你能找到答案。
rand_cs
2023/12/06
6520
6.S081/6.828: 9 Lab file system
目前xv6包含12个直接索引和1个二级索引,共索引12KB+256KB。增加xv6文件的索引范围,使得能够索引256*256KB+256KB+11KB,牺牲一个直接索引,将其转变为三级索引。
冰寒火
2022/12/02
4610
MIT 6.S081 教材第八章内容 -- 文件系统 -- 01
文件系统的目的是组织和存储数据。文件系统通常支持用户和应用程序之间的数据共享,以及持久性,以便在重新启动后数据仍然可用。
大忽悠爱学习
2023/10/11
4110
MIT 6.S081 教材第八章内容 -- 文件系统 -- 01
xv6(14) 文件系统:创建
文件系统的创建在原理上并不复杂,就是创建文件系统所需要的元信息,比如说超级块的位置和大小,日志区的位置和大小,$inode$ 区的位置和大小等等,将这些基本信息写入磁盘相应的地方就是所谓的创建文件系统了。当然这只是基本原理,还有很多细节要处理,我们在 $xv6$ 创建文件系统的程序中再详细了解。
rand_cs
2023/12/07
2790
MIT_6.s081_Lab
输入 file ./kernel/kernel载入符号表,然后target remote loaclhost:26000即可:
用户7267083
2023/03/20
1.3K0
MIT_6.s081_Lab
MIT 6.S081 教材第八章内容 -- File systems -- 03
前面两节,我们介绍了xv6 文件系统教材上的相关小节说明,从本节开始,将整理课程所讲内容
大忽悠爱学习
2023/10/11
2650
MIT 6.S081 教材第八章内容 -- File systems -- 03
MIT 6.S081 教材第八章内容 -- Crash recovery -- 04
今天的课程是有关文件系统中的Crash safety。这里的Crash safety并不是一个通用的解决方案,而是只关注一个特定的问题的解决方案,也就是crash或者电力故障可能会导致在磁盘上的文件系统处于不一致或者不正确状态的问题。当我说不正确的状态时,是指例如一个data block属于两个文件,或者一个inode被分配给了两个不同的文件。
大忽悠爱学习
2023/10/11
2380
MIT 6.S081 教材第八章内容 -- Crash recovery -- 04
[mit6.s081] 笔记 Lab10: Mmap | 文件内存映射
实现 *nix 系统调用 mmap 的简单版:支持将文件映射到一片用户虚拟内存区域内,并且支持将对其的修改写回磁盘。
Miigon
2022/10/27
1.5K0
[mit6.s081] 笔记 Lab10: Mmap | 文件内存映射
6.S081/6.828: 10 Lab mmap
本实验实现mmap和munmap系统调用来更好的控制进程地址空间,可以向数组那样读写文件,写的数据放在buffer cache可以被其他进程所看到。
冰寒火
2022/12/02
4280
MIT 6.S081 教材第五章内容 -- 中断与设备驱动--上
下图是一台Athena计算机(注,MIT内部共享使用的计算机)的top指令输出。如果你查看Mem这一行:
大忽悠爱学习
2023/10/11
5670
MIT 6.S081 教材第五章内容 -- 中断与设备驱动--上
MIT 6.S081 Lab Ten -- mmap
map和munmap系统调用允许UNIX程序对其地址空间进行详细控制。它们可用于在进程之间共享内存,将文件映射到进程地址空间,并作为用户级页面错误方案的一部分,如本课程中讨论的垃圾收集算法。在本实验室中,您将把mmap和munmap添加到xv6中,重点关注内存映射文件(memory-mapped files)。
大忽悠爱学习
2023/10/11
3230
MIT 6.S081 Lab Ten -- mmap
MIT_6.s081_Lab1:Xv6 and Unix utilities
输入 file ./kernel/kernel载入符号表,然后target remote loaclhost:26000即可:
用户7267083
2022/12/08
8180
MIT_6.s081_Lab1:Xv6 and Unix utilities
MIT 6.S081 教材第八章内容 -- ext3 -- 05
今天这节课也是讲解文件系统的logging,这节课讲的是Linux中的广泛使用的ext3文件系统所使用的logging系统,同时我们也会讨论在高性能文件系统中添加log需要面对的一些问题。首先我会花几分钟来回顾一下,为什么我们要学习logging。
大忽悠爱学习
2023/10/11
1980
MIT 6.S081 教材第八章内容 -- ext3 -- 05
虚拟文件系统源码解析之open(基于linux1.2.13)
我们操作一个文件之前都需要先open一下。我们看看open在虚拟文件系统中大致的执行过程。不会分析具体的过程。主要分析一下虚拟文件系统的实现原理。
theanarkh
2019/12/18
7410
推荐阅读
相关推荐
MIT_6.S081_xv6.Information 6:File System
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文