SystemUI 剖析

Android 专栏收录该内容
26 篇文章 0 订阅

序言

17年上半年主要做车机项目SystemUI的工作,一般的来说可以选择在源码的基础上定制,也可以重新编写。具体选择哪种方案,也要根据自身项目的需求和工作量来考虑,但是不管选择哪种,都需要对SystemUI源码有一定的了解。本文主要先从大体上了解整个SystemUI的运行机制,然后再从状态栏的图标如何显示,下拉菜单是如何的实现,来剖析这个模块的运行原理,最后用一个需求来深入的了解SystemUI模块。希望能够帮助有这方面需求的同学,当然了,本文都是笔者自己对代码的理解,如果有不正确的地方,欢迎留言指出!

整体架构

相信很多没有了解过SystemUI的同学都觉得它很神秘,因为这个Application很特殊,它和系统的联系很紧密,而且需要在源码的环境下编译,所以这也是和应用开发者关系比较疏远的原因之一。下图是SystemUI的整体架构图,我看过Android 4.4和6.0的代码,虽然代码变化很大,但是原理和核心代码都是差不多的。

SystemUI架构图

SystemUI的启动是由SystemServer进程来操作的,在整个系统资源,服务加载完成差不多的时候,调用了startSystemUI() 来启动一个服务SystemUIService ,此时就进入了SystemUI的应用程序世界了。

    static final void startSystemUi(Context context) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.android.systemui",
                    "com.android.systemui.SystemUIService"));
        //Slog.d(TAG, "Starting service: " + intent);
        context.startServiceAsUser(intent, UserHandle.OWNER);
    }

首先来说下一个aidl文件,叫做IStatusBar.aidl,它的路径在frameworks/base/core/java/com/android/internal/statusbar 下。它是framework和SystemUI的通信接口,包括我们的StatusBarManagerService封装的也是这个接口。我们的SystemUI主要是显示系统状态的改变,一般的由内部和外部导致,比如系统电量的改变,SystemUI会接收系统广播来修改,这种一般不会暴露给外部使用,不然就导致显示异常,还有种是外部导致SystemUI显示的变化,像其他应用发来通知,隐藏状态栏等,这个就需要我们的IStatusBar.aidl来承担之间的通信规则了。当我们的SystemSever进程来启动我们的SystemUIService,我们SystemUIService会启动很多的类,而这些类都是继承SystemUI接口的,并且都是有自己的职责所在。

   /**
     * Makes sure that all the SystemUI services are running. If they are already running, this is a
     * no-op. This is needed to conditinally start all the services, as we only need to have it in
     * the main process.
     *
     * <p>This method must only be called from the main thread.</p>
     */
    public void startServicesIfNeeded() {
        if (mServicesStarted) {
            return;
        }

        if (!mBootCompleted) {
            // check to see if maybe it was already completed long before we began
            // see ActivityManagerService.finishBooting()
            if ("1".equals(SystemProperties.get("sys.boot_completed"))) {
                mBootCompleted = true;
                if (DEBUG) Log.v(TAG, "BOOT_COMPLETED was already sent");
            }
        }

        Log.v(TAG, "Starting SystemUI services.");
        final int N = SERVICES.length;
        for (int i=0; i<N; i++) {
            Class<?> cl = SERVICES[i];
            if (DEBUG) Log.d(TAG, "loading: " + cl);
            try {
                mServices[i] = (SystemUI)cl.newInstance();
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            } catch (InstantiationException ex) {
                throw new RuntimeException(ex);
            }
            mServices[i].mContext = this;
            mServices[i].mComponents = mComponents;
            if (DEBUG) Log.d(TAG, "running: " + mServices[i]);
            mServices[i].start();

            if (mBootCompleted) {
                mServices[i].onBootCompleted();
            }
        }
        mServicesStarted = true;
    }

下面是启动的所有接口,

    /**
     * The classes of the stuff to start.
     */
    private final Class<?>[] SERVICES = new Class[] {
            com.android.systemui.tuner.TunerService.class,
            com.android.systemui.keyguard.KeyguardViewMediator.class,
            com.android.systemui.recents.Recents.class,
            com.android.systemui.volume.VolumeUI.class,
            com.android.systemui.statusbar.SystemBars.class,
            com.android.systemui.usb.StorageNotification.class,
            com.android.systemui.power.PowerUI.class,
            com.android.systemui.media.RingtonePlayer.class,
            com.android.systemui.keyboard.KeyboardUI.class,
    };

我们这里只研究下主要试图的部分,也就是我们的SystemBars,这个类主要的工作就是通过类加载器生成我们的PhoneStatusBar,然后调用它的start()方法,我们切换到PhoneStatusBar这个类来,PhoneStatusBar这个类承担了最主要的添加,移除,更新视图的重任,它负责解析状态栏,导航栏的布局文件,然后通过WindowManagerService来生成Window对象,它实现了IStatusBar的所有接口,负责接收外界所有的关于SystemUI视图更新的消息。

视图

视图这块主要有三个模块分别是状态栏,导航栏,还有下拉模块。状态栏主要显示系统的状态,导航栏一般提供了快捷Home,最近打开的应用,和回到上一层,下拉菜单提供了一些快捷操作,通知的显示,这里就不讲导航栏了,感兴趣可以自行了解。来看看状态栏吧,小小的一个区域,Google却把它封装成一层又一层的,整个statusbar布局可以说是非常复杂,不花点精力真的不知道哪对哪。


这里写图片描述


statusbar的整体结构可以简化如上图所示,statusbar的图标显示在PhoneStatusBarView 那一层,其中又包裹着moreIcons和notificationIcons,下面是6.0的布局文件。

<com.android.systemui.statusbar.phone.PhoneStatusBarView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res/com.android.systemui"
    android:id="@+id/status_bar"
    android:background="@drawable/system_bar_background"
    android:orientation="vertical"
    android:focusable="false"
    android:descendantFocusability="afterDescendants"
    >

    <ImageView
        android:id="@+id/notification_lights_out"
        android:layout_width="@dimen/status_bar_icon_size"
        android:layout_height="match_parent"
        android:paddingStart="6dip"
        android:paddingBottom="2dip"
        android:src="@drawable/ic_sysbar_lights_out_dot_small"
        android:scaleType="center"
        android:visibility="gone"
        />

    <LinearLayout android:id="@+id/status_bar_contents"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingStart="6dp"
        android:paddingEnd="8dp"
        android:orientation="horizontal"
        >

        <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
            android:id="@+id/notification_icon_area"
            android:layout_width="0dip"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:orientation="horizontal"
            >
            <!-- The alpha of this area is both controlled from PhoneStatusBarTransitions and
                 PhoneStatusBar (DISABLE_NOTIFICATION_ICONS), so we need two views here. -->
            <com.android.keyguard.AlphaOptimizedLinearLayout
                android:id="@+id/notification_icon_area_inner"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                >
                <com.android.systemui.statusbar.StatusBarIconView android:id="@+id/moreIcon"
                    android:layout_width="@dimen/status_bar_icon_size"
                    android:layout_height="match_parent"
                    android:src="@drawable/stat_notify_more"
                    android:visibility="gone"
                    />
                <com.android.systemui.statusbar.phone.IconMerger android:id="@+id/notificationIcons"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_alignParentStart="true"
                    android:gravity="center_vertical"
                    android:orientation="horizontal"/>
            </com.android.keyguard.AlphaOptimizedLinearLayout>
        </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>

        <com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/system_icon_area"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal"
            >

            <include layout="@layout/system_icons" />

            <com.android.systemui.statusbar.policy.Clock
                android:id="@+id/clock"
                android:textAppearance="@style/TextAppearance.StatusBar.Clock"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:singleLine="true"
                android:paddingStart="7dp"
                android:gravity="center_vertical|start"
                />
        </com.android.keyguard.AlphaOptimizedLinearLayout>
    </LinearLayout>

</com.android.systemui.statusbar.phone.PhoneStatusBarView>

我们一般动态显示的图标在标签下system_icons布局中,

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/system_icons"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:gravity="center_vertical">

    <com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/statusIcons"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:gravity="center_vertical"
        android:orientation="horizontal"/>

    <include layout="@layout/signal_cluster_view"
        android:id="@+id/signal_cluster"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="2.5dp"/>

    <!-- battery must be padded below to match assets -->
    <com.android.systemui.BatteryMeterView android:id="@+id/battery"
        android:layout_height="14.5dp"
        android:layout_width="9.5dp"
        android:layout_marginBottom="@dimen/battery_margin_bottom"/>
</LinearLayout>

可以看到电池电量和信号是专门的控件去处理的,因为这块是一直显示在我们的状态栏上的,并且由系统去修改的,上面也提到了,这个是不提供外界修改的接口。其他的图标的更新是在id叫statusIcons的一个线性布局中,代码控制模块是通过CommandQueue将回调传递给PhoneStatusBar,然后PhoneStatusBar又交给StatusBarIconController中,以下是部分代码

    public void addSystemIcon(String slot, int index, int viewIndex, StatusBarIcon icon) {
        boolean blocked = mIconBlacklist.contains(slot);
        StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked);
        view.set(icon);
        mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));
        view = new StatusBarIconView(mContext, slot, null, blocked);
        view.set(icon);
        mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));
        applyIconTint();
    }

    public void updateSystemIcon(String slot, int index, int viewIndex,
            StatusBarIcon old, StatusBarIcon icon) {
        StatusBarIconView view = (StatusBarIconView) mStatusIcons.getChildAt(viewIndex);
        view.set(icon);
        view = (StatusBarIconView) mStatusIconsKeyguard.getChildAt(viewIndex);
        view.set(icon);
        applyIconTint();
    }

    public void removeSystemIcon(String slot, int index, int viewIndex) {
        mStatusIcons.removeViewAt(viewIndex);
        mStatusIconsKeyguard.removeViewAt(viewIndex);
    }

mStatusIcons对应的就是前面介绍的id为statusIcons的布局。我们再来分析下下拉菜单的原理机制,当我们触摸手机的状态栏的位置,下拉菜单会随着我们手势做相应的处理。还记得我们状态栏最外层布局节点是PhoneStatusBarView,

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean barConsumedEvent = mBar.interceptTouchEvent(event);

        if (DEBUG_GESTURES) {
            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
                EventLog.writeEvent(EventLogTags.SYSUI_PANELBAR_TOUCH,
                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),
                        barConsumedEvent ? 1 : 0);
            }
        }

        return barConsumedEvent || super.onTouchEvent(event);
    }

首先先看看mBar是否要拦截这个事件,如果没有拦截则调用父类的onTouchEvent()方法,这里的mBar其实就是PhoneStatusBar这个类,这个类相当于一个控制器的作用,再看看PhoneStatusBar什么时候会拦截这个事件,

    public boolean interceptTouchEvent(MotionEvent event) {
        if (DEBUG_GESTURES) {
            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {
                EventLog.writeEvent(EventLogTags.SYSUI_STATUSBAR_TOUCH,
                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),
                        mDisabled1, mDisabled2);
            }

        }

        if (SPEW) {
            Log.d(TAG, "Touch: rawY=" + event.getRawY() + " event=" + event + " mDisabled1="
                + mDisabled1 + " mDisabled2=" + mDisabled2 + " mTracking=" + mTracking);
        } else if (CHATTY) {
            if (event.getAction() != MotionEvent.ACTION_MOVE) {
                Log.d(TAG, String.format(
                            "panel: %s at (%f, %f) mDisabled1=0x%08x mDisabled2=0x%08x",
                            MotionEvent.actionToString(event.getAction()),
                            event.getRawX(), event.getRawY(), mDisabled1, mDisabled2));
            }
        }

        if (DEBUG_GESTURES) {
            mGestureRec.add(event);
        }

        if (mStatusBarWindowState == WINDOW_STATE_SHOWING) {
            final boolean upOrCancel =
                    event.getAction() == MotionEvent.ACTION_UP ||
                    event.getAction() == MotionEvent.ACTION_CANCEL;
            if (upOrCancel && !mExpandedVisible) {
                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);
            } else {
                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true);
            }
        }
        return false;
    }

从这里可以看出,最后return的是false,而其中执行的一个比较重要的方法是setInteracting(),它的作用隐藏和显示状态栏拓展出来的部分就是下拉菜单这块,不是很难理解,我们在着重分析下panelBar的onTouchEvent()方法,

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Allow subclasses to implement enable/disable semantics
        if (!panelsEnabled()) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                Log.v(TAG, String.format("onTouch: all panels disabled, ignoring touch at (%d,%d)",
                        (int) event.getX(), (int) event.getY()));
            }
            return false;
        }

        // figure out which panel needs to be talked to here
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            final PanelView panel = selectPanelForTouch(event);
            if (panel == null) {
                // panel is not there, so we'll eat the gesture
                Log.v(TAG, String.format("onTouch: no panel for touch at (%d,%d)",
                        (int) event.getX(), (int) event.getY()));
                mTouchingPanel = null;
                return true;
            }
            boolean enabled = panel.isEnabled();
            if (DEBUG) LOG("PanelBar.onTouch: state=%d ACTION_DOWN: panel %s %s", mState, panel,
                    (enabled ? "" : " (disabled)"));
            if (!enabled) {
                // panel is disabled, so we'll eat the gesture
                Log.v(TAG, String.format(
                        "onTouch: panel (%s) is disabled, ignoring touch at (%d,%d)",
                        panel, (int) event.getX(), (int) event.getY()));
                mTouchingPanel = null;
                return true;
            }
            startOpeningPanel(panel);
        }
        final boolean result = mTouchingPanel != null
                ? mTouchingPanel.onTouchEvent(event)
                : true;
        return result;
    }

一般的下拉菜单有两种,一种是SettingPanelView还有种 NotificationPanelView, panelBar的onTouchEvent主要寻找哪块PanelView消费了这个MotionEvent,所以,我们继续跟踪PanelView的onTouchEvent,

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mInstantExpanding || mTouchDisabled
                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
            return false;
        }

        /*
         * We capture touch events here and update the expand height here in case according to
         * the users fingers. This also handles multi-touch.
         *
         * If the user just clicks shortly, we give him a quick peek of the shade.
         *
         * Flinging is also enabled in order to open or close the shade.
         */

        int pointerIndex = event.findPointerIndex(mTrackingPointer);
        if (pointerIndex < 0) {
            pointerIndex = 0;
            mTrackingPointer = event.getPointerId(pointerIndex);
        }
        final float x = event.getX(pointerIndex);
        final float y = event.getY(pointerIndex);

        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();
            mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);
        }

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                mJustPeeked = false;
                mPanelClosedOnDown = isFullyCollapsed();
                mHasLayoutedSinceDown = false;
                mUpdateFlingOnLayout = false;
                mMotionAborted = false;
                mPeekTouching = mPanelClosedOnDown;
                mTouchAboveFalsingThreshold = false;
                mCollapsedAndHeadsUpOnDown = isFullyCollapsed()
                        && mHeadsUpManager.hasPinnedHeadsUp();
                if (mVelocityTracker == null) {
                    initVelocityTracker();
                }
                trackMovement(event);
                if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) ||
                        mPeekPending || mPeekAnimator != null) {
                    cancelHeightAnimator();
                    cancelPeek();
                    mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
                            || mPeekPending || mPeekAnimator != null;
                    onTrackingStarted();
                }
                if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) {
                    schedulePeek();
                }
                break;

            case MotionEvent.ACTION_POINTER_UP:
                final int upPointer = event.getPointerId(event.getActionIndex());
                if (mTrackingPointer == upPointer) {
                    // gesture is ongoing, find a new pointer to track
                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
                    final float newY = event.getY(newIndex);
                    final float newX = event.getX(newIndex);
                    mTrackingPointer = event.getPointerId(newIndex);
                    startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
                    mMotionAborted = true;
                    endMotionEvent(event, x, y, true /* forceCancel */);
                    return false;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                float h = y - mInitialTouchY;

                // If the panel was collapsed when touching, we only need to check for the
                // y-component of the gesture, as we have no conflicting horizontal gesture.
                if (Math.abs(h) > mTouchSlop
                        && (Math.abs(h) > Math.abs(x - mInitialTouchX)
                                || mIgnoreXTouchSlop)) {
                    mTouchSlopExceeded = true;
                    if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {
                        if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
                            startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);
                            h = 0;
                        }
                        cancelHeightAnimator();
                        removeCallbacks(mPeekRunnable);
                        mPeekPending = false;
                        onTrackingStarted();
                    }
                }
                final float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
                if (newHeight > mPeekHeight) {
                    if (mPeekAnimator != null) {
                        mPeekAnimator.cancel();
                    }
                    mJustPeeked = false;
                }
                if (-h >= getFalsingThreshold()) {
                    mTouchAboveFalsingThreshold = true;
                    mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);
                }
                if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {
                    setExpandedHeightInternal(newHeight);
                }

                trackMovement(event);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                trackMovement(event);
                endMotionEvent(event, x, y, false /* forceCancel */);
                break;
        }
        return !mGestureWaitForTouchSlop || mTracking;
    }

PanelView的onTouchEvent()方法主要对手势的处理逻辑比较多,但是也是很常规的对自定义View的处理,和下拉刷新控件逻辑和用到的技术都差不多的东西,手一直下拉过程中,不断的刷新,重绘控件,如果在中途放手,那么会根据你放手的位置来决定到底是关闭or打开panelView。

动态更改状态栏高度

像一般的定制SystemUI,静态的修改状态栏高度是经常都会碰到的,我们会去修改frameworks/base/core/res/res/values/dimens.xml中的值,

    <dimen name="toast_y_offset">64dip</dimen>
    <!-- Height of the status bar -->
    <dimen name="status_bar_height">24dp</dimen>
    <!-- Height of the bottom navigation / system bar. -->
    <dimen name="navigation_bar_height">48dp</dimen>

然后将framework-res.jar push到/system/framework/底下重启机器就可以生效了,但是如何动态的修改状态的高度呢,我们知道在Android源码中,Window这个类非常的抽象,它会在Activity启动的时候附着在Activity中,然后承担着试图显示的职责,而PhoneWindow是Window的唯一实现类,PhoneWindowManager可以说是一个策略类,它实现了WindowManagerPolicy,从名字也可以看出,主要是处理一些和视图相关的逻辑部分,当然了整个Android的任何一个模块都是相当的复杂,我们这里并不去深究,我们知道PhoneWindowManager这个类做了很多初始化工作,包括从framework/res中获取状态栏的高度值

        mStatusBarHeight =
                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);

然后PhoneWindowManager还要确定绘制的各个区域,然后进行测量,绘制的工作,所以我们只要动态的设置PhoneWindowManager中的mStatusBarHeight,然后在重新绘制系统的视图即可。具体的,我们该如何动态修改mStatusBarHeight的值,我们的好好的分析PhoneWindowManager这个类了,可以看到

 ...
 IStatusBarService mStatusBarService;
 ...

在PhoneWindowManager这个类中有mStatusBarService这个属性,这就意味着我们可以在这里直接操作SystemUI,StatusBarManagerService同样也实现了这个接口,而StatusBarManagerService内部有IStatusBar这个代理类,同样,SystemUI中CommandQueue也实现了IStatusBar(上文提到,这里SystemUI相当于服务端),这样在PhoneWindowManager就可以直接控制SystemUI的行为,那么我们怎么样能够修改PhoneWindowManager中的状态栏高度值呢,我们看到PhoneWindowManager实现了WindowPolicy接口,而在WindowManagerService是持有WindowPolicy这个引用的。

    ...
    final WindowManagerPolicy mPolicy = new PhoneWindowManager();
    ...

这样,我们只需要能够调用WindowManagerService里面的方法,就能直接控制PhoneWindowManager,从而控制SystemUI了,再看看我们的WindowManagerService是继承了IWindowManager.Stub这个通信借口,所以,所有问题都解决了,我们只需要在IWindowManager.aidl文件中,添加updateStatusBarHeight()方法,然后在之前我们提到的所有地方都添加方法实现,最终就会调用到PhoneWindowManager,修改状态栏的高度值,当SystemUI接受到回调之后,调用WindowManager的updateLayoutView()方法,就会重新绘制我们的系统UI。

这里写图片描述

上图是整个调用流程,这样就可以动态的修改我们的状态栏高度了。

  • 1
    点赞
  • 0
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页

打赏

Nipuream

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值