王学岗高级UI9、10————事件分发机制

事件分发的流程:
被分发的对象是哪些?被分发的对象是用户触摸屏幕而产生的点击事件,事件主要包括:按下、滑动、抬起和取消。这些事件被封装成MotionEvent对象。该对象中的主要事件如下:
在这里插入图片描述
在这里插入图片描述

事件传递的顺序为:Activity -> Window ->DecorView(当前界面的底层容器)。一个点击操作要是没有被Activity下的任何View处理,即顶层DecorView的dispatchTouchEvent()方法返回false的话,则Activity的onTouchEvent()方法会被调用。
我们下面在源码中追踪下。
当我们点击手机屏幕的时候,硬件会通知软件,软件底层程序(C/C++)会调用java层Activity的dispatchTouchEvent(MotionEvent ev)方法。

           public boolean dispatchTouchEvent(MotionEvent ev) {
                 //如果是down,说明是一个新的事件
3398          if (ev.getAction() == MotionEvent.ACTION_DOWN) {
3399              onUserInteraction();
3400          }
               //调用了PhoneWindow的superDispatchTouchEvent()方法,
               //把事件从Activity分发到DecorView
               //如果找不到消费当前事件的View,getWindow().superDispatchTouchEvent(ev)会返回false
3401          if (getWindow().superDispatchTouchEvent(ev)) {
3402              return true;
3403          }
              //返回Activity的onTouchEvent
3404          return onTouchEvent(ev);
3405      }
3406  

我们看下 onUserInteraction();

  public void onContentChanged() {
    }

这是一个空房法,开发者可以实现这个方法,屏幕点击按下的时候,可以再这里增加逻辑
getWindow()这里返回的是一个Window对象。在Window对象中superDispatchTouchEvent(ev)方法是一个抽象方法,在其子类PhoneWindow中实现。我们看下PhoneWindow中superDispatchTouchEvent(ev)方法。


1828      @Override
1829      public boolean superDispatchTouchEvent(MotionEvent event) {
1830          return mDecor.superDispatchTouchEvent(event);
1831      }

mDecor是DecorView对象
我们进入DecorView对象中


439      public boolean superDispatchTouchEvent(MotionEvent event) {
440          return super.dispatchTouchEvent(event);
441      }

DecorView的父类是ViewGroup,所以 super.dispatchTouchEvent(event)调用的是ViewGroup里的方法。
我们暂且不去管ViewGroup,回过头来看看Activity的dispatchTouchEvent的返回值onTouchEvent(ev);


3401          if (getWindow().superDispatchTouchEvent(ev)) {
3402              return true;
3403          }

假设这段代码不执行,即getWindow().superDispatchTouchEvent(ev)返回为false;
就会执行 onTouchEvent(ev)

 public boolean onTouchEvent(MotionEvent event) {
                  //点击范围是否超过了window边界,比如Dialog类型的Activity,点击外面dialog消失
3143          if (mWindow.shouldCloseOnTouch(this, event)) {
3144              finish();
                       //表示事件被当前Activity消耗掉了
3145              return true;
3146          }
3147  
3148          return false;
3149      }
3150  

我们现在看下ViewGroup的dispatchTouchEvent(MotionEvent ev);


2541      @Override
2542      public boolean dispatchTouchEvent(MotionEvent ev) {
                //辅助功能,跨进程调用,与我们的事件分发作用不大
2543          if (mInputEventConsistencyVerifier != null) {
2544              mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
2545          }
2546  
2547          // If the event targets the accessibility focused view and this is it, start
2548          // normal event dispatch. Maybe a descendant is what will handle the click.
2549          if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
2550              ev.setTargetAccessibilityFocus(false);
2551          }
2552          //本方法的返回值,表示ViewGroup是否消耗了此事件
2553          boolean handled = false;
                  //检测该事件是否在安全范围内
2554          if (onFilterTouchEventForSecurity(ev)) {
2555              final int action = ev.getAction();
2556              final int actionMasked = action & MotionEvent.ACTION_MASK;
2557  
2558              // Handle an initial down.处理最初的按下事件
2559              if (actionMasked == MotionEvent.ACTION_DOWN) {
                         /**
                         当开始一个新的触摸手势时,扔掉所有以前的状态,框架可能由于应用程序的切换,ANR,
                         或其它一些状态更改而放弃了上一个手势的UP或cancel事件
                         */
2560                  // Throw away all previous state when starting a new touch gesture.
2561                  // The framework may have dropped the up or cancel event for the previous gesture
2562                  // due to an app switch, ANR, or some other state change.
                          //取消并清除所有触摸目标
2563                  cancelAndClearTouchTargets(ev);
2564                  resetTouchState();//重置所有触摸状态,为新循环做准备
2565              }
2566  
2567              // Check for interception.事件拦截的检查
2568              final boolean intercepted;
2569              if (actionMasked == MotionEvent.ACTION_DOWN
2570                      || mFirstTouchTarget != null) {
2571                  final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
2572                  if (!disallowIntercept) {//允许拦截,ViewGroup要消耗这个事件
2573                      intercepted = onInterceptTouchEvent(ev);
2574                      ev.setAction(action); // restore action in case it was changed
2575                  } else {
2576                      intercepted = false;
2577                  }
2578              } else {
2579                  // There are no touch targets and this action is not an initial down
2580                  // so this view group continues to intercept touches.
2581                  intercepted = true;
2582              }
2583  
2584              // If intercepted, start normal event dispatch. Also if there is already
2585              // a view that is handling the gesture, do normal event dispatch.
2586              if (intercepted || mFirstTouchTarget != null) {
2587                  ev.setTargetAccessibilityFocus(false);
2588              }
2589  
2590              // Check for cancelation.
2591              final boolean canceled = resetCancelNextUpFlag(this)
2592                      || actionMasked == MotionEvent.ACTION_CANCEL;
2593  
2594              // Update list of touch targets for pointer down, if needed.
2595              final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
2596              TouchTarget newTouchTarget = null;
2597              boolean alreadyDispatchedToNewTouchTarget = false;
2598              if (!canceled && !intercepted) {
2599  
2600                  // If the event is targeting accessibility focus we give it to the
2601                  // view that has accessibility focus and if it does not handle it
2602                  // we clear the flag and dispatch the event to all children as usual.
2603                  // We are looking up the accessibility focused host to avoid keeping
2604                  // state since these events are very rare.
2605                  View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
2606                          ? findChildWithAccessibilityFocus() : null;
2607  
2608                  if (actionMasked == MotionEvent.ACTION_DOWN
2609                          || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
2610                          || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
2611                      final int actionIndex = ev.getActionIndex(); // always 0 for down
2612                      final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
2613                              : TouchTarget.ALL_POINTER_IDS;
2614  
2615                      // Clean up earlier touch targets for this pointer id in case they
2616                      // have become out of sync.
2617                      removePointersFromTouchTargets(idBitsToAssign);
2618  
2619                      final int childrenCount = mChildrenCount;
2620                      if (newTouchTarget == null && childrenCount != 0) {
2621                          final float x = ev.getX(actionIndex);
2622                          final float y = ev.getY(actionIndex);
2623                          // Find a child that can receive the event.
2624                          // Scan children from front to back.
2625                          final ArrayList<View> preorderedList = buildTouchDispatchChildList();
2626                          final boolean customOrder = preorderedList == null
2627                                  && isChildrenDrawingOrderEnabled();
2628                          final View[] children = mChildren;
2629                          for (int i = childrenCount - 1; i >= 0; i--) {
2630                              final int childIndex = getAndVerifyPreorderedIndex(
2631                                      childrenCount, i, customOrder);
2632                              final View child = getAndVerifyPreorderedView(
2633                                      preorderedList, children, childIndex);
2634  
2635                              // If there is a view that has accessibility focus we want it
2636                              // to get the event first and if not handled we will perform a
2637                              // normal dispatch. We may do a double iteration but this is
2638                              // safer given the timeframe.
2639                              if (childWithAccessibilityFocus != null) {
2640                                  if (childWithAccessibilityFocus != child) {
2641                                      continue;
2642                                  }
2643                                  childWithAccessibilityFocus = null;
2644                                  i = childrenCount - 1;
2645                              }
2646  
2647                              if (!canViewReceivePointerEvents(child)
2648                                      || !isTransformedTouchPointInView(x, y, child, null)) {
2649                                  ev.setTargetAccessibilityFocus(false);
2650                                  continue;
2651                              }
2652  
2653                              newTouchTarget = getTouchTarget(child);
2654                              if (newTouchTarget != null) {
2655                                  // Child is already receiving touch within its bounds.
2656                                  // Give it the new pointer in addition to the ones it is handling.
2657                                  newTouchTarget.pointerIdBits |= idBitsToAssign;
2658                                  break;
2659                              }
2660  
2661                              resetCancelNextUpFlag(child);
2662                              if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
2663                                  // Child wants to receive touch within its bounds.
2664                                  mLastTouchDownTime = ev.getDownTime();
2665                                  if (preorderedList != null) {
2666                                      // childIndex points into presorted list, find original index
2667                                      for (int j = 0; j < childrenCount; j++) {
2668                                          if (children[childIndex] == mChildren[j]) {
2669                                              mLastTouchDownIndex = j;
2670                                              break;
2671                                          }
2672                                      }
2673                                  } else {
2674                                      mLastTouchDownIndex = childIndex;
2675                                  }
2676                                  mLastTouchDownX = ev.getX();
2677                                  mLastTouchDownY = ev.getY();
2678                                  newTouchTarget = addTouchTarget(child, idBitsToAssign);
2679                                  alreadyDispatchedToNewTouchTarget = true;
2680                                  break;
2681                              }
2682  
2683                              // The accessibility focus didn't handle the event, so clear
2684                              // the flag and do a normal dispatch to all children.
2685                              ev.setTargetAccessibilityFocus(false);
2686                          }
2687                          if (preorderedList != null) preorderedList.clear();
2688                      }
2689  
2690                      if (newTouchTarget == null && mFirstTouchTarget != null) {
2691                          // Did not find a child to receive the event.
2692                          // Assign the pointer to the least recently added target.
2693                          newTouchTarget = mFirstTouchTarget;
2694                          while (newTouchTarget.next != null) {
2695                              newTouchTarget = newTouchTarget.next;
2696                          }
2697                          newTouchTarget.pointerIdBits |= idBitsToAssign;
2698                      }
2699                  }
2700              }
2701  
2702              // Dispatch to touch targets.//触摸目标
2703              if (mFirstTouchTarget == null) {
2704                  // No touch targets so treat this as an ordinary view.没有触摸目标,将此视图视为普通视图
2705                  handled = dispatchTransformedTouchEvent(ev, canceled, null,
2706                          TouchTarget.ALL_POINTER_IDS);
2707              } else {
2708                  // Dispatch to touch targets, excluding the new touch target if we already
2709                  // dispatched to it.  Cancel touch targets if necessary.
2710                  TouchTarget predecessor = null;
2711                  TouchTarget target = mFirstTouchTarget;
2712                  while (target != null) {
2713                      final TouchTarget next = target.next;
2714                      if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
2715                          handled = true;
2716                      } else {
2717                          final boolean cancelChild = resetCancelNextUpFlag(target.child)
2718                                  || intercepted;
2719                          if (dispatchTransformedTouchEvent(ev, cancelChild,
2720                                  target.child, target.pointerIdBits)) {
2721                              handled = true;
2722                          }
2723                          if (cancelChild) {
2724                              if (predecessor == null) {
2725                                  mFirstTouchTarget = next;
2726                              } else {
2727                                  predecessor.next = next;
2728                              }
2729                              target.recycle();
2730                              target = next;
2731                              continue;
2732                          }
2733                      }
2734                      predecessor = target;
2735                      target = next;
2736                  }
2737              }
2738  
2739              // Update list of touch targets for pointer up or cancel, if needed.
2740              if (canceled
2741                      || actionMasked == MotionEvent.ACTION_UP
2742                      || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
2743                  resetTouchState();
2744              } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
2745                  final int actionIndex = ev.getActionIndex();
2746                  final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
2747                  removePointersFromTouchTargets(idBitsToRemove);
2748              }
2749          }
2750  
2751          if (!handled && mInputEventConsistencyVerifier != null) {
2752              mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
2753          }
2754          return handled;
2755      }

拦截方法只有ViewGroup有,它有两种用法,一种是自定义一种容器,重写

   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

一个是在其它类里面调用该类的

  RelativeLayout relativeLayout = new RelativeLayout(this);
        relativeLayout.requestDisallowInterceptTouchEvent(false);//默认是false

再看下View的这个方法

  public boolean dispatchTouchEvent(MotionEvent event) {
 ……………………………………
12507              if (li != null && li.mOnTouchListener != null
12508                      && (mViewFlags & ENABLED_MASK) == ENABLED
                                   //调用外部设置的监听
12509                      && li.mOnTouchListener.onTouch(this, event)) {
12510                  result = true;
12511              }
12512              //调用View本身的onTouchEcent
12513              if (!result && onTouchEvent(event)) {
12514                  result = true;
12515              }
12516          }
12517  
12518          if (!result && mInputEventConsistencyVerifier != null) {
12519              mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
12520          }
12521  
12522          // Clean up after nested scrolls if this is the end of a gesture;
12523          // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
12524          // of the gesture.
12525          if (actionMasked == MotionEvent.ACTION_UP ||
12526                  actionMasked == MotionEvent.ACTION_CANCEL ||
12527                  (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
12528              stopNestedScroll();
12529          }
12530  
12531          return result;
12532      }

这个mOnTouchListener 是我们自己设置的监听,就是下面这个方法

  RelativeLayout relativeLayout = new RelativeLayout(this);
        relativeLayout.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                return false;
            }
        });

如果没有设置这个监听,会调用它自己的onTouchEvent方法。


12513              if (!result && onTouchEvent(event)) {
12514                  result = true;
12515              }
12516          }

我们看下onTouchEvent(event)方法

 public boolean onTouchEvent(MotionEvent event) {
13719          final float x = event.getX();
13720          final float y = event.getY();
13721          final int viewFlags = mViewFlags;
13722          final int action = event.getAction();
13723  
13724          
13742  
13743          if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
13744              switch (action) {
13745                  case MotionEvent.ACTION_UP:
13746                     
13757                  
13770                     
13799                                  
13820  
13821                    
13843                      break;
13844  
13845                  case MotionEvent.ACTION_CANCEL:
13846                 
13855                      break;
13856  
13857                  case MotionEvent.ACTION_MOVE:
13858                 
13873                      break;
13874              }
13875  
13876              return true;
13877          }
13878  
13879          return false;
13880      }
13881  

在MotionEvent.ACTION_UP里面,存在我们的onClick事件。在MotionEvent.ACTION_DOWN中,存在我们的长按事件。
我们现在有下面这种情况,几个控件叠压在一起,我们点击它重合的地方,事件是如何处理的呢?
首先会遍历所有的子View,然后获取子View的Z值,根据z值进行处理。
我们最后总结下时间分发的流程
在这里插入图片描述

1,多指触控的相关API
在这里插入图片描述

在这里插入图片描述

2,事件冲突
事件冲突分为三种:
(1)外部滑动与内部滑动方向不一致,比如外部是上下滑动,内部是左右滑动
    当我们内部控件斜着滑动的时候,外部控件也可能出现滑动的情况。
(2)外部滑动与内部滑动方向一致,外部和内部都是上下或者都是左右滑动
(3)最外部是上下滑动,次外层是左右滑动,最内层又是上下滑动
事件拦截的策略

外部拦截法:所谓外部拦截,是指点击事件都先经过父容器的拦截处理,如果父容器需要就拦截,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法内部做相应的拦击即可。
在这里插入图片描述
内部拦截法:
是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器处理,这种方法和android的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,跟外部拦截法相比较为复杂。内部拦截法需要重写子元素的dispatchTouchEvent方法
在这里插入图片描述
网上很多自定义控件会造成滑动冲突,就是因为她没使用内部拦截方式去处理滑动事件冲突。以RecyclerView为例,我们ScrollView嵌套一个RecyclerView,即使我们不作任何处理,RecyclerView也可以很顺畅的滑动
我们看下以下布局

<?xml version="1.0" encoding="utf-8"?>
<com.example.shijianfenfa.ConScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.example.shijianfenfa.ConScrollView1
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="200dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/text"
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#f00"
                    android:text="TwoScrollView1" />

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#f00"
                    android:text="TwoScrollView1" />

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:background="#f00"
                    android:text="TwoScrollView1" />

            </LinearLayout>
        </com.example.shijianfenfa.ConScrollView1>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rel_view"
            android:layout_width="match_parent"
            android:layout_height="400dp"
            app:layoutManager="LinearLayoutManager"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:textColor="#f00"
                android:text="TwoScrollView" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:textColor="#f00"
                android:text="TwoScrollView" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="100dp"
                android:textColor="#f00"
                android:text="TwoScrollView" />

        </LinearLayout>

    </LinearLayout>
</com.example.shijianfenfa.ConScrollView>

如果我们不作任何处理的话,ConScrollView1(继承ScrollView)是无法滑动的。我们看下外部拦截法是如何处理的。

package com.example.shijianfenfa;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;

import androidx.annotation.RequiresApi;

public class ConScrollView extends ScrollView {

    private float mTouchSlop;
    private float downY;

    public ConScrollView(Context context) {
        super(context);
        init(context);
    }


    public ConScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ConScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public ConScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        //是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。如果小于这个距离就不触发移动控件,
        // 如viewpager就是用这个距离来判断用户是否翻页。
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }
    //外部拦截法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>down");//false
                downY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>move");//不作任何处理的话会是false false……true。移动一段时间后变为true
                float moveY = getY();
                if(Math.abs(moveY - downY)>mTouchSlop){
                    //外部容器不拦截,交给子控件处理。
                    return false;//如果返回为true,ConScrollView1无法移动
                }
                break;
            case MotionEvent.ACTION_UP:
                Log.i("zhang_xin", super.onInterceptTouchEvent(ev)+"===>up");//不打印输出
                break;
        }

        return super.onInterceptTouchEvent(ev);
    }
}

在这里插入图片描述
在这里插入图片描述
内部拦截法

package com.example.shijianfenfa;

import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
import android.widget.Toast;

import androidx.annotation.RequiresApi;

public class ConScrollView1 extends ScrollView {

    private float mTouchSlop;
    private float downY;

    public ConScrollView1(Context context) {
        super(context);
        init(context);
    }

    public ConScrollView1(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ConScrollView1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public ConScrollView1(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }
    //内部拦截法

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //请求父view不要拦截事件,保证子View能够接收到action_move
                downY = ev.getY();
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float moveY = ev.getY();
                if (Math.abs(moveY - downY) > mTouchSlop) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;


        }
        return super.dispatchTouchEvent(ev);
    }


}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值