美文网首页
[Android开发艺术探索]第三章学习笔记

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

作者: seven_Android | 来源:发表于2017-06-23 15:26 被阅读0次

    最近学习了 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不会发生改变

    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开发艺术探索]第三章学习笔记

          本文链接:https://www.haomeiwen.com/subject/cdomqxtx.html