View的事件体系总结

mac2025-11-07  17

一、基础知识

1、View的坐标系

View的坐标系统是相对于父控件的,如下图:

getTop(); //获取子View左上角距父View顶部的距离 getLeft(); //获取子View左上角距父View左侧的距离 getBottom(); //获取子View右下角距父View顶部的距离 getRight(); //获取子View右下角距父View左侧的距离
getX()、getTranslationX()

Android3.0后增加了:

x、y : 表示View左上角坐标。用getX()、 getY()获得 translationX、translationY : 表示View的左上角相对于父容器的偏移量, 通过 getTranslationX()、getTranslationY()获得。 默认为0

其中:

其中: x = getLeft() + translationX ; y = getTop() + translationY ;

2、MotionEvent

表示触摸屏幕产生的一系列事件。常用的有如下三种:

ACTION_DOWN : 手指刚开始触摸屏幕,事件的起始位置。ACTION_MOVE :手指在屏幕上移动。ACTION_UP :手指离开屏幕的瞬间触发。

从事件开始到结束任意时间内,都可以通过 MotionEvent 内部的 getX/getY和getRawX/getRayY获得相应坐标,两种方式的区别如下图: 两种方式的含义:

event.getX(); //触摸点相对于其所在组件坐标系的坐标 event.getY(); event.getRawX(); //触摸点相对于屏幕默认坐标系的坐标 event.getRawY();

3、VelocityTracker、GestureDetector、Scroller

①、VelocityTracker速度追踪

用法如下:

VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); //1000ms内速度 velocityTracker.computeCurrentVelocity(1000); //x轴方向速度 int xVelocty = (int) velocityTracker.getXVelocity(); //y方向速度 int yVelocty = (int) velocityTracker.getYVelocity(); //释放 velocityTracker.clear(); velocityTracker.recycle();

速度的单位是: 像素/毫秒(px/ms),eg:100像素/每毫秒

②、GestureDetector 手势检测

包含一下方法:

onDown:触摸到屏幕onShowPress:onSingleTapUp:单击onScroll:手指滚动onLongPress:长按onFling: 手指离开,页面滑动
③、Scroller

弹性滑动对象,用于实现view的弹性滑动。

二、View的滑动

1、scrollTo/scrollBy

public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }

可以看到scrollBy也是调用scrollTo方法实现移动。

特点:
无论是scrollTo还是scrollBy都无法改变view的位置,移动的是view的内部位置。scrollTo属于绝对滑动,移动的位置是相对于View的。即:无论移动多少次,位置都是在第一次移动的位置。scrollBy属于相对滑动,移动的位置是相对自己的。即:每次点击移动,都会相对自己的位置再次移动。移动的距离scrollX和scrollY正负和Android坐标系相反。即x移动正100,view的内容向左移动100(不是向右),y移动负100,view内容向下移动100(不是向上)。

2、使用动画实现view的滑动

★ 使用属性动画可以实现view的滑动。

view动画,不能真正改变动画的位置。即位置改变了,但是view的事件还留在原来的位置

nineoldandroids动画兼容库

3、使用LayoutParams改变位置参数。

可用通过改变view的margin属性,或者改变父view的padding属性。实现view的滑动

三、弹性滑动

1、使用scroller
2、使用动画
3、使用延时策略

四、View的事件分发机制

view的事件分发机制指的是从手指按下屏幕开始,事件从屏幕传递到指定view的一系列过程。

1、点击事件的传递规则

View的事件分发其实是对MotionEvent事件的分发过程。

而事件的分发过程由三个很重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。

dispatchTouchEvent(MotionEvent event) :用来分发事件。返回结果受当前view的onTouchEvent和下级View的dispatchTouchEvent方法影响。onInterceptTouchEvent(MotionEvent ev) :用来拦截事件。onTouchEvent(MotionEvent event) :在dispatchTouchEvent方法中调用,表示是否消耗当前事件
三者之间的关系

viewgroup的事件分发可以用下面伪代码表示三者之间的关系:

@Override public boolean dispatchTouchEvent(MotionEvent event) { boolean consume = false; //当前view是否拦截 if (onInterceptTouchEvent(event)){ //拦截后,则调用自己的onTouchEvent, //如果onTouchEvent消耗事件则返回true,否则false,交由父控件处理 consume = onTouchEvent(event); }else { //如果不拦截,则获得子view是否消耗 consume = child.dispatchTouchEvent(event); } return consume; }

2、事件分发源码

当我们点击屏幕产生事件时,最先接收事件的是Activity。所以事件先从Activity的dispatchTouchEvent开始分发。

1、Activity事件分发

Activity中 dispatchTouchEvent 方法源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }

从上面源码可以看到,activity事件分发受到Widow的superDispatchTouchEvent方法影响。 可以看到Window是一个抽象方法。注释方法里面说它有一个子类PhoneWindow。

可以全局搜索PhoneWindow。找到PhoneWindow的位置,在com.android.internal.policy包中

PhoneWindow

查看PhoneWindow的superDispatchTouchEvent方法:

@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }

可以看到该方法的返回值又受到 mDecor 中的方法影响。 查看mDecor 声明的地方

// This is the top-level view of the window, containing the window decor. private DecorView mDecor;

从注释中可以看到,DecorView 是PhoneWindow的顶层视图。

DecorView

可以看到DecorView 继承FrameLayout。DecorView 的superDispatchTouchEvent方法源码如下:

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

因为DecorView 继承自FrameLayout,所以这里DecorView 调用ViewGroup的dispatchTouchEvent将事件向下传递分发。

这个时候我们的事件已经传递到了DecorView 了。 传递顺序如下: Activity --> PhoneWindow --> DecorView

事件是怎么从DecorView传递到我们自己的Layout中的?

Activity & setContentView()

在Activity中我们通过 setContentView()来加载我们的布局。源码如下:

public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }

可以看到,它会调用PhoneWindow的setContentView()方法来加载我们的布局文件。

PhoneWindow & setContentView()
@Override public void setContentView(int layoutResID) { if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } //加载布局 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }

1、当 mContentParent为空时,会执行 installDecor()方法。因为mContentParent是在installDecor()方法中赋值的,所以一定会先执行installDecor()方法来初始化。

2、当mContentParent不为空,则移除mContentParent内部的view,将布局文件添加到mContentParent中。

PhoneWindow & installDecor()
private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } if (mContentParent == null) { mContentParent = generateLayout(mDecor); ... } }

可以看到,在installDecor中会初始化mDecor 和 mContentParent。 而mContentParent = generateLayout(mDecor);

PhoneWindow & generateLayout(mDecor)

从方法名就可以看出来了,这个方法是在mDecor 中生成一个layout布局。

protected ViewGroup generateLayout(DecorView decor) { ...//省略资源加载 mDecor.startChanging(); //layoutResource 在上面加载过了,省略 //mDecor 加载layoutResource布局 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); //通过findViewById找到contentParent // int ID_ANDROID_CONTENT = com.android.internal.R.id.content; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); mDecor.finishChanging(); return contentParent; }

布局文件如下: 在上面的方法中,DecorView会加载layoutResource布局文件,layoutResource如上图,通过findviewbyid找到contentParent 控件,也就是上图红框代表的FrameLayout。

而我们加载的布局文件就是放在红框的contentParent 中。

用一张图来展示他们之间的层级关系如下:

这个时候我们的事件就传递到了ContentParent中了,然后再由ContentParent传递到我们布局文件的最外层View即根View。

findViewById(id)

这里面既然用到了findViewById(id)那我们不妨看一下findViewById的源码:

Activity & findViewById
@Nullable public View findViewById(@IdRes int id) { return getWindow().findViewById(id); }
Window & findViewById
@Nullable public View findViewById(@IdRes int id) { return getDecorView().findViewById(id); }

我的findViewById其实也是在DecorView中查找控件id的

事件从Activity到根View传递顺序:

Activity -> PhoneWindow -> DecorView -> ContentParent -> 根View

3、根View对点击事件的分发

①、ViewGroup事件分发

如果根View是ViewGroup,则会调用ViewGroup 的 dispatchTouchEvent方法,

dispatchTouchEvent 拦截部分源码如下:

// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //子view是否调用requestDisallowInterceptTouchEvent() 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 { intercepted = true; }

默认ViewGroup是不会拦截事件分发的。 可以看到子View可以调用requestDisallowInterceptTouchEvent来影响父view是否拦截。

1、ViewGroup不拦截事件
如果viewgroup不拦截事件的话,viewgroup会遍历所有子view,并调用dispatchTransformedTouchEvent方法,把事件分发给子view。 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } ...//省略部分情况判断 }

可以看到,当子view不为空时,如果child是ViewGroup则会再次执行ViewGroup的dispatchTouchEvent。如果子View为空则执行View的dispatchTouchEvent

view的dispatchTouchEvent方法放到下面讲。

2、ViewGroup拦截事件

如果ViewGroup拦截分发事件,则执行自己的OnTouchEvent()方法。而ViewGroup没有专门实现自己的OnTouchEvent方法的逻辑,仍然使用的是view的OnTouchEvent逻辑。view的OnTouchEvent方法下面讲。

②View的事件分发

上面说viewgroup的事件分发的时候,在ViewGroup的dispatchTouchEvent方法中,不拦截的话最终会执行view的dispatchTouchEvent方法。 view的dispatchTouchEvent部分源码如下:

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

1、可以看到,当view设置了setOnTouchListener的时候,mOnTouchListener不为null,此时view的dispatchTouchEvent方法的返回值受mOnTouchListener.onTouch()方法影响。 如果在onTouch()方法中返回true,则view的dispatchTouchEvent方法返回值就为true。而如果view上面还有ViewGroup,则ViewGroup的dispatchTouchEvent方法也就返回true,则不再继续分发事件。 2、如果没有设置setOnTouchListener或者mOnTouchListener.onTouch()方法返回false,则执行View的onTouchEvent(event)方法

View的onTouchEvent方法 onTouchEvent部分源码如下:

public boolean onTouchEvent(MotionEvent event) { ... if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } ... switch (action) { case MotionEvent.ACTION_UP: ... if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } ... }

1、如果给view设置setTouchDelegate()此时onTouchEvent方法返回值受mTouchDelegate.onTouchEvent(event)方法影响。 2、在MotionEvent.ACTION_UP的时候,会执行performClick()方法,即点击事件的方法。源码如下:

public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }

当我们给View设置点击事件的时候,则在此执行mOnClickListener.onClick()方法。

到这里一个事件从Activity的dispatchTouchEvent方法开始分发,一直到View的onClick()方法响应的整个过程已经分析完了。

View事件优先级总结

dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick

五、View的滑动冲突解决方式

1、外部拦截法

在外部布局的onInterceptTouchEvent 方法中ACTION_MOVE事件中判断是否拦截子view的事件,并 在ACTION_UP和ACTION_DOWN中释放拦截。 伪代码如下:

@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: intercept = false; break; case MotionEvent.ACTION_MOVE: if (父容器需要当前事件){ //拦截 intercept = true; }else { intercept = false; } break; case MotionEvent.ACTION_UP: intercept = false; break; } return intercept; }

2、内部拦截法 指父容器不拦截任何事件,所有的事件都交给子view处理,如果子view需要就消耗掉,否则交给父容器处理。需要配合requestDisallowInterceptTouchEvent使用。

子元素的dispatchTouchEvent方法如下:

@Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //屏蔽父容器事件 parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if (父容器需要当前事件){ //交给父容器处理 parent.requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(ev); }

父容器需要将ACTION_DOWN的拦截事件接触,不然在需要父容器接收的时候,父容器也没有地方接收。

父元素修改如下:

@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN){ return false; }else { return true; } }
相比较内部拦截法,外部拦截更加方便,只需要在一个view内做拦截就行了

参考:Android开发艺术探索。 安卓中的坐标系

最新回复(0)