Android绘图(三)双缓存技术

一、概述

什么叫“双缓存”?说白了就是有两个绘图区,一个是 Bitmap 的 Canvas,另一个就是当前
View 的 Canvas。先将图形绘制在 Bitmap 上,然后再将 Bitmap 绘制在 View 上,也就是说,我们 在 View 上看到的效果其实就是 Bitmap 上的内容。这样做有什么意义呢?概括起来,有以下几
点:
1)高绘图性能

先将内容绘制在 Bitmap 上,再统一将内容绘制在 View 上,可以提高绘图的性能。

2)可以在屏幕上展示绘图的过程

将线条直接绘制在 View 上和先绘制在 Bitmap 上再绘制在 View 上是感受不到这个作用的,但是,如果是画一个矩形呢?情况就完全不一样了。我们用手指在屏幕上按下,斜拉,此时应该从按下的位置开始,拉出一个随手指变化大小的矩形。因为要向用户展示整个过程,所以需要不断绘制矩形,但是,对,但是,手指抬起后留下的其实只需要最后一个,所以,问题就在这里。怎么解决呢?使用双缓存。在 View 的onDraw()方法中绘制用于展示绘制过程的矩形,在手指移动的过程中,会不断刷新重绘,用户总能看到当前应有的大小的矩形,而且不会留下历史痕迹(因为重绘了,只重绘最后一次的)。

3)保存绘图历史

前面提到,因为直接在 View 的 Canvas 上绘图不会保存历史痕迹,所以也带来了副作用,以前绘制的内容也没有了(可能当前绘制的是第二个矩形),这个时候,双缓存的优势就体现出来了,我们可以将绘制的历史结果保存在一个 Bitmap 上,当手指松开时,将最后的矩形绘制在 Bitmap 上,同时再将 Bitmap 的内容整个绘制在 View 上。

二、在屏幕上绘制曲线

这是一个入门级的讨论,在屏幕上绘制曲线根本不会遇到什么问题,只要知道在屏幕上随手指绘制曲线的原理就行了。我们简要的分析一下。我们在屏幕上绘制的曲线,本质上是由无数条直线构成的,就算曲线比较平滑,看不到折线,也是由于构成曲线的直线足够短,我们用下面的示意图来说明这个问题:
在这里插入图片描述
当手指在屏幕上移动时,会产生三个动作:手指按下(ACTION_DOWN)、手指移动(ACTION_MOVE)、手指松开(ACTION_UP)。手指按下时,要记录手指所在的坐标,假设此时的x 方向和 y 方向的坐标分别为 preX 和 preY,当手指在屏幕上移动时,系统会每隔一段时间自动告知手指的当前位置,假设手指的当前位置是 x 和 y。现在,上一个点的坐标为(preX,preY),当前点的坐标是(x,y),调用drawLine(preX, preY, x, y, paint)方法可以将这两个点连接起来,同时,当前点的坐标会成为下一条直线的上一个点的坐标,preX=x,preY=y,如此循环反复,直 到松开手指,一条由若干条直线组成的曲线便绘制好了。另外,虽然我们知道,调用 View 的 invalidate()方法重绘时,最终调用的是 onDraw()方法, 但一定要注意,由于重绘请求最终会一级级往上提交到 ViewRoot,然后ViewRoot 再调用scheduleTraversals()方法发起重绘请求,而 scheduleTraversals()发送的是异步消息,所以,在通过手势绘制线条时,为了解决这个问题,可以使用 Path 绘图,但如果要保存绘图历史,就要使用双缓存技术了。

2.1错误示例-在屏幕上绘制曲线

下面展示错误的代码

public class MyView extends View {
    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private Paint paint;
    // 上一个点的坐标
    private int preX, preY;
    // 当前点的坐标
    private int currentX, currentY;

    private void init() {
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.WHITE);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        // 绘制直线
        canvas.drawLine(preX, preY, currentX, currentY, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 手指按下,记录第一个点的坐标
                preX = x;
                preY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //手指移动,记录当前点的坐标
                currentX = x;
                currentY = y;
                this.invalidate();
                break;
            case MotionEvent.ACTION_UP:
                invalidate();
                break;
        }
        return true;
    }
}

效果图:
在这里插入图片描述
可以看到每次只能画一条线,上一次画的内容会消失不见,这是因为我们没有采用"双缓存技术"来保存历史记录

2.2 使用“双缓存技术”-在屏幕上绘制曲线

代码调整如下:


private Paint paint;
// 上一个点的坐标
private int preX, preY;
// 当前点的坐标
private int currentX, currentY;

/**
 * Bitmap 缓存区
 */
private Bitmap bitmapBuffer;
private Canvas bitmapCanvas;

private void init() {
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // 此方法会在onLayout之后回调,这样就可以确保拿到View的宽高了
    if (bitmapBuffer == null) {
        // 创建和View的宽高等同的bitmap
        bitmapBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        // 关联Canvas
        bitmapCanvas = new Canvas(bitmapBuffer);
    }
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.BLACK);
    //将缓存中的Bitmap内容绘制在 View 上
    canvas.drawBitmap(bitmapBuffer, 0, 0, null);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            preX = x;
            preY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            //手指移动,记录当前点的坐标
            currentX = x;
            currentY = y;
            // 将线条绘制到缓存bitmapBuffer中
            bitmapCanvas.drawLine(preX, preY, currentX, currentY, paint);
            // 刷新View
            this.invalidate();
            //当前点的坐标成为下一个点的起始坐标
            preX = currentX;
            preY = currentY;
            break;
        case MotionEvent.ACTION_UP:
            invalidate();
            break;
    }
    return true;
}

首先定义了一个名为 bitmapBuffer 的 Bitmap 对象,为了在该对象上绘图,创建了一个与之关联的Canvas 对象 bitmapCanvas。创建 Bitmap 对象时,需要考虑它的大小,在 MyView类的构造方法中,因为此时MyView 尚未创建,还不知道宽度和高度,所以,重写了 onSizeChanged()方法,该方法在组件创建后且大小发生改变时回调(View 第一次显示时肯定会调用),代码中看到,Bitmap 对象的宽度和高度与 View 相同。手指按下后,将第一次的坐标值保存在 preX 和 preY两个变量中,手指移动时,获取手指所在的新位置,并保存到 currentX 和 currentY 中,此时,已经知道了起点和终点两个点的坐标,将这两个点确定的一条直线绘制到 bitmapBuffer 对象,然后,立马又将 bitmapBuffer 对象绘制在 View 上,最后,重新设置 preX 和 preY 的值,确保(preX,preY)成为下一个点的起始点坐标。从下面的运行效果中看出,bitmapBuffer 对象保存了所有的绘图历史,这也是双缓存的作用之一。效果图如下:
在这里插入图片描述

2.3 使用Path优化-在屏幕上绘制曲线

上面的案例中,我们直接在 Bitmap 关联的 Canvas 上绘制直线,其实更好的做法是通过 Path来绘图,不管从功能上还是效率上这都是更优的选择,主要体现在:

  1. Path 可以用于保存实时绘图坐标,避免调用 invalidate()方法重绘时因 ViewRoot 的
    scheduleTraversals()方法发送异步请求出现的问题;
  2. Path 可以用来绘制复杂的图形;
  3. 使用 Path 绘图效率更高。

上代码:


private Paint paint;
// 上一个点的坐标
private int preX, preY;
// 操作的路径
private Path path;

private void init() {
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
    path = new Path();
}


@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.BLACK);
    // 绘制路径
    canvas.drawPath(path, paint);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            path.reset();
            preX = x;
            preY = y;
            // 移动到首次按下的点
            path.moveTo(x, y);
            break;
        case MotionEvent.ACTION_MOVE:
            // 连接到目标点,这里控制点和上一个点是同一个,表示控制点在线上
            path.quadTo(preX, preY, x, y);
            // 刷新View
            this.invalidate();
            // 修改控制点
            preX = x;
            preY = y;
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    return true;
}

效果图如下:
在这里插入图片描述

上面使用了 Path 来绘制曲线,Path 对象保存了手指从按下到移动到松开的整个运动轨迹,进行第二次绘制时,Path 调用 reset()方法重置,继续进行下一条曲线的绘图。通过调用 quadTo()方法绘制二阶贝塞尔曲线,因为需要指定一个起始点,所以手指按下时调用了 moveTo(x,y)方法。但是,运行后我们发现,绘制当前曲线没有问题,但绘制下一条曲线的时候前一条曲线消失了(这是因为每次down的时候path都reset了),如果要保存绘图历史,这需要通过“双缓存”技术来解决。

2.4 使用Path优化+“双缓存技术”-在屏幕上绘制曲线

直接上代码:


private Paint paint;
// 上一个点的坐标
private int preX, preY;
// 操作的路径
private Path path;

/**
 * Bitmap 缓存区
 */
private Bitmap bitmapBuffer;
private Canvas bitmapCanvas;

private void init() {
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
    path = new Path();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapBuffer == null) {
        bitmapBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        bitmapCanvas = new Canvas(bitmapBuffer);
    }
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.BLACK);
    // 绘制历史路径
    canvas.drawBitmap(bitmapBuffer, 0, 0, null);
    // 绘制当前路径
    canvas.drawPath(path, paint);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            path.reset();
            preX = x;
            preY = y;
            // 移动到首次按下的点
            path.moveTo(x, y);
            break;
        case MotionEvent.ACTION_MOVE:
            // 连接到目标点,这里控制点和上一个点是同一个,表示控制点在线上
            path.quadTo(preX, preY, x, y);
            // 刷新View
            this.invalidate();
            // 修改控制点
            preX = x;
            preY = y;
            break;
        case MotionEvent.ACTION_UP:
            // 手指松开后将最终的path绘图结果绘制在 bitmapBuffer中,因为path在移动的过程中会不断的记录
            bitmapCanvas.drawPath(path,paint);
            invalidate();
            break;
    }
    return true;
}

效果图:
在这里插入图片描述

2.5 优化path的控制点-在屏幕上绘制曲线(终极方案)

我们在画曲线时,使用了 Path 类的 quadTo()方法,该方法能绘制出相对平滑的贝塞尔曲线, 但是控制点和起点使用了同一个点,这样效果不是很理想。现供一种计算控制点的方法,假如起点坐标为(x1,y1),终点坐标为(x2,y2),控制点坐标即为((x1+x2)/2,(y1+y2)/2)。

下面将case MotionEvent.ACTION_MOV 处的代码可以改为:

case MotionEvent.ACTION_MOVE:
    //使用贝塞尔曲线进行绘图,需要一个起点(preX,preY),一个终点(x,y),一个控制点((preX+x)/2,(preY+y)/2))
    int controlX = (x + preX) / 2;
    int controlY = (y + preY) / 2;
    //手指移动过程中只显示绘制路径过程
    path.quadTo(controlX, controlY, x, y);
    invalidate();
    preX = x;
    preY = y;
break;

效果图:
在这里插入图片描述

是不是感觉圆滑很多了.

三、在屏幕上绘制矩形

绘制矩形的逻辑和曲线不一样,手指按下时,记录初始坐标(firstX,firstY),手指移动过程中,不断获取新的坐标(x,y),然后以(firstX,firstY)为左上角位置,(x,y)为右下角位置画出矩形,矩形的 4 个属性 left、top、right 和 bottom 的值分别为 firstX、firstY、x 和 y。我们首先实现没有使用双缓存技术的效果。

3.1 错误示例-在屏幕上绘制矩形

private Paint paint;
// 上一个点的坐标
private int firstX, firstY;
// 操作的路径
private Path path;

private void init() {
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
    path = new Path();
}


@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.BLACK);
    // 绘制当前路径
    canvas.drawPath(path, paint);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            path.reset();
            firstX = x;
            firstY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            //绘制矩形时,要先清除前一次的结果
            path.reset();
            path.addRect(new RectF(firstX, firstY, x, y), Path.Direction.CCW);
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            invalidate();
            break;
    }
    return true;
}

效果图如下:
在这里插入图片描述
可以看到和前面的曲线一样,并没有显示历史绘图,因为 invalidate 后绘图历史根本没有保存,Path对象中只保存当前正在绘制的矩形信息。要实现正确的效果,必须将每一次的绘图都保存在Bitmap 缓存中,这样,Bitmap 保存绘图历史,Path 中保存当前正在绘制的内容,即实现了功能,又照顾了用户体验。

3.2 使用“双缓冲技术”-在屏幕上绘制矩形

上代码:


private Paint paint;
// 上一个点的坐标
private int firstX, firstY;
// 操作的路径
private Path path;
/**
 * Bitmap 缓存区
 */
private Bitmap bitmapBuffer;
private Canvas bitmapCanvas;


private void init() {
    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE);
    paint.setStrokeJoin(Paint.Join.ROUND);
    paint.setStrokeCap(Paint.Cap.ROUND);
    paint.setStrokeWidth(5);
    path = new Path();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (bitmapBuffer == null) {
        bitmapBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        bitmapCanvas = new Canvas(bitmapBuffer);
    }
}


@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.BLACK);
    // 绘制历史路径
    canvas.drawBitmap(bitmapBuffer, 0, 0, null);
    // 绘制当前路径
    canvas.drawPath(path, paint);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 手指按下,记录第一个点的坐标
            path.reset();
            firstX = x;
            firstY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            //绘制矩形时,要先清除前一次的结果
            path.reset();
            path.addRect(new RectF(firstX, firstY, x, y), Path.Direction.CCW);
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            // 手指松开后将最终的path绘图结果绘制在 bitmapBuffer中,因为path在移动的过程中会不断的记录
            bitmapCanvas.drawPath(path, paint);
            invalidate();
            break;
    }
    return true;
}

效果图如下:
在这里插入图片描述
不过,上面的实现并不完美,只支持↘方向的绘图,另外三个方向↖、↙、↗就无能为力了(大家可以感受一下)。因此,我们需要在手指进行任意方向的移动时,重新计算矩形的 left、top、right 和 bottom 四个属性值。

3.3 实现四个方向-在屏幕上绘制矩形

如下图所示手指的移动方向不同,(firstX,firstY)和(x,y)代表的将是不同的角的坐标,那么,矩形的 left、top、right 和 bottom 四个属性值也会发生变化
在这里插入图片描述
只需要在上一节的基础上修改onTouchEvent的case MotionEvent.ACTION_MOVE语句如下即可:

case MotionEvent.ACTION_MOVE:
    //绘制矩形时,要先清除前一次的结果
    path.reset();
    if (firstX < x && firstY < y) {
        //↘方向
        path.addRect(firstX, firstY, x, y, Path.Direction.CCW);
    } else if (firstX > x && firstY > y) {
        //↖方向
        path.addRect(x, y, firstX, firstY, Path.Direction.CCW);
    } else if (firstX > x && firstY < y) {
        //↙方向
        path.addRect(x, firstY, firstX, y, Path.Direction.CCW);
    } else if (firstX < x && firstY > y) {
        //↗方向
        path.addRect(firstX, y, x, firstY, Path.Direction.CCW);
    }
    invalidate();
    break;

效果图:
在这里插入图片描述

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
封面 1 序 2 捐助说明 5 目 录 7 第一章 View的绘图流程 12 1.1、概述 12 1.2、Activity的组成结构 13 1.3、View树的绘图流程 15 1.3.1 测量组件大小 16 1.3.2 确定子组件的位置 17 1.3.3 绘制组件 18 1.4、说点别的 22 1.5 练习作业 22 第二章 Graphics2D API 23 2.1、概述 23 2.2、Point类和PointF类 23 2.3、Rect类和RectF类 25 2.4、Bitmap类和BitmapDrawable类 32 2.5、Canvas类与Paint类 34 2.5.1 绘图概述 34 2.5.2 Paint类 34 2.5.3 Canvas类 39 2.6 练习作业 63 第章 使用Graphics2D实现动态效果 64 3.1 概述 64 3.2 invalidate()方法 65 3.3 坐标转换 69 3.4 剪切区(Clip) 73 3.5 案例:指针走动的手表 82 3.6 练习作业 88 第四章 缓存技术 89 4.1 缓存 89 4.2 在屏幕上绘制曲线 90 4.3 在屏幕上绘制矩形 99 4.4 案例:绘图App 104 4.4.1 绘图属性 106 4.4.2 软件参数 108 4.4.3 绘图缓冲区 109 4.4.4 撤消操作 111 4.4.5 图形绘制 113 4.4.6 绘图区 118 4.4.7 主界面 119 4.5 练习作业 122 第五章 阴影、渐变和位图运算 123 5.1 概述 123 5.2 阴影 123 5.3 渐变 125 5.3.1 线性渐变(LinearGradient) 126 5.3.2 径向渐变(RadialGradient) 130 5.3.3 扫描渐变(SweepGradient) 135 5.3.4 位图渐变(BitmapShader) 138 5.3.5 混合渐变(ComposeShader) 140 5.3.6 渐变与Matrix 142 5.4 位图运算 143 5.4.1 PorterDuffXfermode 143 5.4.2 图层(Layer) 146 5.4.3 位图运算技巧 148 5.5 案例1:圆形头像 152 5.6 案例2:刮刮乐 156 5.7 练习作业 161 第六章 自定义组件 163 6.1 概述 163 6.2 自定义组件的基本结构 164 6.3 重写onMeasure方法 166 6.4 组件属性 175 6.4.1 属性的基本定义 175 6.4.2 读取来自style和theme中的属性 181 6.5 案例1:圆形ImageView组件 186 6.6 案例2:验证码组件CodeView 190 6.7 练习作业 202 第七章 自定义容器 204 7.1 概述 204 7.2 ViewGroup类 205 7.2.1 ViewGroup常用方法 205 7.2.2 ViewGroup的工作原理 208 7.2.3 重写onLayout()方法 213 7.3 CornerLayout布局 217 7.3.1 基本实现 217 7.3.2 内边距padding 224 7.3.3 外边距margin 228 7.3.4 自定义LayoutParams 238 7.4 案例:流式布局(FlowLayout) 246 7.5 练习作业 256 第八章 Scroller与平滑滚动 257 8.1 概述 257 8.2 认识scrollTo()和scrollBy()方法 258 8.3 Scroller类 264 8.4 平滑滚动的工作原理 271 8.5 案例:触摸滑屏 272 8.5.1 触摸滑屏的技术分析 272 8.5.2 速度跟踪器VelocityTracker 273 8.5.3 触摸滑屏的分步实现 274 8.6 练习作业 285 第九章 侧边栏 287 9.1 概述 287 9.2 使用二进制保存标识数据 289 9.2.1 位运算符 289 9.2.2 位运算的常用功能 292 9.3 继承自ViewGroup的侧边栏 293 9.4 继承自HorizontalScrollView的侧边栏 304 9.5 练习作业 312 第十章 加强版ListView 313 10.1 概述 313 10.2 ListView的基本使用 314 10.3 ListItem随手指左右滑动 318 10.4 向右滑动删除ListItem 326 10.5 滑动ListItem出现删除按钮 336 10.5.1 列表项专用容器ExtendLayout 337 10.5.2 列表项能滑出删除按钮的ListView 342 10.5.3 定义布局文件 350 10.5.4 显示ListView 351 10.6练习作业 353 案例代码说明 354

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值