Android开发Android开发经验谈Android开发

Android View的事件体系

2018-08-07  本文已影响21人  一个有故事的程序员

导语

本章主要介绍View的事件分发和滑动冲突问题的解决方案,可以和Android事件拦截机制分析对比着看。

主要内容

具体内容

view的基础知识

View的位置参数、MotionEvent和TouchSlop对象、VelocityTracker、GestureDetector和Scroller对象。

什么是view

View是Android中所有控件的基类,View的本身可以是单个空间,也可以是多个控件组成的一组控件,即ViewGroup,ViewGroup继承自View,其内部可以有子View,这样就形成了View树的结构。

View的位置参数

View的位置主要由它的四个顶点来决定,即它的四个属性:top、left、right、bottom,分别表示View左上角的坐标点( top,left) 以及右下角的坐标点( right,bottom)。
同时,我们可以得到View的大小:

width = right - left
height = bottom - top

而这四个参数可以由以下方式获取:

Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();

Android3.0后,View增加了x、y、translationX和translationY这几个参数。其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于容器的偏移量。他们之间的换算关系如下:

x = left + translationX;
y = top + translationY;
MotionEvent和TouchSlop
MotionEvent

事件类型:

点击事件类型:

通过MotionEven对象我们可以得到事件发生的x和y坐标,我们可以通过getX/getY和getRawX/getRawY得到。它们的区别是:getX/getY返回的是相对于当前View左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。

TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,这是一个常量,与设备有关,可通过以下方法获得:

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

当我们处理滑动时,比如滑动距离小于这个值,我们就可以过滤这个事件(系统会默认过滤),从而有更好的用户体验。

VelocityTracker、GestureDetector和Scroller
VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平放向速度和竖直方向速度。使用方法:

VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();

注意,获取速度之前必须先计算速度,即调用computeCurrentVelocity方法,这里指的速度是指一段时间内手指滑过的像素数,1000指的是1000毫秒,得到的是1000毫秒内滑过的像素数。速度可正可负:速度 = ( 终点位置 - 起点位置) / 时间段

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

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。使用方法:

GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
Scroller

弹性滑动对象,用于实现View的弹性滑动。其本身无法让View弹性滑动,需要和View的computeScroll方法配合使用才能完成这个功能。使用方法:

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);
    invalidata();
} 
@Override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
    scrollTo(mScroller.getCurrX,mScroller.getCurrY());
    postInvalidate();
    }
}

View的滑动

三种方式实现View滑动。

使用scrollTo/scrollBy

scrollBy实际调用了scrollTo,它实现了基于当前位置的相对滑动,而scrollTo则实现了绝对滑动。

scrollTo和scrollBy只能改变View的内容位置而不能改变View在布局中的位置。滑动偏移量mScrollX和mScrollY的正负与实际滑动方向相反,即从左向右滑动,mScrollX为负值,从上往下滑动mScrollY为负值。

使用动画

使用动画移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果使用属性动画,为了能够兼容3.0以下的版本,需要采用开源动画库nineolddandroids。 如使用属性动画:(View在100ms内向右移动100像素)。

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
改变布局属性

通过改变布局属性来移动View,即改变LayoutParams。

各种滑动方式的对比

弹性滑动

使用Scroller

使用Scroller实现弹性滑动的典型使用方法如下:

Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int dextY){
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    //1000ms内滑向destX,效果就是缓慢滑动
    mScroller.startSscroll(scrollX,0,deltaX,0,1000);
    invalidate();
} 
@override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
    scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    postInvalidate();
    }
}

从上面代码可以知道,我们首先会构造一个Scroller对象,并调用他的startScroll方法,该方法并没有让view实现滑动,只是把参数保存下来,我们来看看startScroll方法的实现就知道了:

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

可以知道,startScroll方法的几个参数的含义,startX和startY表示滑动的起点,dx和dy表示的是滑动的距离,而duration表示的是滑动时间,注意,这里的滑动指的是View内容的滑动,在startScroll方法被调用后,马上调用invalidate方法,这是滑动的开始,invalidate方法会导致View的重绘,在View的draw方法中调用computeScroll方法,computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法进行第二次重绘,一直循环,直到computeScrollOffset()方法返回值为false才结束整个滑动过程。 我们可以看看computeScrollOffset方法是如何获得当前的scrollX和scrollY的:

public boolean computeScrollOffset(){
    ...
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - mStartTime);
    if(timePassed < mDuration){
        switch(mMode){
        case SCROLL_MODE:
        final float x = mInterpolator.getInterpolation(timePassed * mDuratio
        nReciprocal);
        mCurrX = mStartX + Math.round(x * mDeltaX);
        mCurrY = mStartY + Math.round(y * mDeltaY);
        break;
        ...
        }
    } 
    return true;
}

到这里我们就基本明白了,computeScroll向Scroller获取当前的scrollX和scrollY其实是通过计算时间流逝的百分比来获得的,每一次重绘距滑动起始时间会有一个时间间距,通过这个时间间距Scroller就可以得到View当前的滑动位置,然后就可以通过scrollTo方法来完成View的滑动了。

通过动画

动画本身就是一种渐近的过程,因此通过动画来实现的滑动本身就具有弹性。实现也很简单:

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
;    
//当然,我们也可以利用动画来模仿Scroller实现View弹性滑动的过程:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
    @override
    public void onAnimationUpdate(ValueAnimator animator){
    float fraction = animator.getAnimatedFraction();
    mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
    }
});
animator.start();

上面的动画本质上是没有作用于任何对象上的,他只是在1000ms内完成了整个动画过程,利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,根据比例计算出View所滑动的距离。采用这种方法也可以实现其他动画效果,我们可以在onAnimationUpdate方法中加入自定义操作。

使用延时策略

延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。 下面以Handler为例:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
    public void handleMessage(Message msg){
    switch(msg.what){
        case MESSAGE_SCROLL_TO:
        mCount ++ ;
        if (mCount <= FRAME_COUNT){
            float fraction = mCount / (float) FRAME_COUNT;
            int scrollX = (int) (fraction * 100);
            mButton1.scrollTo(scrollX,0);
            mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
            } 
        break;
        default : break;
        }
    }
}

View的事件分发机制

点击事件的传递规则

点击事件是MotionEvent。首先我们先看看下面一段伪代码,通过它我们可以理解到点击事件的传递规则:

public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
    consume = onTouchEvent(ev);
} else {
    consume = child.dispatchTouchEnvet(ev);
} 
return consume;
}

上面代码主要涉及到以下三个方法:

事件分发机制

点击事件的传递规则:对于一个根ViewGroup,点击事件产生后,首先会传递给他,这时候就会调用他的dispatchTouchEvent方法,如果ViewGroup的onInterceptTouchEvent方法返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用ViewGroup的onTouchEvent方法;如果ViewGroup的### onInterceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View的dispatchTouchEvent方法,如此反复直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。

由此可见处理事件时的优先级关系: onTouchListener > onTouchEvent >onClickListener

关于事件传递的机制,这里给出一些结论:

事件分发的源码解析

滑动冲突

在界面中,只要内外两层同时可以滑动,这个时候就会产生滑动冲突。滑动冲突的解决有固定的方法。

常见的滑动冲突场景
  1. 外部滑动和内部滑动方向不一致;
    比如viewpager和listview嵌套,但这种情况下viewpager自身已经对滑动冲突进行了处理。
  2. 外部滑动方向和内部滑动方向一致;
  3. 上面两种情况的嵌套,只要解决1和2即可。
滑动冲突的处理规则

对于场景一,处理的规则是:当用户左右( 上下) 滑动时,需要让外部的View拦截点击事件,当用户上下( 左右) 滑动的时候,需要让内部的View拦截点击事件。根据滑动的方向判断谁来拦截事件。

对于场景二,由于滑动方向一致,这时候只能在业务上找到突破点,根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。

场景三的情况相对比较复杂,同样根据需求在业务上找到突破点。

滑动冲突的解决方式
外部拦截法

所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。下面是伪代码:

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 = flase;
    } 
    break;
    } 
case MotionEvent.ACTION_UP:
    intercepted = false;
    break;
default : 
    break;
} 
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;

针对不同冲突,只需修改父容器需要当前事件的条件即可。其他不需修改也不能修改。

内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是伪代码:

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以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。因此,父元素要做以下修改:

public boolean onInterceptTouchEvent (MotionEvent event) {
    int action = event.getAction();
    if(action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}

外部拦截法实例:HorizontalScrollViewEx

更多内容戳这里(整理好的各种文集)

上一篇下一篇

猜你喜欢

热点阅读