NGUI的事件通知架构和源码剖析

NGUI的事件通知其实是由一个脚本UICamera来实现的,脚本的命名不是太好,其基本的原理很简单,在Update函数中检测用户输入,然后根据自己的策略分发到具体的物体。其定义了一些基本的通知回调函数,你可以查看具体的注释:

/// * OnHover (isOver) is sent when the mouse hovers over a collider or moves away.
/// * OnPress (isDown) is sent when a mouse button gets pressed on the collider.
/// * OnSelect (selected) is sent when a mouse button is first pressed on an object. Repeated presses won't result in an OnSelect(true).
/// * OnClick () is sent when a mouse is pressed and released on the same object.
///   UICamera.currentTouchID tells you which button was clicked.
/// * OnDoubleClick () is sent when the click happens twice within a fourth of a second.
///   UICamera.currentTouchID tells you which button was clicked.
/// 
/// * OnDragStart () is sent to a game object under the touch just before the OnDrag() notifications begin.
/// * OnDrag (delta) is sent to an object that's being dragged.
/// * OnDragOver (draggedObject) is sent to a game object when another object is dragged over its area.
/// * OnDragOut (draggedObject) is sent to a game object when another object is dragged out of its area.
/// * OnDragEnd () is sent to a dragged object when the drag event finishes.
/// 
/// * OnTooltip (show) is sent when the mouse hovers over a collider for some time without moving.
/// * OnScroll (float delta) is sent out when the mouse scroll wheel is moved.
/// * OnKey (KeyCode key) is sent when keyboard or controller input is used.
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

所以从字面上你就可以理解, 其提供了哪些事件通知,这些事件都是在在主线程中完成的。需要特别说明的是,NGUI有自己的事件通知,和MonoBehavior里面的函数OnMouseDown, OnMouseUp, OnMouseOver等消息处理函数重叠,所以只要我们使用了NGUI的处理框架以及NGUI的脚本,如UIButton,UISCrollView等,我们无需重载MonoBehavior的上述事件处理函数。 如果自己处理了,可能同一个用户输入会响应两次。

下面就简单介绍一下关键的函数或者结构体

  • 通知函数, Notify
    /// <summary>
    /// Generic notification function. Used in place of SendMessage to shorten the code and allow for more than one receiver.
    /// </summary>
    static public void Notify (GameObject go, string funcName, object obj)
    {
        if (mNotifying) return;
        mNotifying = true;

        if (NGUITools.GetActive(go))
        {
           // 基本的Unity的GameObject函数.
            go.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);

            if (mGenericHandler != null && mGenericHandler != go)
            {
                mGenericHandler.SendMessage(funcName, obj, SendMessageOptions.DontRequireReceiver);
            }
        }
        mNotifying = false;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

所以,消息通知是通过GameObject::SendMessage ()的方法来实现的,可以查看源码,等到funcName的名字都是上面的事件通知的名字,如: OnClick, OnHover, OnSelect等。

  • Raycast 帮助函数,如何从一个屏幕上的位置信息,找到点击,触摸,滑过的物体
    /// <summary>
    /// Returns the object under the specified position.
    /// </summary>
    static public bool Raycast (Vector3 inPos)
    {
        for (int i = 0; i < list.size; ++i)
        {
            // 当前的UI Camera
            UICamera cam = list.buffer[i];

            // Skip inactive scripts 
            if (!cam.enabled || !NGUITools.GetActive(cam.gameObject)) continue;

            // Convert to view space
            currentCamera = cam.cachedCamera;
             // 将屏幕的位置信息,转换成ViewPort的信息,viewport的空间在0, 1范围内
            Vector3 pos = currentCamera.ScreenToViewportPoint(inPos);
            if (float.IsNaN(pos.x) || float.IsNaN(pos.y)) continue;

            // If it's outside the camera's viewport, do nothing
            if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f) continue;

            // Cast a ray into the screen
            Ray ray = currentCamera.ScreenPointToRay(inPos);

            // Raycast into the screen
            int mask = currentCamera.cullingMask & (int)cam.eventReceiverMask;
            float dist = (cam.rangeDistance > 0f) ? cam.rangeDistance : currentCamera.farClipPlane - currentCamera.nearClipPlane;

            if (cam.eventType == EventType.World_3D)
            {
              ............
            }
            else if (cam.eventType == EventType.UI_3D)
            {
                // 获取当前的所有的RaycastHit的列表,所以在此,需要特别说明的是,任何的NGUI的控件都需要加上BoxColider物体。 disk和camera的距离,mask和层
                RaycastHit[] hits = Physics.RaycastAll(ray, dist, mask);

                if (hits.Length > 1)
                {
                    // 下面所有的代码获取了点击到的物体的列表, 放入到全局的静态变量mhits中
                    // 基本的结构单元为:
                    //  struct DepthEntry
                    //{
                    //  public int depth; // Gameobject 的深度信息
                    //  public RaycastHit hit; // RaycastHit信息,可以查看具体的unity文档
                    //  public Vector3 point; // hit物体的世界空间的地址
                    //  public GameObject go; // hit 物体
                    //}

                    for (int b = 0; b < hits.Length; ++b)
                    {
                        GameObject go = hits[b].collider.gameObject;
                        UIWidget w = go.GetComponent<UIWidget>();

                        if (w != null)
                        {
                            if (!w.isVisible) continue;
                            if (w.hitCheck != null && !w.hitCheck(hits[b].point)) continue;
                        }
                        else
                        {
                            UIRect rect = NGUITools.FindInParents<UIRect>(go);
                            if (rect != null && rect.finalAlpha < 0.001f) continue;
                        }

                        mHit.depth = NGUITools.CalculateRaycastDepth(go);

                        if (mHit.depth != int.MaxValue)
                        {
                            mHit.hit = hits[b];
                            mHit.point = hits[b].point;
                            mHit.go = hits[b].collider.gameObject;
                            mHits.Add(mHit);
                        }
                    }

                    // 按照深度升序排序,
                    mHits.Sort(delegate(DepthEntry r1, DepthEntry r2) { return r2.depth.CompareTo(r1.depth); });
                     // 找到depth最高的可视的物体为当前的接触的物体
                    for (int b = 0; b < mHits.size; ++b)
                    {
#if UNITY_FLASH
                        if (IsVisible(mHits.buffer[b]))
#else
                        if (IsVisible(ref mHits.buffer[b]))
#endif
                        {
                            lastHit = mHits[b].hit;
                            hoveredObject = mHits[b].go;
                            lastWorldPosition = mHits[b].point;
                            mHits.Clear();
                            return true;
                        }
                    }
                    mHits.Clear();
                }
                else if (hits.Length == 1)
                {
                ............
                }
            }
            else if (cam.eventType == EventType.World_2D)
            {
             ..............
            }
            else if (cam.eventType == EventType.UI_2D)
            {
             ..............
            }
        }
        return false;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • ControlScheme 记录当前的输入的类型
    public enum ControlScheme
    {
        Mouse,  // 鼠标事件
        Touch,  // 触摸事件
        Controller, // 控制器输入
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • MouseOrTouch 下面是记录鼠标事件和触摸事件的结构体,个人认为实现的不大合理,把鼠标和触摸需要的信息混合在一起了, NGUI在实现中也把touch,mouse事件合并进行了处理。
    /// <summary>
    /// Ambiguous mouse, touch, or controller event.
    /// </summary>

    public class MouseOrTouch
    {
        public Vector2 pos;             // Current position of the mouse or touch event
        public Vector2 lastPos;         // Previous position of the mouse or touch event
        public Vector2 delta;           // Delta since last update
        public Vector2 totalDelta;      // Delta since the event started being tracked

        public Camera pressedCam;       // Camera that the OnPress(true) was fired with

        public GameObject last;         // Last object under the touch or mouse
        public GameObject current;      // Current game object under the touch or mouse
        public GameObject pressed;      // Last game object to receive OnPress
        public GameObject dragged;      // Game object that's being dragged

        public float pressTime = 0f;    // When the touch event started
        public float clickTime = 0f;    // The last time a click event was sent out

        public ClickNotification clickNotification = ClickNotification.Always;
        public bool touchBegan = true;
        public bool pressStarted = false;
        public bool dragStarted = false;

        /// <summary>
        /// Delta time since the touch operation started.
        /// </summary>

        public float deltaTime { get { return touchBegan ? RealTime.time - pressTime : 0f; } }

        /// <summary>
        /// Returns whether this touch is currently over a UI element.
        /// </summary>
        public bool isOverUI
        {
            get
            {
                return current != null && current != fallThrough && NGUITools.FindInParents<UIRoot>(current) != null;
            }
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 处理输入的事件
    // 处理鼠标按下或者触摸按下
    void ProcessPress (bool pressed, float click, float drag)
    {
        // Send out the press message
        if (pressed)
        {
            if (mTooltip != null) ShowTooltip(false);

            currentTouch.pressStarted = true;
            // 全局的OnPress的事件通知,前面的物体被释放
            if (onPress != null && currentTouch.pressed)
                onPress(currentTouch.pressed, false);

            // OnPress 的事件通知, false,前面的物体
            Notify(currentTouch.pressed, "OnPress", false);

            currentTouch.pressed = currentTouch.current;
            currentTouch.dragged = currentTouch.current;
            currentTouch.clickNotification = ClickNotification.BasedOnDelta;
            currentTouch.totalDelta = Vector2.zero;
            currentTouch.dragStarted = false;

           // 全局的OnPress的事件通知,当前的物体被按下
            if (onPress != null && currentTouch.pressed)
                onPress(currentTouch.pressed, true);

           // OnPress 的事件通知, true,当前的物体
            Notify(currentTouch.pressed, "OnPress", true);

            // Update the selection
            if (currentTouch.pressed != mCurrentSelection)
            {
                if (mTooltip != null) ShowTooltip(false);
                currentScheme = ControlScheme.Touch;
                selectedObject = currentTouch.pressed;
            }
        }
        // 此处就是处理Drag的各项事务, 注意条件
        else if (currentTouch.pressed != null && (currentTouch.delta.sqrMagnitude != 0f || currentTouch.current != currentTouch.last))
        {
            // Keep track of the total movement
            currentTouch.totalDelta += currentTouch.delta;
            float mag = currentTouch.totalDelta.sqrMagnitude;
            bool justStarted = false;

            // If the drag process hasn't started yet but we've already moved off the object, start it immediately
            if (!currentTouch.dragStarted && currentTouch.last != currentTouch.current)
            {
                currentTouch.dragStarted = true;
                currentTouch.delta = currentTouch.totalDelta;

                // OnDragOver is sent for consistency, so that OnDragOut is always preceded by OnDragOver
                isDragging = true;
                // 通知OnDragStart 事件到全局函数和现在的物体
                if (onDragStart != null) onDragStart(currentTouch.dragged);
                Notify(currentTouch.dragged, "OnDragStart", null);

                // 通知OnDragOver 事件到全局函数和上次Drag的物体
                if (onDragOver != null) onDragOver(currentTouch.last, currentTouch.dragged);
                Notify(currentTouch.last, "OnDragOver", currentTouch.dragged);

                isDragging = false;
            }
            else if (!currentTouch.dragStarted && drag < mag)
            {
                // If the drag event has not yet started, see if we've dragged the touch far enough to start it
                justStarted = true;
                currentTouch.dragStarted = true;
                currentTouch.delta = currentTouch.totalDelta;
            }

            // If we're dragging the touch, send out drag events
            // 判断DragStarted 发送相关的事件,如
            if (currentTouch.dragStarted)
            {
                if (mTooltip != null) ShowTooltip(false);

                isDragging = true;
                bool isDisabled = (currentTouch.clickNotification == ClickNotification.None);

                if (justStarted)
                {
                    if (onDragStart != null) onDragStart(currentTouch.dragged);
                    Notify(currentTouch.dragged, "OnDragStart", null);

                    if (onDragOver != null) onDragOver(currentTouch.last, currentTouch.dragged);
                    Notify(currentTouch.current, "OnDragOver", currentTouch.dragged);
                }
                else if (currentTouch.last != currentTouch.current)
                {
                    if (onDragStart != null) onDragStart(currentTouch.dragged);
                    Notify(currentTouch.last, "OnDragOut", currentTouch.dragged);

                    if (onDragOver != null) onDragOver(currentTouch.last, currentTouch.dragged);
                    Notify(currentTouch.current, "OnDragOver", currentTouch.dragged);
                }

                if (onDrag != null) onDrag(currentTouch.dragged, currentTouch.delta);
                Notify(currentTouch.dragged, "OnDrag", currentTouch.delta);

                currentTouch.last = currentTouch.current;
                isDragging = false;

                if (isDisabled)
                {
                    // If the notification status has already been disabled, keep it as such
                    currentTouch.clickNotification = ClickNotification.None;
                }
                else if (currentTouch.clickNotification == ClickNotification.BasedOnDelta && click < mag)
                {
                    // We've dragged far enough to cancel the click
                    currentTouch.clickNotification = ClickNotification.None;
                }
            }
        }
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
/// <summary>
/// 处理Touch,和Mouse release事件。
/// </summary>
void ProcessRelease (bool isMouse, float drag)
{
    // Send out the unpress message
    currentTouch.pressStarted = false;
    if (mTooltip != null) ShowTooltip(false);

    if (currentTouch.pressed != null)
    {
        // If there was a drag event in progress, make sure OnDragOut gets sent
        if (currentTouch.dragStarted)
        {
            if (onDragOut != null) onDragOut(currentTouch.last, currentTouch.dragged);
            Notify(currentTouch.last, "OnDragOut", currentTouch.dragged);

            if (onDragEnd != null) onDragEnd(currentTouch.dragged);
            Notify(currentTouch.dragged, "OnDragEnd", null);
        }

        // Send the notification of a touch ending
        if (onPress != null) onPress(currentTouch.pressed, false);
        Notify(currentTouch.pressed, "OnPress", false);

        // Send a hover message to the object
        if (isMouse)
        {
            if (onHover != null) onHover(currentTouch.current, true);
            Notify(currentTouch.current, "OnHover", true);
        }
        mHover = currentTouch.current;

        // If the button/touch was released on the same object, consider it a click and select it
        if (currentTouch.dragged == currentTouch.current ||
            (currentScheme != ControlScheme.Controller &&
            currentTouch.clickNotification != ClickNotification.None &&
            currentTouch.totalDelta.sqrMagnitude < drag))
        {
            if (currentTouch.pressed != mCurrentSelection)
            {
                mNextSelection = null;
                mCurrentSelection = currentTouch.pressed;
                if (onSelect != null) onSelect(currentTouch.pressed, true);
                Notify(currentTouch.pressed, "OnSelect", true);
            }
            else
            {
                mNextSelection = null;
                mCurrentSelection = currentTouch.pressed;
            }

            // If the touch should consider clicks, send out an OnClick notification
            if (currentTouch.clickNotification != ClickNotification.None && currentTouch.pressed == currentTouch.current)
            {
                float time = RealTime.time;

                if (onClick != null) onClick(currentTouch.pressed);
                Notify(currentTouch.pressed, "OnClick", null);

                if (currentTouch.clickTime + 0.35f > time)
                {
                    if (onDoubleClick != null) onDoubleClick(currentTouch.pressed);
                    Notify(currentTouch.pressed, "OnDoubleClick", null);
                }
                currentTouch.clickTime = time;
            }
        }
        else if (currentTouch.dragStarted) // The button/touch was released on a different object
        {
            // Send a drop notification (for drag & drop)
            if (onDrop != null) onDrop(currentTouch.current, currentTouch.dragged);
            Notify(currentTouch.current, "OnDrop", currentTouch.dragged);
        }
    }
    currentTouch.dragStarted = false;
    currentTouch.pressed = null;
    currentTouch.dragged = null;
}
  • Update 函数,其处理UI 事件,并且发送给具体的物体和回调函数。
    void Update ()
    {
        // Only the first UI layer should be processing events
#if UNITY_EDITOR
        if (!Application.isPlaying || !handlesEvents) return;
#else
        if (!handlesEvents) return;
#endif
        current = this;

        // Process touch events first  
        // 处理触摸和鼠标事件,可以review源代码,都是复用了ProcessPress和ProcessRelease
        // 函数
        if (useTouch) ProcessTouches ();
        else if (useMouse) ProcessMouse();
        ..................
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

总结: 所有整个NGUI的事件处理通知都是由UICamera的update函数,分析当前帧的用户输入,然后发送时间给NGUI的UI控件,如UIButton,UIScrollView等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值