第三章 View的事件体系
##1、获得view的宽高坐标关系:
width = right - left
height= bottom - top
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
getX/getY 返回的是相当于当前View左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
##2、View事件的滑动三种方式
第一种通过View本身提供的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);
}
第二种通过动画给View施加平移效果实现滑动
ObjectAnimator.ofFloat(testButton,"translationX",0,100).setDuration(100).start();
第三种通过改变View的LayoutParams使得view重新布局实现滑动
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) testButton.getLayoutParams();
layoutParams.width +=100;
layoutParams.leftMargin +=100;
testButton.requestLayout();
//或者testButton.setLayoutParams(layoutParams);
1、使用scrollTo和scrollBy方法只能改变view内容的位置而不能改变view在布局中的位置。 scrollBy是基于当前位置的相对滑动,而scrollTo是基于所传参数的绝对滑动。通过View的getScrollX和getScrollY方法可以得到滑动的距离。
2、使用动画 使用动画来移动view主要是操作view的translationX和translationY属性,既可以使用传统的view动画,也可以使用属性动画,使用后者需要考虑兼容性问题,如果要兼容Android3.0一下版本系统的话推荐使用nineoldandroids。使用动画还存在一个交互问题:在android3.0以前的系统上,view动画和属性动画,新位置均无法触发点击事件,同时,老位置仍然可以触发单击事件。从3.0开始,属性动画的单击事件触发位置为移动后的位置,view动画仍然在原位置。
3、改变布局参数 通过改变LayoutParams的方式去实现View的滑动是一种灵活的方法。
4、各种滑动方式的对比
scrollTo/scrollBy:操作简单,适合对View内容的滑动
动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于有交互的View
##3、View的事件分发机制
public boolean dispatchTouchEvent(MotionEvent ev)
如果事件能够传递给当前的View会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回表示是否消耗当前的事件,如果不消耗,则同一个事件序列中,当前View无法再次接受到事件。
这三个方法的关系可以用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent event)
{
boolean consume = false;
if(onInterceptTouchEvent(ev))
{
consume = onTouchEvent(ev);
}
else
{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
##4、View滑动冲突
一、滑动冲突场景
1、外部左右滑动,内部上下滑动 (viewpager和listview的嵌套滑动)
2、内外层两层同时上下滑动或者左右滑动
3、内层有场景1滑动,外层场景2滑动,外层与中层的滑动方向一致,而中层与内层的滑动方向不一致
二、滑动冲突的处理规则
场景1的处理规则:
1、当用户左右滑动时,让外部的View拦截点击事件,当用户上下滑动时,让内部的View拦截点击事件;
2、判断用户的滑动方向,如果用户手指滑动的水平距离大于垂直距离,则左右滑动,反之,上下滑动;还可以根据角度、速度差来做判断;
场景2的处理规则:
当处于某种状态需要外部View响应用户的滑动,而处于另一种状态时则需要内部View响应用户的滑动,可以根据业务的需求得出相应的处理规则。
场景3的处理规则:场景1的处理规则和场景2的处理规则一起用。
三、滑动冲突解决方法
针对场景一有两种解决滑动冲突的方法:外部拦截法和内部拦截法
外部拦截法是指点击事件都先经过父容器的onInterceptTouchEvent方法拦截处理,如果父容器需要此事件就拦截,否则就不拦截 伪代码如下:
public boolean onInterceptTouchEvent (MotionEvent event) {
boolean intercepted = false ;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (父容器需要处理当前事件){
intercepted = true;
} else{
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = Y;
return intercepted;
}
内部拦截法指父容器不拦截任何事件,需要配合requestDisallowInterceptTouchEvent方法工作,重写子元素的dispathTouchEvent方法:
public boolean dispatchtTouchEvent (MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction() ) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要处理当前事件){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY= Y;
return super.dispatchTouchEvent(event);
}
针对场景二有两种解决滑动冲突的方法:外部拦截法和内部拦截法
假设需求:用一个竖直的ScrollView嵌套一个ListView做例子,当ListView滑到顶部了,并且继续向下滑就让ScrollView拦截掉;当ListView滑到底部了,并且继续向下滑,就让ScrollView拦截掉,其余时候都交给ListView自身处理事件
公共部分就是判断 滑动是否到达listView的顶部或者底部
public boolean isBottom(final ListView listView) {
boolean result=false;
if (listView.getLastVisiblePosition() == (listView.getCount() - 1)) {
final View bottomChildView = listView.getChildAt(listView.getLastVisiblePosition() - listView.getFirstVisiblePosition());
result= (listView.getHeight()>=bottomChildView.getBottom());
};
return result;
}
public boolean isTop(final ListView listView) {
boolean result=false;
if(listView.getFirstVisiblePosition()==0){
final View topChildView = listView.getChildAt(0);
result=topChildView.getTop()==0;
}
return result ;
}
第一种 :外部拦截法 重写ScrollView的onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int y = (int) event.getY();
int removeY = 0; //移动Y的距离
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
removeY = y;
//ScrollView在Down方法时需要初始化一些参数
intercepted = super.onInterceptTouchEvent(event);
break;
}
case MotionEvent.ACTION_MOVE: {
if(isTop(mListView) && y>removeY){
intercepted = true;
break;
}
else if(isBottom(mListView) && y<removeY){
intercepted = true;
break;
}
intercepted = false;
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
return intercepted;
}
第二种 :内部拦截法 重写ListView的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent event) {
int y = (int) event.getY();
int removeY = 0; //移动Y的距离
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
removeY = y;
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
if(isTop(mListView) && y>removeY){
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
else if(isBottom(mListView) && y<removeY){
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
return super.dispatchTouchEvent(event);
}