Android开发艺术探索 - 第3章 View的事件体系

46 篇文章 0 订阅
39 篇文章 0 订阅
1.基础知识
  1. 位置参数:

    • left/top/right/bottom是左上和右下的原始坐标位置,不会改变。
    • translation*是相对于left和top的偏移量,默认是0。
    • x/y是左上角当前(所看到的)的坐标。
    • x = left + translationX
    • y = top + translationY
      坐标
  2. MotionEvent:触摸事件

    • ACTION_DOWN/ACTION_MOVE/ACTION_UP
    • 触点位置getX/getY/getRawX/getRawY
  3. TouchSlop:被认为是滑动的最小距离
    ViewConfiguration.get(getContext()).getScaledTouchSlop()

  4. VelocityTracker:速度追踪

    • 绑定event
      VelocityTracker velocityTracker = VelocityTracker.obtain();
      velocityTracker.addMovement(event);
      
    • 计算速度
      velocityTracker.computeCurrentVelocity(1000);
      float xVelocity = velocityTracker.getXVelocity();
      float yVelocity = velocityTracker.getYVelocity();
      
    • 回收
      velocityTracker.clear();
      velocityTracker.recycle();
      
  5. GestrueDetector:手势检测

    • 创建一个实例,实现并绑定一个OnGestrueListener接口,双击行为则实现并绑定OnDoubleTapListener。
    • 接管目标View的onTouchEvent方法:
      boolea consume = mGestureDetector.onTouchEvent(event);
      return consume;
      
    • 常用
      onSingleTapUp(单击)/onFling(快速滑动)/onScroll(拖动)/onLongPress(长按)/onDoubleTap(双击)
  6. Scroller:平滑滚动

2.View的滚动(平移)
  1. 使用scrollTo(x, y)/scrollBy(x, y)
    • scrollTo是绝对滚动,scrollBy是相对滚动。
    • 这两个方法实际上改变的是View content的显示区域,而不是view在布局中的位置。以x方向为例,当参数x为正值,表示显示区域向右侧移动,相对应的content就好像向左滚动了。
  2. offsetLeftAndRight/offsetTopAndBottom
  3. setTranslationX/setTranslationY
  4. 改变布局参数
MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams;
params.width += 100;
params.leftMargin += 100;
view.requestLayout();
  1. 平滑滚动
    • Scroller
      Scroller可以帮助计算平滑滚动过程中的offset,View通过该offset,调用scrollTo方法去执行滚动,直到滚动结束。所以,该方法的使用场景是滚动View的内容,而不是View本身。
      private Scroller mScroller = new Scroller(context);
      
      public void zoomIn() {
          // Revert any animation currently in progress
          mScroller.forceFinished(true);
          // Start scrolling by providing a starting point and
          // the distance to travel
          mScroller.startScroll(0, 0, 100, 0);
          // Invalidate to request a redraw
          invalidate();
      }
      
      @Override
      public void computeScroll() {
          if (mScroller.computeScrollOffset()) {
              // Get current x and y positions
              int currX = mScroller.getCurrX();
              int currY = mScroller.getCurrY();
              
              scrollTo(currX, currY);
              postInvalidate();
          }
      }
      
      自定义的zoomIn方法调用invalidate()进行重绘,darw()中会调用computeScroll()方法决定是否继续滚动,computeScroll()默认为空,需要自己实现。如果滚动没有结束,调用scrollTo执行滚动,并调用postInvalidate()(可以非UI线程调用,底层通过Handler在UI线程调用invalidate())进行下一次重绘,继续执行可能的滚动。
    • 动画
    • 延时策略
      Handler/view.postDelay()
3.View的事件分发机制
  1. Activity事件分发过程
    • 先交由Activity的DecorView的顶层View(即通过setContentView设置的View)去处理:
      Activity#dispatchTouchEvent -> PhoneWindow#superDispatchTouchEvent -> DecorView#dispatchTouchEvent
    • 如果View中没有处理,则交由自己处理:
      Activity#dispatchTouchEvent -> Activity#onTouchEvent
  2. ViewGroup事件分发
    关键方法:dispatchTouchEvent onInterceptTouchEvent
    • event首先分发到ViewGroup的dispatchTouchEvent方法,当为ACTION_DOWN时,会初始化touch target和touch state,并清空FLAG_DISALLOW_INTERCEPT设置。这样即使子view调用了getParent().requestDisallowInterceptTouchEvent,对于ACTION_DOWN是不起作用的。
    // Handle an initial down.
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Throw away all previous state when starting a new touch gesture.
        // The framework may have dropped the up or cancel event for the previous gesture
        // due to an app switch, ANR, or some other state change.
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    
    • 如果event是ACTION_DOWN或者touch target不为null,即产生了一次新的touch或者当前有子view正在消耗event:如果子view没有设置FLAG_DISALLOW_INTERCEPT,那么onInterceptTouchEvent会被调用,决定是否应该intercept event。所以,如果已经进入了event序列,而且target为null,则当前ViewGroup中不存在处理该event的View,直接交由自己处理,不必继续向下分发。
    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    
    • 若果不intercept event,则遍历所有的子View,根据view是否可接收点击事件等条件,决定是否将event分发给子view。注意在进入遍历之前,判断了event的action,只处理了ACTION_DOWN相关的情况,对于ACTION_MOVE和ACTION_UP不会走这个流程。dispatchTransformedTouchEvent具体处理子view的event分发,如果子view消耗了event,返回值即为true,该子view就会添加到target view中:newTouchTarget = addTouchTarget(child, idBitsToAssign);
    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }
    
    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }
    
    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            // childIndex points into presorted list, find original index
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
    
    • dispatchTransformedTouchEvent中就会将event传递给参数中child的dispatchTouchEvent方法。
    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
    
                handled = child.dispatchTouchEvent(event);
    
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    
    • 之后的event就交由child去处理。如果dispatchTransformedTouchEvent返回true,即当前的子view消耗了该event,则ViewGroup就不再继续遍历。
    • 上面是没有intercept的情况,可以将ACTION_DOWN分发到新的子View中。之后,如果当前没有子view正在消耗event,那么event交给ViewGroup自己处理;有view正在消耗event的话,将event分发给他(ACTION_MOVE/ACTION_UP就是在这里被分发出去的),已经刚刚分发到ACTION_DOWN的view不会再次分发。
      同时这里是处理intercept的地方:会直接向所有的touch target发送ACTION_CANCEL,同时touch target会被设置位NULL,这样在下一次处理event的时候,会交由ViewGroup处理。
    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        // Dispatch to touch targets, excluding the new touch target if we already
        // dispatched to it.  Cancel touch targets if necessary.
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    
    • 最后dispatchTouchEvent会返回一个handled标志,来标识当前ViewGroup或其子view是否有消耗event,以告知上一层view。
  3. View事件处理
    关键方法:dispatchTouchEvent onTouchEvent onClick onTouch
    • 当event分发到View的dispatchTouchEvent方法,首先会判断onTouch回调是否有设置,如果有会优先处理;如果没有则交由自己的onTouchEvent处理。onTouch只影响onTouchEvent在此次event的调用。
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
    
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    
    • 如果onTouch没有消耗掉event,则在onTouchEvent中:如果view是clickable的,就会处理并消耗ACTION_DOWN/ACTION_MOVE/ACTION_UP等event,就算view是disable状态,也会消耗,只是不做任何处理。在ACTION_UP的时候,触发performClick方法,onClick回调即会被调用。
  4. 总结:
    • 如果onTouch中不消耗event,即返回false,想要收到后续event,可以将view的clickable设为true。因为只要view是clickable的,onTouchEvent就会返回true,就会消耗event,就可以收到后续event,因为onTouch和onTouchEvent同在view的dispatchTouchEvent中调用,且在onTouchEvent之前,所以同样可以收到。
    • View没有onInterceptTouchEvent方法。
    • View的onTouchEvent主要处理了click和longclick。接收到ACTION_DOWN以后,通过message queue在合适的时刻执行longclick的动作;在ACTION_UP执行click的动作(如果longclick没有执行)。所以如果通过onTouch回调,拦截了ACTION_DOWN,click和longclick就直接失效,但是并不影响onTouchEvent在之后被调用的机会。
    • getParent().requestDisallowInterceptTouchEvent无法作用于ACTION_DOWN,ViewGroup想要intercept掉ACTION_DOWN,子view没有办法干预。
    • ACTION_DOWN的分发非常重要,当子view收到并处理了ACTION_DOWN,ViewGroup会将其添加到touch target中,后续的event都会继续分发给他。如果他收不到ACTION_DOWN,无法收到后续event;如果所有子view收不到ACTION_DOWN,后续event交由ViewGroup自己处理。
    • 当在分发出ACTION_DOWN之后,如果ViewGroup拦截了之后的event,则之前被分发到ACTION_DOWN的子view会收到ACTION_CANCEL,同时touch target会被设置位NULL,这样在下一次处理event的时候,ViewGroup的onTouchEvent就会被调用。
    • 关于onInterceptTouchEvent:
4.View滑动冲突

处理滑动冲突的重点,在于将ACTION_MOVE事件合理的分配到合适的View或ViewGroup。而任何一个事件序列,都始于ACTION_DOWN,所以ACTION_DOWN一定要向子View分发出去。

  1. 外部拦截
    ViewGroup通过重写onInterceptTouchEvent,决定事件是自己处理,还是交由子View处理。
    public boolean onInterceptTouchEvent(MotionEvent evnet) {
        boolean intercepted = false;
    
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
    
            case MotionEvent.ACTION_MOVE:
                if (parent need to handle the event) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
    
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
    
        return intercepted;
    }
    
  2. 内部拦截
    ViewGroup不拦截任何事件,交由子View处理;子view视情况恢复父ViewGroup的拦截,交由ViewGroup处理。
    // ViewGroup
    public boolean onInterceptTouchEvent(MotionEvent evnet) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                return false;
    
            default:
                return true;
        }
    }
    
    // View
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                parent.requestDisallowInterceptTouchEvent(true);
                break;
    
            case MotionEvent.ACTION_MOVE:
                if (parent need to handle the event) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
    
        return super.dispatchTouchEvent(evnet);
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值