android开发艺术第三章读书笔记

android开发艺术探索——第三章View的事件体系

3.1

View的基础知识点

(一) View 和 ViewGroup

  Veiw 是Android中所有控件的基类。View是一种界面层的控件的抽象。
   ViewGroup也是继承之View,翻译为控件组。ViewGroup内部包含了许多控件。所以View本身可以是单个控件也可以是由多个控件组成的一个控件。
   例:
      LinearLayout不但是一个View还是一个ViewGroup,而ViewGroup内部可以有子View的,这个子View同样还可以是ViewGroup。

(二)View的位置参数

   View的位置由四个顶点来决定,分别对应于四个属性:top,left,right,bottom。这四个参数都是相对于Veiw的父容器来说的,是相对坐标。

(三)MotionEvent 和 TouchSlop

1.MotionEvent
    · ACTION_DOWN 手指刚接触屏幕
    · ACTION_MOVE 手指在屏幕上移动
    · ACTION_UP 手指从屏幕上松开的一瞬间
    通过MotionEvent对象可以得到点击事件发生的x 和 y坐标。系统提供了两组方法:
        getX/getY 和 getRawX/getRawY
    区别:
        getX/getY返回的是相对于当前View的左上角的x和y坐标;
        getRawX/getRawY返回的是相对于屏幕的左上角的x和y的坐标
2.TouchSolp
    系统所能识别出的被认为是滑动的最小距离。也就是说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,
    系统会认为这不是一次滑动。需要注意的是,不同的
    设备这个常量值可能是不相同的。可通过如下的方式获取这个常量值:
    ViewConfiguration.get(getContext()).getScaledTouchSlop()

(四)VelocityTracker、GestureDetector和Scroller

1.VelocityTracker
     速度跟踪,用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度。
     首先,在View的onTouchEvent方法中追踪当前的点击事件的速度:
       > 
        VelocityTracker velocityTracker = VelocityTracker.obtain();
        velocityTracler.addMovement(event);
     接着,
       >
        velocityTracker.computeCurrentVelocity(1000);
        int xVelocity= (int)velocityTracker.getXVelocity();
        int yVelocity= (int)velocityTracker.getYVelocity();
     这里需要注意两点:
         第一: 获取速度之前必须先计算速度,即必须先调用computeCurrentVelocity(1000)之后是getXVelocity(),getYVelocity()
         第二: 这里的速度是在指定时间内划过的像素数
                例:将时间间隔设为1s,在1s内,手指在水平方向从左向右滑动100像素,水平速度就是100;
                速度也可以为负数。当手指从右向左滑过100像素,水平方向的速度就是负值。
                公式表示:
                         速度 = (终点位置-起始位置)/ 时间段
     最后,当不需要的时候,需要调用clear方法重置并回收内存。
       >
       velocityTracker.clear();
       velocityTracker.recycle();
2.GestureDetector
      手势检测,用于辅助检测用户单击、滑动、长按和双击等行为。(如果只是监听滑动相关,建议在onTouchEvent;
      如果是双击,需要使用GestureDetector)
      首先,需要创建一个GestureDetector对象实现onGestureListener接口,
       这里根据需要还可以实现OnDoubleTapListener 实现双击监听
       >
         GestureDetector mGestureDetector = new GestureDetector(this);
         //解决长按屏幕后无法拖动的现象
         mGestureDetector.setIsLongpressEnabled(false);
      接着,接管目标的View的OnTouchEvent方法,在待监听的View的OnTouchEvent中添加:
         >
            boolean consume = mGestureDetector.onTouchEvent(event);
            return consume;
      然后,可以有选择地实现OnGestureListener 和 OnDoubleTapListener中的方法
3.Scroller
            Socroller本身无法让View弹性滑动,需要和View的computeScroll配合使用
               >
                  Scroller scroller = new Scroller(this);

                  //缓慢滑到指定位置
                  private void smoothScrollTo(int destX, int destY){
                       int scrollX = getScrollX();
                       int delta= destX - getScrollX();
                       //1000ms内滑向destX,效果就是慢慢滑动
                       mScroller.startScroll(scrollX,0 , delta,0 , 1000);

                       invalidata();
                  }
                  @Override
                  public void computeScroll(){
                      if(mScroller.computeScrollOffset()){
                          scrollTo(mScroller.getCurrX(),mScroller.getCurrY());

                          postiInvalidate();
                      }
                 }

3.2

View的滑动

 一般View的滑动通过三种方式实现:
     第一种通过View本身的提供的scrollTo/scrollBy方法实现
     第二种通过动画给View施加平移效果实现滑动
     第三种通过改变View的LayoutParams使得View重新布局从而实现滑动

(一)使用scrollTo/scrollBy

    public void scrollTo(int x,int y){
           if( mScrollX != x && mScrollY != null){
                int oldX = mScrollX;
                int oldY = mScrollY;

                mScrollX = x;
                mScrollY = y;

                invalidateParentCaches();
                onScrollChanged(mScrollX,,mScrollY,oldX, oldY);
                if( !awakenScrollBars()){
                      postInvalidateOnAnimation();
                }
           }
    }
    /*************************************/
    public void scrollBy(int x , int y){
           scrollTo(mScrollX + x, mScrollY + y);
    }
scrollBy()调用scrollTo()方法。scrollTBy()基于当前位置的相对滑动,scrollTo()实现了基于
所传递参数的绝对滑动。

* 注意:*
使用scrollTo()和scrollBy()来实现View的滑动,只能将View上的内容进行移动,
并不能将View本身进行移动。
无论怎么滑动都不可能将当前的View滑动到附近的View所在区域。

(二)使用动画

 使用动画主要是操作View的transklationX和transklationY属性
 动画代码,此动画在100ms内将一个View从原始位置向右下角移动100个像素。
 <set xmlns:android="http://schemas.android.com/apk/res/android"
      android:fillAfter="true"
      android:zAdjustment="normal">
      <translate
         adnroid:duration = "100"
         android:fromXDelta = "0"
         android:fromYDelta = "0"
         android:interpolator = "@android:anim/linear_interpolator"
         android:toXDelta = "100"
         android:toYDelta = "100" />
 </set>
 属性动画代码
 ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
 View的动画是对View的影像做操作,并不能真正改变View的位置参数,包括宽/高,
 如果希望保留动画后的状态,将
 fillAfter设置为true;

(三)改变参数布局

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

(四)对比

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

3.3弹性滑动

(一)使用Scroller

    Scroller源码:
    Scroller scroller = new Scroller(mContext);

    //缓慢滚动到指定位置
    private void smoothScrollTo(int destX,int destY){
      int scrollX = getScrollX();
      int deltaX = destX - srollX;
      //1000ms内滑向destX,效果就是慢慢滑动
      mScroller.startScroll(scrollX,0,deltaX,1,1000);
      invalidate();
    }

    @Override
    public void computeScroll(){
      if(mScroller.computeScrollOffset()){
          scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
          postInvalidate();
      }
    }

3.4

View的事件分发

(一)点击事件的传递规则

* public boolean dispatchTouchEvent(MotionEvent ev) *
如果事件能够传递给当前View,此方法一定会被调用,返回结果受当前View的oTouchEvent 和
下级View的dispatchTouchEvent 方法的影响,表示是否消耗当前事件

* public boolean onInterceptTouchEvent(MotionEvent event) *
在上述内部调用,用来判断是否拦截某个事件,如果当前View拦截某个事件,
那么同一事件序列当中,此方法不会再调用,返回结果表示是否拦截当前事件

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

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

传递规则:
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个
ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即
它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示不拦截当前事件,
这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会调用,如此反复,直到事件被处理。

当一个View需要处理事件时,如果它设置了onTouchListener,onTouchListener中的onTouch方法会被回调。这件事如何处理还要
看onTouch的返回值,如果返回false。则当前View的onTouch方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。
由此可见,给View设置onTouchListener,其优先级比onTouchEvent要高。在onTouch方法中,如果设置的有onClickListener,那么
onClick方法会被调用。
点击事件的传递顺序: activity --> window --> View

事件分布的源码分析

(一)Activity对点击事件的分布 过程

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

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

  事件开始交给Acitivity所属的Winow进行分布,如果返回true,整个事件循环就结束,返回false
意味着事件没人处理,所有View的onTouchEvent都返回了false,Activity的onTouchEvent会调用。Window
将事件传递给ViewGroup的。
  Window是个抽象类,Window的dispatchTouchEvent是个抽象方法,实现类是PhoneWindow。

 源码:PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent ev){
     return mDecor.superDispatchTouchEvent(ev);
}

  PhoneWindow将事件直接传递给了DecorView。
  通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)
获得Activity所设置的View,这个mDecor就是getWindow().getDecorView()返回的View,通过setContentView
设置的View就是mDecor的子View。目前事件传递到了这里。

(二)顶级View 对点击事件的分发过程

View事件分发回顾:
    点击事件达到顶级View(一般是一个ViewGroup)以后,会调用ViewGroup的dispatchTouchEvent方法;
此时,
      · 如果顶级ViewGroup拦截事件onInterceptTouchEvent返回 true,则事件由ViewGroup本身处理。
        这时如果ViewGroup的mOnTouchListener被设置,则OnTouch被调用,否则OnTouchEvent会被调
        用。也就是说如果能提供的话,onTouch会屏蔽掉onToucheEvent。在onTouchEvent中,如果设
        置了mOnClickListener,则onClick会被调用。

      · 如果顶级ViewGroup不拦截事件,则事件会传递到它所在的点击事件链上的子View,这个时候子
        View的dispatchTouchEvent会被调用。

(三)View对点击事件的处理过程

* 分发过程 和 处理过程第一遍没看懂*
  View的setClickable 和 setOnLongClickListener会自动将View的 CLICKABLE 和 LONG_CLICKABLE属性
设置为true。

3.5

View的滑动冲突

(一)常见的滑动冲突场景

常见的滑动冲突场景,简单分为三种:

  • 场景1——外部滑动方向和内部滑动方向不一致
  • 场景2——外部滑动方向和内部滑动方向一致
  • 场景3——上面两种情况的嵌套

(二)滑动冲突的解决方式

* 针对场景1的解决方式 *

1.外部拦截

  所谓外部拦截是指事情都经过父容器的拦截处理,如果父容器需要此事件就拦截;
如果不需要就不拦截。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在
内部做相应的拦截处理。

 伪码如下:
public boolean onInterceptTouchEvent(MotionEvent event){
       boolean intercepted = false;
       int x = (int) event.getX();
       int y = (int) event.getY();
       switch(event.getAction()){
             case MotionEvent.ACTION_MOVE:
                  intercepted = true;
             break;
             case MotionEvent.Move:
                  if(父容器需要当前点击事件){
                     intercepted = true;
                  }else{
                     intercepted = false; 
                  }
             break;
             case MotionEvent.ACTION_UP:
                  intercepted = false;
             break;
      }
      mLastXIntercept = x;
      mLastYIntercept = y;
      return intercepted;
}

  在onInterceptTouchEvent方法中,首先是在ACTION_DOWN,父容器必须返回false,即不拦截
ACTION_DOWN,ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递
子元素了;其次是ACTION_MOVE事件,这个事件可以根据需求决定是否需要拦截,如果父容器需要
拦截就返回true,否则返回false;最后是ACTION_UP事件,必须要返回false,因为ACTION_UP时事
件本身没有啥意义。

2.内部拦截

  内部拦截法指父容器不拦截任何事件,所有的事件都传给子元素,如果子元素需要此事件就
直接消耗掉,若不需要,就交由父容器进行处理,这种方法和Andorid中的事件分发机制不一致,
需要配合requestDisallowInterceptTouchEvent方法才能正常工作m,使用起来外部拦截稍显复杂。

  伪代码如下:
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.requestDisallowInterceptTopuchEvent(false);
               }
          break;
          case MotionEvent.ACTION_UP:
               break;
          break;
       }
       mLastX = x ;
       mLastY = y ;
       return super.dispatchTouchEvent(event);
}

  当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动也不能动。除了
子元素需要做处理以外,父元素也要默认拦截了 ACTION_DOWN 以外的其他事件,这样当子元素调用parent.requestDisalowInterceptTouchEvent(false)方法时,父元素才能拦截所需的
事件。
  为什么父容器不能拦截ACTION_DOWN 事件呢?那是因为ACTION_DOWN 事件并不受FLAG_DISALLOW_INTERCEPT这个标记的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就起不了作用,所以父元素做下面的改动。

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

你可能感兴趣的:(android开发艺术第三章读书笔记)