View的事件体系小结

View的事件体系小结

一、view的基础概念

(1)啥为View?
View为Android中所有控件的基类(控件:Button、TextView等),它是界面层控件的一种抽象,我们日常所用的View以及ViewGroup都是继承于view。
View树结构:已知View和ViewGroup都是继承于View,ViewGroup里面可以包含其他子View,这些子View又可以为其他ViewGroup,以此类推可形成View树的结构,这个结构有利于我们去理解View的事件分发机制。
(2)View的位置参数
View的位置主要由其四顶点决定
top:View的左上角纵坐标、left:View的左上角横坐标、right:View的右下角横坐标、bottom:View的右下角纵坐标。需注意这几个坐标都是相对于当前View的父View来说的。

Android中提供了对应的方法来获取四个值:

Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
//view的宽度
width = Right - Left
//view的高度
height = Bottom - Top

这里还有几个值需要注意的值(其坐标都是相对于当前view的父容器来说的)
x、y:分别为view的Left和Top变化的后的坐标值。
translationX、translationY:为View左上角相对于父容器的偏移量。

x = Left + translationX
y = top + translationY

(3)MotionEvent(触摸事件)
主要为三种事件:
Action_Down:手指刚接触屏幕
Action_Move:手指在屏幕上移动
Action_Up:手指离开屏幕瞬间

Android提供两钟方法来获取点击事件发生的坐标:
getX/getY:返回相对于当前View左上角的x、y坐标。
getRawX/getRawY:返回相对于手机屏幕左上角的x、y坐标。(全屏滑动使用)

这里还得提到另一个属性:TouchSlop(系统所能识别的滑动的最小距离)
获取这个常量的方法:

ViewConfiguration. get(getContext()).getScaledTouchSlop()。

这个常量定义在frameworks/base/core/res/res/values/config.xml文件中

<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>

(4)VelocityTracker、GestureDetector、Scroller(粗略介绍)
1、VelocityTracker:顾名思义速度追踪器,用来获得手机在屏幕上滑动的速度。
使用方法
(1)在View的onTouchEvent加上两行代码来记录当前单击事件的速度,至于onTouchEvent这个方法,后续将会讨论到。

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

(2)获取速度,通过以下api来实现

velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

在使用getXVelocity()和getYVelocity()获取速度之前,需要调用velocityTracker.computeCurrentVelocity(1000);来计算速度。速度为矢量,所以有方向,手指顺着坐标系正方向来移动,所得到的值为正值。
(3)不使用它时,需要通过以下API来回收

velocityTracker.clear();
velocityTracker.recycle();

2、GestureDetector:手势识别器,用于检测用户单击、滑动、双击等等动作。
使用方法

//(1)新建手势识别器对象
GestureDetector mGestureDetector = new GestureDetector(this);

//(2)接管目标View的onTouchEvent方法
boolean flag = mGestureDetector.onTouchEvent(event);
return flag;

下面列举几种常用到的方法:(1)onSingleTapUp(检测单击事件) (2)onDoubleTap(检测双击时间) (3)onLongPress(检测长按事件)。

3、Scroller:弹性滑动对象,滑动过程有滑动效果,增加用户体验。
前因:使用scrollTo、scrollBy进行滑动时,都是瞬间完成,体验不佳。scrollTo、scrollBy这两个方法,后面会有具体的解释。
使用方法

Scroller scroller = new Scroller(mContext);
private void smoothScrollTo(int destX,int destY) {
	int scrollX = getScrollX();
	int delta = destX -scrollX;
	// 1000ms内滑向destX,效果就是慢慢滑动
	mScroller.startScroll(scrollX,0,delta,0,1000);
	invalidate();
	}
@Override
public void computeScroll() {
	if (mScroller.computeScrollOffset()) {
	scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
	postInvalidate();
	}
}

二、view的滑动实现

view的滑动方法主要有三种:
(1)scrollTo/scrollBy
以下是上述两个方法的实现代码:

//scrollTo方法的实现
public void scrollTo(int x,int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;//mScrollX的值为View左边缘和View内容左边缘水平方向的距离
        int oldY = mScrollY;//mScrolly的值为View上边缘和View内容上边缘水平方向的距离
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX,mScrollY,oldX,oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}
//scrollBy方法的实现
public void scrollBy(int x,int y) {
    scrollTo(mScrollX + x,mScrollY + y);
}        

注意点
(1)scrollTo和scrollBy只能改变view的内容的位置,不能改变view在布局中的位置。
(2)view的左边缘在view内容左边缘的右边时,mScrollX为正值,反之为负值。
(3)view的上边缘在view内容上边缘的下边时,mScrollY为正值,反之为负值。

(2)使用动画
使用View动画来操作view,主要就是操作View的translationX和translationY属性(移动的还是View的内容),除非用属性动画,才能真正移动View。
View动画的使用:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"  android:zAdjustment="normal" >
<translate android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="100"
android:toYDelta="100" />
</set>

View动画是对View的影像做操作,想要保留动画后的状态,需要把fillAfter属性设为true。

属性动画的使用:

ObjectAnimator.ofFloat(View,"translationX",0,100).setDuration(100).start();

(3)通过布局参数
例:

MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
params.width += 100;// 保持button原本的大小,不被挤压变形。
params.leftMargin += 100;//左外边距增加100px
mButton1.requestLayout();

三种方式对比:
1、scrollTo、scrollBy:对View内容的移动,操作简单,适合无交互的View。
2、动画:(1)View动画:对View内容的移动,适合无交互的View,可以实现相对复杂的效果 。 (2)属性动画:操作View的属性,适合有交互的View,可以实现复杂的效果。
3、改变布局参数:适合有交互的View,但是操作比较复杂。

三、弹性滑动的实现

使用弹性活动的方法主要有三种:
(1)通过Scroller

Scroller scroller = new Scroller(mContext);//创建对象
// 缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY) {
    int scrollX = getScrollX();//mScrollX的值为View左边缘和View内容左边缘水平方向的距离
    int deltaX = destX -scrollX;
   // 1000ms内滑向destX
    mScroller.startScroll(scrollX,0,deltaX,0,1000);//只是用来保存数据
    invalidate();
}

startScroll()方法的具体实现:(可以得知,只起到保存数据的作用)

public void startScroll(int startX,int startY,int dx,int dy,int duration){
	 mMode = SCROLL_MODE;
	 mFinished = false;
	 mDuration = duration;
	 mStartTime = AnimationUtils.currentAnimationTimeMillis();
	 mStartX = startX;
	 mStartY = startY;
	 mFinalX = startX + dx;
	 mFinalY = startY + dy;
	 mDeltaX = dx;
	 mDeltaY = dy;
	 mDurationReciprocal = 1.0f / (float) mDuration;
}

startScroll方法只是起到保存数据的作用。invalidate方法才是真正实现View的弹性滑动,其原因是:invalidate会导致View的重绘,所以会调用View的draw方法,View的draw方法又会调用computeScroll()方法,接下来看computeScroll()的具体实现。

//这是一个空方法,需要自己来实现
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
}

由上述函数可发现,最后还是调用了scrollTo方法,来实现View的滑动,然后再调用postInvalidate()来实现重绘,并没有看到弹性是在哪里实现,所以我们把问题定位到mScroller.computeScrollOffset()上。接下来来看下computeScrollOffset()的一个实现。

public boolean computeScrollOffset() {
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() -mStartTime);//经过的时间
    if (timePassed < mDuration) {
        switch (mMode) {
            case SCROLL_MODE:
            final float x = mInterpolator.getInterpolation(timePassed *
            mDurationReciprocal);//插值器,根据时间流逝的百分比计算出动画改变的百分比
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        }
    }
	return true;
}

通过上述函数可得知,在一段时间内,computeScrollOffset函数会根据时间的流逝计算出View当前移动到哪个位置,所以当前View不会出现瞬间移动到的情况,弹性滑动实现。

(2)通过动画
前因:总所周知,动画本来就是随着时间流逝慢慢播放的,所以其本身以实现弹性滑动的效果

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

这里的动画只是一个粗略的讲解,有兴趣可以关注稍后写的关于动画的小结。

(3)通过延时操作
实现原理:通过不断发送延时消息来更新UI,从而实现View的弹性滑动。
实现方法:使用Handler或者View的postDelay方法
示例:通过Handler来实现

private static final int MESSAGE = 1;
private static final int COUNT = 50;
private int mCount = 0;
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
    switch (msg.what) {
        case MESSAGE: {
            mCount++;
            if (mCount <= COUNT) {
                float fraction = mCount / (float) COUNT;
                int scrollX = (int) (fraction * 100);
                mButton1.scrollTo(scrollX,0);
                mHandler.sendEmptyMessageDelayed(MESSAGE,
                50);
            }
            break;
        }
        default:
        break;
        }
    };
};

总结:其实以上三种方法的本质都是一样的,要想实现弹性滑动,就不能让View的滑动瞬间完成,通过给View设置一定的时间慢慢移动,弹性滑动效果实现。

四、View的事件分发机制

1、点击事件的传递规则
首先要了解点击事件,先要了解事件分发过程中的三个重要方法:
(1)public boolean dispatchTouchEvent(MotionEvent ev)
事件传给当前View,则此方法一定会被调用,至于这个方法返回false或者返回true,由当前View的onTouchEvent和下级View(如果有下级View的话)的dispatchTouchEvent影响,表示是否消耗当前事件。
(2)public boolean onInterceptTouchEvent(MotionEvent event)(此方法存在于ViewGroup中)
此方法表示是否拦截某事件,如果拦截某事件,那么在同一事件序列中(例如:down-move-move-up),此方法不会被再次调用,返回的结果表示是否拦截当前事件。
(3)public boolean onTouchEvent(MotionEvent event)
用来处理点击事件,如果不消耗,在同一事件序列中,当前View无法再接收到事件。

优先级问题:onTouchListener>onTouchEvent>onClickListener

补充几个概念
(1)事件序列:手指从接触到屏幕,到离开屏幕,这个过程所产生的一系列事件。以down开始,以move结束。
(2)正常情况下,一旦某个View拦截了某事件,那么这个事件序列都会交给这个View来处理,除非这个View又在onTouchevent把事件抛出。
(3)如果当前View不消耗除ACTION_DOWN以外的事件,此点击事件会消失,父元素的onTouchEvent也不会被调用,当前View可以接收到后续事件,消失的点击事件最后会交给Activity来处理。
(4)View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
(5)onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件

2、深入解析事件分发机制
1、Activity对点击事件的分发过程:当一个点击事件发生时,事件是最先传递给当前Activity,接下来看看它的一个代码实现
Activity的dispatchTouchEvent:

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

流程
(1)点击事件首先传递到Activity,然后Activity的dispatchTouchEvent方法被调用。
(2)在上述代码中可看到做了一个这样的判断if (getWindow().superDispatchTouchEvent(ev)) ,这是把事件交给Activity所附属的Window进行分发。
(3)来看看getWindow().superDispatchTouchEvent(ev)这个方法的一个实现,从代码上可得知window是一个抽象类,而他的方法superDispatchTouchEvent也是抽象方法。所以要找到它们的具体实现,分析Android源码可得知,Window在Android中的唯一实现类就是PhoneWindow。
(4)来到PhoneWindow中,找到superDispatchTouchEvent方法:

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

从代码中可得知,将事件交给mDecor来处理,这个mDecor就是DecorView,至于DecorView在我的另一篇博客《View的工作原理小结》有提到,这里就不再详解。
(5)现在事件传递到DecorView(DecorView本身为一个ViewGroup)接下来的流程就是常规的事件分发流程。接下来附图详解这个流程:

2、FLAG_DISALLOW_INTERCEPT:这个标记位,能让子View控制父ViewGroup无法拦截除ACTION_DOWN之外点击事件,为何除了ACTION_DOWN? 拦截ACTION_DOWN,会重置FLAG_DISALLOW_INTERCEPT这个标志位,导致这个标志位无效。所以要想使用这个标志位阻止父ViewGroup拦截事件,需要让父ViewGroup不拦截ACTION_DOWN。

//父ViewGroup在拦截ACTION_DOWN后所作的操作。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();//重置标志位
}

子View通过调用

requestDisallInterceptRouchEvent(boolean disallowIntercept)

来改变这个标志位。

3、view能否接受点击事件有两点来衡量:
(1)子元素是否在播放动画。
(2)点击事件的坐标是否落在子元素的区域内。

4、View对点击事件的处理过程(这里指的是非ViewGroup)
View的dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
    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;
    }
}

return result;
}

从上面函数可得知,首先会判断有没有OnTouchListener,如果onTouchListener中的onTouch方法返回true,那么View的onTouchEvent就不会被调用。
接下来看看View的onTouchEvent方法

if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
    setPressed(false);
}

return (((viewFlags & CLICKABLE) == CLICKABLE ||
        (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

从上述可得知,就算View处于不可用的状态,还是会消耗点击事件。
接下来看看onTouchEvent中对点击事件的处理

if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
	switch (event.getAction()) {
		case MotionEvent.ACTION_UP:
		boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
		if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
    ...
			if (!mHasPerformedLongPress) {

			removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
				if (!focusTaken) {

					if (mPerformClick == null) {
						mPerformClick = new PerformClick();
 				}
				if (!post(mPerformClick)) {
					 performClick();
					}
				}
		}
...
	}
		 break;
}
...
	return true;
}

首先只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管这个View是不是可用(DISABLE)。然后当ACTION_UP事件发生时,会触发performClick方法,当View中有设置OnClickListener,performClick方法就会调用onClick方法。

五、View的滑动冲突

起因:我们的布局经常是View嵌套View,不同的View又接收不同滑动事件,所以哪个滑动由哪个View来处理显得至关重要。

1、常见的滑动冲突场景(三种)
(1)内外滑动方向不一致
(2)内外滑动方向一致
(3)第一第二两种情况的混合
处理原则:具体场景,具体分析。判断在具体情况下,应该由哪个View来处理这个事件。
处理滑动冲突的方法:
1、外部拦截法

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;
}

对于ACTION_DOWN,如果ViewGroup拦截了,那么接下来的整个事件序列都会交给它来处理,所以一般返回为false,对于ACTION_UP一般ViewGroup都要返回false(不管拦不拦截事件),一但返回true会导致子元素中的onClick事件无法触发。
2、内部拦截法
内部拦截法是指父容器不拦截任何事件,全部传给子元素去处理,子元素需要就消耗掉,不然最后还是会传递给父容器处理。这种方法需要通过上文所说到的一个标志位来帮忙实现:FLAG_DISALLOW_INTERCEPT,通过parent.requestDisallowInterceptTouchEvent(true);这个方法来控制父容器不拦截事件,伪代码如下所示:

//子元素的dispatchTouchEvent方法
public boolean dispatchTouchEvent(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);
}

除了子元素需要处理,还要记得父容器不能拦截ACTION_DOWN,至于原因,上面已经提及过了。

发布了14 篇原创文章 · 获赞 45 · 访问量 2455

猜你喜欢

转载自blog.csdn.net/weixin_42683077/article/details/99618376