android事件分发过程

View

View的基础知识

View是所有控件的基类,是所有界面层的抽象,一个View可以由一个控件组成也可以由一组控件组成,ViewGroup也继承自View,由此可以得出一个View树。

 public class View implements Drawable.Callback, KeyEvent.Callback,
         AccessibilityEventSource {。。。}

View的参数位置

主要是根据四个顶点来决定,top是左上纵坐标(左上角距离父容器的纵坐标大小,以此类推),right是右下横坐标,left是左上横坐标,bottom是右下纵坐标,都是相对于父容器的坐标。

可得:

width = right - left

height = bottom - top

这四个元素对应View中的getXXXX(),比如getRight();

还有几个参数(相对父容器):

  • (x ,y) 是View左上角的坐标

 

  • translationX,translationY,默认值为0,是View左上角相对于父容器的偏移量。

可得:

x = left + translationX

y = top + translationY

点击事件

img

MotionEvent

  • ACTION_DOWN 刚接触屏幕

  • ACTION_MOVE 在屏幕上移动

  • ACTION_UP 从屏幕松开的一瞬间

两种典型事件:

  1. down-up

  2. down - move -move -。。。。-up

通过MotionEvent对象可以得到点击事件发生的x和y坐标,getX/getY是相对于当前View左上角的,getRawX/getRawY是相对于手机屏幕左上角。

TouchSlop

系统能够识别的最小滑动距离。可以用来过滤一些滑动操作。

ViewConfiguration.get(getContext()).getScaledTouchSlop()获得。

VelocityTracker 速度追踪器

在View的onTouchEvent中追踪当前点击事件的速度。

 val velocityTracker:VelocityTracker = VelocityTracker.obtain()
 velocityTracker.addMovement(event)
 //然后可以使用下列方法获得滑动速度,获取速度之前要先计算,1000表示这个计算的是一秒内划过的像素数量,默认从上到下,从左到右为正
 velocityTracker.computeCurrentVelocity(1000)//单位是ms
 velocityTracker.xVelocity
 velocityTracker.yVelocity
 //停止使用的时候进行回收
 velocityTracker.clear()
 velocityTracker.recycle()

GestureDetetor 手势检测

用于辅助判断单机、长按、滑动、双击等过程。

使用:

 val gestureDetector = GestureDetector(applicationContext, @RequiresApi(Build.VERSION_CODES.M)
 object : GestureDetector.OnGestureListener{
     //触碰屏幕瞬间
     override fun onDown(e: MotionEvent?): Boolean {
         TODO("Not yet implemented")
    }
  //尚未松开或拖动
     override fun onShowPress(e: MotionEvent?) {
         TODO("Not yet implemented")
    }
  //轻触后松开
     override fun onSingleTapUp(e: MotionEvent?): Boolean {
         TODO("Not yet implemented")
    }
  //按下屏幕然后拖动
     override fun onScroll(
         e1: MotionEvent?,
         e2: MotionEvent?,
         distanceX: Float,
         distanceY: Float
    ): Boolean {
         TODO("Not yet implemented")
    }
  //长按不放开
     override fun onLongPress(e: MotionEvent?) {
         TODO("Not yet implemented")
    }
  //按下触碰屏幕后快速滑动然后松开
     override fun onFling(
         e1: MotionEvent?,
         e2: MotionEvent?,
         velocityX: Float,
         velocityY: Float
    ): Boolean {
         TODO("Not yet implemented")
    }
 
 
 })
 

还可以根据需要实现双击的监听行为:

 gestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener{
    //严格的单击
     override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
         TODO("Not yet implemented")
    }
  //两个连续的单击,不可与onSingleTapConfirmed共存
     override fun onDoubleTap(e: MotionEvent?): Boolean {
         TODO("Not yet implemented")
    }
  //发生了双击行为,在双击期间down,move,up都会触发该回调
     override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
         TODO("Not yet implemented")
    }
 
 })  

然后接管View中的onTouchEvent方法即可。

 View#onTouchEvent
 return gestureDetector.onTouchEvent(event);

建议:若只是滑动则使用onTouchEvent,若要监听双击事件则使用GestureDetector。

Scroller

使用View的scrollTo和scrollBy方法进行滑动是瞬间完成的,可以使用scroller实现过渡效果,结合View与computeScroll完成。以下是典型代码:

 val scroll = Scroller(this)
  public void startScroll(int startX, int startY, int dx, int dy, int duration)
 //duration单位是ms

 

View的滑动

  1. 通过View本身的scrollTo/scrollBy实现。

  2. 通过动画给View施加平移效果实现。

  3. 通过改变View的LayoutParams使得View重新布局。

通过View本身的scrollTo/scrollBy实现

scrollBy实际上也是调用了scrollTo

 /**
  * Set the scrolled position of your view. This will cause a call to
  * {@link #onScrollChanged(int, int, int, int)} and the view will be
  * invalidated.
  * @param x the x position to scroll to
  * @param y the y position to scroll to
  */
 public void scrollTo(int x, int y) {
     if (mScrollX != x || mScrollY != y) {
         int oldX = mScrollX;
         int oldY = mScrollY;
         mScrollX = x;
         mScrollY = y;
         
         invalidateParentCaches();
         onScrollChanged(mScrollX, mScrollY, oldX, oldY);
         if (!awakenScrollBars()) {
             postInvalidateOnAnimation();
        }
    }
 }
 

mScrollX:View的内容左边缘和和View的左边缘的距离,以像素为单位。

mScrollY: View的内容上边缘和View内容的上边缘的距离,以像素为单位。

View边缘:View的位置,四个顶点组成。

View内容边缘:View中内容的位置。

scrollBy与scrollTo只可以改变内容的位置,不可以改变view的位置。

View左边缘在View内容左边缘的右边时,mScrollX为正值。反之为负。

View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负。

即内容向左向上滑为正,向右向下滑为负。

使用动画进行滑动

对View进行移动,主要是操作View的translationX与translationY元素点此查看

采用View动画代码

如果没有设置fillAfter= true会在结束的时候弹回原来位置,并且并不可以改变View的位置信息。只会改变影像信息。

采用属性动画

3.0以上可以解决以上问题。

首先引入nineoldandroids.jar

重写onTouchEvent

 class MyView @JvmOverloads constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
 ) : androidx.appcompat.widget.AppCompatTextView(context,attrs,defStyleAttr) {
     private val TAG = "MyView"
     //记录最后一个坐标
     private var mLastX: Float = 0f
     private var mLastY: Float = 0f
 
     override fun onTouchEvent(event: MotionEvent?): Boolean {
         val x = event?.rawX
         val y = event?.rawY
         when(event?.action){
             MotionEvent.ACTION_DOWN ->{
                 Log.d(TAG, "onTouchEvent: ACTION_DOWN")
            }
 
             MotionEvent.ACTION_UP ->{
                 Log.d(TAG, "onTouchEvent: ACTION_UP")
 
            }
 
             MotionEvent.ACTION_MOVE ->{
                 val deltaX = x?.minus(mLastX)
                 val deltaY = y?.minus(mLastY)
  //需要偏移到的位置
                 val translationX = ViewHelper.getTranslationX(this) + deltaX!!
                 val translationY = ViewHelper.getTranslationY(this) + deltaY!!
                 ViewHelper.setTranslationX(this, translationX)
                 ViewHelper.setTranslationY(this, translationY)
 
            }
             else ->{
 
            }
 
        }
         if (y != null) {
             mLastY = y
        }
         if (x != null) {
             mLastX = x
        }
         return true
    }
 
 }

 

改变布局参数

改变布局参数,比如在旁边设置一个空的View然后增加其大小。

View的弹性滑动

Scroll

  • 设置duration,在整个时间段内缓慢移动

     //这个方法会根据时间的流逝计算出当前要滑动到的距离
     public boolean computeScrollOffset() {
         if (mFinished) {
             return false;
        }
     
         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
     
         if (timePassed < mDuration) {
             switch (mMode) {
             case SCROLL_MODE:
                 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                 mCurrX = mStartX + Math.round(x * mDeltaX);
                 mCurrY = mStartY + Math.round(x * mDeltaY);
                 break;
             case FLING_MODE:
                 final float t = (float) timePassed / mDuration;
                 final int index = (int) (NB_SAMPLES * t);
                 float distanceCoef = 1.f;
                 float velocityCoef = 0.f;
                 if (index < NB_SAMPLES) {
                     final float t_inf = (float) index / NB_SAMPLES;
                     final float t_sup = (float) (index + 1) / NB_SAMPLES;
                     final float d_inf = SPLINE_POSITION[index];
                     final float d_sup = SPLINE_POSITION[index + 1];
                     velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                     distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }
     
                 mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
                 
                 mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                 // Pin to mMinX <= mCurrX <= mMaxX
                 mCurrX = Math.min(mCurrX, mMaxX);
                 mCurrX = Math.max(mCurrX, mMinX);
                 
                 mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                 // Pin to mMinY <= mCurrY <= mMaxY
                 mCurrY = Math.min(mCurrY, mMaxY);
                 mCurrY = Math.max(mCurrY, mMinY);
     
                 if (mCurrX == mFinalX && mCurrY == mFinalY) {
                     mFinished = true;
                }
     
                 break;
            }
        }
         else {
             mCurrX = mFinalX;
             mCurrY = mFinalY;
             mFinished = true;
        }
         return true;
     }

    由startScroll可以知道,仅仅调用此方法也不会滑动,他只是设置了一些基本的参数

 public void startScroll(int startX, int startY, int dx, int dy, int duration) {
     //滑动的模式
     mMode = SCROLL_MODE;
     //是否结束
     mFinished = false;
     //滑动时间
     mDuration = duration;
     //开始时间
     mStartTime = AnimationUtils.currentAnimationTimeMillis();
     mStartX = startX;
     mStartY = startY;
     mFinalX = startX + dx;
     mFinalY = startY + dy;
     mDeltaX = dx;
     mDeltaY = dy;
     mDurationReciprocal = 1.0f / (float) mDuration;
 }

弹性滑动的整个过程就是设置好滑动的模式以及时间方位等参数之后,使用invalidate对View进行重绘,重绘之后在再次重新计算下一次需要滑动的方位以及时间戳等参数,computeScrollOffset判断,如果有不同的坐标就使用postInvalidate进行重绘,直到没有更新。

 MyView#onTouchEvent
 //按下该控件就会产生动画效果
  MotionEvent.ACTION_DOWN ->{
                 Log.d(TAG, "onTouchEvent: ACTION_DOWN")
                 smoothScrollTo(-400, 0)
  }
 
 private fun smoothScrollTo(desX: Int, desY:Int){
     val scrollX = scrollX
     val deltaX = desX - scrollX
     mScroller.startScroll(scrollX,0,deltaX,0,1000)
     invalidate()
 }
 
 override fun computeScroll() {
     if(mScroller.computeScrollOffset()){
         scrollTo(mScroller.currX, mScroller.currY)
         postInvalidate()
    }
 }

通过动画

 binding.animator.setOnClickListener {
     ObjectAnimator.ofFloat(binding.myView, "translationX",0f,200f).setDuration(100).start()
     //或者
     //向左 所以是-200
     val startX = 0
             val deltaX = -200
             val valueAnimator: ValueAnimator = ValueAnimator.ofInt(0,1).setDuration(1000)
             valueAnimator.addUpdateListener {
                 val fraction = valueAnimator.animatedFraction
                 binding.myView.scrollTo(startX + (deltaX * fraction).toInt(), 0)
            }
             valueAnimator.start()
 }

使用延时策略

该方法是通过postDelayed操作,注意并不一定严格按照设定的时间,因为该方法涉及到线程的调度。

 private val mHandler = object : Handler(Looper.myLooper()!!) {
     override fun handleMessage(msg: Message) {
         when(msg.what){
             1 ->{
                 Log.d("handleMessage", "handleMessage: $count")
                count += 1
                 if(count < 30){
                     val fraction = count / 33f
                     val scrollX = (fraction * -100 ).toInt()
                     Log.d("handleMessage", "scrollX: $scrollX")
 
                     binding.myView.scrollTo(scrollX, 0)
                     sendEmptyMessageDelayed(1,33)
                }
            }
        }
    }
 
 }
 
 
   binding.animator.setOnClickListener {
             mHandler.sendEmptyMessageDelayed(1,33)
        }

 

View的事件分发机制

 view#dispatchTouchEvent
 public boolean dispatchTouchEvent(MotionEvent event) {
     // If the event should be handled by accessibility focus first.
     if (event.isTargetAccessibilityFocus()) {
         // We don't have focus or no virtual descendant has it, do not handle the event.
         if (!isAccessibilityFocusedViewOrHost()) {
             return false;
        }
         // We have focus and got the event, then use normal event dispatch.
         event.setTargetAccessibilityFocus(false);
    }
     boolean result = false;
 
     if (mInputEventConsistencyVerifier != null) {
         mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }
 
     final int actionMasked = event.getActionMasked();
     if (actionMasked == MotionEvent.ACTION_DOWN) {
         // Defensive cleanup for new gesture
         stopNestedScroll();
    }
 
     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;
        }
    }
 
     if (!result && mInputEventConsistencyVerifier != null) {
         mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
 
     // Clean up after nested scrolls if this is the end of a gesture;
     // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
     // of the gesture.
     if (actionMasked == MotionEvent.ACTION_UP ||
             actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
         stopNestedScroll();
    }
 
     return result;
 }
 view#onTouchEvent
 public boolean onTouchEvent(MotionEvent event) {
     final float x = event.getX();
     final float y = event.getY();
     final int viewFlags = mViewFlags;
     final int action = event.getAction();
 
     //是否有clickable事件
     final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
             || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
             || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
 
     if ((viewFlags & ENABLED_MASK) == DISABLED
             && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
         if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
             setPressed(false);
        }
         mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
         // A disabled view that is clickable still consumes the touch
         // events, it just doesn't respond to them.
         return clickable;
    }
     if (mTouchDelegate != null) {
         if (mTouchDelegate.onTouchEvent(event)) {
             return true;
        }
    }
 
     if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
         switch (action) {
             case MotionEvent.ACTION_UP:
                 mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                 if ((viewFlags & TOOLTIP) == TOOLTIP) {
                     handleTooltipUp();
                }
                 if (!clickable) {
                     removeTapCallback();
                     removeLongPressCallback();
                     mInContextButtonPress = false;
                     mHasPerformedLongPress = false;
                     mIgnoreNextUpEvent = false;
                     break;
                }
                 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                     // take focus if we don't have it already and we should in
                     // touch mode.
                     boolean focusTaken = false;
                     if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                         focusTaken = requestFocus();
                    }
 
                     if (prepressed) {
                         // The button is being released before we actually
                         // showed it as pressed. Make it show the pressed
                         // state now (before scheduling the click) to ensure
                         // the user sees it.
                         setPressed(true, x, y);
                    }
 
                     if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                         // This is a tap, so remove the longpress check
                         removeLongPressCallback();
 
                         // Only perform take click actions if we were in the pressed state
                         if (!focusTaken) {
                             // Use a Runnable and post this rather than calling
                             // performClick directly. This lets other visual state
                             // of the view update before click actions start.
                             if (mPerformClick == null) {
                                 mPerformClick = new PerformClick();
                            }
                             if (!post(mPerformClick)) {
                                 performClickInternal();
                            }
                        }
                    }
 
                     if (mUnsetPressedState == null) {
                         mUnsetPressedState = new UnsetPressedState();
                    }
 
                     if (prepressed) {
                         postDelayed(mUnsetPressedState,
                                 ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                         // If the post failed, unpress right now
                         mUnsetPressedState.run();
                    }
 
                     removeTapCallback();
                }
                 mIgnoreNextUpEvent = false;
                 break;
 
             case MotionEvent.ACTION_DOWN:
                 if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                     mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                 mHasPerformedLongPress = false;
 
                 if (!clickable) {
                     checkForLongClick(
                             ViewConfiguration.getLongPressTimeout(),
                             x,
                             y,
                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                     break;
                }
 
                 if (performButtonActionOnTouchDown(event)) {
                     break;
                }
 
                 // Walk up the hierarchy to determine if we're inside a scrolling container.
                 boolean isInScrollingContainer = isInScrollingContainer();
 
                 // For views inside a scrolling container, delay the pressed feedback for
                 // a short period in case this is a scroll.
                 if (isInScrollingContainer) {
                     mPrivateFlags |= PFLAG_PREPRESSED;
                     if (mPendingCheckForTap == null) {
                         mPendingCheckForTap = new CheckForTap();
                    }
                     mPendingCheckForTap.x = event.getX();
                     mPendingCheckForTap.y = event.getY();
                     postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                     // Not inside a scrolling container, so show the feedback right away
                     setPressed(true, x, y);
                     checkForLongClick(
                             ViewConfiguration.getLongPressTimeout(),
                             x,
                             y,
                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                }
                 break;
 
             case MotionEvent.ACTION_CANCEL:
                 if (clickable) {
                     setPressed(false);
                }
                 removeTapCallback();
                 removeLongPressCallback();
                 mInContextButtonPress = false;
                 mHasPerformedLongPress = false;
                 mIgnoreNextUpEvent = false;
                 mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                 break;
 
             case MotionEvent.ACTION_MOVE:
                 if (clickable) {
                     drawableHotspotChanged(x, y);
                }
 
                 final int motionClassification = event.getClassification(); 
               final boolean ambiguousGesture
                       motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; 
               int touchSlop = mTouchSlop; 
               if (ambiguousGesture && hasPendingLongPressCallback()) { 
                   if (!pointInView(x, y, touchSlop)) { 
                       // The default action here is to cancel long press. But instead, we 
                       // just extend the timeout here, in case the classification 
                       // stays ambiguous. 
                       removeLongPressCallback(); 
                       long delay = (long) (ViewConfiguration.getLongPressTimeout() 
                               * mAmbiguousGestureMultiplier); 
                       // Subtract the time already spent 
                       delay -= event.getEventTime() - event.getDownTime(); 
                       checkForLongClick( 
                               delay, 
                               x, 
                               y, 
                               TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS); 
                  } 
                   touchSlop *= mAmbiguousGestureMultiplier; 
              } 
​ 
               // Be lenient about moving outside of buttons 
               if (!pointInView(x, y, touchSlop)) { 
                   // Outside button 
                   // Remove any future long press/tap checks 
                   removeTapCallback(); 
                   removeLongPressCallback(); 
                   if ((mPrivateFlags & PFLAG_PRESSED) != 0) { 
                       setPressed(false); 
                  } 
                   mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; 
              } 
​ 
               final boolean deepPress
                       motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS; 
               if (deepPress && hasPendingLongPressCallback()) { 
                   // process the long click action immediately 
                   removeLongPressCallback(); 
                   checkForLongClick( 
                           0 /* send immediately */, 
                           x, 
                           y, 
                           TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); 
              } 
​ 
               break; 
      } 
​ 
       return true; 
  } 
​ 
   return false; 
}
 ViewGroup# onInterceptTouchEvent()
  public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                 && ev.getAction() == MotionEvent.ACTION_DOWN
                 && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                 && isOnScrollbarThumb(ev.getX(), ev.getY())) {
             return true;
        }
         return false;
    }

 

分发过程伪代码主要如下:

  1. ViewGroup会判断是否要拦截事件,如果拦截则事件转为自身的onTouchEvent进行处理。

  2. 如果不拦截,则分发下去。

事件分发过程就是从底层一直派发下去,直到最上层的View判断是否进行处理,如果不进行处理就返回给上一层。好比一个任务,董事长下发给经理,经理下发给组长,组长再下发给组员,然后由组员先判断自身是否要进行处理,如果不处理就返回给组长进行处理,依次到经理、董事长。。。。。。

 public fun dispatchTouchEvent(event: MotionEvent?):Boolean{
     val consume = false
     if(onInterceptTouchEvent(event)){
         consume = onTouchEvent(event)
    }else{
         consume = child.dispatchTouchEvent(event)
    }
     return consume
 }

处理过程主要如下

在自身的dispatchTouchEvent中,返回true代表已处理,返回false代表不处理。

一个view处理事件时一般有以下步骤:

  1. 如果设置了onTouchListener,则会调用onTouch方法。

  2. 如果1中onTouch返回值为false,那么会调用onTouchEvent方法。

  3. 如果有设置onClickListener那么2中如果调用onTouchEvent,onClick方法会被调用。

源码分析

事件分发的源码解析

事件分发过程分别从Activity -> ViewGroup -> View ,Activity也主要负责添加视图、显示试视图以及通过回调与Window、View进行交互。

Activity的分发

点击事件发生后Activity首先收到点击事件(MotionEvent),然后会调用Activity内的Window进行事件派发:

 Activity#dispatchTouchEvent
 //注释表明可以在当该事件被传递到Window之前拦截点击事件
 public boolean dispatchTouchEvent(MotionEvent ev) {
     if (ev.getAction() == MotionEvent.ACTION_DOWN) {
         //需要知道用户用什么方式对对屏幕进行交互,默认为空实现
         onUserInteraction();
    }
     //将事件交给Window进行分发,window是个抽象类,superDispatchTouchEvent是个抽象方法,不同的window实现的方式不一样,在PhoneWindow中该方法调用了mDecorView的superDispatchTouchEvent方法/
     //若之后的传递的View/ViewGroup处理并返回true,代表事件结束。
     if (getWindow().superDispatchTouchEvent(ev)) {
         return true;
    }
     return onTouchEvent(ev);
 }

Tips:通过:

 (window.decorView.findViewById(android.R.id.content) as ViewGroup).getChildAt(0)

该方式可以获得Activity所设置的View,而DecorView就是window.decorView。

通过decorView之后事件被传递到顶级View,也就是setContentView所设置的View(此view不是decorView),一般是ViewGroup。

 Activity#onTouchEvent
 public boolean onTouchEvent(MotionEvent event) {
     //只有在Window边界外才返回true,也就是说一般Activity不处理事件
     if (mWindow.shouldCloseOnTouch(this, event)) {
         finish();
         return true;
    }
 
     return false;
 }
ViewGroup分发

当ViewGroup的dispatchTouchEvent被调用之后(一般情况下是顶级View进行调用)。

默认不拦截:

 ViewGroup#onInterceptTouchEvent
 public boolean onInterceptTouchEvent(MotionEvent ev) {
     if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
             && ev.getAction() == MotionEvent.ACTION_DOWN
             && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
             && isOnScrollbarThumb(ev.getX(), ev.getY())) {
         return true;
    }
     return false;
 }
  1. 如果ViewGroup拦截方法生效,那么会让ViewGroup处理;

    • 如果mOnTouchListener设置了,那么onTouch方法会被调用;

    • 如果没有设置,那么调用onTouchEvent,也就是说onTouch会屏蔽掉onTouchEvent

    • 如果设置了了mOnClickListener,那么onClick会调用

  2. 如果不拦截事件,则事件传递到ViewGroup的子View上。

 ViewGroup#dispatchTouchEvent[1]
 public boolean dispatchTouchEvent(MotionEvent ev) {
         // Check for interception.
         final boolean intercepted;
    //在事件为down还有mFirstTouchTarget不为空的时候判断是否要拦截,该变量在ViewGroup拦截之后会设置为空,在ViewGroup不拦截传递给子元素处理并返回true的时候会指向子元素。
         //!!! 也就是说拦截事件之后事件up和move都不会进行拦截(onInterceptTouchEvent都不会再调用),默认被这个ViewGroup处理
         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.
             //当子View没有处理事件,也就是mFirstTouchTarget为空的时候,并且不是down事件,默认进行拦截,注意,没有调用onInterceptTouchEvent
             intercepted = true;
        }
  //。。。。。。。[2]
 }

子View通过requestDisallowInterceptTouchEvent设置FLAG_DISALLOW_INTERCEPT之后,ViewGroup不可以拦截到除了down之外的motionEvent。 因为Down事件会重置FLAG_DISALLOW_INTERCEPT这个标记位。

 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();
 }

[1],当ViewGroup不进行拦截的时候,事件会分发给子View。

 ViewGroup#dispatchTouchEvent[2]
 final View[] children = mChildren;
 for (int i = childrenCount - 1; i >= 0; i--) {
     final int childIndex = getAndVerifyPreorderedIndex(
             childrenCount, i, customOrder);
     final View child = getAndVerifyPreorderedView(
             preorderedList, children, childIndex);
 
     
     if (childWithAccessibilityFocus != null) {
         if (childWithAccessibilityFocus != child) {
             continue;
        }
         childWithAccessibilityFocus = null;
         i = childrenCount - 1;
    }
 
     if (!child.canReceivePointerEvents()
             || !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);
     // dispatchTransformedTouchEvent实际上就是调用dispatchTouchEvent方法
     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();
         //如果子元素的dispatchTouchEvent返回true,那么会newTouchTarget赋值然后跳出循环,也就是找到一个能够处理事件的View,addTouchTarget对mFirstTouchTarget赋值令其不为空,如此一来所有事件都要进入判断是否进行拦截。如果设置了FLAG_DISALLOW_INTERCEPT就不调用onInterceptTouchEvent!
         newTouchTarget = addTouchTarget(child, idBitsToAssign);
         alreadyDispatchedToNewTouchTarget = true;
         break;
    }
 
     // 没有处理完事件,所以标记并继续进入for循环
     ev.setTargetAccessibilityFocus(false);
 }
 ViewGroup#addTouchTarget
 private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
     final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
     target.next = mFirstTouchTarget;
     mFirstTouchTarget = target;
     return target;
 }

此时ViewGroup的处理进入尾声,若没有子View处理事件,或者处理之后返回false对于ViewGroup来说是一样的,那么ViewGroup会自己处理事件:

 ViewGroup#dispatchTouchEvent[3]
 if (mFirstTouchTarget == null) {
     // No touch targets so treat this as an ordinary view.
     //第三个参数为null,所以会调用super.dispatchTouchEvent(event);
     handled = dispatchTransformedTouchEvent(ev, canceled, null,
             TouchTarget.ALL_POINTER_IDS);
 }

由于ViewGroup继承自View,所以此时是调用ViewGroup对象的View中的dispatchTouchEvent。

View分发
 View# dispatchTouchEvent
 public boolean dispatchTouchEvent(MotionEvent event) {
     // If the event should be handled by accessibility focus first.
     if (event.isTargetAccessibilityFocus()) {
         // We don't have focus or no virtual descendant has it, do not handle the event.
         if (!isAccessibilityFocusedViewOrHost()) {
             return false;
        }
         // We have focus and got the event, then use normal event dispatch.
         event.setTargetAccessibilityFocus(false);
    }
     boolean result = false;
 
     if (mInputEventConsistencyVerifier != null) {
         mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }
 
     final int actionMasked = event.getActionMasked();
     if (actionMasked == MotionEvent.ACTION_DOWN) {
         // Defensive cleanup for new gesture
         stopNestedScroll();
    }
 
     if (onFilterTouchEventForSecurity(event)) {
         if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
             result = true;
        }
         //noinspection SimplifiableIfStatement
         ListenerInfo li = mListenerInfo;
     //首先判断有无onTouchListenter
         if (li != null && li.mOnTouchListener != null
                 && (mViewFlags & ENABLED_MASK) == ENABLED
                 && li.mOnTouchListener.onTouch(this, event)) {
             result = true;
        }
  //只有onTouch方法返回false才会调用onTouchEvent
         if (!result && onTouchEvent(event)) {
             result = true;
        }
    }
 
     if (!result && mInputEventConsistencyVerifier != null) {
         mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
 
     // Clean up after nested scrolls if this is the end of a gesture;
     // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
     // of the gesture.
     if (actionMasked == MotionEvent.ACTION_UP ||
             actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
         stopNestedScroll();
    }
 
     return result;
 }

可见onTouchListener优先级高于onTouchEvent。

当View不可用的时候仍然会消耗事件,

 View#onTouchEvent[1]
 if ((viewFlags & ENABLED_MASK) == DISABLED
         && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
     if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
         setPressed(false);
    }
     mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
     // A disabled view that is clickable still consumes the touch
     // events, it just doesn't respond to them.
     return clickable;
 }

如果View设置了代理,那么还会先执行代理中的方法,如果其返回true,那么onTouchEvent本身的处理将不会执行:

 View#onTouchEvent[2]
 if (mTouchDelegate != null) {
     if (mTouchDelegate.onTouchEvent(event)) {
         return true;
    }
 }

只要CLICKABEL和LONG_CLICKABLE之间有一个为true,那么他就会消耗这个事件,即返回true。所以无论是否DISABLE都会消耗事件。

 View#onTouchEvent[3]
 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
     switch (action) {
         case MotionEvent.ACTION_UP:
             mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
             if ((viewFlags & TOOLTIP) == TOOLTIP) {
                 handleTooltipUp();
            }
             if (!clickable) {
                 removeTapCallback();
                 removeLongPressCallback();
                 mInContextButtonPress = false;
                 mHasPerformedLongPress = false;
                 mIgnoreNextUpEvent = false;
                 break;
            }
             boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
             if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                 // take focus if we don't have it already and we should in
                 // touch mode.
                 boolean focusTaken = false;
                 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                     focusTaken = requestFocus();
                }
 
                 if (prepressed) {
                     // The button is being released before we actually
                     // showed it as pressed. Make it show the pressed
                     // state now (before scheduling the click) to ensure
                     // the user sees it.
                     setPressed(true, x, y);
                }
 
                 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                     // This is a tap, so remove the longpress check
                     removeLongPressCallback();
 
                     // Only perform take click actions if we were in the pressed state
                     if (!focusTaken) {
                         // Use a Runnable and post this rather than calling
                         // performClick directly. This lets other visual state
                         // of the view update before click actions start.
                         //当up的时候会调用PerformClick()方法,也就是调用OnClickListener中的onClick方法。
                         if (mPerformClick == null) {
                             mPerformClick = new PerformClick();
                        }
                         if (!post(mPerformClick)) {
                             performClickInternal();
                        }
                    }
                }
 
                 if (mUnsetPressedState == null) {
                     mUnsetPressedState = new UnsetPressedState();
                }
 
                 if (prepressed) {
                     postDelayed(mUnsetPressedState,
                             ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                     // If the post failed, unpress right now
                     mUnsetPressedState.run();
                }
 
                 removeTapCallback();
            }
             mIgnoreNextUpEvent = false;
             break;
 
         case MotionEvent.ACTION_DOWN:
             if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                 mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
            }
             mHasPerformedLongPress = false;
 
             if (!clickable) {
                 checkForLongClick(
                         ViewConfiguration.getLongPressTimeout(),
                         x,
                         y,
                         TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                 break;
            }
 
             if (performButtonActionOnTouchDown(event)) {
                 break;
            }
 
             // Walk up the hierarchy to determine if we're inside a scrolling container.
             boolean isInScrollingContainer = isInScrollingContainer();
 
             // For views inside a scrolling container, delay the pressed feedback for
             // a short period in case this is a scroll.
             if (isInScrollingContainer) {
                 mPrivateFlags |= PFLAG_PREPRESSED;
                 if (mPendingCheckForTap == null) {
                     mPendingCheckForTap = new CheckForTap();
                }
                 mPendingCheckForTap.x = event.getX();
                 mPendingCheckForTap.y = event.getY();
                 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
            } else {
                 // Not inside a scrolling container, so show the feedback right away
                 setPressed(true, x, y);
                 checkForLongClick(
                         ViewConfiguration.getLongPressTimeout(),
                         x,
                         y,
                         TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
            }
             break;
 
         case MotionEvent.ACTION_CANCEL:
             if (clickable) {
                 setPressed(false);
            }
             removeTapCallback();
             removeLongPressCallback();
             mInContextButtonPress = false;
             mHasPerformedLongPress = false;
             mIgnoreNextUpEvent = false;
             mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
             break;
 
         case MotionEvent.ACTION_MOVE:
             if (clickable) {
                 drawableHotspotChanged(x, y);
            }
 
             final int motionClassification = event.getClassification();
             final boolean ambiguousGesture =
                     motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
             int touchSlop = mTouchSlop;
             if (ambiguousGesture && hasPendingLongPressCallback()) {
                 if (!pointInView(x, y, touchSlop)) {
                     // The default action here is to cancel long press. But instead, we
                     // just extend the timeout here, in case the classification
                     // stays ambiguous.
                     removeLongPressCallback();
                     long delay = (long) (ViewConfiguration.getLongPressTimeout()
                             * mAmbiguousGestureMultiplier);
                     // Subtract the time already spent
                     delay -= event.getEventTime() - event.getDownTime();
                     checkForLongClick(
                             delay,
                             x,
                             y,
                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                }
                 touchSlop *= mAmbiguousGestureMultiplier;
            }
 
             // Be lenient about moving outside of buttons
             if (!pointInView(x, y, touchSlop)) {
                 // Outside button
                 // Remove any future long press/tap checks
                 removeTapCallback();
                 removeLongPressCallback();
                 if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                     setPressed(false);
                }
                 mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            }
 
             final boolean deepPress =
                     motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS; 
           if (deepPress && hasPendingLongPressCallback()) { 
               // process the long click action immediately 
               removeLongPressCallback(); 
               checkForLongClick( 
                       0 /* send immediately */, 
                       x, 
                       y, 
                       TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS); 
          } 
​ 
           break; 
  } 
​ 
   return true; 
}

最后,CLICKABLE初始值与View相关,不同View有不同值(button和TextView).LONG_CLICKABLE默认为false,setOnClickLIstener和setOnLongClickLIstener会将这些值设置为true。

 

对于这一章的总结(一共11点):

  • 同一个事件总是从手指触摸到屏幕开始,经过一系列的操作,最终以up结束。

  • 正常情况下,一个事件序列只能由一个View拦截消耗,但是可以在onTouchEvent中强行传递给其他view。

  • 如果ViewGroup一旦拦截,那么后来的整个序列都是由它处理。

  • 某个View如果不处理Down事件,那么后续的所有事件都不由他处理。

  • 如果View不处理除了Down之后的后续事件,那么这个事件序列会消失。最终给activity处理。

  • ViewGroup默认不拦截事件。

  • View没有onInterceptTouchEvent方法。

  • View中的onTouchEvent默认消耗事件,除非他是不可点击的。

  • View的enable属性不影响onTouchEvent的返回值。

  • onClick事件发生的前提是当前的View是可点击的,并且收到了down事件。(不懂)

  • 事件传递是父元素再到子元素,子View通过requestDisallowInterceptTouchEvent可以干预父元素的分发过程,但是down事件除外。

posted @ 2022-05-26 15:37  码虫垒起代码之城  阅读(21)  评论(0编辑  收藏  举报