《Android开发艺术探索》之View的事件体系(四)

                                                                                  View的事件体系
        Activity承担这可视化的功能,同时Android系统提供了很多基础控件,比如TextView、CheckBox等。系统提供的控件不能满足需求,控件的自定义就需要对Android的View体系有深入的了解,一个典型的场景:屏幕的滑动,用户可以通过滑动来切换到不同的界面。很多情况下我们的应用都需要滑动操作,当处于不同层级的View都可以响应用户的滑动操作时,就会带来一个问题,滑动冲突,使用事件分发机制解决滑动冲突。本章包括:View的基础知识;View的滑动;弹性滑动;View的事件分发机制以及View的滑动冲突。
(一)View基础知识
      主要介绍内容:View的位置参数、MotionEvent和TouchSlop、VelocityTracker,GestureDetector和Scroller对象。
1.1 什么是View?
       View是Android中所有控件的基类,Button、TextView、RelativeLayout和Listview的共同基类都是View。View是一种界面层的控件的一种抽象,它代表了一个控件,除了View,还有ViewGroup,ViewGroup内部包含了许多个控件,即一组View。View本身就可以是单个控件也可以是由多个控件组成的一组控件。eg:Button是一个View,而LinearLayout不但是一个View而且还是一个ViewGroup,而ViewGroup内部是可以有子View的,这个子View同样还可以是ViewGroup。
1.2 View的位置参数
       View的位置主要由它的四个顶点来决定,对应于View的四个属性:top(左上角纵坐标)、left(左上角纵坐标)、right(右下角横坐标),bottom(右下角纵坐标)。高:width= right- left宽:height = bottom - top。可以通过 getLeft()、getTop()...来获取四个参数。

                                                             
       从Android3.0开始,View增加了额外的几个参数:x,y,translationX,translationY,其中x,y是View左上角的图标,而translationX,translationY是左上角相对父容器的偏移量,这几个参数也是相对于父容器的坐标,并且translationX,translationY的默认值为0;x = left + translationX;y = top + translationY。View平移时,top,left不会改变,但其他四者会改变。
1.3.MotionEvent和TouchSlop
1.3.1 MotionEvent(屏幕事件)

       手指接触屏幕后的事件:ACTION_DOWN一手指刚接触屏幕;ACTION_MOVE一—手指在屏幕上移动;ACTION_UP——手机从屏幕上松开的一瞬间。场景一:点击屏幕后离开松开:DOWN->UP;场景二:点击屏幕滑动一会再松开,事件序列为DOWN > MOVE >…..>MOVE->UP。
      通过MotionEvent对象我们可以得到点击事件发生的x和y坐标。getX/getY返回的是相对于当前View左上角的x和y坐标,而geiRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
1.3.2 TouchSlop(滑动最小距离)
       TouchSlop是系统所能识别出的被认为是滑动的最小距离,滑动的距离太短,系统不认为他在滑动,这是一个常量,和设备有关。为了追求更好地用户体验。获取该常量的方式:ViewConfigurtion.get(getContext()).getScaledTouchSlop。
1.4 VelocityTracker,GestureDetector和Scroller
1.4.1 VelocityTracker(速度追踪)

       用于追踪手指在屏幕上滑动的速度,包括水平和竖直方向上的速度。注意计算滑动速度时:1.使用View的onTouchEvent方法进行追踪;2.获取速度的之前必须先使用computeCurrentVelocity计算速度;3.速度是指一段时间内手指滑动的屏幕像素,比如将时间设置为1000ms时,在1s内,手指在水平方向手指滑动100像素,那么水平速度就是100,速度可为负数(逆着坐标正方向)。速度 = (终点位置 -  起点位置)/时间段。

//onTouchEvent方法进行追踪
  VelocityTracker velocityTracker = VelocityTracker.obtain();
  velocityTracker.addMovement(event);
  //获取速度之前先计算
velocityTracker.computeCurrentVelocity(1000);
//获取X和Y方向的速度,可为负数。
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
//调用clear方法来重置并回收内存:
velocityTracker.clear();
velocityTracker.recycle();

1.4.2 GestureDetector(手势检测)
         手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。比较常用的onSingleTapUp(单击),onFling(快速滑动),onScroll(推动),onLongPress(长按)和onDoubleTap(双击)。在实际开发中可以不使用GestureDetector(检测双击必须要用),完全可以自己在view中的onTouchEvent中去实现。

//1.需创建一个GestureDetector对象并实现OnGestureListener接口,OnDoubleTapListener从而能够监听双击行为
GestureDetector mGestureDetector = new GestureDetector(this);
//2.解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
//3.接管目标View的onTouchEvent方法
boolean consum = mGestureDetector.onTouchEvent(event);
return consum;
//4.有选择地实现OnGestureListener和OnDoubleTapListener中的方法了

1.4.3 Scroller(弹性滑动对象)

       弹性滑动对象,用于实现View的弹性滑动,View的scrollTo/scrollBy方法来进行滑动过程是瞬间完成,用户体验不好。Scroller来实现过渡效果的滑动,Scroller本身是无法让View弹性滑动,他需要和view的computeScroll方法配合才能完成这个功能。

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

(二)View的滑动
       掌握滑动的方法是实现绚丽的自定义控件的基础。通过三种方式可以实现View的滑动:第一种是通过View本身提供的scrollTo/scrollBy方法来实现快速滑动;第二种是通过动画给View施加平移效果来实现滑动;第三种是通过改变Viev的LayoutParams使得View重新布局从而实现滑动。
2.1 使用scrollTo/scrollBy
       为了实现View的滑动,View提供了专门的方法来实现这个功能,那就是scrollTo/scrollBy。scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传递参数的绝对滑动。
       mScrollX和mscrollY的单位为像素,mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。View边缘是指View的位置,由四个顶点组成,而View内容边缘是指View中的内容的边缘,scrollTo和scrollBy只能改变View内容的位置而不能变View在布局中的位置。
       当View左边缘在View内容左边缘的右边时,mScrolX为正值,反之为负值;当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。换句话说,如果从左向右滑动,那么mScrollX负值,反之为正值:如果从上往下滑动,那么mScrollY为负值,反之为正值。

                            

/**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    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();
            }
        }
    }
    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
}

2.2使用动画
       通过动画,我们来让一个View移动,而平移就是一种滑动,使用动画来移动View,主要是操作View的translationX,translationY属性,即可以采用传统的View动画,也可以采用属性动画。View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括高宽。
       传统View动画:100ms里让一个View从初始的位置向右下角移动100个像素。希望动画后的状态得以保存还必须将fillAfter属性设置为true,否则动画完成之后就会消失,比如我们要把View向右移动100个像素,如果fillAfter为false,那么动画完成的一刹那,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:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"
        />
</set>

       属性动画:3.0以下可能存在兼容性问题。用nineoldandroids开源库解决。

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

      场景:View的动画并不能真正改变View的位置,通过一个View动画将一个button向右移动100px,并且这个View设置点击事件,然后你会发现,在新位置无法触发,而在老位置可以触发点击事件。解决方法:从3.0开始,使用属性动画可以解决上面的问题。但2.2无法使用属性动画,新位置预创一个一模一样的Button,外观OnClick也想同,当完成平移,将之前Button隐藏。
2.3.改变布局参数
       改变布局参数LayoutParams实现View滑动。eg:一个Button向右平移100px,我们只需要将这个Bution的LayoutParams里的marginLeft参数的值增加100px即可;或者左边放一个view,默认宽度为0,当我们需要向右移动Button时,只需要重新设置空View的宽度即可,就自动被挤向右边,即实现了向右平移的效果。

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)
layoutParams.width +=100;
layoutParams.leftMargin +=100;
button.requestLayout();
//或者button.setLayoutParams(layoutParams);

2.4.各种滑动方式的对比
2.4.1.三者的对比

        scrollTo/scrollBy:操作简单,适合对View内容的滑动。优点:实现滑动效果并且不影响内部元素的单击事件;缺点:只能滑动View的内容,并不能滑动View本身。
        动画:操作简单,主要适用于没有交互的Visw和实现复杂的动画效果。缺点:使用View动画会影响点击事件。
        改变布局参数:操作稍微复杂,适用于有交互的View。
2.4.2自定义View滑动
      实现一个手滑动的效果,这是一个自定义的View,拖动他可以让他在整个屏幕上随意滑动。

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getRawX();//获取手指在屏幕中的坐标,而不能使用getX。
        int y = (int) event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:{
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);//我们要得到两次滑动之间的位移
                int translationX = (int) ViewHelper.getTranslationX(this) + deltaX;
   //nineoldandroids中的ViewHelper类所提供的setTranslationX 和setTranslationy
                int translationY = (int)ViewHelper.getTranslationY(this) + deltaY;
                ViewHelper.setTranslationX(this, translationX);
//进行移动
                ViewHelper.setTranslationY(this, translationY);
                break;
            }
        }
}

(三)弹性滑动
       如何实现View的弹性滑动,生硬地滑动过去用户体验差,因此我们要实现渐进式滑动,实现方法很多,都有一个共同的思想:将一次大的滑动分成若干个小的滑动,并且在一个时间段完成。比如Scroller,Handler#PostDelayed以及Thread#Sleep等。
3.1 Scroller
       Scroller本身并不会滑动,需要配合View的computeScroll方法才能完成弹性滑动的效果,不断的让View重绘,而每次都有一些时间间隔,通过这个事件间隔就能得到他的滑动位置,这样就可以用ScrollTo方法来完成View的滑动了,就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动形成了弹性滑动,整个过程他对于View没有丝毫的引用,甚至在他内部连计时器都没有。

    //当我们构建一个scroller对象并且调用它的startScroll方法
    Scroller scroller = new Scroller(getContext());
    
    private void smootthScrollTo(int destX,int destY){
        int scrollX = getScrollX();
        int deltaX = destX - scrollX;
        //1000ms内滑向destX,效果是慢慢滑动
        //根据源码,只是保存了我们传递的参数
        scroller.startScroll(scrollX,0,deltaX,0,1000); 
//实现View的弹性滑动,导致View的重绘Draw,Draw会调用computeScroll方法。
Invalidiate();
    }
    @Override
public void computeScroll() {
//根据源码,时间流逝的百分比来计算scrollX和Y,改变的百分比值和,这个过程相当于动画的插值器的概念
        if(scroller.computeScrollOffset()){
//使用ScrollTo方法实现滑动
            scrollTo(scroller.getCurrX(),scroller.getCurrY());
            //二次重绘
postInvalidate();
        }
}

3.2 通过动画
       动画本身就是一种渐进的过程,因此通过他来实现滑动天然就具有弹性效果。利用动画的特性来实现一些动画不能实现的效果,还拿scorllTo来说,我们想模仿scroller来实现View的弹性滑动。

        final int startX = 0;
        final int startY = 100;
        Final int deltaX = 0;
        final ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
//可以在onAnimationUpdate方法中加上我们想要的其他操作譬如弹性滑动等等。
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float fraction = animator.getAnimatedFraction();
                testView.scrollTo(startX + (int)(deltaX * fraction),0);
            }
        });

3.3 使用延时策略
      延时策略实现弹性滑动。核心思想通过发送一系列延时消息达到一种渐近式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中来进行View的滑动,如果接连不断地发送这种延时消息,那么就可以实现弹性滑动的效果。对于sleep方法来说,通过在while循环中不断的滑动View和sleep,就可以实现弹性滑动的效果。

//下面采用Handler来做个示例,在大约1000ms内将View的内容向左移动了100像素。
    private static final int MESSAGE_SCOLL_TO = 1;
    private static final int FRAME_COUNT = 30;
    private static final int DELAYED_TIME = 33;
    private int count =1;
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MESSAGE_SCOLL_TO:
                    count++;
                    if(count<=FRAME_COUNT){
                        float fraction = count/(float)FRAME_COUNT;
                        int scrollx = (int)fraction*100;
                        new TestButton(getApplicationContext()).scrollTo(scrollx,0);
                        handler.sendEmptyMessageDelayed(MESSAGE_SCOLL_TO,DELAYED_TIME);
                    }
                    break;
            }
        }
    };

(四)View的事件分发机制
4.1.点击事件的传递规则

       分析的点击事件是我们MotionEvent,即点击事件,而非View.OnClickListener。当一个MoonEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的分发来完成.dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent。
4.1.1 三个方法作用及区别
       puhlic boolean dispatchTouchEvent(MotionEvent ev); 用来进行事件的分发,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent影响,表示是否消耗当前事件。
       public boolean onInterceptTouchEvent(MotionEvent event);判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件;
       public boolean onTouchEvent(MotionEvent event);在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
       调用伪代码:

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

4.1.2 事件传递规则:
       根据上面伪代码,传递的规则就是:对于一个根ViewGroup来说,点击事件产生以后,首先传递给它,dispatchTouchEvent就会被调用,如果这个ViewGroup的onIntereptTouchEvent方法返回true就表示它要控截当前事件,接着事件就会交给这个ViewGroup处理,则他的onTouchEvent方法就会被调用;如果这个ViewGroup的onIntereptTouchEvent方法返回false,就表示不需要拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的onIntereptTouchEvent方法就会被调用,如此反复直到事件被最终处理。
       给View设置的OnTouchListener,其优先级比onTouchEvent要高,在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会用。
      当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个view的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。(程序员世界里的能力强弱问题,难题由上而下的分配,解决不了,交给上级解决)
4.1.3 结论
    (1)同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏慕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最后以up结束;
    (2)正常情况下,一个事件序列只能被一个Visw拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个Vew将本该自己处理的事件通过onTouchEvent强行传递给其他View处理;
    (3)某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceprTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
    (4)某个View一旦开始处理事件,如果它不消耗ACTON_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了。
    (5)如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
   (6) ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
   (7)View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
   (8)view的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView 的clickable属性默认为false
   (9)View 的enable.属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longclickable有一个为true,那么它的onTouchEvent就返会true。
  (10)onclick会发生的前提实际当前的View是可点击的,并且他收到了down和up的事件。
  (11)事件传递过程是由外到内的,理解就是事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterptTouchEvent方法可以再子元素中干预元素的事件分发过程,但是ACTION_DOWN除外。
4.2.事件分发的源码解析
4.2.1.Activity对点击事件的分发过程

       点击事件用MotionEvent来表示,当一个点击操作发生的时候,事件最先传递给Activity,由Activity的dispatchTouchEvent来进行事件的派发,具体的工作是由Activity内部的window来完成的,window会将事件传递给decor view,decor view一般都是当前界面的底层容器(setContentView所设置的父容器),通过Activity.getWindow.getDecorView()获得。

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
//事件交给Activity所依附的window,如果true那就结束了
   //superdispatchTouchEvent(ev)方法也是抽象的,必须找到window的实现类,window的实现类是phonewindow,phoneWindow将事件传递给了DecorView。
     if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}
//phoneWindow将事件传递给了DecorView.
public boolean superDispatchTouchEvent(MotionEvent ev){
        return mDecor.superDispatchTouchEvent(ev);
    }

//从这里开始,事件已经传递到顶级View了,就是在Activity中通过setContentview所设置的View,另外顶级View也叫根View,顶级View一般来说都是VewGroup。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    private DecorView mDecor;
    @Override
    public final View getDecorView(){
        if(mDecor == null){
            installDesor():
        }
        return mDecor;
    }
}

4.2.2顶级View对事件的分发过程
       点击事件达到顶级View(一般是一个ViewGroup)以后,(1)会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:(2)如果顶级ViewGroup拦截事件即 onIntercepTouchEvent返回true,则事件由ViewGroup处理,(3)如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是说如果都提供的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了 mOnTouchListener,则onClick会被调用。(4)如果顶级ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的, 如此循环,完成整个事件的分发。
       ViewGroup不拦截事件的时候,事件会向下分发由他的子View进行处理。首先遍历的是ViewGroup的所有子元素,然后判断子元素是否能接受这个点击事件主要是两点来衡量,子元素是否在播动画和点击事件的坐标是否落在子元素的区域内,如果某子元素满足这两个条件,那么事件就会传递给他处理。
       如果遍历所有的子元素后事件都没有被合适的处理,这包含两种情况,第一是Viewgroup没有子元素,第二是子元素处理了点击事件,但是在 dispatchTouchEvent中返回false,这一版是因为子元素在onTouchEvent返回了false,这两种情况下,ViewGroup会自己处理点击事件。
4.2.3 View对点击事件的处理
       View对点击事件的处理过程,首选会判断你有没有设置onTouchListener,如果onTouchListener中的onTouch为true,那么onTouchEvent就不会被调用,可见onTouchListener的优先级高于onTouchEvent,这样做到好处就是方便在外界处理点击事件。
       View的CLICKABLE和LONG_CLICKABLE有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管他是不是DISABLE状态,这就证实了上面的第8,9,10结论,然后就是当ACTION_UP事件发生之后,会触发performClick方法,如果View设置了onClickListener,那么performClick方法内部就会调用他的onClick方法。
       View其CLICKABLE为true,不可点击的为false,比如button是可点击的,textview是不可点击的,通过setonclick或者longclick都是可以改变状态的。
(五).View的滑动冲突
       界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突,如何解决滑动冲突?
5.1 常见的滑动冲突场景
       常见的滑动一般有三个方面:外部滑动方向和内部滑动方向不一致;外部滑动方向和内部滑动方向一致;上面两种情况的嵌套。

                              
       场景一:ViewPager+Fragment配合使用组成的页面滑动效果;左右滑动来切换页面,而每个页面内部往往又是一个Listview;ViewPager内部处理了这种滑动冲突。如果我们采用的不是ViewPager而是ScrollView等,那就必须手动处理滑动冲突了,否则造成的后果就是内外两层只能有一层能够滑动。还有外部上下滑动、内部左右滑动。
      场景二:主要是指内外两层同时能上下滑动或者内外两层同时能左右滑动。
      场景三:场景3是场景1和场景2两种情况的嵌套,外部有一个SlidingMenu效果,然后内部有一个ViewPager,ViewPager的每一个页面中又是一个Listview。只需要分别处理内层和中层、中层和外层之间的滑动冲突即可。
5.2.滑动冲突的处理规则
       对于场景1,它的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。具体来说:根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件,可以通过水平和竖直方向的距离差来判断。也可以依据滑动路径和水平方向做形成的夹角。

                                                       

        对于场景2来说,比较特殊,据这种业务上的需求我们也能得出相应的处理规则,有了处理规则同样可以进行下一步处理,当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时则需要内部View来响应View的滑动。
       对于场景3来说,根据业务。
5.3.滑动冲突的解决方式
       ViewPager已经处理滑动冲突,不管多复杂的滑动冲突,它们之间的区别仅仅是滑动的规则不同而已,将点击事件交给合适的View去做。两个解决办法:外部拦截法和内部拦截法。
5.3.1.外部拦截法
        外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要这个事件就给他,比较类似事件的分发机制,我们还得重写父容器的onInterceptTouchEvent,如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.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;
        }
        mLastXIntercept = x;
        mLastYIntercept = x;
        return intercepted;
}

       针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需做修改并且也不能修改。这里对上述代码再描述一下,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它处理。
5.3.2.内部拦截法
       内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素要消耗此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,需要重写子元素的dispatchTouchEvent方法。

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX =  x - mLastX;
                int deltaY =  x - mLastY;
                if("父容器的点击事件"){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:

                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
}

        注意:除了子元素需要处理之外,父元素默认也要拦截除ACTION_DOWN之外的其他事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(true)方法时,父元素才能继续拦截所需要的事件。父元素的修改:

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

5.3.3场景一示例
        一个例子:我们来实现一个类似于ViewPgaer中嵌套ListView的效果,为了制造滑动冲突,我们写一个类似ViewPager的控件即可,名字叫做HorizontalScrollViewEx,创建三个ListView并添加进HorizontalScrollViewEx,开始解决滑动冲突。

public class MainActivity extends AppCompatActivity {
    public static final String TAG = "MainActivity";
    private HorizontalScrollViewEx mListContainer;
    private int w,h;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i(TAG,"onCreate");
        initView();
    }
    private void initView() {
        LayoutInflater inflater = getLayoutInflater();
        mListContainer = findViewById(R.id.container);
        //屏幕宽高
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        w = wm.getDefaultDisplay().getWidth();
        h = wm.getDefaultDisplay().getHeight();
        for (int i = 0; i < 3; i++) {
            ViewGroup layout = inflater.inflate(R.layout.content_layout,mListContainer,false);
            layout.getLayoutParams().width = w;
            TextView textview = (TextView) layout.findViewById(R.id.title);
            textview.setText("page"  + (i+1));
            layout.setBackgroundColor(Color.rgb(255/(i+1),255/(i+1),0));
            createList(layout);
            mListContainer.addView(layout);
        }
    }
    private void createList(ViewGroup layout) {
        ListView listview = (ListView) layout.findViewById(R.id.list);
        ArrayList<String>datas= new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            datas.add("names" + i);
        }
        ArrayAdapter<String>adapter = new ArrayAdapter<String>(this,R.layout.content_list_item,R.id.name,datas);
        listview.setAdapter(adapter);
    }
}

       外部拦截法:把父容器的拦截条件换成了实际的逻辑,在滑动过程中,当水平方向的距离大就判断水平滑动,为了能够水平滑动所以让父容器拦截事件,而竖直距离大于就不拦截,于是事件传递给了ListView,所以ListView能上下滑动,这就解决了冲突了。考虑到一种情况,如果此时用户正在水平滑动,但是水平滑动停止之前如果用户再迅速的进行竖直滑动,就会导致界面在水平滑动无法滑动到终点,而处于一种中间状态,为了避免这种不友好的体验,我们水平正在滑动的时候,下一个序列的点击仍然交给父容器。

package com.liuguilin.viewsample;
import android.content.Context;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewGroup;
import android.widget.Scroller;
public class HorizontalScrollViewEx extends ViewGroup {
    public static final String TAG = "HorizontalScrollViewEx";
    private int mChindrensize;
    private int mChindrenWidth;
    private int mChindrenIndex;
    //分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    //分别记录上次滑动的坐标
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;
    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    ....
    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = true;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltax = x - mLastXIntercept;
                int deltaY = y = mLastYIntercept;
                if (Math.abs(deltax) > Math.abs(deltaY)) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                int scrollX = getScrollX();
                int scrollToChildIndex = scrollX / mChindrenWidth;
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChindrenIndex = xVelocity > 0 ? mChindrenIndex - 1 : mChindrenIndex + 1;
                } else {
                    mChindrenIndex = (scrollX + mChindrenWidth / 2) / mChindrenWidth;
                }
                mChindrenIndex = Math.max(0, Math.min(mChindrenIndex, mChindrensize - 1));
                int dx = mChindrenIndex * mChindrenWidth - scrollX;
                ssmoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }
    private void ssmoothScrollBy(int dx, int i) {
        mScroller.startScroll(getScrollX(),0,dx,500);
        invalidate();
    }
    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }
} 

       内部拦截:只需要修改ListView的dispatchTouchEvent方法中的父容器的拦截逻辑,同时让父拦截MOVE和Up事件即可。为了重写Listview的dispatchTouchfvent方法,我们必须自定义一个ListView,称为ListViewEx,然后对内部拦截法的模板代码进行修改。

public class ListViewEx extends ListView {
    public static final String TAG = "ListViewEx";
    private HorizontalScrollViewEx mHorizontalScrollViewEx;
    private int mLastX = 0;
    private int mLastY = 0;
    public ListViewEx(Context context) {
        super(context);
    }
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int delatX = x - mLastX;
                int delatY = y - mLastY;
                if (Math.abs(delatX) > Math.abs(delatY)) {
                    mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
}


 @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN){
            mLastX = x;
            mLastY = y;
            if(!mScroller.isFinished()){
                mScroller.abortAnimation();
                return true;
            }
            return false;
        }else {
            return true;
        }
    }

5.3.4场景二示例
       提供一个可以上下滑动的父容器,这里就叫 StickyLayout,它看起来就像是可以上下滑动的竖直的LinearLayout,然后在它的内部中的滑动冲突了。当然这个StickyLayout是有滑动规则的:当Header显示时或者ListView 滑动到顶部时,由StickyLayout拦截事件:当Header隐藏时,这要分情况,如果Listview已经滑动到顶部并且当前手势是向下滑动的话,这个时候还是StickyLayout拦截事件,其他情况则由ListView拦截事件。
       外部拦截法:当事件落在Header上面时,就不会拦截该事件;接着,如果竖直距离差小于水平距离差,那么父容器也不会拦截该事件;当Header是展开状态并且是向上滑动时,父容器拦截事件;当ListView滑动到顶部并且向下滑动的时候,父容器也会拦截事件。

            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
                    intercepted = 0;
                } else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
                    intercepted = 0;
                } else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
                    intercepted = 1;
                } else if (mGiveUpTouchEventListener != null) {
                    if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
                        intercepted = 1;
                    }
                }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张云瀚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值