一、基础知识
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开发艺术探索。
安卓中的坐标系