美文网首页
Android 事件传递机制解析,保证说的是人话!

Android 事件传递机制解析,保证说的是人话!

作者: Android程序员老鸦 | 来源:发表于2022-03-26 23:55 被阅读0次
    Android View的事件传递机制一直都是开发者绕不开的一个知识点,即使你工作中不需要处理事件冲突,但是面试官总喜欢拿出来问。
    我以前也是在里面挣扎过一段时间,说明白吧但好像有些地方总是模模糊糊,说不明白吧,却也能说出个大概来。后来也是反复跳坑研究忽然就顿悟了,而且悟了之后我深刻明白了之前迷惑自己认清真相的点在哪里,今天就斗胆用踩坑过来人的方式写一篇讲解事件传递的博文,如果能给新进的开发者提供点帮助那也是美事一件。

    Android事件的传递无非就是ViewGroup和它的子孩子View之间的逻辑关系,要搞明白他们之间的事件分发,看源码必然不可少,在这之前首先强调几个重要且容易模糊的知识点:

    • ViewGrop的子View是指ViewGrop容器里装的childView
    • ViewGrop本身是View的子类(注意是子类,不是上面说的子View!!)
    • 处理事件和拦截事件是两码事,你可以处理事件但不拦截它
    • 事件有多个动作,down,move,up这些,注意每次动作都是会走一遍分发代码
    • dispatchTouchEvent方法主要负责分发拦截逻辑,viewGroup写好了这个方法,一般不需要开发者重写;onTouchEvent方法负责具体的事件动作逻辑,它的返回值跟dispatchTouchEvent一样决定事件是否继续往下传,这是比较容易让人记忆错乱的地方。
    着重强调上面概念,是因为笔者自己以前在分析中看到子类和子View的时候没有额外注意,导致后面很多逻辑不知不觉就迷糊了。

    为了使分析流程简单化,我们假定ViewGrop是LinearLayout,子View就是一个TextView。我们分析好LinearLayout传递到TextView的事件逻辑就明白了无论多少层级的View都是一样的逻辑,因为多层逻辑无非就是重复这一层关键逻辑。

    我们向上追溯到Activity开始事件的传递(至于事件怎么传递到activity的有兴趣的自行研究),然后Activity会把事件传递到phoneWindow,phoneWindow传递到decorView,decorView传递到我们的例子LinnearLayout上,再传到TextView上。

    部分代码如下,首先是Activity里的事件分发:

    /**
    * ActivitydispatchTouchEvent
    **/
     public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                 //空实现,若需要了解用户点击界面,可以自行实现该接口
                // 这里是页面down事件的必经之路,可以在这里统计页面点击等情况
                onUserInteraction();
            }
            //这个getWindow()得到的就是phoneWindow
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            // 如果页面上的控件都没有拦截事件,则会走到activity的onTouchEvent
            return onTouchEvent(ev);
        }
    

    再看看phoneWindow的superDispatchTouchEvent:

        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
              //可以看到其实是交给了mDecor去处理,这个mDecor看名字就知道是DecorView
            return mDecor.superDispatchTouchEvent(event);
        }
    

    继续跟踪DecorView的superDispatchTouchEvent:

    public boolean superDispatchTouchEvent(MotionEvent event) {
            //调用的是父类的dispatchTouchEvent
            return super.dispatchTouchEvent(event);
        }
    

    然而我们知道DecorView就是个FrameLayout,FrameLayout本身没有重写dispatchTouchEvent,所以这个就是ViewGroup的dispatchTouchEvent方法。
    接下来事件的传递都是ViewGroup之间,直至最后到我们上面的例子TextView上。

    以上的事件传递顺序不是重点,目的只是说一下事件的来源。我们重点分析之前说的例子,LinearLayout上的点击事件是怎么传递到他的子View TextView上的,也即是ViewGroup和子View的事件关系。

    ViewGroup的dispatchTouchEvent方法的源码太多,没必要一次性贴出来,我们逐步分析,免得懵逼,结合官方的注释来捋逻辑,首先说一下MotionEvent的主要的几个类型,就是DOWM、MOVE、UP:

        public static final int ACTION_DOWN             = 0;
    
        public static final int ACTION_UP               = 1;
    
        public static final int ACTION_MOVE             = 2;
    

    我们以典型的手指摁下→然后移动小短距离→最后抬起这一次事件为例,实际传递过程中触发了DOWM、MOVE、UP三次MotionEvent 的ViewGroup的dispatchTouchEvent(MotionEvent ev)方法,第一次是Down事件,我们看一段ViewGroup的dispatchTouchEvent(MotionEvent ev)方法里的源码:

              final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
    
                // Handle an initial down. 处理初始化的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.
                    //开始一次新的事件的时候丢弃掉之前所有的状态,down事件标志此时是一次新的点击事件
                    //framework层可能由于应用程序切换、ANR 或其他一些状态更改,放弃了上一次手势的up或者cancel事件
                    cancelAndClearTouchTargets(ev);  // 清除掉点击对象
                    resetTouchState();   // 重置拦截标志
                }
    
                // Check for interception.
                // 检查interception参数,这里梳理viewGroup是否拦截的逻辑
                final boolean intercepted;
                // 第一个是DOWN事件,则肯定会进入if语句里,mFirstTouchTarget这个参数很重要,后面会多次提到
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                  // disallowIntercept 代表ViewGroup是否允许拦截,这个参数一般由子View调用
                //requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法以达到控制事件
               //冲突的目的,但是这个标志会在每次的down事件都清除,就是前面的resetTouchState()方法
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    // 由上面的代码可知,只要是down事件,都会执行重置这个标志的动作,也就是说这里必然是false,
                  // 这个概念非常重要,说明每次事件的开始的down动作都会进入到onInterceptTouchEvent()方
                //法里,父View有权利在事件的一开始就决定要不要传给孩子View
                    if (!disallowIntercept) {
                      // 如果不是down事件disallowIntercept也为false,代表子View允许ViewGroup拦截事
                    //件,所以会走ViewGroup的onInterceptTouchEvent方法,很多事件冲突就是在这处理的
                        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消费事件),并且
                //不是初始化的Down事件,那么viewGroup就会一直拦截它
                    intercepted = true;
                }
    

    这里贴一下requestDisallowInterceptTouchEvent(boolean disallowIntercept)这个方法,这是一个公开方法,一般被子view调度处理事件冲突:

        @Override
        public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
            // disallowIntercept 为true表示孩子view不希望这个viewGroup拦截事件,那么
            // (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0则会是true,对应上面viewGroup的那个判断
            if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
                // We're already in this state, assume our ancestors are too
                return;
            }
            // 根据情况处理这些标记,一些或与运算,常用来作为标记状态
            if (disallowIntercept) {
                mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
            } else {
                mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
            }
    
            // Pass it up to our parent
            // 这个方法还会向上层的viewGroup传递,可见权限非常大
            if (mParent != null) {
                mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
            }
        }
    

    以上是down事件和interecepted初步拦截逻辑处理,我们假设ViewGroup的onInterceptTouchEvent()方法不拦截事件,也就是intercepted为false,点击事件继续往下传,代码逻辑会走到这里:

      if (!canceled && !intercepted) {
                    // If the event is targeting accessibility focus we give it to the
                    // view that has accessibility focus and if it does not handle it
                    // we clear the flag and dispatch the event to all children as usual.
                    // We are looking up the accessibility focused host to avoid keeping
                    // state since these events are very rare.
                  // 这是针对焦点处理相关的,我们不在这里深究
                    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                            ? findChildWithAccessibilityFocus() : null;
                    // ACTION_POINTER_DOWN指的是已经有手指在屏幕上的down事件,即多指触控
                    // ACTION_HOVER_MOVE是鼠标移动、
                    // 我们主要关注ACTION_DOWN事件
                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                      // always 0 for down,down事件的index总是0
                        final int actionIndex = ev.getActionIndex(); 
                        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                                : TouchTarget.ALL_POINTER_IDS;
    
                        // Clean up earlier touch targets for this pointer id in case they
                        // have become out of sync.
                        removePointersFromTouchTargets(idBitsToAssign);
    
                        final int childrenCount = mChildrenCount;
                        // newTouchTarget此时必然是null,childrenCount 不为零,代表有子View
                        if (newTouchTarget == null && childrenCount != 0) {
                            final float x =
                                    isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                            final float y =
                                    isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                            // Find a child that can receive the event.
                            // Scan children from front to back.
                            //  从上到下扫描孩子View,找到可以接收事件的孩子View
                            // buildTouchDispatchChildList()方法会把所有的子View按照z轴的大
                          //小排序在一个list里,z轴越大的View越靠后,用到了插入排序
                            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            final View[] children = mChildren;
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                // 会根据绘制顺序来找孩子View响应事件的优先级
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);
                                // 排除掉不可响应的view和不在事件坐标范围内的view
                                if (!child.canReceivePointerEvents()
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    continue;
                                }
                                // 这里普通down事件newTouchTarget肯定还是null
                                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.
                                    // 进到这里表示孩子view想要在它的大小范围内响应事件
                                    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();
                                    // 将当前孩子View设置为mFirstTouchTarget,newTouchTarget 此时不为空了
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
    
                                // The accessibility focus didn't handle the event, so clear
                                // the flag and do a normal dispatch to all children.
                                ev.setTargetAccessibilityFocus(false);
                            }
                            if (preorderedList != null) preorderedList.clear();
                        }
          
                        if (newTouchTarget == null && mFirstTouchTarget != null) {
                            // Did not find a child to receive the event.
                            // Assign the pointer to the least recently added target.
                            // 没有找到接收事件的孩子。
                             // 将newTouchTarget 分配给最近添加的目标。
                            newTouchTarget = mFirstTouchTarget;
                            while (newTouchTarget.next != null) {
                                newTouchTarget = newTouchTarget.next;
                            }
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                        }
                    }
                }
    

    上面说到了一个插入排序,感兴趣的可以看看这篇文章:常见排序算法
    接着往主干代码走:

               // Dispatch to touch targets.
              //  注意,down的事件在上面的代码里走过一次了,到了这里则有两种情况,
              // 1.mFirstTouchTarget 还是为null,代表事件没被孩子View消费,则不管此时
              //是什么事件,都将由viewGroup自己去处理该事件
              // 2.mFirstTouchTarget 不为null,代表事件down被孩子view拦截了
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    //没有触摸目标就把这个viewGroup当作普通的view处理
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                    // 进入到这里,可能是ACTION_DOWN事件,也可能是其它类型的事件
                    // 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) {
                            // 这里是处理了down的事件,alreadyDispatchedToNewTouchTarget在上面被置为了true
                            handled = true;
                        } else {
                              // 处理其他事件,这时候alreadyDispatchedToNewTouchTarget为false
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                            // 继续分发下去给子view处理
                            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;
                    }
                }
    
                // Update list of touch targets for pointer up or cancel, if needed.
                // up等cancel事件 重置状态
                if (canceled
                        || actionMasked == MotionEvent.ACTION_UP
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    resetTouchState();
                } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                    final int actionIndex = ev.getActionIndex();
                    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                    removePointersFromTouchTargets(idBitsToRemove);
                }
            }
    
            if (!handled && mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
            }
            return handled;
    

    回过头再来看看 dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits)方法,这里处理了分发给孩子view事件和孩子View是否消费事件的逻辑。

      private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
    
            // Canceling motions is a special case.  We don't need to perform any transformations
            // or filtering.  The important part is the action, not the contents.
            final int oldAction = event.getAction();
            if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
                event.setAction(MotionEvent.ACTION_CANCEL);
                if (child == null) {
                    // viewGroup自己处理,调用父类(注意是父类,不是父亲view)view的分发方法
                    handled = super.dispatchTouchEvent(event);
                } else {
                    // 分发下去给孩子view处理,并且拿到结果是否拦截
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
    
            ...略
            return handled;
        }
    

    再来看看View的dispatchTouchEvent()和onTouchEvent()方法:

    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
                //重点,有设置的mOnTouchListener的会先调用
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
                //重点,调用onTouchEvent,我们耳熟能详的onClick点击事件是在onTouchEvent()方法里的up事件触发的
                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(MotionEvent event)方法

    
     public boolean onTouchEvent(MotionEvent event) {
           ...略
    
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        ...略
                                    if (!post(mPerformClick)) {
                                        performClickInternal(); // 点击事件!!!!
                                    }
                                }
                            }
             ...略
    

    下面来个处理事件冲突的例子,一个ViewPager + ListView,ViewPager能左右滑动,ListView支持上下滑动。
    ViewPager :

    package com.hans.viewexe.views
    
    import android.content.Context
    import android.util.AttributeSet
    import android.util.Log
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewGroup
    import android.widget.Scroller
    import kotlin.math.absoluteValue
    
    /**
     * @author: lookey
     * @date: 2021/12/19
     */
    class DefinedViewPager:ViewGroup {
        private var mLastX: Float = 0f
        private var mLastY: Float = 0f
        private var mChildWidth: Int = 0
        private var mChildrenSize: Int = 0
        private var currentPage = 0
    
        private var isNextMove = true
    
        private var mScroller:Scroller = Scroller(context)
    
        companion object{
            private const val TAG: String = "DefinedViewPager"
        }
    
    
        constructor(context: Context, attributes: AttributeSet):super(context,attributes)
    
    
        var eventX = 0f
        var eventY = 0f
    
        var eventDownX = 0f
        var eventDownY = 0f
    
        override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
            val x = event.x.toInt()
            val y = event.y.toInt()
            val action = event.action
            return if (action == MotionEvent.ACTION_DOWN) {
                Log.d(TAG, "父View onInterceptTouchEvent ACTION_DOWN")
                eventDownX = event.x
                eventDownY = event.y
                mLastX = x.toFloat()
                mLastY = y.toFloat()
                if (!mScroller.isFinished) {
                    mScroller.abortAnimation()
                    return true
                }
                false  // 只是记录一下坐标,不拦截
            } else {
                // 不是down事件,此时还能进来onInterceptTouchEvent()方法则表示此时是ListViewEx故意放给父
                // View处理的,这里就返回true,拦截掉自己处理,这样才会走到自己的onTouchEvent(event: MotionEvent)方法
                true
            }
        }
         val TAG = "DefinedViewPager"
        // 具体事件的效果实现
        override fun onTouchEvent(event: MotionEvent): Boolean {
    
            eventX = event.x  //相对父view的x坐标
            eventY = event.y //相对父view的y坐标
            // down事件是不会流转到这里的,无需处理
            when(event.action){
    
                    MotionEvent.ACTION_MOVE -> {
                        //防止向左滚动过头
                        if((scrollX <= 0 && (eventX - mLastX) > 0) || (eventX - mLastX) > scrollX){
                            mLastX = eventX
                            return true
                        }
    
                        //防止向右滚过头
                        if((scrollX >= mChildWidth*(childCount-1) && (eventX - mLastX) < 0) || (mLastX - eventX) + scrollX > mChildWidth*(childCount-1)){
                            mLastX = eventX
                            return true
                        }
                        val deltaX = eventX - mLastX
                        scrollBy(-deltaX.toInt() , 0)
                    }
                    //手离开屏幕的时候判断根据move时候的滑动方向和滑动距离判断要不要翻过当前页
                    MotionEvent.ACTION_UP -> {
                        Log.d("11scrollX","${event.x - eventDownX}")
                        val deltaX2 = event.x - eventDownX
                        if(deltaX2.absoluteValue > 200){ //防止误触
                            if(deltaX2 < 0){
                                Log.d("22scrollX ==>","next下一页")
                                moveToNextPage()
                            }else if(deltaX2 > 0){
                                Log.d("22scrollX ==>","pre上一页")
                                moveToPrePage()
                            }
                        }else{
                            restorePosition()
                        }
    
                    }
            }
                    mLastX = event.x
                    mLastY = event.y
            return true
        }
    
        private fun restorePosition() {
            val targetScrollX = currentPage * mChildWidth
            smoothScroll(targetScrollX - scrollX)
        }
    
        private fun moveToNextPage(){
            if(currentPage == childCount - 1){
                return
            }
            currentPage ++
            val targetScrollX = currentPage * mChildWidth
            smoothScroll(targetScrollX - scrollX)
        }
        private fun moveToPrePage(){
            if(currentPage == 0){
                return
            }
            currentPage --
            val targetScrollX = currentPage * mChildWidth
            smoothScroll(targetScrollX - scrollX)
    
        }
        private fun smoothScroll(dx:Int){
            mScroller.startScroll(scrollX,0,dx,0,500)
            invalidate()
        }
        override fun computeScroll() {
            if(mScroller.computeScrollOffset()){
                scrollTo(mScroller.currX,mScroller.currY)
                postInvalidate()
            }
        }
    
    
    
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec:  Int) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            //调用viewGroup提供的测量子View的方法
            measureChildren(widthMeasureSpec,heightMeasureSpec)
    
            var widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec)
            var widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
            var heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
            var heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
    
            if(childCount == 0){
                setMeasuredDimension(0,0)
            }else if(heightSpecMode == MeasureSpec.AT_MOST){
                val childView = getChildAt(0)
                val measuredHeight = childView.measuredHeight
                setMeasuredDimension(widthSpaceSize,measuredHeight)
            }else if(widthSpecMode == MeasureSpec.AT_MOST){
                val childView = getChildAt(0)
                val measuredWidth = childView.measuredWidth
                setMeasuredDimension(measuredWidth,heightSpecSize)
            }else{
                val childView = getChildAt(0)
                val measuredWidth = childView.measuredWidth * childCount
                val measuredHeight = childView.measuredHeight
                setMeasuredDimension(measuredWidth,measuredHeight)
            }
    
        }
    
        override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
            Log.d(TAG,"width:${width}")
    
            var childLeft = 0
            val childCounts = childCount
            mChildrenSize = childCounts
    
            for (index in 0 until childCounts){
                val childView = getChildAt(index)
                if(childView.visibility != View.GONE){
                    val childWidth = childView.measuredWidth
                    mChildWidth = childWidth
                    childView.layout(childLeft,0,childLeft + childWidth,childView.measuredHeight)
                    childLeft += childWidth
                }
            }
        }
    }
    

    ListView:

    package com.hans.viewexe.views;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.widget.ListView;
    
    /**
     * @author: lookey
     * @date: 2021/12/19
     */
    public class ListViewEx extends ListView {
        private static final String TAG = "ListViewEx";
    
        private DefinedViewPager mHorizontalScrollViewEx2;
    
        // 分别记录上次滑动的坐标
        private int mLastX = 0;
        private int mLastY = 0;
    
        public ListViewEx(Context context) {
            super(context);
        }
    
        public ListViewEx(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public ListViewEx(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public void setHorizontalScrollViewEx2(
                DefinedViewPager definedViewPager) {
            mHorizontalScrollViewEx2 = definedViewPager;
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    Log.d(TAG, "孩子 dispatchTouchEvent ACTION_DOWN");
                    mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);  //父容器不拦截
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
                    if (Math.abs(deltaX) > Math.abs(deltaY)) {
                        mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);//父容器可以拦截
                    }
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    break;
                }
                default:
                    break;
            }
    
            mLastX = x;
            mLastY = y;
            return super.dispatchTouchEvent(event);
        }
    }
    

    布局文件:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        android:orientation="vertical" >
    
        <com.hans.viewexe.views.DefinedViewPager
            android:id="@+id/container"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    
    </LinearLayout>
    

    activity:

    package com.hans.viewexe
    
    import android.graphics.Color
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.view.MotionEvent
    
    import android.widget.AdapterView.OnItemClickListener
    
    import android.util.Log
    
    import android.view.ViewGroup
    
    import com.hans.viewexe.views.HorizontalScrollViewEx2
    
    import android.view.LayoutInflater
    import android.view.View
    import android.widget.*
    import com.hans.viewexe.utils.MyUtils
    import com.hans.viewexe.views.DefinedViewPager
    import com.hans.viewexe.views.ListViewEx
    
    
    class MainActivity : AppCompatActivity() {
        private val TAG = "MainActivity"
    
        private var mListContainer: DefinedViewPager? = null
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.demo_2)
            Log.d(TAG, "onCreate")
            initView()
        }
    
        private fun initView() {
            val inflater = layoutInflater
            mListContainer = findViewById(R.id.container)
            val screenWidth: Int = MyUtils.getScreenMetrics(this).widthPixels
            val screenHeight: Int = MyUtils.getScreenMetrics(this).heightPixels
            for (i in 0..2) {
                val layout = inflater.inflate(
                    R.layout.content_layout2, mListContainer, false
                ) as ViewGroup
                layout.layoutParams.width = screenWidth
                val textView = layout.findViewById<View>(R.id.title) as TextView
                textView.text = "page " + (i + 1)
                layout.setBackgroundColor(
                    Color
                        .rgb(255 / (i + 1), 255 / (i + 1), 0)
                )
                createList(layout)
                mListContainer!!.addView(layout)
            }
        }
    
        private fun createList(layout: ViewGroup) {
            val listView: ListViewEx = layout.findViewById(R.id.list)
            val datas = ArrayList<String>()
            for (i in 0..49) {
                datas.add("name $i")
            }
            val adapter = ArrayAdapter(
                this,
                R.layout.content_list_item, R.id.name, datas
            )
            listView.setAdapter(adapter)
            listView.setHorizontalScrollViewEx2(mListContainer)
            listView.setOnItemClickListener(OnItemClickListener { parent, view, position, id ->
                Toast.makeText(
                    this, "click item",
                    Toast.LENGTH_SHORT
                ).show()
            })
        }
    
        override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
            Log.d(TAG, "dispatchTouchEvent action:" + ev.action)
            return super.dispatchTouchEvent(ev)
        }
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            Log.d(TAG, "onTouchEvent action:" + event.action)
            return super.onTouchEvent(event)
        }
    }
    

    相关文章

      网友评论

          本文标题:Android 事件传递机制解析,保证说的是人话!

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