从零开始的自定义View(一)——View和ViewGroup绘制过程源码解析

一、View的绘制过程

view的绘制需要经过三个过程:measure、layout、draw,即测量、布局和绘制。
下面依次来看一个每个过程的作用以及具体的实现。

1、measure
下面是View中的measure()方法,可以看到只是一个final方法,不可以被重写。那么,对于继承View的子类来说,需要重写的方法是onMeasure(widthMeasureSpec, heightMeasureSpec);

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        // 特例,暂时不需要考虑
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth = insets.left + insets.right;
            int oHeight = insets.top + insets.bottom;
            widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        // 因为measure是可能要进行多次的,每一次的值都要保存下来
        // 将宽高组合成一个长整型作为键值,后面会用到这个键值来保存计算出来的宽高
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        // long long Map,避免自动装箱,节约内存,提高性能
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        // 本次测量使用的MeasureSpec和前一次不同
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        // 宽高的模式均为Exactly
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        // 本次测量的size与保留的结果相同
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);

        // 标志位,是否需要Layout:MeasureSpec发生变化,且(非Exactly或size不同)
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        // forceLayout和needsLayout组合判断是否真的需要measure:比如当前的宽高已经计算过了;Exactly模式,宽高由parent指定,不需要重新计算
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            // 清除PFLAG_MEASURED_DIMENSION_SET的标志位,开始准备测量
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            // RTL环境
            resolveRtlPropertiesIfNeeded();

            // forceLayout: -1; 已包含键值: 返回index; 未包含键值:负数
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            // sIgnoreMeasureCache低版本使用,每一次Layout都强制重新measure
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                // onMeasure方法,包含了默认实现,可以重写
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                // 清除mPrivateFlags3的标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                // 已经计算过,直接使用index获取测量值
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                // 置位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT, layout时需要重新测量
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            // 测量值未更新,抛出异常
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }
            // 置位PFLAG_LAYOUT_REQUIRED,需要Layout
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        // 将前一次测量使用的MeasureSpec保存
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        // 测量结果使用key作为键值保存
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

对于View来说,onMeasure()提供了一个默认的实现,其中使用了getDefaultSize()来取得默认的size。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // getSuggestedMinimumWidth(): view自身给出的最小建议值,无背景是mMinWidth, 有背景是mMinWidth和背景宽度中较大值
        // getDefaultSize(): 根据父view的模式,给出最终的size
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            // 父view不指定大小,直接使用子view的size
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            // 父view给出精确值或者最大值,使用父view指定的size
            result = specSize;
            break;

2、layout
接下来来看layout的过程:对于单一的view来说,onLayout实际上是一个空实现,其layout的位置实际上在setFrame中被确定了。setFrame会返回一个boolean值,表示布局是否发生了改变。如果布局发生改变,则会获取此view的OnLayoutChangeListener,然后遍历调用onLayoutChange来通知布局发生变化。

    @SuppressWarnings({"unchecked"})
    public void layout(int l, int t, int r, int b) {
        // PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT被置位,那么这里要重新测量一次
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        // Optical,特例,暂不考虑,只是改变了setFrame传入值
        // setFrame是view进行布局的重要方法
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        // 如果布局发生了变化,或者mPrivateFlags的标志位PFLAG_LAYOUT_REQUIRED被置位需要layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            // 调用onLayout, 这是一个空实现
            onLayout(changed, l, t, r, b);

            // 针对圆形穿戴设备的处理,可忽略
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                 mRoundScrollbarRenderer = null;
            }

            // 清除标志位PFLAG_LAYOUT_REQUIRED
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            // 获取OnLayoutChangeListener,遍历通知布局发生改变
            ListenerInfo li = mListenerInfo;
                        if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        // 清除标志位PFLAG_FORCE_LAYOUT
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        // 置位标志位PFLAG3_IS_LAID_OUT
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

3、draw
然后是来看draw。draw方法主要是绘制背景前景滚动条等,当前view的实际绘制动作发生在onDraw()方法内。对于View,onDraw()是一个空实现,子类需要重写来进行自己的绘制。

    @CallSuper
    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        // 第一步:在canvas上绘制背景
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)

        // Fading Edge是一个边缘阴影渐变的效果
        // 如果没有fadingEdge的话,跳过25,只进行346即可
                final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            // onDraw()空实现,需要重写
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            // 遍历绘制子view,view中为空实现,viewGroup提供了默认实现,一般不需要重写
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            // 覆盖层,如果有的话进行绘制
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            // 绘制前景、滚动条等
            onDrawForeground(canvas);

            // we're done...
            return;
        }

        /*
         * Here we do the full fledged routine...
         * (this is an uncommon case where speed matters less,
         * this is why we repeat some of the tests that have been
         * done above)
         */
         // 有Fading Edge的情况下,需要进行完成的流程

        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;

        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;

        // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
                if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
                if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
                    scrollabilityCache.setFadeColor(solidColor);
        }

        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 5, draw the fade effect and restore layers
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
    }

综上所述,如果需要写一个继承自View的自定义View,一般需要重写的就是onMeasure()和onDraw()。

ViewGroup的绘制过程

1、measure
ViewGroup本身没有重写onMeasure()方法,因为ViewGroup的子类的布局特性各不相同,难以对其进行一个统一的基本实现,对于子类来说,需要重写onMeasure()来实现自己的测量。
因此,在ViewGroup中的measure过程我们主要看的是其对于子类的测量,主要的方法是measureChildren()、measureChild()和getChildMeasureSpec()这三个。

首先,调用measureChildren方法,对所有的未隐藏的子View进行遍历,然后分别使用measureChild()方法对其进行测量。其中widthMeasureSpec和heightMeasureSpec分别是ViewGroup宽高的MeasureSpec。
而在measureChild()中使用getChildMeasureSpec()方法来计算子View的MeasureSpec,并且直接调用子View的measure()方法来进行测量。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        // 遍历所有的子view,如果不是GONE的话,使用measureChild对子view进行测量
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        // 根据父view的宽高MeasureSpec,padding,子view的LayoutParams给出的宽高来获取父view对子view的指导宽高
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        // 使用获取到的MeasureSpec来对子View进行measure
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        // 取ViewGroup的mode和size
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        // specSize减去padding,子view可以使用的最大size值
        // specSize可能为0,0最为最小保留值
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
            // ViewGroup的模式为EXACTLY,其size为match_parent或者固定dimen,具体值已经确定
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 子view的size也为固定值,使用子view值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                // match_parent,子view希望和ViewGroup的size相等
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                // wrap_content,子View希望显示完整,size由父view决定,模式为AT_MOST,
                // 子view的大小不超过ViewGroup
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
            // ViewGroup为AT_MOST, warp_content,最大值由ViewGroup的父View决定
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                // 和之前一样,子View的size为固定值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                // 子View希望充满ViewGroup,但是此时ViewGroup的size还没有真正确定
                // 因此,使用ViewGroup的size,模式为AT_MOST,只要不超过ViewGroup即可
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                // 这种情况和match_parent一样
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
            // 这种情况比较少,一般用在ListView、GridView
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                // 子View给出固定值,使用子Viwe的size,模式为EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                // 后面两种情况都是一样的,因为ViewGroup的实际size还是不确定的
                // 所以子View使用ViewGroup当前的size,模式为UNSPECIFIED
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        // 合成MeasureSpec并返回
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

2、layout
ViewGroup的layout()其实和View是相同的,只是多了一个判断。
值得注意的是,ViewGroup的onLayout()是一个抽象方法,也就是说,子类必须要重写onLayout()方法来实现布局逻辑。一般来说,在onLayout()方法中都是遍历子View,然后调用子View的layout()方法来分别进行布局。

    @Override
    public final void layout(int l, int t, int r, int b) {
        // 相比View的layout,只是多了一个判断
        // 如果layout call被阻断,或者正在进行布局动画,则置位标志位
        // 否则调用super.layout()走正常View layout的逻辑
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

3、draw
之前在View的draw过程中,有说过dispatchDraw()这个方法,因为单个View实际上不存在子View,在View.java中,这个方法是个空实现。在ViewGroup中,实现了此方法,实际上并不需要完全地去关注方法的内容,其中主要还是遍历所有子View,然后使用more |= drawChild(canvas, transientChild, drawingTime);来绘制子View。

未完待改

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值