Android事件分发

news2024/11/26 14:55:40

Android事件分发是指触摸屏幕的事件分发,在手指触摸屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

  • MotionEvent.ACTION_DOWN ——手指刚接触屏幕
  • MotionEvent.ACTION_MOVE——手指在屏幕上面滑动
  • MotionEvent.ACTION_UP——手指从屏幕上松开的一瞬间
    点击事件的分发过程由三个很重要的方法来共同完成:
    public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent(MotionEvent event)和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
    public boolean onInterceptTouchEvent(MotionEvent ev)
    在上述方法内部调用,ViewGroup独有方法,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
    public boolean onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
    事件分发是按照Activity -> ViewGroup->View进行的
    代码演示
class MainActivity : ComponentActivity() {
    companion object{
        private const val TAG = "MainActivity"
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_layout)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when(ev?.action){
            MotionEvent.ACTION_DOWN -> Log.d(TAG, "dispatchTouchEvent: DOWN")
            MotionEvent.ACTION_UP -> Log.d(TAG, "dispatchTouchEvent: UP")
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> Log.d(TAG, "onTouchEvent: DOWN")
            MotionEvent.ACTION_UP -> Log.d(TAG, "onTouchEvent: UP")
        }
        return super.onTouchEvent(event)
    }
}

private const val TAG = "DispatchViewGroup"
class DispatchViewGroup : LinearLayout {
    constructor(context : Context) : this(context ,null)
    constructor(context : Context,attributeSet: AttributeSet?) : this(context,attributeSet,0)
    constructor(context : Context,attributeSet: AttributeSet?,defStyleAttr: Int):super(context,attributeSet,defStyleAttr)

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when(ev?.action){
            MotionEvent.ACTION_DOWN -> Log.d(TAG, "dispatchTouchEvent: DOWN")
            MotionEvent.ACTION_UP -> Log.d(TAG, "dispatchTouchEvent: UP")
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when(ev?.action){
            MotionEvent.ACTION_DOWN -> Log.d(TAG, "onInterceptTouchEvent: DOWN")
            MotionEvent.ACTION_UP -> Log.d(TAG, "onInterceptTouchEvent: UP")
        }
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> Log.d(TAG, "onTouchEvent: DOWN")
            MotionEvent.ACTION_UP -> Log.d(TAG, "onTouchEvent: UP")
        }
        return super.onTouchEvent(event)
    }
}

class DispatchView : TextView {
    constructor(context : Context) : super(context)
    constructor(context : Context, attributeSet: AttributeSet?) : super(context,attributeSet)
    constructor(context : Context, attributeSet: AttributeSet?, defStyleAttr: Int):super(context,attributeSet,defStyleAttr)

    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> Log.d(TAG, "dispatchTouchEvent: down")
            MotionEvent.ACTION_UP -> Log.d(TAG, "dispatchTouchEvent: up")
        }
        return super.dispatchTouchEvent(event)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> Log.d(TAG, "onTouchEvent: down")
            MotionEvent.ACTION_UP -> Log.d(TAG, "onTouchEvent: up")
        }
        return super.onTouchEvent(event)
    }
}

点击Button,打印的log是:

2023-08-23 20:01:41.150 14174-14174 MIUIInput               com.example.dispathceventdemo        D  [MotionEvent] ViewRootImpl windowName 'com.example.dispathceventdemo/com.example.dispathceventdemo.MainActivity', { action=ACTION_DOWN, id[0]=0, pointerCount=1, eventTime=144660345, downTime=144660345, phoneEventTime=20:01:41.140 } moveCount:0
2023-08-23 20:01:41.151 14174-14174 MainActivity            com.example.dispathceventdemo        D  dispatchTouchEvent: DOWN
2023-08-23 20:01:41.151 14174-14174 DispatchViewGroup       com.example.dispathceventdemo        D  dispatchTouchEvent: DOWN
2023-08-23 20:01:41.151 14174-14174 DispatchViewGroup       com.example.dispathceventdemo        D  onInterceptTouchEvent: DOWN
2023-08-23 20:01:41.151 14174-14174 DispatchView            com.example.dispathceventdemo        D  dispatchTouchEvent: down
2023-08-23 20:01:41.151 14174-14174 DispatchView            com.example.dispathceventdemo        D  onTouchEvent: down
2023-08-23 20:01:41.152 14174-14174 DispatchViewGroup       com.example.dispathceventdemo        D  onTouchEvent: DOWN
2023-08-23 20:01:41.152 14174-14174 MainActivity            com.example.dispathceventdemo        D  onTouchEvent: DOWN
2023-08-23 20:01:41.379 14174-14174 MIUIInput               com.example.dispathceventdemo        D  [MotionEvent] ViewRootImpl windowName 'com.example.dispathceventdemo/com.example.dispathceventdemo.MainActivity', { action=ACTION_UP, id[0]=0, pointerCount=1, eventTime=144660578, downTime=144660345, phoneEventTime=20:01:41.373 } moveCount:1
2023-08-23 20:01:41.379 14174-14174 MainActivity            com.example.dispathceventdemo        D  dispatchTouchEvent: UP
2023-08-23 20:01:41.379 14174-14174 MainActivity            com.example.dispathceventdemo        D  onTouchEvent: UP

当点击事件没有处理时,可以看到时间处理的流程大致如下:
在这里插入图片描述

源码分析

Activity中dispatchTouchEvent

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

1.根据描述,dispatchTouchEvent被调用来处理屏幕触摸事件,可以在Window接收到触摸事件之前拦截触摸事件。
2.ACTION_DOWN时,会调用onUserInteraction,表示用户在和屏幕交互,onUserInteraction是一个空方法,可以重写该方法,可以在此实现与用户的交互功能。
3.getWindow().superDispatchTouchEvent 是调用Window中的superDispatchTouchEvent,Window是一个抽象类,唯一实现类是PhoneWindow。
4.superDispatchTouchEvent返回true,dispatchTouchEvent就结束,不会执行Activity中的onTouchEvent方法。

   /**
   *PhoneWindow
   **/
   @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

PhoneWindow中调用了DecorView中 superDispatchTouchEvent(MotionEvent event)

    //DecorView
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

这里的super.dispatchTouchEvent调用的是ViewGroup中的dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        //mInputEventConsistencyVerifier是用来调试使用的,正式版本中为null
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        //处理辅助功能,在设置里面开启辅助功能才能生效
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }
        //申明事件是否被消费
        boolean handled = false;
        //onFilterTouchEventForSecurity进行安全检查,为了防范恶意软件误导用户
        //主要过滤的是当前Window被遮挡的情况下的触摸事件。
        if (onFilterTouchEventForSecurity(ev)) {
            //Android是用一个32位的Int值表示一次TouchEvent,低8位表示具体动作,比如按下,抬起,滑动
            //这里涉及到多点触控
            //第一根手指按下产生事件:ACTION_DOWN
            //第二根手指按下产生事件:ACTION_POINTER_DOWN
            //此时抬起一根手指产生事件:ACTION_POINTER_UP
            //再抬起另一根手指产生事件:ACTION_UP
            //通过 ev.getAction() 得到的值包含了 动作(低8位)、触控点索引(9-16位)等,而不单单是上述的几种行为动作
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 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.
                //设置状态和初始化,为一个新的TouchEvent做准备。会调用cancelAndClearTouchTargets()和resetTouchState()
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            //ViewGroup拦截结果
            final boolean intercepted;
            //刚刚按下屏幕时,mFirstTouchTarget是为null,cancelAndClearTouchTargets中清除上一次点击事件时,会将mFirstTouchTarget置空
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //disallowIntercept = 是否禁用事件拦截的功能(默认为false),可通过调用requestDisallowInterceptTouchEvent() 修改
                //用来确定ViewGroup是否禁止拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //默认onInterceptTouchEvent返回false,不拦截,如果想拦截,应该重写onInterceptTouchEvent这个方法
                    //如果ACTION_DOWN 时候返回true,子View怎么都无法响应触摸事件
                    intercepted = onInterceptTouchEvent(ev);
                    //防止onInterceptTouchEvent 中对action 修改
                    //所以重新设置一遍
                    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;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            //canceled = true条件
            //1.此View 被ViewGroup或Window移除
            //2. actionMasked == MotionEvent.ACTION_CANCEL 
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
            //split 作用是判断可以把事件分发到多个子View , 多点触摸开关
            //这个同样在ViewGroup中提供了public的方法: setMotionEventSplittingEnabled ( boolean  split)来设置.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                    && !isMouseEvent;
                    
            <!-- view处理触摸事件的关键代码 -->        
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                //如果是辅助功能事件,会寻找他的targetview来接收这个事件
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                 
                //ACTION_POINTER_DOWN :代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,有新出现了一个触摸点。
                //ACTION_HOVER_MOVE : 鼠标事件 , 指针在窗口或者View区域移动,但没有按下。
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 多点触控会有不同的索引,获取索引号
                    //该索引位于MotionEvent中的一个数组没索引值就是数组的下标值
                    //只有up或者down事件才会携带索引值
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    // 这个整型变量记录了TouchTarget中view所对应的触控点id
                    // 触控点id的范围是0-31,整型变量中一个二进制为1,则对应绑定该id的触控点
                    //这里根据是否需要分离,对触控点id进行记录
                    // 而如果不需要分离,则默认接受所有触控点的事件
                    // ALL_POINTER_IDS = -1; (-1 二进制 = 11111111111111111111111111111111)
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    //清空这个手指idBitsToAssign对应的TouchTarget链表。
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    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.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //倒序遍历子view
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount;
                            }
                            //1.触摸坐标(x,y)在child的可视范围内
                            //2.child可接受触摸事件,是指child的是可见的(VISIBLE);或者虽然不可见,但是位于动画状态。
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            // 从缓存中获取newTouchTarget 
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                 //newTouchTarget != null 表示之前处理过 
                                // 重新设置手指id 跳出循环
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            // 调用dispatchTransformedTouchEvent()将触摸事件分发给child。
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //如果child.dispatchTouchEvent(event) = true
                                //子view包装成TouchTarge , 头插法插入到mFirstTouchTarget 的单链表中
                                //把 mFirstTouchTarget 指向了child,同时把newTouchTarget也指向了child
                                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) {
                        //这种情况 , mFirstTouchTarget!=null  ,newTouchTarget == null
                        //表示之前能消费事件 , 但是现在不行了(手指不在之前能消费那个view范围内)
                        //但是newTouchTarget = mFirstTouchTarget , 事件还是交给之前能消费那个view处理 
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
               // 没有能消费的子view . 自己super.dispatchTouchEvent处理
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //如果子view消费了事件,handled = true
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //如果cancelChild =true , 给子view发送ACTION_CANCEL事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        //如果cancelChild =true , 将子view从mFirstTouchTarget 链表移除
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    //遍历mFirstTouchTarget链表
                    predecessor = target;
                    target = next;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            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) {
            //移除idBitsToRemove 对应的TouchTarget
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

viewGroup中处理事件流程比较长。主要处理步骤如下:
1.接收到ACTION_DOWN时,将之前保存的触摸状态全部清空
2.检查disallowIntercept 是否禁用事件拦截的功能(默认为false),可通过调用requestDisallowInterceptTouchEvent() 修改,如果是disallowIntercept = true,禁止调用onInterceptTouchEvent方法
3.当!canceled && !intercepted即没有被取消和ViewGroup拦截,会进入到childView的接受流程
4.通过dispatchTransformedTouchEvent来处理子view是否接收点击事件,

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        //如果取消事件,那么不需要做其他额外的操作,直接派发事件即可
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        //oldPointerIdBits表示现在所有的触控id
        // desiredPointerIdBits来自于该view所在的touchTarget,表示该view感兴趣的触控点id
        // 因为desiredPointerIdBits有可能全是1,所以需要和oldPointerIdBits进行位与
        // 得到真正可接受的触控点信息
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        // 控件处于不一致的状态。正在接受事件序列却没有一个触控点id符合
        if (newPointerIdBits == 0) {
            return false;
        }
       // 来自原始MotionEvent 的新的MotionEvent,只包含目标感兴趣的触控点
       // 最终派发的是这个MotionEvent
        final MotionEvent transformedEvent;
        //两者相等,表示该view接受所有的触控点的事件
        //这个时候transfromedEvent相当于原始MotionEvent的复制
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    // 当目标控件不存在通过setScaleX()等方法进行的变换时
       				// 为了效率会将原市事件简单地进行控件位置与棍定量变换之后
       				// 发送给目标的dispatchTouchEvent()方法返回
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            //如果两者不相等,对事件进行拆分
            //只生成目标感兴趣的触控点信息
            //这里返回事件包括了许该事件的类型,触控点索引等。
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        // 对Motion Event的坐标系,转换为目标控件的坐标系并进行分发
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            // 计算滚动量偏移
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            // 调用子view的方法进行分发
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // 分发完毕,回收MotionEvent
        transformedEvent.recycle();
        return handled;
    }

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;
        //系统调试的代码,在ViewGroup的dispatchTouchEvent中也有
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            //停止滚动
            stopNestedScroll();
        }
        //安全检查,过滤掉一些不合法的事件,比如当前的View的窗口被遮挡了。 
        if (onFilterTouchEventForSecurity(event)) {
            //handleScrollBarDragging 处理鼠标拖拽时的滚动,手指触摸时,返回false
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //ListenerInfo是View的内部类,里面有各种各样的listener,例如OnClickListener,OnLongClickListener,OnTouchListener等等
            ListenerInfo li = mListenerInfo;
            //1.设置了mOnTouchListener
            //2.View可点击
            //3.执行OnTouchListener.onTouch方法
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                //执行了OnTouchListener.onTouch,表示该View处理了当前触摸事件
                result = true;
            }
            //1.没有设置setOnTouchListener
            //2.执行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;
    }

View中dispatchTouchEvent大致流程如下:
在这里插入图片描述
1.View中dispatchTouchEvent默认返回false。
2.OnTouchListener.onTouch是先于onTouchEvent执行的
3. 当OnTouchListener.onTouch 返回true时,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();
        //判断是否可点击,长按和上下文点击(上下文菜单,鼠标右键)
        //mViewFlags设置了CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE中任一一个flags
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        //view 是DISABLED,通过setEnable设置
        //PFLAG4_ALLOW_CLICK_WHEN_DISABLED 标志位被设置为 0,即不允许 View 在 disable 状态下被点击
        //PFLAG4_ALLOW_CLICK_WHEN_DISABLED 是在Android12时引入,用来设置是否允许 View 在 disable 状态下被点击
        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;
        }
        //如果控件设置了触摸代理,需要通过代理判断是否消耗了触摸事件
        //mTouchDelegate是通过setTouchDelegate()设置
        //TouchDelegate可以用来扩大触摸面积
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        //View是clickable is true
        //调用 setTooltipText()给 View 设置了提示文本,当用户将鼠标悬停在视图上或长按视图时,Android 会显示 tooltipText 属性的值
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    //抬手后 tooltip延迟一会消失
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        //触控笔按键被按下或者鼠标右键按下的时候为true,在按键释放或触控笔提起来的时候为false
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        //下一个ACTION_UP类型事件应该被忽略,按键释放或触控笔提起来的时候被设置为true
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    //PFLAG_PREPRESSED是在ACTION_DOWN事件发生时,在可滚动容器内设置延迟
                    //如果PFLAG_PRESSED标识存在,说明控件处于按压状态
                    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.
                        //变量focusTaken默认为false,影响点击事件是否执行
                        boolean focusTaken = false;
                        //isFocusable 是否可以去获取焦点
                        //isFocusableInTouchMode 判断该控件在触摸模式下是否可以获取焦点
                        //isFocused 是判断当前是否已经获取到焦点
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();//获取焦点
                        }
                        //prepressed 代表CheckForTap类型事件消息还没有执行,就触发了ACTION_UP类型事件
                        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);
                        }
                        //mHasPerformedLongPress 变量代表已经执行了长按事件,如果执行了长按事件,就不会执行点击事件了
                        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:
                    //如果触摸源是触摸屏
                    //将会在变量mPrivateFlags3上设置PFLAG3_FINGER_DOWN标志
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    //将变量mHasPerformedLongPress的值设为false
                    //该变量是为了在一次长按过程中执行过长按之后,避免执行点击事件
                    mHasPerformedLongPress = false;
                    //clickable = false,会进入mHasPerformedLongPress
                    if (!clickable) {
                        //长按处理
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }
                    //判断鼠标右键点击了,会调用showContextMenu()方法,显示上下文菜单
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    //isInScrollingContainer 判断当前控件是否处于一个可以滚动的容器
                    boolean isInScrollingContainer = isInScrollingContainer();

                    //如果在一个滚动的容器中,会将按压延迟一段时间,来区分该事件是不是滚动事件
                    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) {
                        //设置RippleDrawable的控件使用
                        drawableHotspotChanged(x, y);
                    }
                    //getClassification得到手势事件的类别,返回有三种类型
                    //CLASSIFICATION_NONE,没有额外的信息事件
                    //CLASSIFICATION_AMBIGUOUS_GESTURE 分类常量:模糊手势。用户对当前事件流的意图尚未确定。手势动作(如滚动)应被禁止,直到分类解析为另一个值或事件流结束。
                    //CLASSIFICATION_DEEP_PRESS 用户有意使劲压在屏幕上,这种类型的事件可以应用于加速长按事件的发生
                    final int motionClassification = event.getClassification();
                    final boolean ambiguousGesture =
                            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                    int touchSlop = mTouchSlop;//最小滑动距离
                    //hasPendingLongPressCallback 检查消息队列中是否有待执行的长按事件消息
                    if (ambiguousGesture && hasPendingLongPressCallback()) {
                        //pointInView 检查触摸点是否已经超出了控件的范围
                        if (!pointInView(x, y, touchSlop)) {
                            //取消掉长按事件
                            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;
    }
    /**
     * Entry point for {@link #performClick()} - other methods on View should call it instead of
     * {@code performClick()} directly to make sure the autofill manager is notified when
     * necessary (as subclasses could extend {@code performClick()} without calling the parent's
     * method).
     */
    private boolean performClickInternal() {
        //是为了通知自动填充服务该控件执行了点击事件,然后接着执行performClick()方法。
        notifyAutofillManagerOnClick();
        
        return performClick();
    }
    
    //主要为了执行注册的点击事件
    public boolean performClick() {
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            //先播放点击音效
            playSoundEffect(SoundEffectConstants.CLICK);
            //执行setOnClickListener时注册的回调
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

从OnToucEvent代码中可以看出:
1.检查是否设置了TouchDelegate,设置了就调用mTouchDelegate.onTouchEvent,返回true就直接返回true
2.长按事件是在ACTION_DOWN时处理的
3.短按事件是在ACTION_UP中处理,调用performClickInternal
3.OnClickListener.onClick 和 playSoundEffect点击音效都是在performClickInternal中执行

常见问题

1.activity viewgroup和view都不消费action_down,那么action_up事件是怎么传递的?
当ViewGroup和View都不处理ACTION_DOWN时,ACTION_MOVE和ACTION_UP事件就不会传递到ViewGroup和View的dispathTouchEvent。

2.为什么子 View 不消费 ACTION_DOWN,之后的所有事件都不会向下传递了?
主要是因为mFirstTouchTarget,mFirstTouchTarget是一个链表,存储了TouchTarget对象,TouchTarget的作用场景在事件派发流程中,用于记录派发目标,即消费了事件的子View。当View或ViewGroup没有处理DOWN事件时,DecorView中的mFirstTouchTarget == null

dispatchTouchEvent(MotionEvent ev){
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
}

可以看到intercepted = true,默认拦截。
3. 同时对父 View 和子 View 设置点击方法,优先响应哪个?
优先响应View,ViewGroup在dispatchTocuEvent中,如果先响应父 view,那么子 view 将永远无法响应,父 view 要优先响应事件,必须先调用 onInterceptTouchEvent 对事件进行拦截,那么事件不会再往下传递,直接交给父 view 的 onTouchEvent 处理。
4.View中OnTouchEvent,OnClickListener和OnTouchListeners,OnLongClickListener三者的优先级是什么样的?
OnTouchListeners 》OnTouchEvent 》OnLongClickListener 》OnClickListener
OnTouchListeners 和 OnTouchEvent都是在dispatchTouchEvent中执行的,OnTouchListeners比OnTouchEvent执行顺序早,
如果OnTouchListeners中onTouch方法返回true,后面的OnTouchEvent不会执行,会导致OnTouchListeners,OnLongClickListener也不会被执行。OnLongClickListener在OnTouchEvent中的DOWN事件就会被执行,OnClickListener会在UP时被调用,而且OnLongClickListener返回true,OnTouchEvent就不会被执行。

参考

https://blog.csdn.net/q1165328963/article/details/120773934

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/940932.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言每日一题 ----计算日期到天数转换(Day 2)

本专栏为c语言练习专栏&#xff0c;适合刚刚学完c语言的初学者。本专栏每天会不定时更新&#xff0c;通过每天练习&#xff0c;进一步对c语言的重难点知识进行更深入的学习。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;C语言天天练 &#x…

m3u8 blob视频免费下载

F12点开找到这个视频url最后是.m3u8结尾 http://blog.luckly-mjw.cn/tool-show/m3u8-downloader/index.html 在上边的网址转Mp4下载即可

HBase集群环境搭建与测试

&#x1f947;&#x1f947;【大数据学习记录篇】-持续更新中~&#x1f947;&#x1f947; 个人主页&#xff1a;beixi 本文章收录于专栏&#xff08;点击传送&#xff09;&#xff1a;【大数据学习】 &#x1f493;&#x1f493;持续更新中&#xff0c;感谢各位前辈朋友们支持…

【第四阶段】kotlin语言的List创建与元素获取

1.list创建 val list listOf("jave","kotlin","c","c")2.list集合获取的三种方式 开发过程中尽量使用getOrElse()或者getOrNull&#xff0c;因为他可以防止崩溃取值 package Stage4fun main() {//list 创建val list listOf("jav…

m4s格式转换mp4

先安装 ffmpeg&#xff0c;具体从官网可以查到&#xff0c;https://ffmpeg.org&#xff0c;按流程走。 转换代码如下&#xff0c;可以任意选择格式导出 import subprocess import osdef merge_audio_video(input_audio_path, input_video_path, output_mp4_path):# 构建 FFmpe…

ViT论文Pytorch代码解读

ViT论文代码实现 论文地址&#xff1a;https://arxiv.org/abs/2010.11929 Pytorch代码地址&#xff1a;https://github.com/lucidrains/vit-pytorch ViT结构图 调用代码 import torch from vit_pytorch import ViTdef test():v ViT(image_size 256, patch_size 32, num_cl…

【第四阶段】kotlin语言可变list集合

1.可变list集合 完整写法 var list:MutableList<String> mutableListOf<String>("java","kotlin","c","c") 省略写法 var list mutableListOf("java","kotlin","c","c")fun ma…

CobaltStrike提权

攻击机&#xff1a;Kali Linux 靶 机&#xff1a;Windows 7 一、上线CS 复制命令&#xff0c;在靶机执行上线CS 2.安装插件&#xff0c;获取shell https://github.com/rsmudge/ElevateKit 上线CS 右击shell&#xff0c;选择插件 有七个模块可以利用&#xff0c;可以逐一…

C++实现YOLOP

C实现YOLOP 一、简介 使用OpenCV部署全景驾驶感知网络YOLOP&#xff0c;可同时处理交通目标检测、可驾驶区域分割、车道线检测&#xff0c;三项视觉感知任务&#xff0c;依然是包含C和Python两种版本的程序实现 onnx文件从百度云盘下载&#xff0c;链接&#xff1a;https://…

【AutoLayout案例07-如何通过代码添加约束 Objective-C语言】

一、那么,接下来,我们就给大家介绍一下,如何通过代码,来实现这个AutoLayout 1.咱们之前是不是都是通过,storyboard、来拖、拉、拽、的方式实现的吧 现在给大家介绍一下,如何通过代码,来实现 在继续介绍,如何通过代码,来实现AutoLayout之前呢, 我们先要给大家补充一…

基于Java+SpringBoot+Vue前后端分离疫苗发布和接种预约系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

前端开发之Element Plus的分页组件el-pagination显示英文转变为中文

前言 在使用element的时候分页提示语句是中文的到了element-plus中式英文的&#xff0c;本文讲解的就是怎样将英文转变为中文 效果图 解决方案 如果你的element-plus版本为2.2.29以下的 import { createApp } from vue import App from ./App.vue import ElementPlus from …

ruoyi-vue-plus 配置邮箱

ruoyi-vue-plus 配置邮箱 &#x1f4d4; 千寻简笔记介绍 千寻简笔记已开源&#xff0c;Gitee与GitHub搜索chihiro-notes&#xff0c;包含笔记源文件.md&#xff0c;以及PDF版本方便阅读&#xff0c;且是用了精美主题&#xff0c;阅读体验更佳&#xff0c;如果文章对你有帮助请…

Java实现根据短连接获取1688商品详情数据,1688淘口令接口,1688API接口封装方法

要通过1688的API获取商品详情数据&#xff0c;您可以使用1688开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例&#xff0c;展示如何通过1688开放平台API获取商品详情属性数据接口&#xff1a; 首先&#xff0c;确保您已注册成为1688开放平台的开发者&#xf…

网络工程师的尽头是……

大家好&#xff0c;我是许公子。 最近工作挺忙&#xff0c;很久没有给你们输出文章了&#xff0c;抽空和大家唠嗑唠嗑。 前两天&#xff0c;一个实习生问了我这个问题&#xff1a; “网络工程师的尽头是什么&#xff1f;” 我当时一下子&#xff0c;脑子空白了&#xff0c;…

stackoverflow问题

Stack Overflow requires external JavaScript from another domain, which is blocked or failed to load. stackoverflow引用了谷歌中被屏ajax.googleapis.com的jquery.min.js文件。“https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js” 方案1.打开网站…

Viobot算法控制

一.上位机控制 状态反馈在系统反馈出会根据当前系统状态显示。 控制是在操作栏里面的一个选项三个按键。具体的已经在前面一篇基本功能使用及介绍中讲过了。 二.ROS控制 算法的控制我们也整合成了一个ROS msg&#xff0c;具体的msg信息可以查看demo里面的msg包的algo_ctrl.m…

Arduino RGBLED灯 模块学习与使用

Arduino RGBLED灯模块学习与使用 硬件原理制作衍生连接线Mixly程序Arduino程序演示视频 人生如逆旅&#xff0c;我亦是行人。 —— 苏轼江客:时荒 硬件原理 RGBLED灯三个引脚分别控制三个LED灯的亮度&#xff0c;RGB分别是red&#xff0c;green&#xff0c;blue的英文缩写&…

iOS - 资源按需加载 - ODR

一、瘦身技术大图 二、On-Demand Resources 简介 将其保存管理在苹果的服务器&#xff0c;按需使用资源、优化包体积&#xff0c;实现更小的应用程序。ODR 的好处&#xff1a; 应用体积更小&#xff0c;下载更快&#xff0c;提升初次启动速度资源会在后台下载操作系统将会在磁…

《C/C++等级考试(1~8级)历届真题解析》专栏总目录

❤️ 专栏名称&#xff1a;《C/C等级考试&#xff08;1~8级&#xff09;历届真题解析》 &#x1f338; 专栏介绍&#xff1a;中国电子学会《全国青少年软件编程等级考试》C/C编程&#xff08;1~8级&#xff09;历届真题解析。 &#x1f680; 订阅专栏&#xff1a;订阅后可阅读专…