View的坐标系统是相对于父控件的,如下图:
getTop(); //获取子View左上角距父View顶部的距离 getLeft(); //获取子View左上角距父View左侧的距离 getBottom(); //获取子View右下角距父View顶部的距离 getRight(); //获取子View右下角距父View左侧的距离Android3.0后增加了:
x、y : 表示View左上角坐标。用getX()、 getY()获得 translationX、translationY : 表示View的左上角相对于父容器的偏移量, 通过 getTranslationX()、getTranslationY()获得。 默认为0其中:
其中: x = getLeft() + translationX ; y = getTop() + translationY ;表示触摸屏幕产生的一系列事件。常用的有如下三种:
ACTION_DOWN : 手指刚开始触摸屏幕,事件的起始位置。ACTION_MOVE :手指在屏幕上移动。ACTION_UP :手指离开屏幕的瞬间触发。从事件开始到结束任意时间内,都可以通过 MotionEvent 内部的 getX/getY和getRawX/getRayY获得相应坐标,两种方式的区别如下图: 两种方式的含义:
event.getX(); //触摸点相对于其所在组件坐标系的坐标 event.getY(); event.getRawX(); //触摸点相对于屏幕默认坐标系的坐标 event.getRawY();用法如下:
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像素/每毫秒
包含一下方法:
onDown:触摸到屏幕onShowPress:onSingleTapUp:单击onScroll:手指滚动onLongPress:长按onFling: 手指离开,页面滑动弹性滑动对象,用于实现view的弹性滑动。
可以看到scrollBy也是调用scrollTo方法实现移动。
★ 使用属性动画可以实现view的滑动。
view动画,不能真正改变动画的位置。即位置改变了,但是view的事件还留在原来的位置
可用通过改变view的margin属性,或者改变父view的padding属性。实现view的滑动
view的事件分发机制指的是从手指按下屏幕开始,事件从屏幕传递到指定view的一系列过程。
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; }当我们点击屏幕产生事件时,最先接收事件的是Activity。所以事件先从Activity的dispatchTouchEvent开始分发。
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的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 继承FrameLayout。DecorView 的superDispatchTouchEvent方法源码如下:
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }因为DecorView 继承自FrameLayout,所以这里DecorView 调用ViewGroup的dispatchTouchEvent将事件向下传递分发。
这个时候我们的事件已经传递到了DecorView 了。 传递顺序如下: Activity --> PhoneWindow --> DecorView
在Activity中我们通过 setContentView()来加载我们的布局。源码如下:
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }可以看到,它会调用PhoneWindow的setContentView()方法来加载我们的布局文件。
1、当 mContentParent为空时,会执行 installDecor()方法。因为mContentParent是在installDecor()方法中赋值的,所以一定会先执行installDecor()方法来初始化。
2、当mContentParent不为空,则移除mContentParent内部的view,将布局文件添加到mContentParent中。
可以看到,在installDecor中会初始化mDecor 和 mContentParent。 而mContentParent = 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的源码:
我的findViewById其实也是在DecorView中查找控件id的
如果根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是否拦截。
可以看到,当子view不为空时,如果child是ViewGroup则会再次执行ViewGroup的dispatchTouchEvent。如果子View为空则执行View的dispatchTouchEvent
view的dispatchTouchEvent方法放到下面讲。
如果ViewGroup拦截分发事件,则执行自己的OnTouchEvent()方法。而ViewGroup没有专门实现自己的OnTouchEvent方法的逻辑,仍然使用的是view的OnTouchEvent逻辑。view的OnTouchEvent方法下面讲。
上面说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()方法响应的整个过程已经分析完了。
dispatchTouchEvent -> onTouch -> onTouchEvent -> onClick
在外部布局的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; } }参考:Android开发艺术探索。 安卓中的坐标系
