Android View的事件分发机制

1. 使View滑动

1.1 View基本知识

在这里插入图片描述
x并不是等于left,x与left的关系如下

  1. x = left + translationX
  2. y = top + translationY

1.2 使用scrollTo/scrollBy

此方法移动的是VIew的内容,并非VIew的实际位置

   protected int mScrollX;
   protected int mScrollY;

将view像左移动100像素,向上移动30像素,scrollTo的移动方向和我们熟知的x轴和y轴方向是相反的

 mTextView.scrollTo(100, 30)

scrollBy本质来说也调用的是scrollTo,scrollTo是绝对移动,scrollBy是相对移动
在这里插入图片描述

1.3 使用动画来移动View

这种方式改变的是View的以下属性
translationX 和 translationY

   public float getTranslationX() {
        return mRenderNode.getTranslationX();
    }

使用ObjectAnimator来移动View

   ObjectAnimator.ofFloat(animaTextView, "translationX", a, 100f).apply {
       duration = 100
       start()
   }

1.4 修改layoutParams来移动View

val marginLayoutParams = (layoutView.layoutParams as MarginLayoutParams)
marginLayoutParams.marginStart = 300  
layoutView.layoutParams = marginLayoutParams

2. View的弹性滑动

如上1.View的滑动所示,这样移动VIew,有点像闪现,没有移动的过程,在现实使用中并不好看,所以为了有更好的移动效果,需要使用弹性滑动。
实现弹性滑动的方式也有三种,Scroller, 属性动画, 使用延时策略

2.1 Scroller

比如使用Scroller去弹性滑动移动一个TestView

    private lateinit var scrollView: TextView
    private lateinit var mScroller: Scroller

    override fun onCreate(savedInstanceState: Bundle?){
        val scrollBtn: Button = findViewById(R.id.smooth_scroller_btn);
        scrollView = findViewById(R.id.ScrollerText)
        scrollBtn.setOnClickListener {
            scrollView.setScroller(mScroller)
            smoothScrollerTo(-200, 0)
        }
        Log.d(TAG, "btn.scrollX: " + btn.scrollX + " btn.scrollY: " +  btn.scrollY)
    }

    private fun smoothScrollerTo(destX: Int, destY: Int) {
        val curX = scrollView.scrollX
        val deltaX = destX - curX
        mScroller.startScroll(curX, 0, deltaX, 0, 2000)
        scrollView.invalidate()
    }

Scroller弹性滑动原理

  1. 首先看看Scroller.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;
    }

2.真正开始弹性滑动的方法为scrollView.invalidate
在这里插入图片描述
其中最终要的是computeScroll方法, 以TextView的为例

    @Override
    public void computeScroll() {
        if (mScroller != null) {
            if (mScroller.computeScrollOffset()) {
                mScrollX = mScroller.getCurrX();
                mScrollY = mScroller.getCurrY();
                invalidateParentCaches();
                postInvalidate();  // 再一次重绘View
            }
        }
    }

大致逻辑就是不断地改变mScrollX和mScroll,然后重新drawView来实现弹性滑动,可以间接理解成不断的去调用scrollTo方法
那不断地drawView什么时候停止?
这就需要看mScroller.computeScrollOffset()方法,这个方法主要做了如下几个事情

 public boolean computeScrollOffset() {
 		//如果动画已经完成,return false,从而中断computeScroller方法的调用
        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;
    }

2.2使用动画

像上面有使用ObjectAnimator去实现完全是可以的,这里我们换种方式,使用ValueAnimator去实现

        val valueAnimatorBtn: Button = findViewById(R.id.smooth_animter_btn);
        val valueAnimatorTextView: TextView = findViewById(R.id.smooth_text_btn)
        val startX = 0
        val deltaX = 400
        valueAnimatorBtn.setOnClickListener {
            ValueAnimator.ofInt(0, 1).apply {
                duration = 2000
                addUpdateListener {
                    val fraction = it.animatedFraction
                    valueAnimatorTextView.scrollTo(-(startX + (deltaX * fraction).toInt()), 0)
                }
                start()
            }
        }

2. 3 使用延时策略

           /*
    *  假设需要1000ms 移动 300px, 每次移动30像素
    *  总共要移动 300 / 30 = 10次  delay时间 1000 / 10 = 100ms
    * */
    private var count = 0
    private val mHandler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            count ++
            when (msg.what) {
                1 -> {
                    count++
                    if (count <= 10) {
                        handleView.scrollBy(-30, 0)
                        sendEmptyMessageDelayed(1, 100)
                    }
                }
                else -> {}
            }
        }
    }

View的事件分发机制

MotionEvent是对应点击事件的封装类,常见的点击事件是down、up、move,分别对应按下,抬起,移动
如何把一个点击事件分发给某一个特定的VIew,这就需要了解Android的事件分发机制。
点击事件分发机制由3个重要方法组成

public boolean dispatchTouchEvent(MotionEvent ev)
//onInterceptTouchEvent方法存在于ViewGroup中,View没有
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent event)

这三个方法的关系是
在这里插入图片描述
通过dispatchTouchEvent方法去给VIew分发事件,VIew收到事件后再判断是否拦截该事件,如果要拦截,这个事件才真正的被这个View所消费,否则就将这个事件分发给子View

除了上述三个方法外,还有一个比较重要的Listener,OnTouchListener,可以通过View的setOnTouchListener设置

    public interface OnTouchListener {
        boolean onTouch(View v, MotionEvent event);
    }

OnTouchListener的优先级比onTouchEvent高,如果onTouch的方法返回true。代表OnTouchListenr消费这个事件,即该View的onTouchEvent不会被调用,因为onClickListener再OnTouchEvent中被调用,所以该View的点击事件也会失效
最终的事件分发的总体框架如下图所示
在这里插入图片描述
关于View的事件分发有如下几个结论

  1. 从手指按压屏幕,移动,再到抬起,被称作一个事件序列
  2. 正常情况下,一个事件序列只能被同一个View所消费
  3. 一个ViewGroup一但决定拦截(onInterceptTouchEvent返回ture),那么这一个事件序列都有它来处理
  4. 某个VIew如果不消耗down事件,那么一个事件序列中的其他事件也不会让他来处理
  5. ViewGroup默认不消耗任何事件,因为它的onInterceptTouchEvent方法默认返回false
  6. View没有onInterceptTouchEvent方法,默认都消耗事件,除非View不可点击(clickable, longClickable同时为false)
  7. View的enbale不会改变onTouchEvent的返回值

事件分发源码解析

1.Activty事件分发的过程

当一个事件出现时,会以以下的顺序进行传递,Activity–> window ->顶层View(DecorView)
在这里插入图片描述
decorView是顶层View,我们Activity通过setContentView方法设置的VIew最终会设置到DecorView的子元素上(android.R.id.content),DecorVIew的大致结构如下
在这里插入图片描述
事件最终流传到了ViewGroup的dispatchTouchEvent方法中,接下来重点看看这个方法

2.ViewGroup事件分发

先看ViewGrop如何判断是否拦截某个事件

     // 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();
     }
     final boolean intercepted;
     //如果是down事件 或者 有子元素处理事件时
     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;
     }

一个ViewGroup是否需要通过onInterceptTouchEvent方法来判断是否拦截事件,取决于两个因素,并且必须都满足

  1. 是否是down事件 || 或者有子View接受事件,mFirstTouchTarget 是接受事件的子View的引用
 actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null
  1. FLAG_DISALLOW_INTERCEPT标记位,如果没有这个标记位,才会调用onInterceptTouchEvent方法
    这个标记位可通过requestDisallowInterceptTouchEvent方法却设置,并且绝对不会影响down事件,因为resetTouchState方法会把这个标记位抹除
  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

结论1:如果VIewGroup拦截down事件,那么其他事件也会被VIewGroup拦截,并且onInterceptTouchEvent方法只会再down事件判断一次,其他事件序列将不用onInterceptTouchEvent再次判断 原因如下:
在这里插入图片描述
接下来看看如果ViewGroup不拦截事件,事件是如何分发到子VIew的

if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
       //此处省略一些源码
	  for (int i = childrenCount - 1; i >= 0; i--) {
			//此处省略一些源码
	        if (!child.canReceivePointerEvents()
	                || !isTransformedTouchPointInView(x, y, child, null)) {
	            ev.setTargetAccessibilityFocus(false);
	            continue;
	        }
	       //此处省略一些源码                  
	 }
 }

首先如上源码所示,在down事件时,遍历所有的子元素,判断子View是否有资格被分发事件,判断条件有2条,并且条件是或的关系

  1. 通过canReceivePointerEvents方法去判断 View是VISIBLE,或者正在播放动画
    /**
     * Returns whether this view can receive pointer events.
     * @return {@code true} if this view can receive pointer events.
     * @hide
     */
    protected boolean canReceivePointerEvents() {
        return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
    }
  1. 通过isTransformedTouchPointInView方法去判断 点击的是否是这个View的区域
    这个方法就不详细讲了,大致实现方式就是通过的点击的x和y坐标结合View的长宽和位置去判断

判断完子View是否有资格被分发事件后就会走到下面的逻辑

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方法,在这个方法中回去调用子View的dispatchTouchEvent方法,大致伪代码如下所示

//如果子View为null,就会调用父类(View)的dispatchTouchEvent方法
 if (child == null) {
     handled = super.dispatchTouchEvent(event);
 } else {
    //将事件分发给子View
     handled = child.dispatchTouchEvent(event);
 }

再刚开始分析源码时提到了mFirstTouchTarget成员,他会影响ViewGroup是否拦截某个事件的判断,该成员的赋值时机就在addTouchTarget方法中

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

根据衣裳代码会发现mFirstTouchTarget 本质是一个链表头,连接着每个被分发事件的子VIew,并采用头插法插入新元素,所以之前才会说mFirstTouchTarget != null代表着已经有事件成功分发给的子元素。当一个事件序列被分发完mFirstTouchTarget 链表就会被清空

  if (canceled
          || actionMasked == MotionEvent.ACTION_UP
          || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
      //该方法会便利链表全部置空
      resetTouchState();

当然也会有这样一种情况,如果将事件分发给子元素,但是子元素也不消耗这个事件,即onTouchEvent方法返回false,如果时这种情况mFirstTouchTarget还是为null的状态,就会走到如下的逻辑中

 if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
 }

dispatchTransformedTouchEvent方法的child参数为空,上面也分析过这个方法,如果child==null,就会调用super.dispatchTouchEvent方法,该方法最终又会调用ViewGroup的onTouchEvent方法
结论:如果一个子VIew不消费分过来的事件,即onTouchEvent返回false,最终事件会交给ViewGroup的onTouchEvent来处理, 即子元素不处理事件,父亲来处理
down事件时才会遍历子元素,才有机会给mFirstTouchTarget赋值,所以这就得出一个结论:
如果一个VIew不接受down事件,一个事件序列中后面的事件也一定不会给它了

3.View的事件分发

上面说到了如果子View不接受ViewGroup分发的事件,就会调用super.dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,我们接下来分析下这个方法

    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }

    if (!result && onTouchEvent(event)) {
        result = true;
    }

上面的源码也印证了上面之前说的一个结论:mOnTouchListener的优先级别比onTouchEvent高,如果mOnTouchListener的onTouch方法的返回值时true,那onTouchEvent就不是执行

接下来好好看看onTouchEvent方法

        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是否消费事件与是否enable无关,只与是否click有关

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    .................省略
    switch (action) {
    	 case MotionEvent.ACTION_UP:
    	      performClickInternal();
           .....................省略                    
       }
       .................省略
    }
     .................省略
}

如果VIew的可点击的,会在up事件时通过performClickInternal方法去调用onClickListener的onClick方法

onTouchEvent主要是View怎么处理事件的逻辑,这里不再细讲

总结

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值