从 ViewRoot 来分析 TouchEvent 触摸事件

在 ViewRoot 中:

  • 有这几个数据成员:

    InputChannel        mInputChannel;
    InputQueue.Callback mInputQueueCallback;
    InputQueue          mInputQueue;
    
    private final InputHandler mInputHandler = new InputHandler() {
        public void handleKey(KeyEvent event, Runnable finishedCallback) {
            startInputEvent(finishedCallback);
            dispatchKey(event, true);
        }
    
        public void handleMotion(MotionEvent event, Runnable finishedCallback) {
            startInputEvent(finishedCallback);
            dispatchMotion(event, true);
        }
    };
    
  • 这个 mInputHandler 是在 setView 中注册的:

    /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs,
            View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                mWindowAttributes.copyFrom(attrs);
                attrs = mWindowAttributes;
                if (view instanceof RootViewSurfaceTaker) {
                    mSurfaceHolderCallback =
                            ((RootViewSurfaceTaker)view).willYouTakeTheSurface();
                    if (mSurfaceHolderCallback != null) {
                        mSurfaceHolder = new TakenSurfaceHolder();
                        mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
                    }
                }
                Resources resources = mView.getContext().getResources();
                CompatibilityInfo compatibilityInfo = resources.getCompatibilityInfo();
                mTranslator = compatibilityInfo.getTranslator();
    
                if (mTranslator != null || !compatibilityInfo.supportsScreen()) {
                    mSurface.setCompatibleDisplayMetrics(resources.getDisplayMetrics(),
                            mTranslator);
                }
    
                boolean restore = false;
                if (mTranslator != null) {
                    restore = true;
                    attrs.backup();
                    mTranslator.translateWindowLayout(attrs);
                }
                if (DEBUG_LAYOUT) Log.d(TAG, "WindowLayout in setView:" + attrs);
    
                if (!compatibilityInfo.supportsScreen()) {
                    attrs.flags |= WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW;
                }
    
                mSoftInputMode = attrs.softInputMode;
                mWindowAttributesChanged = true;
                mAttachInfo.mRootView = view;
                mAttachInfo.mScalingRequired = mTranslator != null;
                mAttachInfo.mApplicationScale =
                        mTranslator == null ? 1.0f : mTranslator.applicationScale;
                if (panelParentView != null) {
                    mAttachInfo.mPanelParentWindowToken
                            = panelParentView.getApplicationWindowToken();
                }
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */
    
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();
                mInputChannel = new InputChannel();
                try {
                    res = sWindowSession.add(mWindow, mWindowAttributes,
                            getHostVisibility(), mAttachInfo.mContentInsets,
                            mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    unscheduleTraversals();
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
    
                if (mTranslator != null) {
                    mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets);
                }
                mPendingContentInsets.set(mAttachInfo.mContentInsets);
                mPendingVisibleInsets.set(0, 0, 0, 0);
                if (Config.LOGV) Log.v(TAG, "Added window " + mWindow);
                if (res < WindowManagerImpl.ADD_OKAY) {
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mAdded = false;
                    unscheduleTraversals();
                    switch (res) {
                        case WindowManagerImpl.ADD_BAD_APP_TOKEN:
                        case WindowManagerImpl.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not valid; is your activity running?");
                        case WindowManagerImpl.ADD_NOT_APP_TOKEN:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");
                        case WindowManagerImpl.ADD_APP_EXITING:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- app for token " + attrs.token
                                + " is exiting");
                        case WindowManagerImpl.ADD_DUPLICATE_ADD:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window -- window " + mWindow
                                + " has already been added");
                        case WindowManagerImpl.ADD_STARTING_NOT_NEEDED:
                            // Silently ignore -- we would have just removed it
                            // right away, anyway.
                            return;
                        case WindowManagerImpl.ADD_MULTIPLE_SINGLETON:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window " + mWindow +
                                " -- another window of this type already exists");
                        case WindowManagerImpl.ADD_PERMISSION_DENIED:
                            throw new WindowManagerImpl.BadTokenException(
                                "Unable to add window " + mWindow +
                                " -- permission denied for this window type");
                    }
                    throw new RuntimeException(
                        "Unable to add window -- unknown error code " + res);
                }
    
                if (view instanceof RootViewSurfaceTaker) {
                    mInputQueueCallback =
                        ((RootViewSurfaceTaker)view).willYouTakeTheInputQueue();
                }
                if (mInputQueueCallback != null) {
                    mInputQueue = new InputQueue(mInputChannel);
                    mInputQueueCallback.onInputQueueCreated(mInputQueue);
                } else {
                    InputQueue.registerInputChannel(mInputChannel, 
                                                    mInputHandler,   //这个地方注意一下.
                                                    Looper.myQueue());
                }
    
                view.assignParent(this);
                mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0;
                mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0;
            }
        }
    }
    
  • 这里最主要的是 mInputHandler 中的 handleMotion 中调用到了 dispatchMotion 方法:

    public void handleMotion(MotionEvent event, Runnable finishedCallback) {
        startInputEvent(finishedCallback);
        dispatchMotion(event, true);
    }
    

    见 ViewRoot 中的 dispatchMotion 方法:

        private void dispatchMotion(MotionEvent event, boolean sendDone) {
            int source = event.getSource();
            if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                dispatchPointer(event, sendDone);  //这个地方!
            } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                dispatchTrackball(event, sendDone);
            } else {
                // TODO
                Log.v(TAG, "Dropping unsupported motion event (unimplemented): " + event);
                if (sendDone) {
                    finishInputEvent();
                }
            }
        }
    
        private void dispatchPointer(MotionEvent event, boolean sendDone) {
            Message msg = obtainMessage(DISPATCH_POINTER);  //发出这样的消息.
            msg.obj = event;
            msg.arg1 = sendDone ? 1 : 0;
            sendMessageAtTime(msg, event.getEventTime());
        }        
    
  • 处理这个消息:

    case DISPATCH_POINTER: {//触摸事件消息的处理
        MotionEvent event = (MotionEvent) msg.obj;
        try {
            deliverPointerEvent(event);//走入到这里.----这个地方
        } finally {
            event.recycle(); //处理完这个事件后, 把这个event回收掉.
            if (msg.arg1 != 0) {
                finishInputEvent();//向发出消息模块发一个回执, 以便进行下一次的消息派发.
            }
            if (LOCAL_LOGV || WATCH_POINTER) Log.i(TAG, "Done dispatching!");
        }
    } break;
    
    private void deliverPointerEvent(MotionEvent event) {
        if (mTranslator != null) {
            mTranslator.translateEventInScreenToAppWindow(event);//物理坐标向逻辑坐标的转换.
        }
    
        boolean handled;
        if (mView != null && mAdded) {
    
            // enter touch mode on the down
            boolean isDown = event.getAction() == MotionEvent.ACTION_DOWN;
            if (isDown) {
                ensureTouchMode(true);//进入触摸模式.----这个方法见下面
            }
            if(Config.LOGV) {
                captureMotionLog("captureDispatchPointer", event);
            }
            if (mCurScrollY != 0) {
                event.offsetLocation(0, mCurScrollY);
            }
            if (MEASURE_LATENCY) {
                lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano());
            }
    
            //进行事件的派发, 对view和activity系统产生影响. 见 DecorView 和 ViewGroup中的方法.
            handled = mView.dispatchTouchEvent(event); //--------------------------这句话是最重要的.
    
            if (MEASURE_LATENCY) {
                lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano());
            }
            if (!handled && isDown) {//对于上面没有处理的事件, 进行屏幕边界偏移.屏幕偏移用(edge slop)进行表示. 
                //它的作用是当用户正好触摸到屏幕边界时,系统自动对原始消息进行一定的偏移,
                //然后在新的偏移后的位置上寻找是否有匹配的视图,
                //为什么要有"屏幕偏移"呢? 因为对于触摸屏而言, 尤其是电容触摸屏, 人类手指尖有一定的大小, 
                //当触摸到边界时, 力量会被自动吸附到屏幕边界, 
                //所以, 此处根据上下左右不同的边界对象消息原始位置进行一定的偏移.
                int edgeSlop = mViewConfiguration.getScaledEdgeSlop();
    
                final int edgeFlags = event.getEdgeFlags();
                int direction = View.FOCUS_UP;
                int x = (int)event.getX();
                int y = (int)event.getY();
                final int[] deltas = new int[2];
    
                if ((edgeFlags & MotionEvent.EDGE_TOP) != 0) {
                    direction = View.FOCUS_DOWN;
                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                        deltas[0] = edgeSlop;
                        x += edgeSlop;
                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                        deltas[0] = -edgeSlop;
                        x -= edgeSlop;
                    }
                } else if ((edgeFlags & MotionEvent.EDGE_BOTTOM) != 0) {
                    direction = View.FOCUS_UP;
                    if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                        deltas[0] = edgeSlop;
                        x += edgeSlop;
                    } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                        deltas[0] = -edgeSlop;
                        x -= edgeSlop;
                    }
                } else if ((edgeFlags & MotionEvent.EDGE_LEFT) != 0) {
                    direction = View.FOCUS_RIGHT;
                } else if ((edgeFlags & MotionEvent.EDGE_RIGHT) != 0) {
                    direction = View.FOCUS_LEFT;
                }
    
                if (edgeFlags != 0 && mView instanceof ViewGroup) {
                    View nearest = FocusFinder.getInstance().findNearestTouchable(
                            ((ViewGroup) mView), x, y, direction, deltas);
                    if (nearest != null) {
                        event.offsetLocation(deltas[0], deltas[1]);
                        event.setEdgeFlags(0);
                        mView.dispatchTouchEvent(event);
                    }
                }
            }
        }
    }
    
    其中 ensureTouchMode 如下所示:
    boolean ensureTouchMode(boolean inTouchMode) {//进否进入触摸模式.---即 非触摸模式 与 触摸模式 之间的切换.
        if (DBG) Log.d("touchmode", "ensureTouchMode(" + inTouchMode + "), current "
                + "touch mode is " + mAttachInfo.mInTouchMode);
        //如果当前触摸 与 原来的触摸模式 相同, 则没有改变, 所以返回false.
        if (mAttachInfo.mInTouchMode == inTouchMode) return false;
    
        // tell the window manager----即通知window----因为 wms在布局窗口时, 会根据不同的touch模式进行不同的处理
        try {
            //通知窗口, WmS在进行客户窗口布局时, 需要根据客户窗口的Touch模式进行不同的处理.
            sWindowSession.setInTouchMode(inTouchMode);
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
    
        // handle the change ----view自身的改变.----如清除焦点, 或者requestFocus 之类的 可能涉及 界面更新的操作.
        return ensureTouchModeLocally(inTouchMode);//---点进去去看下. 这个方法 其实就在这下面.
    }
    
  • 如果这个 mView是 DecorView 而言, 执行这个:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        //注意, activity实现了 Window.CallBack接口, 这里获得的cb, 就是这个activity.
        final Callback cb = getCallback(); 
        return cb != null && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super
                .dispatchTouchEvent(ev);
    }
    
    • 如果 cb为空, 则直接执行 ViewGroup中的 dispatchTouchEvent方法.—-下面会讲到.
    • 如果 cb不为空, 则执行 activity中的 dispatchTouchEvent方法:

      public boolean dispatchTouchEvent(MotionEvent ev) {
          if (ev.getAction() == MotionEvent.ACTION_DOWN) { 
              //如果是down事件的话, activity有机会在事件响应之前做点事情.
              onUserInteraction();
          }
          if (getWindow().superDispatchTouchEvent(ev)) {  //调的是PhoneWindow中的superDispatchTouchEvent
                                                          //--->DecorView中的superDispatchTouchEvent
                                                          //--->ViewGroup中的dispatchTouchEvent方法.
              return true;
          }
          return onTouchEvent(ev); //如果view系统不处理, 则调用 activity中的 onTouchEvent.
      }
      
  • 如果这个 mView直接就是 ViewGroup的话, 那直接调到 ViewGroup中的 dispatchTouchEvent.

  • 反正先 处理 viewgroup的 dispatchTouchEvent, 如果没有消化掉, 才去处理 activity中的 onTouchEvent方法.

  • 至于 ViewGroup中的 dispatchTouchEvent(event)方法:

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {//touch到时, 会从 ViewRoot那里调到 ViewGroup的这个方法.
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }
    
        final int action = ev.getAction();
        //当前ViewGroup布局坐标系的坐标.  当前ViewGroup视图坐标原点在布局坐标系中的位置为(-mScrollX, -mScrollY)
        final float xf = ev.getX();
        final float yf = ev.getY();
        //坐标系 转换成 当前ViewGroup视图坐标系的坐标. 这个混算要整明白.----不要误以为是child什么的.
        //因为当前这个viewgroup可能会在scroll的, 
        //所以要算上(mScrollX, mScrollY)来得到这个触摸点相对于当前这个Viewgroup视图坐标原点的坐标.
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;
    
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//true表示不允许拦截
    
        if (action == MotionEvent.ACTION_DOWN) { //先处理 action_down 情况
            if (mMotionTarget != null) {//这个mMotionTarget是指这个viewgroup中的捕获事件的child.
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                //当action_down时, 通常情况下, 这个mMotionTarget当然应为null. 不为空则可能是出错的.
                mMotionTarget = null;
            }
            // If we're disallowing intercept or if we're allowing and we didn't
            // intercept
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {//不允许拦截 或者 没有拦截
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);//重置
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;//视图坐标
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;//要对孩子进行遍历, 这个孩子可能是相邻, 也可能是相互前后叠加.
                final int count = mChildrenCount;
                //被触摸的点处, 可能会叠加多个孩子. 
                //让序号最后面的child先拿事件试试, 如果不要的话, 再让序号前面的孩子拿事件.
                for (int i = count - 1; i >= 0; i--) {//遍历孩子, 确定孩子要不要这个down事件.
                    final View child = children[i];
                    //只有当child是可见或者动画时, 才可以响应这个down.
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        child.getHitRect(frame);//获得该child的布局区域----父view视图坐标系中的.
                        if (frame.contains(scrolledXInt, scrolledYInt)) {//判断点击的位置是否在这个child上.
                            // offset the event to the view's coordinate system
                            //坐标系切到孩子的布局坐标系统上. 这个要理解好.
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            //点击事件在child上, 则现在坐标转换到以child的原点为基准,---但非child显示区域坐标啊.
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            if (child.dispatchTouchEvent(ev))  {//交给child来分发了.
                                // Event handled, we have a target now.
                                //如果孩子消费了这个down事件, 则这个mMotionTarget就记录这个孩子, 然后返回.
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }
    
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||//指是不是up或cancel事件, true表示是.
                (action == MotionEvent.ACTION_CANCEL);
    
        if (isUpOrCancel) {
            //如果现在的事件是up或者cancel掉了, 那么应当允许拦截. 因为在按下还没有释放时, 要拦截消息的.
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            //现在允许拦截.----因为 这一系列的(down/move/up/cacel)事件 已经结束了!
            //----所以没有是否允许拦截的意义了.
            //----即 设一个不允许拦截, 其有效期仅这么一套down/move/up/cancel周期而已.
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
    
        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        final View target = mMotionTarget;
        if (target == null) {  
            //如果child没有消耗这个down事件的话, 说明move和up也不会响应. 所以, 应由这个viewgroup自己响应.
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);//坐标移回viewgroup自己的坐标体系, 即布局坐标.
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {//这个CANCEL_NEXT_UP_EVENT通常是不存在的
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            return super.dispatchTouchEvent(ev);//调用viewgroup的view部分的dispatchTouchEvent方法.
        }
        //如果有孩子响应了down事件, 那么往下走.----上面刚处理的是 没有孩子响应的事情, 现在处理有孩子响应的情况.
        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            //如果允许viewgroup截获, 并且确实被viewgroup截获了, 
            //那么child应当放弃down,move,up事件, 所以下面用cancel来取消child.
            //这个target是指捕获down事件的child
            //即, 将获得点击位置---即以child的布局坐标系统来算的.
            final float xc = scrolledXFloat - (float) target.mLeft; 
            final float yc = scrolledYFloat - (float) target.mTop;  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);//事件改为action_cancel事件.
            ev.setLocation(xc, yc);//坐标换到child的布局坐标来.
            if (!target.dispatchTouchEvent(ev)) {
                //用于让child处理cancel事件
                //----因为原来的事件被父viewgroup给拦截了,所以用cancel来逐个逐级通知child处理cancel.
                // target didn't handle ACTION_CANCEL. not much we can do
                //这个cancel事件可以通知child去取消之前对事件的追踪, 如长按, 特定手势之类.
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;//把其置为null
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;//返回true, 表示事件被消耗了.----这里是被viewgroup消耗了, 而不是child.
        }
        //
        if (isUpOrCancel) {//true表示当前事件是 up或cancel事件, 
            //表示 事件处理 处于 尾声了. 
            //mMotionTarget置回空, 不过target仍在, 以便下面调用 target.dispatchTouchEvent.
            //对于 move事件, 因为事件 后面还会有, 所以 mMotionTarget不能为空的.
            mMotionTarget = null;
        }                        
        //下面这些, 都是指 由child来响应 move, up事件!
        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;//转到child的布局坐标方式.
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);//更改坐标系统为child的布局坐标方式.
    
        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            //通常不走进来.这个CANCEL_NEXT_UP_EVENT表示 取消 随后的up事件.
            ev.setAction(MotionEvent.ACTION_CANCEL); //走进来的话, 表示要取消随后的up事件, 所以事件改为cancel事件.
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;  //然后 也把 这个置为null
        }
    
        return target.dispatchTouchEvent(ev);//由child来处理这个move和up事件.----以及可能的cancel事件.
    }
    
  • View 中的 dispatchTouchEvent 方法:

    //这里注意的是: 这里先处理 外界设置的 OnTouchEventListener
    //    如果返回 true, 说明外界要 抢占 这个事件, 所以不执行 控件自身的 onTouchEvent.
    //    如果返回 false, 说明外界 认为可以 把这个事件 分发给 控件自身的 onTouchEvent处理.
    //主要是这点:
    //(1) 提供了一个接口给外界设置, 即通过 setOnTouchEventListener 设置一个监听器.
    //(2) 自身处理的方法: onTouchEvent ----在自定义一个view时写的.
    //优先执行 外界的要求(即监听器中的方法), 如果返回 false, 才去执行 控件自身的onTouchEvent.
    public boolean dispatchTouchEvent(MotionEvent event) {           
        if (!onFilterTouchEventForSecurity(event)) {
            //处理当窗口处于模糊状态下的事件.---返回true表示, 事件应当处理; 为false时, 表示事件不处理.
            return false;
        }
    
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            //如果 ENABLE, 并且该view注册了OnTouchListener监听器, 则执行这个监听器的onTouch, 处理完直接返回true
            return true;
        }
        return onTouchEvent(event); //如果没有设置监听器, 则执行 onTouchEvent方法.
    }
    
  • View 中的 onTouchEvent 方法:
    这个呢, 在其它的笔记中已做了说明, 这里就不列出来了.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值