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

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

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

着重强调上面概念,是因为笔者自己以前在分析中看到子类和子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)
    }
}
上一篇下一篇

猜你喜欢

热点阅读