Android开发艺术探索之第三章 view的事件体系

前言:
        1.view不属于四大组件,但是它的作用堪比四大组件,甚至比Receiver和Provider还要重要
        2.Activity提供可视化的功能,Android系统提供了很多基础控件,比例button,textview.checkbox,
        但是很多时候控件不能满足我们的需求,这个时候就需要自定义控件,而控件的自定义就需要对于android整个view的事件体系有深入的理解。
        3.如何解决滑动冲突问题?需要对view的事件分发机制有一定的了解
一.view的位置参数
        view的位置主要有他的四个顶点决定,分别对应view的四个属性:mLeft,mTop,mRight,mBottom这几个只代表View的原始位置,这个值是不变的,
        如何获取view的四个属性呢?
                mleft = getLeft();
                mRight = getRight();
                mTop = getTop();
                mBottom = getBottom();
        从3.0开始,view增加了额外的几个属性:x,y,translateX,translateY;
        x,y: 是view的左上角的坐标;
        translateX,translateY:是view左上角相对之前左上角的偏移量

        如果view发生了偏移,那么x,y,translateX,translateY就会变化,x,y,translateX,translateY之间的对应关系如下:
                x = mLeft + translateX;
                y = mTop + translateY;

        注意:这些坐标是相对于view的父容器来的,因此它是一种相对坐标,由此我们可以得出view的宽高和坐标关系:
                int with = right - left;
                int height = bottom - top;
二.MotionEvent和TouchSlop对象
        1.获取点击事件发生的坐标,系统提供了两组方法:getX/getRawX,getY/getRawY ;
                getX/getY:返回的是相对于当前view左上角的x,y坐标;
                getRawX/getRawY:返回时相对于屏幕左上角的x,y坐标
                举个例子:假如现在有一个LinearLayout,它相对屏幕的绝对坐标是222,333,linearlayout中有一个button,
                这个button相对LinearLayout是39,34,所以如果getX,getY就是39,34,
                如果是getRawX,getRawY就是 222+39,333+34 
        2、TouchSlop是系统所能识别出的被认为是滑动的最小距离,这是一个常量,这个值在framework/base/core/res/res/values/config.xml中,这个值和设备有关,不同的设备这个值也不尽相同
                TouchSlop获取方式: ViewConfiguration.get(getContex()).getScaledTouchSlop();
三.VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度,在view的ontouchEvent()方法中追踪当前滑动的速度,
                代码如下: 
                VelocityTracker vVelocityTracker = VelocityTracker.obtain();
                vVelocityTracker.addMovement(event);//此方法在onTouchEvent(Event event)中调用 
                // 如果想知道当前滑动的速度,可以采用以下方式: 
                vVelocityTracker.computeCurrentVelocity(1000);//表示 1000ms 
                int xVelocity = vVelocityTracker.getXVelocity();
                int yVelocity = vVelocityTracker.getYVelocity();
                注意两点: 
                1.获取速度之前必须先计算速度,vVelocityTracker.computeCurrentVelocity(1000)就是计算速度,
                2.速度单位是像素,比如将时间设置1000ms,就是1s,手指在水平方向划过200,那么水平速度就是200,
                速度可以是负数:手指从右向左滑就是负数,速度 = (终点 - 起点)/ 时间段
                3.最后不需要的时候调用clear方法重置并回收: 
                VelocityTracker.clear(); 
                vVelocityTracker.recycle();
四.GestureDetector:用户辅助检测用户的单击,滑动,长按,双击等行为,使用GestureDector也并不复杂,参考如下:
            GestureDetector gestureDector = new GestureDetector(this);
            解决长按屏幕后无法拖动的现象gestureDector.setIsLongPressEnabled(false);
            接着接管View的ontouchEvent()方法,在view的onTouchevent()方法中添加如下代码:
            int consume = gestureDector.onTouchEvent(event);
            return consume;
            做完上面两步就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法
            OnlGestureListener:
                onDown():一个action_down触发
                onShowPress():一个action_down触发,他和onDown()的区别就是,没有松开,没有拖动
                onSingleTapup();这是一个单击行为
                onScroll();手指按下屏幕并拖动:有一个Action_down和多个Action)_move这是拖动行为
                onLongPress:用户长时间按着屏幕不放,长按 onFling:用户按下屏幕,快速滑动后松开,一个down,多个move,一个up
            OnDoubleTapListener:
                nDoubleTap:双击,两次连续的单击组成,他不可能和onSingleTapComfirmed()并存
                onSingleTapComfirmed:严格的单击行为,注意它和onSingleTapUp的区别,如果触发了onSingleTapComfirmed后面不能跟着单击行为,这只能是单击,而不可能是双击
                onDoubleTapEvent:发生了双击行为,down,move.up都会触发
五.Scroller弹性滑动对象:
             Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用Scroller的场景并不多,
             但是很多大家所熟知的控件在内部都是使用Scroller来实现的,如ViewPager、ListView等。
             而如果能够把Scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似于ViewPager这样的功能。 用于实现view的弹性滑动,
             我们知道,当使用view的scrollTo/scrollBy方法来滑动时,其过程是瞬间的,这种用户体验很不好,
             这个时候需要使用scroller来实现过度效果的滑动,Scroller本身无法让view弹性滑动,他需要和view的computeScroll()方法配合使用才能完成这个功能, 
             如何实现Scroller呢?
             它的典型代码是固定的:Scroller Scroller = new Scroller(this);
             缓慢滚动到指定位置
              private void smoothScrollTo(int destX,int destY){ 
                  int scrollX = getScrolllX(); 
                  int delta= destX - scrollX;//100ms滑动到destX、效果就是慢慢滑动
                   scroller.startScroll(scrollX,0,delta,0,100);
                   invalidate();//调用onDraw()---->调用computeScroll()
              }
              @override
              public void computeScroll(){ 
                    if(mScroller.computeScrollOffset()){ 
                         scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                         postInvalidate();//--->调用onDraw()--->computeScroll(),如此反复	
                    } 
               }
              解析:scroller的startScroll(int startx,int starty,int dx,int dy,int duration)其实很简单,
               startx,starty表示滑动的起点,dx,dy表示的是要滑动的距离,duration是滑动的时间
               Scroller的startScroll是无法滑动的,真正滑动的方法是invalidate();此方法会回调ondraw(),onDraw()方法又会回调computeScroll(),
               在computeScroll()中通过mScroller.computeScrollOffset()判断当前时刻应该所在的位置,如果当前位置不是startScroll设置的终点位置,
               就会调用scrollTo滑动到这个时刻应该位于的位置
六、View的滑动
   其实任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法。
   通过三种方法可是实现view的滑动:
   1.view本身提供的scrollTo/scrollBy方法  
        scrollBy实际内部也是调用了scrollTo(),他是基于当前位置的相对移动,而scrollTo则是基于所传参数的绝对移动 
        view的内部有两个属性mScrollX,mScrollY,这个两个属性通过getScrollX,getScrollY获取, 
        在滑动过程中mScrollX总是等于view的左边缘和view内容的左边缘的距离,mscrollY等于view上边缘与view内容上边缘的距离 
        scrollTo和ScrollBy只改变view内容的位置,并不改变view的位置 
        view的左边缘在view内容左边缘的右边时,mScrollX为正,反之为负; 
        view的上边缘在view内容上边缘的下边时,mScrollY为正,反之为负; 
        换句话说,上滑为正,左滑为正,否则反之
   2.通过动画给view添加平移效果来实现滑动 
        其实平移就是一种滑动,他可以采用传统的view动画,也可以使用属性动画,
        如果属性动画为了兼容3.0以下的版本,需要采用开源动画库nineoldandroids

   3、改变布局参数
        LinearLayout.LayoutParams marginLayoutParams = (LinearLayout.LayoutParams) scrollByBtn.getLayoutParams(); 
        marginLayoutParams.leftMargin += 100; scrollByBtn.setLayoutParams(marginLayoutParams);
        
   各种滑动方式的对比:
        scrollTo/scrollBy是专门用于view的滑动,但是只能针对内容,不能滑动view本身 
        动画:操作简单,主要用于没交互的view和实现复杂的动画效果 
        改变布局参数:操作稍微复杂,适用于有交互的view
   qi、View的弹性滑动
   1.使用scroller 	
   2.通过动画(使用valueAnimator,通过监听某一个值的变化,来计算view应该在什么位置,并使用scrollto滑动,这种方式和scrooller非常相似 
   3.使用延时:核心思想就是发送一系列延迟消息从而达到一个渐进的效果,具体可以使用Handler和view的postDelayed方法,
        使用线程的sleep也可以实现,在while循环中不断的滑动view合理sleep
八、View的事件分发机制
    1.点击事件的传递规则
        点击事件的分发其实就是MotionEvent事件分发的过程,当一个MotionEvent产生以后,系统需要把这个事件传递给具体的view,这个传递的过程就是分发过程,
        点击事件的分发过程由三个重要的方法共同完成:dispathTouchEvent(),onInterceptTouchEvent(),onTouchEvent();
        dispathTouchEvent:
            如果事件能够传递到当前view,那么此方法一定被调用,返回值受View的onInterceptTouchEvent和下级view的dispathTouchEvent的影响
        onInterceptTouchEvent:
            在dispathTouchEvent方法的内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么同一事件序列当中此方法不会被再次调用,
            返回值表示是否拦截当前事件
        onTouchEvent:
            在dispathTouchEvent方法中调用,用来处理点击事件,返回值表示是否消耗此事件,如果不消耗,同一个事件序列中,当前view不会再次接受事件

    以下代码来表示三者之间的区别:
    @Override protected boolean dispatchHoverEvent(MotionEvent event) {
        boolean consume = false;
        if(onInterceptTouchEvent(event)){
           consume = onTouchEvent(event);
        }else {
            consume = child.dispatchTouchEvent(event);
        }
          return consume;
     }
     流程梳理:
        1.对于一个viewGroup,当点击事件产生以后,他的dispatchTouchEvent就会调用,如果这个ViewGroup的onInterceptTouchEvent返回true就表示要拦截当前事件,
        接着事件就会交给viewgroup处理,即它的onTouchEvent就会被调用; 如果这个Viewgroup的onInterceptTouchEvent返回false就表示不拦截当前事件,
        这时当前事件就会传递给它的子元素, 接着子元素的dispatchTouchEvent方法就会被调用,如此反复,直到事件被最终处理
        2.当一个view需要处理事件时,如果它设置了OnTouchListener,那么OnTouchEvent的onTouch()方法就会回调,
        如果ontouch返回true,onTouchEvent不会回调 如果ontouch返回false,onTouchEvent就会回调,
        由此可见,view设置的onTouchListener的优先级比onTouchEvent高,
        在onTouchEvent方法中,如果设置了onClickListener,那么它的onClick()方法就会被调用可以看出我们平时用的OnClickListener,
        其优先级最低,处于事件的末尾3.当一个点击事件产生后,事件传递顺序:actiivty---》windows---》view,view收到事件以后就会按照分发机制进行分发,
        考虑一种情况:如果view的onTouchEvent返回false,那么它的父容器的onTouchEvent()就会调用,
        一次类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传给activity,即Actiivty的ontouchEvent调用
     结论:
        1.同一个事件是指从手指触屏到手指离开,这个过程所产生的一系列事件
        2.正常情况下,一个事件只能被一个view拦截消耗(但也可以通过特殊手段,强行传递给其他的view处理),
        因为一旦一个元素拦截了某个事件,那么同一系列的所有事件都会直接交由他处理 因此就不会再调用onInterceptTouchEvent来询问了,
        3,viewGroup默认不拦截任何事件
        4.view没有onInterceptTouchEvent方法,一旦点击事件传递给他,它的ontouchEvent就会调用
        5.view的ontouchEvent默认是消耗事件的,除非他是不可点击(clickable,longClickable同时为false)
        6、view的enable属性不影响onTouchEvent的默认返回值,哪怕一个view是disable状态,只要它的clickable和longclickable返回true,
        那么ontouchEvent就返回true
        7.onclick发生的前提是当前view是可点击的,并且它收到download和up事件
        8.事件总是由外到内,从父元素传递给子元素,通过requestDisallowInterceptTouchEvent方法可以在子元素干预父元素的事件分发过程,但是action_down事件除外
九.事件分发的源码解析;
        Activity.dispatchTouchEvent-->getWindow().superdispatchTouchEvent--->mDecor.superDispatchtouchEvent()

        getWindow.getDecorView().findViewByID(android.R.id.content).getChildAt(0)这种方式可以获取通过setContentView()设置的内容


        viewGroup在如下两种情况会判断是否要拦截当前事件:
            1.事件类型是Action_down 或者 mFirstTouchTarget != null,当事件有viewGroup的子元素成功处理,mFirstTouchTarget会被赋值并指向子元素,
            反过来一旦viewGroup拦截此事件,mFirstTouchTarget != null不成立,导致viewGroup的onInterceptTouchEvent不会再调用

            当然有一种情况:FLAG_DISALLOW_INTERCEPT标志位,这个标志位是通过requestDisallowInterceptTouchEvent方法设置的,
            FLAG_DISALLOW_INTERCEPT一旦设立,viewGroup无法拦截除ACTION_DOWN以外的事件,为什么说是ACTION_DOWN以外的事件呢?
            这是因为ACTION_DOWN会重置此标志

            View的CLICKABLE和LONGCLICKABLEL有一个为true,那么他就会消耗这个事件,即onTouchEvent返回true,不管它是不是DISABLE状态,
             当ACTION_UP事件发生时,会触发performClick方法 如果设置了onClickListener,那么performClick方法内部会调用它的onclick方法

            view的LONGCLICKABLE默认是false,view的CLICKABLE是否为false,跟具体的view有关,button的CLICKABLE默认是true,textview默认是false
            setonClickListener自动将view的clickable设为true,setlongClicklistener()默认将longClickable设为true;

十.滑动冲突解决方案:
     这里给出了两种解决滑动冲突的方式:外部拦截法和内部拦截法

     外部拦截法:点击事件经过父容器的拦截处理,如果父容器需要此事件就拦截,不需要就不拦截,外部拦截法需要重写父容器 的onInterceptTouchEvent方法,在内部做相应的拦截即可,代码如下:

  

父类中:
     int mLastXIntercept;
     int mLastYIntercept;
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            intercept = false;//必须返回false,如果返回true,move,up就没法传给子view
            break;
        case MotionEvent.ACTION_MOVE:

            if(父容器需要当前点击事件){
               intercept = true;
            }else {
               intercept = false;
            }

        break;
        case MotionEvent.ACTION_UP:
            intercept = false;//必须返回false
            break;
         default:
            break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercept;
        }

     内部拦截法:需要配合requestDisallowInterceptTouchEvent方法才能正常工作,

       子类中:
              int mLastXIntercept;
              int mLastYIntercept;
      
              @Override
              public boolean dispatchTouchEvent(MotionEvent ev) {
                   int x = (int) ev.getX();
                   int y = (int) ev.getY();
                   switch (ev.getAction()){
                   case MotionEvent.ACTION_DOWN:
                      getParent().requestDisallowInterceptTouchEvent(true);//请求父控件不要拦截
                      break;
                   case MotionEvent.ACTION_MOVE:
                      int deltX = x - mLastXIntercept;
                      int deltaY = y - mLastYIntercept;
                      if(父容器需要当前点击事件){
                          getParent().requestDisallowInterceptTouchEvent(false);//请求要拦截
                      }
                      break;
                   case MotionEvent.ACTION_UP:
                       break;
                   default:
                       break;
                   }
                   mLastXIntercept = x;
                   mLastYIntercept = y;
                   return super.dispatchTouchEvent(ev);
              }
        父类:
              @Override
              public boolean onInterceptTouchEvent(MotionEvent ev) {
                  int action = ev.getAction();
                  if(action == MotionEvent.ACTION_DOWN){
                     return false;
                  }else {
                     return true;
                  }
              }

    







猜你喜欢

转载自blog.csdn.net/a1527238987/article/details/80208285
今日推荐