View的事件分发机制,你想要的都在里面

重新认识LayoutInflater


LayoutInflater inflater = LayoutInflater.from(this); //简写
//另外一种写法
LayoutInflater  inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	inflater.inflate(); //开始分析
  1. inflater.inflate(); //开始分析
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")");
    }

    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot); 
    } finally {
        parser.close();
    }
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  ...
    //使用pull解析解析XML文件
    
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //查找并且添加视图
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml 找根视图
                        //得到一个跟节点 通过反射创建一个视图 返回一个View
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    ...
                    // Inflate all children under temp against its context. 子节点
                      //这里去查找并且添加子视图
                    rInflateChildren(parser, temp, attrs, true);

                    ...
                }
    ...
}

2.layoutinfalter如何查找添加子视图的呢, 看下面

 void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
       ...
            if (TAG_REQUEST_FOCUS.equals(name)) {
               ...//pull解析子节点
            } else {
                //通过递归方法查找下一个子视图 并且返回来
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                //继续执行递归查找
                rInflateChildren(parser, view, attrs, true);
                //m每次递归都会把当前视图树添加到ViewGroup中
                viewGroup.addView(view, params);
            }
        } 
    }

这样的话,把整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束


View的事件分发

引言: 最基础的事件分发

//一个点击事件:
view.setOnClickLisenter(new View.onClickLisenter){
  @override
    public void onClick(View view){
    log.i("MainActivity","点击事件")
  }
}
//一个触摸事件
view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
              	//返回值表示是否消费事件,即是否需要往下一个View传递
                return false;
            }
        });

首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法, 比如点击一个Button的时候, 会调用button.dispatchTouchEvent,但是button里面没有这个方法,继续找它的父View,Text也没有,最后在View中找到

2019-04-08 15:57:01.579 4448-4448/com.sincerity.interviewdemo D/admin: dispatchTouchEvent-up
2019-04-08 15:57:01.579 4448-4448/com.sincerity.interviewdemo D/admin: OnTouchListener-up
2019-04-08 15:57:01.579 4448-4448/com.sincerity.interviewdemo D/admin: onTouchEvent-up
2019-04-08 15:57:01.589 4448-4448/com.sincerity.interviewdemo D/admin: 点击事件
View的dispatchTouchEvent
 					//存放事件信息
				ListenerInfo li = mListenerInfo;
					//有其他事件&触摸事件不为空&判断当前点击的控件是否是enable & mOnTouchListener中返回true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
						
            if (!result && onTouchEvent(event)) {
                result = true;
            }

得出结论: 最先执行的事件 1.dispatchTouchEvent 2.OnTouchListener 3.onTouchEvent 4. 点击事件

如果在1初返回True 表示在1处已经消费事件,234事件都会被拦截,不会被执行

对应事件就是onTouchLisenter事件

View的onTouchEvent
  if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                      	//不是点击事件移除这些监听
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                //得到View的按下状态
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        if (prepressed) {
                            setPressed(true, x, y);
                        }
												//不是长按或者忽略事件
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                //分析Down事件 判断当前事件是否是点击事件还是长按时间mHasPerformedLongPress 
                //表示是否长按状态
                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;
                如果不是点击事件,判断是否是长按时间
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }
                //单独处理Button的点击事件
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                //处理滚动布局的点击事件
                    boolean isInScrollingContainer = isInScrollingContainer();
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;
                //
                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                      //拿到触摸点的x y的坐标
                        drawableHotspotChanged(x, y);
                    }
                //判断触摸点是否在View的范围之内
                    if (!pointInView(x, y, mTouchSlop)) {
                      //如果不在view之内,移除点击和长按的检查,
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                      //标记设置为down事件
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }
            return true;
        }
        return false;
    }

View设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;

View没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;

对应事件就是View.setOnClickLisenter事件

  • 如果想扩大点击事件的范围 可以使用TouchDelegate()

ViewGroup的事件分发

2019-04-08 17:02:20.628 5479-5479/com.sincerity.interviewdemo D/admin: dispatchTouchEvent-up
2019-04-08 17:02:20.628 5479-5479/com.sincerity.interviewdemo D/admin: onInterceptTouchEvent-up
2019-04-08 17:02:20.628 5479-5479/com.sincerity.interviewdemo D/admin: dispatchTouchEvent-up
2019-04-08 17:02:20.628 5479-5479/com.sincerity.interviewdemo D/admin: View OnTouchListener-up
2019-04-08 17:02:20.628 5479-5479/com.sincerity.interviewdemo D/admin: onTouchEvent-up
2019-04-08 17:02:20.636 5479-5479/com.sincerity.interviewdemo D/admin: 点击事件

从上可以得出一个结论 : ViewGroup的事件执行顺序

dispatchTouchEvent -->onInterceptTouchEvent–>dispatchTouchEvent -->OnTouchListener–onTouchEvent–>点击事件

ViewGroup#dispatchTouchEvent
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    
     			boolean handled = false;
         if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
               //清除状态,防止上一个触摸事件未注销,并且重置ViewGroup的状态
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
						//检查拦截状态
     //ViewGroup在如下两种情况下会判断是否拦截当前事件:事件类型为down或者mFirstTouchTarget != null。
           //当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget会被赋值也就是                        mFirstTouchTarget != null。
            // 这样当move事件和up事件到来时,并且事件已经被分发下去,那么onInterceptTouchEvent这个方法将不会再被调用。
            //所以当前ViewGroup拦截事件之后就不会再次调用onInterceptTouchEvent方法;
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }        
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }
            // 检查是否取消
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
             // 如果事件未被取消且未被拦截,如果拦截事件会将intercepted置为true;
           //不取消不拦截的情况
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                    removePointersFromTouchTargets(idBitsToAssign);
									//开始处理子View
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 遍历ViewGroup的所有子元素,然后判断子元素是否能够收到点击事件
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
												//判断坐标是否在子view中 
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                               the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            resetCancelNextUpFlag(child);
                            // 如果某个子元素满足条件,那么事件就会传递给它处理,
                            // dispatchTransformedTouchEvent这个方法实际上就是调用子元素的dispatchTouchEvent方法,
                            // 如果子元素仍然是一个ViewGroup,则递归调用重复此过程。
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 子View在其边界范围内接收事件
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                               // 如果子元素的dispatchTouchEvent返回true,表示子元素已经处理完事件,
                                // 那么mFirstTouchTarget就会被赋值同时跳出for循环。
                                // mFirstTouchTarget的赋值在addTouchTarget内部完成
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
           	// 如果遍历完所有的子元素事件没有被合适处理,有两种情况:
            // 1. ViewGroup没有子元素
            // 2. 子元素处理了点击事件,但是dispatchTouchEvent返回false
            // 这时ViewGroup会自己处理点击事件。
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
               //最终会调用ViewGroup自身的onTouchEvent来处理事件;
                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;
						//该方法最终会调用子元素的dispatchTouchEvent,传给给子元素来处理事件    
                        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事件到来,重置状态,例如将处理该事件的子view mFirstTouchTarget置为null;   
            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;
  • 如果onInterceptTouchEvent返回true,表示ViewGroup将拦截事件,会将intercepted设置true,这样就不会遍历子view寻找事件接受者;这样mFirstTouchTarget为null,会调用dispatchTransformedTouchEvent(ev, canceled, null,
    TouchTarget.ALL_POINTER_IDS);
  • 当子元素处理事件时会调用addTouchTarget();
  • 如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
  • 可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
  • 子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
  • 父View可以通过onInterceptTouchEvent来拦截事件,但是如果父view不拦截down事件,子view如果调用requestDisallowInterceptTouchEvent方法,那么即使父View在move和up的时候return true,也不会将事件拦截掉,也只会调用子view的onTouchEvent;当面对滑动冲突时,我们可以考虑通过requestDisallowInterceptTouchEvent设置FLAG_DISALLOW_INTERCEPT标志位来解决滑动冲突;如果父View拦截Down事件,那么子View将不会收到事件;
  • 滑动冲突的理论基础是事件分发机制,所以熟悉事件分发机制有助于我们解决滑动冲突相关问题;
拦截事件

复写ViewGroup的onInterceptTouchEvent事件

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("admin", "onInterceptTouchEvent-down");
              //拦截就返回true
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("admin", "onInterceptTouchEvent-move");
              //拦截就返回true
                return true;
            case MotionEvent.ACTION_UP:
                Log.d("admin", "onInterceptTouchEvent-up");
            //拦截就返回true
                return true;
        }
      //这里返回true表示拦截所有事件
        return super.onInterceptTouchEvent(event);
    }
如何不拦截
@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
      //下面一行表示不被拦截,前提就是ViewGroup的Down事件不被拦截
        getParent().requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d("admin", "dispatchTouchEvent-down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d("admin", "dispatchTouchEvent-move");
                break;
            case MotionEvent.ACTION_UP:
                Log.d("admin", "dispatchTouchEvent-up");
                break;
        }
        return super.dispatchTouchEvent(event);
    }
如果没有合适的子View
  • 如果所有子元素都没有处理事件,这里包含两种情况,第一ViewGroup没有子元素第二子元素处理了点击事件,但是在dispatchTouchEvent中返回false,一般是因为子元素在onTouchEvent返回false,这两种情况ViewGroup会自己处理点击事件
1、ACTION_DOWN的时候,子View.dispatchTouchEvent(ev)返回的为false ;
  • 在child.dispatchTouchEvent(ev)返回true了,才会认为找到了能够处理当前事件的View,其实ViewGroup也是一个View的子类,如果没有找到能够处理该事件的子View,或者干脆就没有子View;那么,它作为一个View,就相当于View的事件转发了~~直接super.dispatchTouchEvent(ev);
2. 那么什么时候子View.dispatchTouchEvent(ev)返回的为true
  • 你会发现只要子View支持点击或者长按事件一定返回true
总结
  • 如果ViewGroup能够找到子View ,那么就会把事件分发给子View去执行,自己的OnTouchEvent事件不会触发
  • 如果想拦截事件,则需要ViewGroup重写onInterceptTouchEvent事件,拦截子View的事件,并且触发自己的OnTouchEvent去处理事件
  • 子View可以重写ondispatchEvent事件通过getparent.requestDisallowInterceptTouchEvent(true)来阻止父View对move或up事件的拦截, 如果ViewGroup拦截了down事件,则不会执行子View的dispatchEvent事件.

Activity的事件分发

dispatchTouchevent
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          //空方法,主要实现屏保功能,,并且当此 Activity 在栈顶的时候,触屏点击 Home、Back、Recent 键等	            //都会触发这个方法
            onUserInteraction();
        }
  			//getWindow得到Window对象PhoneWindow是window的实现类,
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
//PhoneWindow.java
 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
      //调用DecorView的superDispatchTouchEvent
        return mDecor.superDispatchTouchEvent(event);
    }
//DecorView是一个FrameLayout,而FrameLayout又是ViewGrop的子类
public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
//viewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
  //调用ViewGroup的事件分发
  //里面循环去查找子View
  //下面查看view的dispatchtouchEvent事件
}
//View的dispatchtouchEvent()
if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //1. view是否是enable 2.事件不为空 2. 如果是touchListener必须覆盖ontouch方法
            
//onTouch方法
  • Activity的事件分发示意图

    Activity的事件分发示意图.png

  • ViewGroup的事件分发示意图

    ViewGroup的事件分发示意图.png

  • View的事件分发示意图

    View的事件分发示意图.png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值