android4.4按键分析--触屏事件1

4.3         触屏事件

之前讲的是按键的整体处理流程,并以物理按键为例讲解了物理按键如何被activity处理,事件是对应于界面的,对应触屏事件,点击是如何对应到控件的呢,本章将在这个点上描述,并进行扩展。

 

 

 

4.3.1         事件流程

 

4.3.1.1                                  触屏事件流程

 

同样,为了便于分析问题,我们使用一个调用栈,这里使用拨号盘应用里面点击一个数字按钮的调用栈:

DialActivity$16.onTouch(View, MotionEvent) line: 3499  

ImageButton(View).dispatchTouchEvent(MotionEvent) line: 7701       

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

SlidingDrawer(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216        

SlidingDrawer(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917   

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

FrameLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

CallKey(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216

CallKey(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917    

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

LinearLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

LinearLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

FrameLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

FrameLayout(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216 

FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

PhoneWindow$DecorView(ViewGroup).dispatchTransformedTouchEvent(MotionEvent, boolean, View, int) line: 2216       

PhoneWindow$DecorView(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917 

PhoneWindow$DecorView.superDispatchTouchEvent(MotionEvent) line: 2076    

PhoneWindow.superDispatchTouchEvent(MotionEvent) line: 1515     

DialActivity(Activity).dispatchTouchEvent(MotionEvent) line: 2471    

PhoneWindow$DecorView.dispatchTouchEvent(MotionEvent) line: 2024    

PhoneWindow$DecorView(View).dispatchPointerEvent(MotionEvent) line: 7886 

ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl$QueuedInputEvent) line: 3952     

ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl$QueuedInputEvent) line: 3831  

ViewRootImpl$ViewPostImeInputStage(ViewRootImpl$InputStage).deliver(ViewRootImpl$QueuedInputEvent) line: 3396

ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).onDeliverToNext(ViewRootImpl$QueuedInputEvent) line: 3446

ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).forward(ViewRootImpl$QueuedInputEvent) line: 3415      

ViewRootImpl$NativePostImeInputStage(ViewRootImpl$AsyncInputStage).forward(ViewRootImpl$QueuedInputEvent) line: 3522     

ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).apply(ViewRootImpl$QueuedInputEvent, int) line: 3423    

ViewRootImpl$NativePostImeInputStage(ViewRootImpl$AsyncInputStage).apply(ViewRootImpl$QueuedInputEvent, int) line: 3579   

ViewRootImpl$NativePostImeInputStage(ViewRootImpl$InputStage).deliver(ViewRootImpl$QueuedInputEvent) line: 3396        

ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).onDeliverToNext(ViewRootImpl$QueuedInputEvent) line: 3446  

ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).forward(ViewRootImpl$QueuedInputEvent) line: 3415        

ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).apply(ViewRootImpl$QueuedInputEvent, int) line: 3423      

ViewRootImpl$EarlyPostImeInputStage(ViewRootImpl$InputStage).deliver(ViewRootImpl$QueuedInputEvent) line: 3396

ViewRootImpl.deliverInputEvent(ViewRootImpl$QueuedInputEvent) line: 5540   

ViewRootImpl.doProcessInputEvents() line: 5520    

ViewRootImpl.enqueueInputEvent(InputEvent, InputEventReceiver, int, boolean) line: 5491    

ViewRootImpl$WindowInputEventReceiver.onInputEvent(InputEvent) line: 5620 

ViewRootImpl$WindowInputEventReceiver(InputEventReceiver).dispatchInputEvent(int, InputEvent) line: 185

MessageQueue.nativePollOnce(int, int) line: not available [native method]    

MessageQueue.next() line: 138      

Looper.loop() line: 123

ActivityThread.main(String[]) line: 5241

Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]     

Method.invoke(Object, Object...) line: 515       

ZygoteInit$MethodAndArgsCaller.run() line: 818     

ZygoteInit.main(String[]) line: 634

NativeStart.main(String[]) line: not available [native method]      

 

从栈中我们截取有代表性的流程节点如下,

DialActivity$16.onTouch(View, MotionEvent) line: 3499  

ImageButton(View).dispatchTouchEvent(MotionEvent) line: 7701

FrameLayout(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917     

PhoneWindow$DecorView(ViewGroup).dispatchTouchEvent(MotionEvent) line: 1917 

DialActivity(Activity).dispatchTouchEvent(MotionEvent) line: 2471    

PhoneWindow$DecorView.dispatchTouchEvent(MotionEvent) line: 2024    

PhoneWindow$DecorView(View).dispatchPointerEvent(MotionEvent) line: 7886 

ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl$QueuedInputEvent) line: 3952     

 

在这里我们不再关注底层的实现,从应用开始识别到触屏事件开始分析,其大致流程如下图,




如上图所示,事件经过ViewPostImeInputStage的processPointerEvent,到View. dispatchPointerEvent,根据DecorView重写的dispatchTouchEvent方法,到Activity的实例引用,使用方法Activity.dispatchTouchEvent,将事件先传递到界面上。

再根据管理到的窗口,让PhoneWindow窗口处理事件,处理事件的方法是superDispatchTouchEvent,其内部主要有两个分支,先判断是否要处理OnTouchListener,根据结果判断是否继续处理OnClickListener,如果dispatchTouchEvent内部能正常处理事件,就消耗掉事件,如果不能,就要将事件传递下去。

    如果事件能传递下来,则使用Activity.onTouchEvent继续做一些边界判断等处理,最终完成本View对事件的处理。

 

 

4.3.1.2                                  触屏流程代码分析

如前所述,在应用层,事件的处理通道如下,

 

对于触屏产生的MotionEvent事件,通过JNI传递到应用后,在通道内,会传递给ViewPostImeInputStage,这里会根据事件的类型,使用相应的处理函数,对于物理按键,使用processKeyEvent,这个流程我们前面讨论过;对于触屏类的输入设备,则会使用processPointerEvent,我们现在讨论这个流程;对于轨迹球,使用processTrackballEvent;其他情况使用processGenericMotionEvent。

        protected int onProcess(QueuedInputEvent q) {

            if (q.mEvent instanceof KeyEvent) {

                return processKeyEvent(q);

            } else {

                // If delivering a new non-key event, make sure the window is

                // now allowed to start updating.

                handleDispatchDoneAnimating();

                final int source = q.mEvent.getSource();

                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {

                    return processPointerEvent(q);

                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {

                    return processTrackballEvent(q);

                } else {

                    return processGenericMotionEvent(q);

                }

            }

        }

 

processPointerEvent会调用mView. dispatchPointerEvent(),而mView正是DecorView的实例,这点之前的章节有分析过,故而事件就此传递给PhoneWindow的DecorView,实际上代码实现在view.java类里面,它判断是否是Touch事件,是的话就分发这个事件dispatchTouchEvent,

    public final boolean dispatchPointerEvent(MotionEvent event) {

        if (event.isTouchEvent()) {

            return dispatchTouchEvent(event);

        } else {

            return dispatchGenericMotionEvent(event);

        }

    }

 

dispatchTouchEvent()的执行代码在哪里实现的,这点比较重要,在不同的环境下,其执行过程不一样,它依赖于类的重写方式和相关变量的初始值。一般看代码都会默认的看到view.java的dispatchTouchEvent(),并依此进行分析,

     public boolean dispatchTouchEvent(MotionEvent event) {

        if (mInputEventConsistencyVerifier != null) {

            mInputEventConsistencyVerifier.onTouchEvent(event, 0);

        }

 

        if (onFilterTouchEventForSecurity(event)) {

            //noinspection SimplifiableIfStatement

            ListenerInfo li = mListenerInfo;

            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED

                    && li.mOnTouchListener.onTouch(this, event)) {

                return true;

            }

 

            if (onTouchEvent(event)) {

                return true;

            }

        }

 

        if (mInputEventConsistencyVerifier != null) {

            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);

        }

        return false;

    }

 

 

其实不然,在本例中,DecorView间接继承了View,重写了dispatchTouchEvent(其继承关系为DecorView-FrameLayout-ViewGroup-View,只有DecorView重写了dispatchTouchEvent),

 DecorView:

        @Override

        public boolean dispatchTouchEvent(MotionEvent ev) {

            final Callback cb = getCallback();

            return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev)

                    : super.dispatchTouchEvent(ev);

        }

可以看到,如果DecorView的callback不为空,才执行View里面的方法,对于在activity界面上的触屏事件,这里对应的callback就是activity的实例,如果Activity的界面子类又重写了dispatchTouchEvent,就执行界面Activity的dispatchTouchEvent,没有就执行基类Activity的dispatchTouchEvent,本例中就是执行Activity的dispatchTouchEvent,

     public boolean dispatchTouchEvent(MotionEvent ev) {

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {

            onUserInteraction();

        }

        if (getWindow().superDispatchTouchEvent(ev)) {

            return true;

        }

        return onTouchEvent(ev);

    }

如上,Activity的dispatchTouchEvent在处理事件时有几个分支,先交付给PhoneWindow.superDispatchTouchEvent(MotionEvent)处理,如果返回true,则代表事件被PhoneWindow 处理完成,消耗掉该事件,否则,还要继续使用onTouchEvent处理。

 

onTouchEvent处理的情况是:    

* Called when a touch screen event was not handled by any of the views

     * under it.  This is most useful to process touch events that happen

     * outside of your window bounds, where there is no view to receive it.

就是说触点落在窗口之外,或未被任何控件接收到的时候。

     public boolean onTouchEvent(MotionEvent event) {

        if (mWindow.shouldCloseOnTouch(this, event)) {

            finish();

            return true;

        }

       

        return false;

    }

 

    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {

        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN

                && isOutOfBounds(context, event) && peekDecorView() != null) {

            return true;

        }

        return false;

    }

 

在PhoneWindow.superDispatchTouchEvent里,进一步调用了DecorView的superDispatchTouchEvent,它又使用了父类ViewGroup的dispatchTouchEvent,其代码如下,

 …

 

ViewGroup的dispatchTouchEvent过程比较复杂,之后再详细分析其实现过程,我们先看事件的主要处理流程,它会再调用到dispatchTransformedTouchEvent,这个方法会根据传入的view的属性和相关变量,调用view实例类的dispatchTouchEvent方法,因为ViewGroup的嵌套关系,所以产生了很长的调用栈。这些linearLayout、frameLayout、slidingDrawer及自定义的类,都是使用的ViewGroup的dispatchTouchEvent方法。

 

 

这个事件在viewGroup树中分发后,最终会到具体的控件上面,本例就是数字按钮对应的ImageButton控件,其继承自imageView—View,实际调用View的dispatchTouchEvent方法,

     public boolean dispatchTouchEvent(MotionEvent event) {

        if (mInputEventConsistencyVerifier != null) {

            mInputEventConsistencyVerifier.onTouchEvent(event, 0);

        }

 

        if (onFilterTouchEventForSecurity(event)) {

            //noinspection SimplifiableIfStatement

            ListenerInfo li = mListenerInfo;

            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED

                    && li.mOnTouchListener.onTouch(this, event)) {

                return true;

            }

 

            if (onTouchEvent(event)) {

                return true;

            }

        }

 

        if (mInputEventConsistencyVerifier != null) {

            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);

        }

        return false;

    }

在本例中,由于界面Activity注册了OnTouchListener,所以执行ontouch()方法,完成事件的处理,并消耗掉该事件。Ontouch()方法是用户自定义的,完成用户期望点击控件后的行为,不在本文讨论之内。

 

如果界面Activity没有注册OnTouchListener监听器,就会执行View的onTouchEvent()方法,从其注释来看,   

* Implement this method to handle touch screen motion events.

     * <p>

     * If this method is used to detect click actions, it is recommended that

     * the actions be performed by implementing and calling

     * {@link #performClick()}

就是说onTouchEvent这个方法是处理onClick()事件的,这里就是让很多人困惑的onClick和onTouch的地方了,从上面的逻辑看来,就是如果实现了onTouch,在onTouch返回true就消耗掉了事件,onClick得不到执行。简单来理解就是在事件处理流程中,onTouch比onClick先执行,如果onTouch消耗掉事件,onClick就得不到执行。

 

该执行哪个类的onTouchEvent,依赖于子类是否重写了方法,例如textView有进行重写,它会先执行父类的onTouchEvent,再继续执行自己的处理。

onTouchEvent的实现如下,

     public boolean onTouchEvent(MotionEvent event) {

        if (mTouchDelegate != null) {

            if (mTouchDelegate.onTouchEvent(event)) {

                return true;

            }

        }

 

        if (((viewFlags & CLICKABLE) == CLICKABLE ||

                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

            switch (event.getAction()) {

                case MotionEvent.ACTION_UP:

                case MotionEvent.ACTION_DOWN:

           Return true;

}

Return false;

}

 

在这个方法中,处理到了TouchDelegate,这个是针对让控件有更大点击区域而改善用户体验的一个辅助类,有兴趣的同学可以自行研究。

 

接下来会根据控件的状态和按键的状态处理事件,这个方法能处理四个按键行为,DOWN、UP、CANCEL 、MOVE,我们现分析两个常见的按键DOWN、UP。

 

ACTION_DOWN的处理代码如下,主要是清除长按处理的flag,按钮菜单的处理,滚动容器类的处理,

                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

                        setPressed(true);

                        checkForLongClick(0);

                    }

                    break;

一般流程是直接走到checkForLongClick,在checkForLongClick方法里面,是对长按的处理,和普通短按流程关系不大,所以对于触屏短按事件,如果流程能走到这里,就走完了,该返回true消耗掉这个事件了。(也可以总结出对于触屏事件,Ontouch可以携带事件是否按下的状态,而onClick是没有这种状态的,它只在UP事件中产生)。

 

长按流程后续会使用专门篇幅讨论,在此不详述。

分析完上面流程后,有个问题需要思考,如果一个控件需要同时支持短按和长按,该如何设计监听函数?其执行流程又是怎样?

 

 

MotionEvent.ACTION_UP事件处理主要代码如下,

                case MotionEvent.ACTION_UP:

                        if (!mHasPerformedLongPress) {

                            removeLongPressCallback();

                            if (!focusTaken) {

                                if (mPerformClick == null) {

                                    mPerformClick = new PerformClick();

                                }

                                if (!post(mPerformClick)) {

                                    performClick();

                                }

                            }

                        }

它主要完成的工作有设置view的press状态,清除长按相关变量、消息等状态,并Post一个消息出去,完成Click监听器的事件处理。Post比直接调用的好处在于,Post的同时就可以更新View的外观变化,而直接调用需要等到Click被执行后才能看到。

 

至此,一个简单的按键分析流程就分析完成了。

在按键分析中我们还有很多知识点没有分析到,需要在后面不断学习,如组合按键的产生、手势、长按的设计流程,还有一些细节的东西,如控件单击后的背景属性的变化流程等。

 

 

4.3.1.3                                   OnClickListener代码分析 

应用可以使用SetOnClickListener()给一个View控件注册监听器,其实现在View里面,实际就是给View实例的成员类ListenerInfo的成员mOnClickListener赋值,

     public void setOnClickListener(OnClickListener l) {

        if (!isClickable()) {

            setClickable(true);

        }

        getListenerInfo().mOnClickListener = l;

    }

 

OnClickListener只是一个接口,定义如下,

     public interface OnClickListener {

        /**

         * Called when a view has been clicked.

         *

         * @param v The view that was clicked.

         */

        void onClick(View v);

    }

 

 

对于注册的OnClickListener监听器,不能想当然的根据代码分析,任务认为他们的会在onTouchEvent()方法里面执行,为了明白他们的执行时机,我们在一个Activity上定义一个按钮,再注册OnClickListener,点击按钮的时候会得到如下的调用栈,可以看出OnClickListener走了一条完全不同的执行过程,所以分析代码一定要结合调试,否则会误入歧途,

DialActivity$15.onClick(View) line: 3484       

ImageButton(View).performClick() line: 4240 

View$PerformClick.run() line: 17721     

Handler.handleCallback(Message) line: 730     

ViewRootImpl$ViewRootHandler(Handler).dispatchMessage(Message) line: 92   

Looper.loop() line: 137

ActivityThread.main(String[]) line: 5265

Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]     

Method.invoke(Object, Object...) line: 525       

ZygoteInit$MethodAndArgsCaller.run() line: 760     

ZygoteInit.main(String[]) line: 576

NativeStart.main(String[]) line: not available [native method]      

 

从调用栈可以看出,OnClick()的执行调用源头是Handler.dispatchMessage(),事件是发送给ViewRootImpl的ViewRootHandler实例的,最终调用Handler的handleCallback,根据我们在另外的文章分析的Handler的三种消息处理方式中了解到,这个消息是在应用层Post出来的,而不是HAL层通过JNI方式上传过来。现在我们有两个方向分析问题,一个是当前消息事件向上的分发处理,一个方向是Post消息的源头。

 

1)向上:OnClick的执行过程

handleCallback(msg)处理的消息在生成的时候,会有Callback和run方法,所以这种方式就是直接调用run方法,

     private static void handleCallback(Message message) {

        message.callback.run();

    }

 

 

本例的callback是一个runnable类PerformClick,它的实现主体是

    private final class PerformClick implements Runnable {

        public void run() {

            performClick();

        }

    }

 

    public boolean performClick() {

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

 

        ListenerInfo li = mListenerInfo;

        if (li != null && li.mOnClickListener != null) {

            playSoundEffect(SoundEffectConstants.CLICK);

            li.mOnClickListener.onClick(this);

            return true;

        }

 

        return false;

    }

从代码里就很好理解了,如果View注册了OnClickListener,则执行其onClick方法,到此流程就结束了。

 

另外,根据代码分析,OnClickListener还有另外一种调用方法,即通过调用callOnClick()来执行onClick,有兴趣的同学可单独分析。

 

2)向下:谁Post了这个消息

   因为我们点击一个控件View的时候,是物理事件,现在onClick处理的是一个间接的应用层消息,我们还要分析物理按键引发的事件和这个消息的关联。

 

这个在前面分析View的onTouchEvent方法时有讲到,在处理MotionEvent.ACTION_UP事件时,会Post这个消息,进而触发OnClickListener的执行。

                case MotionEvent.ACTION_UP:

                        if (!mHasPerformedLongPress) {

                            removeLongPressCallback();

                            if (!focusTaken) {

                                if (mPerformClick == null) {

                                    mPerformClick = new PerformClick();

                                }

                                if (!post(mPerformClick)) {

                                    performClick();

                                }

                            }

                        }

 

 

 


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值