《Android开发艺术探索》笔记总结——第三章:View的事件体系

View的基础知识

坐标:

ViewGroup继承了View,所以ViewGroup本身就是个View
View的的位置是有四个顶点来决定的,分别对应着top、left、right、bottom四个属性,Android中的坐标系是倒着的,x轴的正方向向右,y轴的正方向向下,一个View的坐标表示如图:
在这里插入图片描述

所以View的宽和高需要坐标相减来得到

width = right - left
height = bottom - top

注意:上面的的四个坐标是相对于View的父容器来说的,它是一种相对坐标

获取这四个参数的方法:

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

从Android3.0开始View额外增加了几个参数:x、y、translationX、translationY,这四个参数也是相对于父容器来说的,其中 x 和 y 是View左上角的坐标,而 translationX 和 translationY 是View左上角相对于父容器的偏移量,这两个参数默认值是0,所以上面的参数的换算关系是:

x = left + translationX
y = top + translationY

需要注意的是:View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。
到此,我们就明白了这三个参数的具体含义,我们在查看SDK中的 get/set 方法的时候就不会感到特别乱了。

MotionEvent

MotionEvent主要是三种时间类型:ACTION_DOWN、ACTION_MOVE、ACTION_UP,分别对应按下、移动、抬起的三种操作,如果用户点击屏幕滑动一会再松开对应的事件序列为: DOWN -> MOVE --> MOVE --> ...> MOVE --> UP

在事件中我们一般都会获取事件发生时的x和y坐标,系统提供了两组方法:getX/getY 和 getRawX/getRawY,他们的区别是:getX/getY返回的是相对于当前View左上角的x和y坐标,而 getRawX/getRawY返回的是相对于手机屏幕左上角的 x和y 坐标。

TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,这是一个常量,我们可以通过此常量和用户滑动的距离进行比较来决定事件是不是滑动事件,因为滑动的距离太短,系统不会认为它是滑动,这个常量可以通过以下方式获取:

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

VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度,使用方法如下:
首先在View的onTouchEvent方法中追踪当前点击事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//获取当前的速度
velocityTracker.computeCurrentVelocity(1000); //参数为1000,表示获取1s时间内的速度
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();

//最后不再使用的时候,调用clear方法重置并回收内存
velocityTracker.clear();
velocityTracker.recycle();

这里的速度是手指在水平方向划过的像素数,比如上面的设置的时间是1s,获取的速度是在1s内,手指在水平方向划过的像素数,需要注意的是:速度可以为负数,手指从右向左滑动的时候,水平方向的速度即为负值。

GestureDetector

手势检测,用来辅助检测用户的单击、滑动、长按、双击等行为,实际使用也比较简单,书中的使用方式看起来可能比较难理解,这里使用简单一点的方式来说明,比如这里对一个ImageView控件来监听它的双击事件:

ImageView imageGesture = (ImageView) findViewById(R.id.image_gesture);
//创建GestureDetector对象,设置双击监听
final GestureDetector detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener(){
    @Override
    public boolean onDoubleTap(MotionEvent e) {
        Toast.makeText(GestureActivity.this, "双击666", Toast.LENGTH_SHORT).show();
        Log.e(TAG,"onDoubleTap");
    return super.onDoubleTap(e);

}
});
//接管ImageView的onTouche方法
imageGesture.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return detector.onTouchEvent(event);
    }
});

上面我只重写了一个监听双击的方法,除此之外还有很多其他的方法,如下:
在这里插入图片描述
在日常开发中,比较常用的有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap(双击),建议:如果只是监听滑动相关的,建议自己在onTouchEvent中自己实现,如果要监听双击这种行为的话,就使用GestureDetetor

Scroller

用来实现View的滑动效果,使用代码是固定的,如下:

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内部来添加上面的方法,最后在去调用 smoothScrollTo 方法去让View滑动,对于系统提供的View,可以使用动画来完成滑动

View的滑动

三种效果可以实现View的滑动:
1:使用scrollTo/scrollBy
2:使用动画
3:改变布局参数

使用scrollTo/scrollBy

需要注意的是:这两个方法只能改变View的内容的位置而不能改变View在布局中的位置,使用它们进行的滑动是View的内容进行移动,并不能将View本身进行移动,所以如果View的宽和高都使用的 wrap_content 的情况下,使用这两个方法进行滑动,View的内容会被遮挡,甚至无法看到。

其中scrollBy实际上是用scrollTo来实现的,它是基于当前位置的相对滑动,scrollTo是基于所传递参数的绝对滑动,输入的参数都为正数的时候,内容会相对于VIew向左和向上滑动,反之是下右和向下滑动。

经过验证,这两个方法并不是让View平滑的移动过去,会突然转移到目标位置,没有过程的展示。

使用动画

使用动画的方式就不具体说了,在之前的文章中总结过动画的使用,具体见Android 中动画的使用总结

改变布局参数

这种方法就是通过改变要滑动的View的参数来实现效果,即改变LayoutParams,比如我们要把一个Button向右平移100px,我们只需要将这个Button的LayoutParams里的marginLeft参数的值增加100px即可。

还有一种方式是可以在Button的左边放置一个空View,这个空View的默认宽度为0,当我们需要移动目标Button的时候,只需要重新设置空View的宽度即可。重新设置一个View的LayoutParams的代码实现如下:

MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton1.requestLayout();
//或者mButton1.setLayoutParams(params);

三种方式的对比:

scrollTo/scrollBy:操作简单,适合对View内容的滑动;
动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
改变布局参数:操作稍微复杂,适用于有交互的View。

弹性滑动

实现弹性滑动的三种方式:
1:使用Scroller
2:使用动画
3:使用延时策略

在上一节中提到的三种View滑动的策略,除了动画都是生硬的将View直接移动到设定的坐标位置,不会有中间的过度过程,这样显然很不友好,所以就有了这一节的弹性滑动,就类似动画一样,View的移动是有过程的。

使用Scroller

在第一节的最后提到了Scroller的使用方法,在这里书中主要通过源码的形式来分析scroller怎么实现的弹性滑动,这里我就不再说了,有一点需要注意的是:Scroller实现的弹性滑动也是滑动的View的内容,并没有对VIew本身的位置进行滑动。

通过动画

使用动画,这里就不再说了,属性动画和View动画都可以,属性动画改变了View的实际位置,View动画没有改变VIew的实际位置,具体看我之前总结过的文章Android 中动画的使用总结

使用延时策略

这种方式核心思想是通过发送一系列的延时消息达到的渐进式的效果,具体的可以使用Handler或者VIew的postDelayed方法,也可以使用sleep方法,每次接收到延时信息就进行VIew的滑动,通过不断的发送和接收延时信息就会不断的去改变View的位置,达到弹性滑动的效果。

书中给出了一个例子

private static final int MESSAGE_SCROLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 33;
    private int mCount = 0;
    @SuppressLint("HandlerLeak")
    private Handler mHandler = 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);
                                mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,
                                DELAYED_TIME);
                        }
                        break;
                }
                default:
                        break;
                }
        };
    };

例子中通过Hander的延时消息,使用scrollTo来完成弹性滑动,上面已经说过使用scrollTo滑动的是View的内容,这里滑动的是Button上的text文本。

View的事件分发机制

View的事件分发主要是对MotionEvent的时间分发,在上面已经说过,主要有三个事件:按下、滑动、抬起,在时间分发的时候主要有三个方法来功能完成:dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev)

这个方法用来时间分发。如果时间能够传递给当前View,此方法一定会被调用,返回结果受当前View的onTouchEvent 和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

这个方法用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件

public boolean onTouchEvent(MotionEvent event)

这个方法在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

注意:View类中没有onInterceptTouchEvent,只有其他两个方法,ViewGroup类中三个方法都有,也就是说只有ViewGroup才可以拦截事件

以上三个方法的关系可以用下面的伪代码表示:

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

看完这段伪代码再回去理解上面三个方法的解释就豁然开朗了。

注意:当一个View设置了上面的三个方法都是View的内部方法,我们在自定义View的时候可以重写这三个方法来处理对应的事件传递,但是当一个View设置了OnTouchListener,当VIew需要处理事件时,OnTouchListener中的onTouch方法会被回调,如果返回true,那么此View内部的onTouchEvent方法将不会被调用,如果返回false,当前View的onTouchEvent 方法会被调用。由此可见,给View设置OnTouchListener,其优先级比onTouchEvent要高。

当一个View的onTouchEvent返回false的时候,表示它的父容器的onTouchEvent将会被调用,也就是说当子View的onTouchEvent不消耗这个事件的时候,事件会向上传递,传递给父View的onTouchEvent,如果都不消耗,最终会传递给Activity的onTouchEvent方法。这种情况和现实的情况一样,领导下发任务,一级一级的往下发,当下属无法解决的时候,下属会一级一级的项上反馈。

一个点击事件产生后,它的传递过程为:Activity --> Window --> View

除了上面的知识点,书中列举了11条关于时间分发的知识,这里就不一一列举了,我挑出了几个自己感觉新鲜的:

1:同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

2:正常情况下,一个事件序列只能被一个View拦截且消耗。

3:事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外

注意:一个事件序列是分为down,move、up,requestDisallowInterceptTouchEvent这个方法只能影响父布局的move和up事件,也就是说子View可以通过调用 requestDisallowInterceptTouchEvent来让父布局不要拦截此事件序列,但是前提是父布局没有拦截down事件,一旦父布局拦截了down事件,后续的move和up事件,不会再去调用父布局的onInterceptTouchEvent去询问,都会交给父布局来处理。具体源码分析,可以看书中3.4.2节(事件分发的源码分析)

从上面的知识点可以知道:onInterceptTouchEvent不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用,前提是事件能够传递到当前的ViewGroup

View的滑动冲突

滑动冲突的场景

滑动冲突主要是由于界面中内外两层都可以滑动,这个时候当用户去滑动的时候系统不知道滑动事件是数据哪一层的,所以就会出现滑动冲突,滑动冲突主要分为三种:
1:玩不滑动方向和内部滑动方向不一致
2:外部滑动方向和内部滑动方向一致
3:上面两种情况的嵌套
在这里插入图片描述

处理规则

根据上面可以知道滑动冲突的原因是因为可以滑动的View不知道滑动事件是不是自己的,所以冲突的处理规则是根据具体的事件来判断到底由谁来拦截事件。

场景1可以根据用户的手势是水平滑动还是竖直滑动来判断是由谁来拦截事件,而用户水平滑动还是竖直滑动可以通过滑动过程中两个点的坐标来判断;场景2则需要从业务的角度来判断处于什么状态的时候外部View来响应滑动,什么状态内部View来响应滑动;同样场景3也要从业务的角度去区分。

解决方式

先来针对场景1来说,解决方式就要针对处理规则,场景1的处理规则是通过判断用户是上下滑动还是左右滑动然后进行事件的拦截,在事件拦截的时候又有了两种方式,是外部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 = false;
                 }
                break;
        }
        case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
        }
        default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

在上面的模板代码中,我们只需要在ACTION_MOVE事件中根据规则来处理父View是否拦截事件就可以了,同时要在DOWN事件和UP事件中不拦截事件,因为根据前面的分析,如果在DOWN事件中拦截了事件,后面的MOVE和UP事件就会不在去询问是否拦截事件直接全部交给了父View来拦截事件,而UP事件没有太多的意义,所以它也不要拦截事件,以免出现特殊的问题。

内部拦截法:

内部拦截法就是父View不去拦截任何事件,所有事件都传递给子View,如果子View需要此事件就直接消耗掉,否则就交给父容器处理。伪代码如下:

//子View的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);
    }

如上,内部拦截法也要根据处理规则去修改对应的条件,父容器不能拦截DOWN事件,因为根据上面几节的讲解,DOWN事件并不受FLAFG_DISALLOW_INTERCEPT这个标记位的控制,一旦父容器拦截DOWN事件,那么所有的时间都无法传递给子元素中。同时父元素中要做如下修改

//父元素中的方法
public boolean onInterceptTouchEvent(MotionEvent event) {
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
                return false;
        } else {
                return true;
        }
    }

可以看到内部拦截法要比外部拦截法要复杂一点,因为要分别处理内外两个View的方法,而外部拦截法只需要处理父布局就可以了,同时外部拦截法也更好理解。具体的实例演示可以参考书中的具体实例,这里就不在列出了。

猜你喜欢

转载自blog.csdn.net/static_zh/article/details/85338299
今日推荐