andriod事件处理流程解析

事件处理就是定义和响应用户的操作事件(按键、触屏等),也是控件开发中的一个难题。比如onIntrceptTouchEvent,onTouchEvent,onClick,onLongClick等一系列的方法容易让人混淆。为了厘清这些难题,我们有必要看看整个事件处理的流程。

android对事件的管理主要有完成按键、触摸板、鼠标等输入设备的输入,向焦点窗口和焦点视图的事件派发,事件的插入,事件的过滤,事件的拦截等。服务端部分主要完成输入设备事件的读取、事件的映射、事件的插入、事件的过滤、事件的拦截等功能;客户端部分主要完成事件向焦点窗口和焦点视图的派发。具体来说,按键、触屏等事件是经由WindowManagerService获取,并通过共享内存和管道的方式传递给ViewRoot,ViewRoot再dispatch给Application的View。这里的进程间通信没有采用Binder机制,因为要即时响应用户事件。管道通信,就是其中一个进程在管道的读端等待新的内空可读,另一个进程在管道的写端写入新的内容以唤醒在管道读端等待的进程。


图1 内存共享



图2 管道通信


在ViewRoot和WMS(WindowManagerService)建立起连接之前首先会创建一个InputChannel对象,同样的WMS端也会创建一个InputChannel对象,不过WMS的创建过程是在ViewRoot调用add()方法时调用的。这一对InputChannel中实现了一组全双工管道。在创建InputChannel对的同时,会申请共享内存,并向2个InputChannel对象中各自保存一个共享内存的文件描述符。

WindowManagerService--->ViewRoot方向的管道通信,表示WMS通知ViewRoot有新事件被写入到共享内存;
ViewRoot-->WindowManagerService方向的管道通信,表示ViewRoot已经消化完共享内存中的新事件,特此通知WMS。

由于系统中存在不只一对通信管道,所以需要对这些管道统一管理。ViewRoot端的管道一般情况下会注册到一个NativeInputQueue对象中(这是一个Native的对象,而JAVA端的InputQueue类仅仅是提供了一些static方法与NativeInputQueue通信),WMS端注册在InputManager对象中(实际上创建了C++层的NativeInputManager,它整个业务的核心又分InputReader和InputDispatcher两个模块)。下面我们看事件处理的整个类图和时序图。


图3 输入系统服务端类图



图4 客户端事件派发类图



图5 客户端事件派发时序图


我们重点关注客户端的事件派发。以key事件为例,在客户端收到服务端管道发送的事件时,就触发客户端管道注册时注册的回调函数(图27 NativeInputQueue对象的handleReceiveCallback函数):
 
     1.首先NativeInputQueue对象的 handleReceiveCallback根据回调来的接收管道参数receiveFd在NativeInputQueue队列中找到connectionIndex,根据connectionIndex找到对应的connection对象;然后调用connection对象中的inputConsumer对象的receiveDispatchSignal函数读取服务端通过管道发送的DispatchSignal信号,并判断发送信号标志是否正确;接着调用inputConsume对象的consume函数从共享内存中读取mSharedMessage消息,并根据消息类型构造事件,如按键对应的keyEvent事件,并转换为JAVA事件;然后通过JNI回调JAVA对象 InputQueue的dispatchKeyEvent函数

     2.在InputQueue对象的dispatchKeyEvent函数中调用ViewRootImpl对象内部的 inputHandler对象的handleKey函数;

     3.接着又调用 ViewRootImpl对象的dispatchKey函数;dispatchKey函数通过enqueueInputEvent向ViewRootImpl对象的事件处理线程发送异步消息;ViewRootImpl对象的事件处理函数handleMessage根据消息类型进行相应处理,这里调用的是 deliverKeyEvent函数;deliverKeyEvent函数首先调用主视图的dispatchKeyEventPreIme函数,接着调用deliverKeyEventPostIme函数;deliverKeyEventPostIme函数调用主视图的dispatchKeyEvent函数在视图树中派发事件;dispatchKeyEvent函数派发完后返回进行其它处理如聚焦切换等;

      4. 主视图DecorView的dispatchKeyEvent函数首先判断是否进行快捷键调用,然后调用主视图绑定的ACTIVITY的dispatchKeyEvent函数,如果没有处理事件,则根据按键状态调用PhoneWindow的onKeyDown函数或onKeyUp函数;

      5.PhoneWindow的superDispatchKeyEvent在视图树(DecorView)中派发事件,DecorView继承于ViewGroup,因此ViewGroup的dispatchKeyEvent函数被调用,再通过ViewGroup对象的mFocused(焦点子视图)成员向下一级焦点视图派发事件。这样一级级向各个焦点ViewGroup和Focuse View视图派发事件,完成整个视图树的事件派发。

     6.如果视图树没有处理事件则调用event.dispatch函数派发事件由ACTIVITY对象本身处理,ACTIVITY对象的事件回调接口在这里被调用;
下面,我们再以Touch事件为例,从源码角度分析 Activity、View和ViewGroup的事件处理流程


class Activity {
     ...
     //DecorView把事件分发过来
      public   boolean  dispatchTouchEvent(MotionEvent ev) {
         if  (ev. getAction() == MotionEvent.  ACTION_DOWN ) {
            onUserInteraction();
        }
      //分发给PhoneWindow,再分发给DecorView
         if  (g t eWindow().superDispatchTouchEvent(ev)) {
             return   true ;
        }
     //如果没处理,自己处理
         return  onTouchEvent(ev);
    }
      public  boolean  onTouchEvent (MotionEvent event) {
         if  ( mWindow .shouldCloseOnTouch(  this , event)) {
            finish();
             return  true ;
        }
       
         return  false ;
    }
     ...
}
------------------------------------------------------------------------------------------------

class   PhoneWindow{
     public boolean superDispatchTouchEvent(MotionEvent event) {
               //mDecor会调用父类FrameLayout的父类ViewGroup的方法
            return mDecor.superDispatchTouchEvent(event);
     }
}

------------------------------------------------------------------------------------------------

class  ViewGroup{
   public   boolean  dispatchTouchEvent(MotionEvent ev) {
     ...
   if  (onFilterTouchEventForSecurity(ev)) {
             final  int  action = ev.getAction();
             final  int  actionMasked = action & MotionEvent.  ACTION_MASK ;

             // Handle an initial down.
             if  (actionMasked == MotionEvent.  ACTION_DOWN ) {
                //当新开始一个touch事件时,抛弃先前的touch状态 
                //当app切换,发生ANR或一些其他的touch状态发生时,framework会丢弃或取消先前的touch状态  
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

             //检查拦截事件
             final  boolean  intercepted;  
             if  (actionMasked == MotionEvent.  ACTION_DOWN
                    ||  mFirstTouchTarget  !=  null ) {
                 final  boolean  disallowIntercept = (  mGroupFlags  &  FLAG_DISALLOW_INTERCEPT ) != 0;
              // 这个值默认是false, 可通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)设置
                 if  (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);//是否拦截
                    ev.setAction(action);  // restore action in case it was changed
                }  else  {
                    intercepted =  false ;
                }
            }  else  {
                         //没有touch事件的传递对象,同时touch动作不是初始动作down,ViewGroup就拦截这个事件
                intercepted =  true ;
            }
                   ...
             if  (!canceled && !intercepted) {
                 if  ( actionMasked == MotionEvent. ACTION_DOWN
                        || (split && actionMasked == MotionEvent. ACTION_POINTER_DOWN  )
                        || actionMasked == MotionEvent. ACTION_HOVER_MOVE  ) {
                    ...
                   final  int  childrenCount =  mChildrenCount ;
                     if  (newTouchTarget ==  null  && childrenCount != 0) {
                         final  float  x = ev.getX( actionIndex);
                         final  float  y = ev.getY( actionIndex);
                         // Scan children from front to back.
                         final  View[] children =  mChildren ;
                       final  boolean  customOrder = isChildrenDrawingOrderEnabled();//是否自定义了子view的顺序
                  for  (  int  i = childrenCount - 1; i >= 0; i--) {   //2.遍历子view,把事件传给获焦的子view
                        final  int  childIndex = customOrder ?
                                    getChildDrawingOrder( childrenCount, i) : i;
                             final  View child = children[childIndex];
                             if  (! canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child,  null )) {
                              //如果子view不接收pointer event或超出其区域,直接找下一个
                                 continue ;
                            }
                               newTouchTarget = getTouchTarget(child);//通过getTouchTarget去查找View是否在TouchTarget中了。
                             if  (newTouchTarget !=  null ) {
                                                  // 若子View处于touch目标中,同时已经接收了touch事件,则为器增加新的touch点??
                                 // 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 ;
                            }
                       if  (dispatchTransformedTouchEvent(ev,  false , child, idBitsToAssign)) {
                         //分发给子View,看是否处理了这个事件
                         //把MotionEvent的点坐标转换到子View的坐标系中,为ViewGroup创建一个新TouchTarget,TouchTarget包含了子View
                                 mLastTouchDownTime  = ev.getDownTime();
                                 mLastTouchDownIndex  = childIndex;
                                 mLastTouchDownX  = ev.getX();
                                 mLastTouchDownY  = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget =  true ;
                                 break ;
                            }
                         }
                    }
                           // 没有发现接收event的子View,把Touch点赋给最早添加到TouchTarget链中的对象  
                     if  (newTouchTarget ==  null  &&  mFirstTouchTarget  !=  null ) {
                        newTouchTarget =  mFirstTouchTarget ;
                         while  (newTouchTarget.  next  !=  null ) {
                            newTouchTarget = newTouchTarget.  next ;
                        }
                        newTouchTarget.  pointerIdBits  |= idBitsToAssign;
                    }
                }
           }

             // 分发到触摸对象Dispatch to touch targets.
             if  (  mFirstTouchTarget  ==  null ) {
                 //没有触摸的子view,就把自己当成一般view,自己处理
                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) {
                        handled =  true ;
                    }  else  {
                         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;
                }
            }
                    // 若在触摸点发生了up或cancel,则更新TouchTarget链表
             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;
    }
      
  /**如果子view不处理事件,就交给viewgroup自己处理
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
     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 ) {
                handled =  super .dispatchTouchEvent(event);//如果没有子View处理,就自己处理
            }  else  {
                handled = child.dispatchTouchEvent(event);//分发给子View处理
            }
            event.setAction(oldAction);
             return  handled;
        }

         // Calculate the number of pointers to deliver.
         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.
         if  (newPointerIdBits == 0) {
             return  false ;
        }

         // If the number of pointers is the same and we don't need to perform any fancy
         // irreversible transformations, then we can reuse the motion event for this
         // dispatch as long as we are careful to revert any changes we make.
         // Otherwise we need to make a copy.
         final  MotionEvent transformedEvent;
         if  (newPointerIdBits == oldPointerIdBits) {
             if  (child ==  null  || child.hasIdentityMatrix()) {
                 if  (child ==  null ) {
                    handled =  super .dispatchTouchEvent(event);
                }  else  {
                     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.
         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());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

         // Done.
        transformedEvent.recycle();
         return  handled;
    }
  public  boolean  onInterceptTouchEvent (MotionEvent ev) {//自定义ViewGroup重载此方法
         return  false ;
    }

}

------------------------------------------------------------------------------------------------

class View{

    public  boolean  dispatchTouchEvent(MotionEvent event) {
         ...

         if  (onFilterTouchEventForSecurity(event)) {
             //noinspection SimplifiableIfStatement
            ListenerInfo li =  mListenerInfo ;
             if  (li !=  null  && li.  mOnTouchListener  !=  null  && (  mViewFlags  &  ENABLED_MASK ) ==  ENABLED
                    && li.  mOnTouchListener .onTouch(  this , event)) {//如果设置了OnTouchListener就调onTouch
                 return  true ;
            }

             if  (onTouchEvent(event)) {//如果没有,就发给onTouchEvent
                 return  true ;
            }
        }

        ...
         return  false ;
    }

     public  void  setOnTouchListener (OnTouchListener l) {
        getListenerInfo().  mOnTouchListener  = l;
    }

   public  boolean  onTouchEvent(MotionEvent event) {//自定义view重载此方法
         final  int  viewFlags =  mViewFlags ;
        ...
       //如果是disable的view,同时是 clickable或long_clickable的,会消费事件,但不处理
         if  ((viewFlags &  ENABLED_MASK ) ==  DISABLED ) {
             if  (event.getAction() == MotionEvent.  ACTION_UP  && ( mPrivateFlags  &  PFLAG_PRESSED  ) != 0) {
                setPressed(  false );
            }
             // A disabled view that is clickable still consumes the touch
             // events, it just doesn't respond to them.
             return  (((viewFlags &  CLICKABLE ) ==  CLICKABLE  ||
                    (viewFlags &  LONG_CLICKABLE ) ==  LONG_CLICKABLE ));
        }
        //如果是enable的view,触摸区域发生变化了(不是view实际大小),就在设置 setTouchDelegate(TouchDelegate
      //delegate)来处理
         if  ( mTouchDelegate  !=  null ) {
             if  (  mTouchDelegate .onTouchEvent(event)) {
                 return  true ;
            }
        }
    // 如果是enable的view,同时是clickable或long_clickable的,处理事件
         if  (((viewFlags &  CLICKABLE ) ==  CLICKABLE  ||
                (viewFlags &  LONG_CLICKABLE ) ==  LONG_CLICKABLE )) {//不是clickable返回false
             switch  (event.getAction()) {
                         // ACTION_DOWN:如果不处理,返回false,事件会向上回传。只有接收了down事件才会产生后续事件
                         // ACTION_MOVE:不回传,
                         // ACTION_CANCLE:不回传,在ACTION_DOWN事件之后,后续事件被父控件拦截,此控件就会收到cancle事件
                 case  MotionEvent.  ACTION_UP :
                     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.
                         boolean  focusTaken =  false ;
                         if  (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                         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 );
                       }

                         if  (!  mHasPerformedLongPress ) {
                             // 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 )) {
                                    performClick(); //处理clikable事件
                                }
                            }
                        }

                         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();
                    }
                     break ;

                 case  MotionEvent.  ACTION_DOWN :
                     mHasPerformedLongPress  =  false ;

                     if  (performButtonActionOnTouchDown(event)) {
                         break ;
                    }

                     // Walk up the hierarchy to determine if we're inside a scrolling container.
                     boolean  isInScrollingContainer = isInScrollingContainer();

                     // For views inside a scrolling container, delay the pressed feedback for
                     // a short period in case this is a scroll.
                     if  (isInScrollingContainer) {
                         mPrivateFlags  |=  PFLAG_PREPRESSED ;
                         if  (  mPendingCheckForTap  ==  null ) {
                             mPendingCheckForTap  =  new  CheckForTap();
                        }
                        postDelayed(  mPendingCheckForTap , ViewConfiguration.getTapTimeout());
                    }  else  {
                         // Not inside a scrolling container, so show the feedback right away
                        // refreshDrawableState()--> drawableStateChanged()-->
                        // Drawable. setState(getDrawableState()) --> onStateChange(  int [] stateSet)
                        setPressed(  true );
                        checkForLongClick(0);
                    }
                     break ;

                 case  MotionEvent.  ACTION_CANCEL :
                    setPressed(  false );
                    removeTapCallback();
                    removeLongPressCallback();
                     break ;

                 case  MotionEvent.  ACTION_MOVE :
                     final  int  x = (  int ) event.getX();
                     final  int  y = (  int ) event.getY();
                       //当手指在View上面滑动超过View的边界
                     if  (!pointInView(x, y,  mTouchSlop )) {
                         // Outside button
                        removeTapCallback();
                         if  ((  mPrivateFlags  &  PFLAG_PRESSED ) != 0) {
                             // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(  false );
                        }
                    }
                     break ;
            }
             return  true ;
        }
         return  false ;
    }
}

经过以上分析,我们可以总结几点:
2.事件处理方式有:监听和回调。监听的处理逻辑可写在Activity中,便于调用context资源;回调的处理逻辑写在该view中,适合较为固定的处理流程。先处理监听,再处理回调。
3.一个clickable或者longClickable的View会永远消费Touch事件,enabled的会处理事件,disabled的只消耗不处理事件。
4.longClickable是在ACTION_DOWN中执行,要想执行长按事件该View必须是longClickable的,并且不能产生ACTION_MOVE;
View的clickable是在ACTION_UP中执行,想要执行点击事件的前提是消费了ACTION_DOWN和ACTION_MOVE,并且没有设置OnLongClickListener的情况下,如设置了OnLongClickListener的情况,则必须使onLongClick()返回false;
5.Touch事件的分发过程中,如果消费了ACTION_DOWN,而在分发ACTION_MOVE的时候,某个ViewGroup拦截了Touch事件,则会将ACTION_CANCEL分发给该ViewGroup下面的Touch到的View,然后将Touch事件交给ViewGroup处理,并返回true;
6.针对系统事件的监听:activity中的onConfigurationChanged()处理。


最后,总结一下整个窗口管理的类图:

图6 窗口管理系统类图


参考资料:
掌握android Touch 系统
第五篇 窗口管理机制之输入机制
Andriod 从源码的角度详解View,ViewGroup的Touch事件的分发机制
Android事件分发机制完全解析,带你从源码的角度彻底理解(上)(下)
android的窗口机制分析------事件处理
Android应用程序键盘(Keyboard)消息处理机制分析
Android Configuration change引发的问题及解决方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android中,View的按键派发流程可以分为三个阶段:事件捕获阶段、事件处理阶段和事件分发阶段。具体流程如下: 1. 事件捕获阶段:从根View开始,依次向下遍历其所有的子View,直到找到最深层的子View。在这个过程中,每个View都有机会处理该事件,即调用onKeyDown()、onKeyUp()等方法进行事件处理。 2. 事件处理阶段:当找到最深层的子View之后,事件开始进行处理。在这个阶段,View会根据自身的状态和属性来处理该事件,例如,判断是否处于可用状态、是否需要获取焦点等。 3. 事件分发阶段:当View处理完该事件之后,事件会根据事件分发规则,向上传递给父View进行处理。如果父View需要处理该事件,则继续进行事件捕获和事件处理阶段;如果不需要处理,则事件传递到下一个父View进行处理,直到传递到根View,或者事件被某个View消费掉。 需要注意的是,在事件分发阶段,View可以通过返回值来控制事件是否被消费。如果View处理了该事件,并认为该事件不需要再传递给下一个View,可以返回true,表示该事件已被消费;如果View没有处理该事件,或者认为该事件需要继续传递给下一个View,可以返回false,表示该事件需要继续传递。 总之,View的按键派发流程是一个非常复杂的过程,需要开发者深入理解和掌握。只有理解了该流程,才能正确地处理按键事件,提升应用程序的交互性和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值