[Android开发艺术探索]第三章学习笔记

最近学习了 Android 开发艺术探索的三、四章,本来是想写一篇关于 VIew 的文章。翻了几遍书,又看一些相关文章,觉得题目太大,知识量也不够,不如老实把View相关两章写个学习笔记,除了列出一些结论性知识便于查阅之外,也尝试把一些概念性的东西用自己的方式叙述出来,算是检验与巩固一下所学。

View 基础知识

View 是所有控件的基类,其子类 ViewGroup 是控件组的基类。本节主要讲了 View 的基础知识、点击事件和几个常用对象。

View的位置参数

View 的位置主要由四个顶点决定,对应属性:top 、left、right、bottom,其坐标以相对于父容器位置为标准。对应View源码中mTop 、mLeft、mRight、mBottom 四个成员变量。获取方式类似于Left = getLeft()
View的宽高由位置参数得出:

width = right - left  
height = bottom - top。

Android 3.0 开始,View新增表示位移后位置信息的 4 个变量:x、y、translationX、translationY。View提供了相应 get/set 方法。
与left、top的换算关系如下:

x = left  + translationX
y = top + translationY

translationX、translationY 的默认值为 0。View在位移后top、left不会发生改变

[Android开发艺术探索]第三章学习笔记_第1张图片
View的位置参数

MotionEvent(点击事件)

事件类型
ACTION_DOWN:手指刚接触到屏幕。
ACTION_MOVE:手指在屏幕上移动。
ACTION_UP:手指离开屏幕。

事件序列,指从点击屏幕到离开屏幕的一次操作期间,发生一系列不同类型的点击事件。常见例子如下:
点击后离开屏幕:事件序列为DOWN->UP。
点击后滑动一会再离开屏幕:事件序列为DOWN->MOVE->...->MOVE->UP。

MotionEvent对象可以获取点击事件发生的 x、y 坐标
getX / getY 方法获取相对当前 View 左上角的 x、y 坐标。
getRawX / getRawY 方法获取相对手机屏幕左上角的 x、y 坐标。

TouchSlop(最小滑动距离)

系统能识别的最小滑动距离。
获取方法:ViewConfiguration.get(getContext()).getScaledTouchSlop()

VelocityTracker(速度追踪)

用于追踪手指滑动速度的对象。用法如下:

 @Override public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    //建立对象,添加事件
    VelocityTracker velocityTracker = VelocityTracker.obtain();
    velocityTracker.addMovement(event);

    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        break;
      case MotionEvent.ACTION_MOVE:
        //计算速度参数为时间段 单位为ms  
        //速度 = (起点位置 - 终点位置)/  时间段
        velocityTracker.computeCurrentVelocity(1000); 
        int xV = (int) velocityTracker.getXVelocity(); //单位为像素 下同
        int yV = (int) velocityTracker.getYVelocity();
        Log.e(TAG, "onTouchEvent: ------------->" + xV + "  " + yV);
        break;
      case MotionEvent.ACTION_UP:
        //重置、回收内存
        velocityTracker.clear();
        velocityTracker.recycle();
        break;
      default:
        break;
    }
    return true;
  }

GestureDetector(手势检测)

用于检测双击、长按等行为。

//该类实现onGestureListener
    GestureDetector mGestureDetector=new GestureDetector(this);
//解决长按后无法拖动
    mGestureDetector.setIsLongpressEnabled(false);
//设定双击监听
mGestureDetector.setOnDoubleTapListener(....);

------------------------------------
//在View的onTouchEvent中实现以下,以接管View的onTouchEvent方法
    mGestureDetector.setIsLongpressEnabled(false);
    boolean consume=mGestureDetector.onTouchEvent(event);
    return consume;

实现以上步骤后,只需要在GestureListener 、DoubleTapListener中实现相应方法即可。相应表格在书的 p127 上。

View的滑动

3种滑动方式对比

  • scrollTo/scrollBy :操作简单,适合对View内容滑动。
  • 动画 :操作简单,交互上比较麻烦,属性动画则无此缺点。
  • 改变参数 :操作较复杂。

scrollTo/scrollBy

mScrollX、mScrollY 两个参数表示 View 内容与 View 左、上边缘的距离,单位为像素,上、左方向为正值。
scrollTo(int x,int y) 实现基于传入参数绝对滑动(本质上是把mScrollX、mScrollY改变为传入参数)。
scrollBy(int x,int y) 实现基于当前位置的相对滑动(实际上也是调用了scrollTo,做了参数处理而已)。
该方法只能改变mScrollX、mScrollY ,即只能改变View内容的位置,改变不了View本身位置。且滑动是瞬时完成体验不佳。

动画

View动画存在问题是:View动画不能改变View的位置(参数),需要保留动画效果的话要设定动画的 fillAfter 属性为 true,且 View 在使用动画移动后,由于位置没变,交互上会出现问题。
Android 3.0后提供了属性动画,解决了上述问题。动画的执行类可以设置动画操作的对象的属性、持续时间,开始和结束的属性值,时间差值等,然后系统会根据设置的参数动态的变化对象的属性。

//使 Button 的 "translationX" 属性在 3000 ms 时间内从 0 增加到 300
ObjectAnimator.ofFloat(mButton, "translationX", 0, 300).setDuration(3000).start();

布局参数

直接改变View的布局参数,从而实现滑动或其他效果。

ViewGroup.MarginLayoutParams params =
            (ViewGroup.MarginLayoutParams) mButton.getLayoutParams();
        params.width += 100;
        params.leftMargin += 100;
        mButton.setLayoutParams(params);

View的弹性滑动

弹性滑动其实就是渐进式滑动,可以得到较好用户体验。实现方式很多,如 Scroller、Handler 的postDelayed、Thread 的 sleep等,共同的思路是:将一次完整滑动分成多次小滑动,并在一定时间段内完成。

Scroller

Scroller是一个用于记录滑动行为起始位置、经历时间的对象,且可以根据时间计算相应的值,需配合View的computeScroll方法才能实现弹性滑动。

  private void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int scrollY = getScrollY();
    int deltaX = destX - scrollX;
    int deltaY = destY - scrollY;
    //使得Scroller记录滑动行为相关信息 并不是进行滑动
    mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
    //使View重绘,调用draw方法,其中调用了computeScroll方法
    invalidate();
  }

  //View的draw方法中被调用 重写前是一个空实现
  @Override public void computeScroll() {
    //判断滑动是否结束 具体时间位置等信息由 Scroller # startScroll方法决定
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      //在滑动完成前会再次重绘 循环直到if条件不成立(滑动完毕)
      postInvalidate();
    }
  }

动画

动画本身就是带渐进效果的。

ObjectAnimator.ofFloat(mButton, "translationX", 100, 300).setDuration(3000).start();

另外,也可以利用动画的特性,来实现一些动画不能实现的效果。

    final int startX = 0;
    final int deltaX = 100;
    final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
      @Override public void onAnimationUpdate(ValueAnimator animation) {
        float fraction = animator.getAnimatedFraction();
        mButton.scrollTo(startX + (int) (deltaX * fraction), 0);
      }
    });
    animator.start();

上面这段代码动画并不作用于任何对象上,我们只是利用了其1000ms内完成动画的过程,在没一帧到来时根据动画时间比例去使用 scrollTo 方法,思路和使用 Scroller 类似。

延时策略

思路是通过发送延时消息达到效果。
View 或者 Handler 的 postDelayed 方法,发送延时消息,记录接收消息次数,在消息中根据次数比例滑动,且再次发送消息,如此循环实现弹性滑动。
sleep方法则可通过 while 循环不断滑动 View 和 sleep,从而实现弹性滑动效果。

View的事件分发机制

传递规则

点击事件的事件分发实际上就是对MotionEvent事件的分发过程,即一个 MotionEvent 产生后,系统把这个事件传递给一个具体 View 的过程。
该过程主要由 3 个方法共同完成:

  • dispatchTouchEvent:事件只要能传给当前View,必定调用。返回值表示是否消耗事件,受另外两个方法影响。
  • onInterceptTouchEvent: 上个方法内部调用,判断是否拦截事件。一旦拦截,同一事件序列中不会再次调用
  • onTouchEvent:第一个方法中调用,用于具体处理事件。返回值表示是否消耗该事件。若不消耗,同一事件序列中,当前View无法再次接收到事件
事件分发.png
  public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;

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

3 个方法间的关系可以参照上面伪代码和图片。

View处理事件中各方法优先度

需要注意,onTouchListener 的 onTouch 有可能屏蔽掉 onTouchEvent 方法

11个结论

(1)同一事件序列指手指接触屏幕到离开屏幕期间产生系列事件。从 down 开始,中间有多个 move,以 up 结束。
(2)正常情况下,一个事件序列只能被一个View拦截且消耗。
(3)一个View拦截事件后,同一事件序列都会交由其处理。即同一事件序列中事件不能由两个 View 同时处理。但是,通过特殊手段可以实现。比如,在 onTouchEvent 中强行传递给其他 View。
(4)某个View一旦开始处理事件,如果不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列其他事件不会再交给它处理,且将事件重新交由其父 View 处理(即调用父 View 的onTouchEvent)。
(5)如果View不消耗除 ACTION_DOWN 以外其他事件,那么这个点击事件会消失,View 可以接收后续事件。最终消失的事件交由 Activity 处理。
(6)ViewGroup 默认不拦截任何事件。
(7)View 没有 onInterceptTouchEvent 方法,一旦有事件传递给它,就会调用 onTouchEvent 方法。
(8)View 的onTouchEvent 方法默认消耗事件(返回 true)。除非它设定为不可点击(即clickable 和 longClickable 同时为 false)。longClickable 默认为 false。clickable 部分View(如 Button)为 true。
(9)View 的 enable 属性不影响 onTouchEvent 的默认返回值。
(10)onClick 会发生的前提上当前 View 是可点击的,且收到了 down 和 up 事件。
(11)事件传递过程是由外向内、由父到子的。但是,requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

比较值得注意4、5 条结论,可以理解为一个事件序列传递过来,且其 down 事件传递到某个 View 的onTouchEvent 方法中,且返回 false,事件(包括后续事件,即整个事件序列)会向上传递,依次调用父 View 的 onTouchEvent 方法,直到事件处理或者到达最顶层的 Activity 为止。
如果除了 down 事件的其他事件不被处理(也就是说 View 已经处理的 down 事件),View 可以继续接收后续事件,相当于这个事件序列依然是由该View处理,只是部分不处理的事件会直接交由 Activity 处理。

源码分析

这部分只能自己照着看了,要详细写可以写多一篇了,就只提出一些单独的知识点。
Activity对事件的分发过程:
Acitivity -> Window -> DecorView ->顶级View
Window:可以控制顶级 View 的外观和行为策略,唯一实现类位于 android.policy.PhoneWindow中。
DecorView:可以通过 getWindow().getDecorView 获取,继承于 FrameLayout,是一个 ViewGroup。
顶级 View:我们平常通过setContentView 方法设置的 View,可以通过 ((ViewGroup)getWindow().getDecorView.findViewById(android.R.id.content)).getChildAt(0) 获取。

ViewGroup 有一个 requestDisallowInterceptTouchEvent方法值得注意,该方法用于设定 FLAG_DISALLOW_INTERCEPT 标记位,一旦设置该标记位,ViewGroup 将无法拦截除 down 之外(down 事件会重置标记位,即只对当前事件序列有效,下次事件序列到来时会重置)所有事件。一般用于子 View 中,解决滑动冲突的内部拦截法也需要这个方法才可以实现。

View的滑动冲突

外部拦截法

比较简单好复用的方法,重写滑动冲突外部容器的 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:
        //必须返回false 否则后续事件只能向外传递
        intercepted = false;
        break;
      case MotionEvent.ACTION_MOVE:
        if (父容器需要当前点击事件的条件) {
          intercepted = true;
        } else {
          intercepted = false;
        }
        break;
      case MotionEvent.ACTION_UP:
        //必须返回false 否则影响子View接收事件 
        // 且父容器的特性决定,一旦其开始拦截事件,后续事件都由其处理,即使此处返回false
        intercepted = false;
        break;
      default:
        break;
    }
    mLastXIntercept = x;
    mLastXIntercept = y;
    return intercepted;
  }

内部拦截法

利用 ViewGroup 的 requestDisallowInterceptTouchEvent方法,使得父容器不拦截任何事件,子容器接收事件后消耗,否则交由父容器处理。
父容器中重写 onInterceptTouchEvent :

  @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
      return false;
    } else {
      // 拦截除 down 之外所有事件,
      // 但是由于子 View 会调用 requestDisallowInterceptTouchEvent 方法 
      // 实际上只有特定条件下才会拦截
      return true;
    }
  }

子容器中重写 dispatchTouchEvent :

  @Override public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) getX();
    int y = (int) 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);
  }

你可能感兴趣的:([Android开发艺术探索]第三章学习笔记)