自定义View(二-番外3-Matrix)

From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige

跟着爱哥打天下

自定义控件其实很简单4:

在讲ColorMatrix的时候说过其是一个4x5的颜色矩阵,而同样,我们的Matrix也是一个矩阵,只不过不是4*5而是3*3的位置坐标矩阵:

这里写图片描述

变换变换,既然说到变换那么必定涉及最基本的旋转啊、缩放啊、平移之类,而在Matrix中除了该三者还多了一种:错切,什么叫错切呢?所谓错切数学中也称之为剪切变换,原理呢就是将图形上所有点的X/Y坐标保持不变而按比例平移Y/X坐标,并且平移的大小和某点到X/Y轴的垂直距离成正比,变换前后图形的面积不变。其实对于Matrix可以这样说:图形的变换实质上就是图形上点的变换,而我们的Matrix的计算也正是基于此,比如点P(x0,y0)经过上面的矩阵变换后会去到P(x,y)的位置:

这里写图片描述

注:除了平移外,缩放、旋转、错切、透视都是需要一个中心点作为参照的,如果没有平移,默认为图形的[0,0]点,平移只需要指定移动的距离即可,平移操作会改变中心点的位置!非常重要!记牢了!

有一点需要注意的是,矩阵的乘法运算是不符合交换律的,因此矩阵B*A和A*B是两个截然不同的结果,前者表示A右乘B,是列变换;后者表示A左乘B,是行变换。如果有心的童鞋会发现Matrix类中会有三大类方法:setXXX、preXXX和postXXX,而preXXX和postXXX就是分别表示矩阵的左右乘,也有前后乘的说法,对于不懂矩阵的人来说都一样 = = ……但是要注意一点!!!大家在理解Matrix的时候要把它想象成一个容器,什么容器呢?存放我们变换信息的容器,Matrix的所有方法都是针对其自身的!!!!当我们把所有的变换操作做完后再“一次性地”把它注入我们想要的地方,比如上面我们为shader注入了一个Matrix。还有一点要注意,一定要注意:ColorMatrix和Matrix在应用给其他对象时都是左乘的,而其自身内部是可以左右乘的!千万别搞混了!

上图的公式中,GHI都表示的是透视参数,一般情况下我们不会去处理,三维的透视我更乐意使用Camare,所以很多时候G和H的值都为0而I的值恒为1

所有的Matrix变换中最好理解的其实是缩放变换,因为缩放的本质其实就是图形上所有的点X/Y轴沿着中心点放大一定的倍数,比如:

这里写图片描述

这么一个矩阵变换实质就是x = x0 * a、y = y0 * b,难度系数:0

这里写图片描述

X/Y轴分别放大a\b倍
相对来说平移稍难但是也好理解:

这里写图片描述

同理x = x0 + a、y = y0 + b,难度系数:0

这里写图片描述

旋转就很复杂了……分为两种:一种是直接绕默认中点[0,0]旋转,另一种是指定中点,也就是将中点[0,0]平移后在旋转:
直接绕[0,0]顺时针转:

这里写图片描述

唉、这个先看图吧:

这里写图片描述

根据三角函数的关系我们可以得出p(x,y)的坐标:

同样根据三角函数的关系我们也可以得出p(x0,y0)的坐标:

上述两公式结合我们则可以得出简化后的p(x,y)的坐标:

这是什么公式呢?是不是就是上面矩阵的乘积呢?囧……

绕点p(a,b)顺时针转:
其实绕某个点旋转没有想象中的那么复杂,相对于绕中心点来说就多了两步:先将坐标原点移到我们的p(a,b)处然后执行旋转最后再把坐标圆点移回去:

这里写图片描述

对了……开头忘说了……矩阵的乘法是从右边开始的,额,其实也只有上面这算式才有多个矩阵相乘 = = 冏,也就是说最右边的两个会先乘,大家看看最右边的两个乘积是什么……是不是就是我们把原点移动到P(a,b)后[x0,y0]的新坐标啊?然后继续往左乘,旋转一定得角度这跟上面[0,0]旋转是一样的,最后往左乘把坐标还原

这里写图片描述

Android给我们封装的方法:setXXX会重置数据

matrix.preScale(0.5f, 1);   
matrix.setScale(1, 0.6f);   
matrix.postScale(0.7f, 1);   
matrix.preTranslate(15, 0);  

那么Matrix的计算过程即为:translate (15, 0) -> scale (1, 0.6f) -> scale (0.7f, 1),我们说过set会重置数据,所以最开始的

matrix.preScale(0.5f, 1); 

也就GG了
同样地,对于类似的变换:

matrix.preScale(0.5f, 1);   
matrix.preTranslate(10, 0);  
matrix.postScale(0.7f, 1);    
matrix.postTranslate(15, 0);  

其计算过程为:translate (10, 0) -> scale (0.5f, 1) -> scale (0.7f, 1) -> translate (15, 0)

那么对于上图的结果真的是一样的吗?这里我教给大家一个方法自己去验证,Matrix有一个getValues方法可以获取当前Matrix的变换浮点数组,也就是我们之前说的矩阵:

/* 
 * 新建一个9个单位长度的浮点数组 
 * 因为我们的Matrix矩阵是9个单位长的对吧 
 */  
float[] fs = new float[9];  

// 将从matrix中获取到的浮点数组装载进我们的fs里  
matrix.getValues(fs);  
Log.d("Aige", Arrays.toString(fs));// 输出看看呗!

大家觉得好奇的都可以去验证,这三类方法我就不多说了,Matrix中还有其他很多实用的方法,以后我们用到的时候在讲,因为Matrix太常用了

上面我们说到Matrix矩阵最后的3个数是用来设置透视变换的,为什么最后一个值恒为1?因为其表示的是在Z轴向的透视缩放,这三个值都可以被设置,前两个值跟右手坐标系的XY轴有关,大家可以尝试去发现它们之间的规律,我就不多说了。这里多扯一点,大家一定要学会如何透过现象看本质,即便看到的本质不一定就是实质,但是离实质已经不远了,不要一来就去追求什么底层源码啊、逻辑什么的,就像上面的矩阵变换一样,矩阵的9个数作用其实很多人都说不清,与其听别人胡扯还不如自己动手试试你说是吧,不然苦逼的只是你自己。
在实际应用中我们极少会使用到Matrix的尾三数做透视变换,更多的是使用Camare摄像机,比如我们使用Camare让ListView看起来像倒下去一样:(这里只做了解,已超出我们本系列的范畴)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.xiey94.view.view.ag.view4.AnimListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"></com.xiey94.view.view.ag.view4.AnimListView>

</LinearLayout>
public class AnimListView extends ListView {

    //相机
    private Camera mCamera;

    //矩阵
    private Matrix mMatrix;

    public AnimListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mCamera = new Camera();
        mMatrix = new Matrix();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //初始保存当前绘制
        mCamera.save();
        //X轴位面旋转30度,三维
        mCamera.rotate(30, 0, 0);
        //获取矩阵
        mCamera.getMatrix(mMatrix);

        //设置变换矩阵
        mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
        mMatrix.postTranslate(getWidth() / 2, getHeight() / 2);

        //设置变换矩阵:先旋转,变换矩阵,然后平移后矩阵乘以旋转矩阵,最后再乘以平移矩阵,不是两个矩阵的无用操作,而是三个矩阵的操作
        canvas.concat(mMatrix);
        super.onDraw(canvas);
        //还原画布,保留绘制
        mCamera.restore();
    }
}

这里写图片描述

矩阵这一块确实比较头疼,之前的矩阵没学好,所以还得在这里跟着推理计算。

最后绘制的那个图,开始是不想敲的,但是反过来想象,给了自己一巴掌,敲!
这里写图片描述

public class MultiCricleView extends View {
    /**
     * 描边宽度占比
     */
    public static final float STROKE_WIDTH = 1F / 256F,

    /**
     * 大圆小圆线段两端间隔占比
     */
    SPACE = 1F / 64F,

    /**
     * 线段长度占比<连接线>
     */
    LINE_LENGTH = 3F / 32F,

    /**
     * 大圆半径占比
     */
    CRICLE_LARGER_RADIU = 3F / 32F,

    /**
     * 小圆半径
     */
    CRICLE_SMALL_RADIU = 5F / 64F,

    /**
     * 弧半径
     */
    ARC_RADIU = 1F / 8F,

    /**
     * 弧围绕文字半径
     */
    ARC_TEXT_RADIU = 5F / 32F;

    /**
     * 描边画笔、文字画笔、圆弧画笔
     */
    private Paint strokePaint, textPaint, arcPaint;

    /**
     * 控件边长
     */
    private int size;

    /**
     * 描边宽度
     */
    private float strokeWidth;

    /**
     * 中心圆圆心坐标
     */
    private float ccX, ccY;

    /**
     * 大圆半径
     */
    private float largeCircleRadiu;

    /**
     * 线段长宽
     */
    private float lineLength;

    /**
     * 大圆小圆线段两端间隔
     */
    private float space;

    /**
     * 小圆半径
     */
    private float smallCircleRadiu;

    /**
     * 文字的Y轴偏移量
     */
    private float textOffsetY;


    private enum Type {
        LARGER, SAMLL
    }


    //-----------------------------------------属性分割线---------------------------------------------


    //------------------------------------------构造函数----------------------------------------------

    /**
     * 构造函数
     */
    public MultiCricleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        //初始化画笔
        initPaint(context);
    }

    //-----------------------------------------初始化画笔---------------------------------------------

    /**
     * 初始化画笔
     */
    private void initPaint(Context context) {
        /**
         * 初始化描边画笔
         */
        //抗锯齿、抗抖动
        strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        //风格:描边
        strokePaint.setStyle(Paint.Style.STROKE);
        //画笔颜色:白色
        strokePaint.setColor(Color.WHITE);
        //画笔圆润
        strokePaint.setStrokeCap(Paint.Cap.ROUND);

        /**
         * 初始化文字画笔
         */
        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(30);
        //绘制到中间区域
        textPaint.setTextAlign(Paint.Align.CENTER);

        //计算文字画笔Y轴偏移量
        textOffsetY = (textPaint.descent() + textPaint.ascent()) / 2;

        /**
         * 圆弧画笔初始化
         */
        arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        //风格:描边
        arcPaint.setStyle(Paint.Style.STROKE);
        //画笔颜色:白色
        arcPaint.setColor(Color.WHITE);
        //画笔圆润
        arcPaint.setStrokeCap(Paint.Cap.ROUND);


    }


    //--------------------------------------------测量-----------------------------------------------

    /**
     * 测量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //强制宽高一致
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        //控件宽度
        size = w;

        //参数计算
        calculation();

    }


    //------------------------------------------参数计算----------------------------------------------

    /**
     * 参数计算
     */
    private void calculation() {
        //计算描边宽度
        strokeWidth = STROKE_WIDTH * size;

        //计算大圆半径
        largeCircleRadiu = size * CRICLE_LARGER_RADIU;

        //计算线段长度
        lineLength = size * LINE_LENGTH;

        //计算大圆小圆线段两端间隔
        space = size * SPACE;

        //计算小圆半径
        smallCircleRadiu = size * CRICLE_SMALL_RADIU;

        //计算中心圆圆心坐标
        ccX = size / 2;
        ccY = size / 2 + size * CRICLE_LARGER_RADIU;

        //设置参数
        setPara();


    }


    //------------------------------------------设置参数----------------------------------------------

    /**
     * 设置参数
     */
    private void setPara() {
        //设置描边宽度
        strokePaint.setStrokeWidth(strokeWidth);
        arcPaint.setStrokeWidth(strokeWidth);
    }


    //--------------------------------------------绘制-----------------------------------------------

    /**
     * 绘制
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //绘制背景
        canvas.drawColor(0xFFF29B76);

        //绘制中心圆
        canvas.drawCircle(ccX, ccY, largeCircleRadiu, strokePaint);
        //绘制中心圆文字
        canvas.drawText("Studio", ccX, ccY - textOffsetY, textPaint);

        //绘制左上方圆形
        drawTopLeft(canvas);

        //绘制右上方图形
        drawTopRight(canvas);

        //绘制左下方图形
        drawBottomLeft(canvas);

        //绘制下方图形
        drawBottom(canvas);

        //绘制右下方图形
        drawBottomRight(canvas);

    }


    //----------------------------------------绘制左上方圆形-------------------------------------------

    /**
     * 绘制左上方圆形
     */
    private void drawTopLeft(Canvas canvas) {
        //锁定画布
        canvas.save();

        //平移和旋转画布
        canvas.translate(ccX, ccY);
        canvas.rotate(-30);

        //依次画:线、圈、线、圈
        canvas.drawLine(0, -largeCircleRadiu, 0, -lineLength * 2, strokePaint);
        canvas.drawCircle(0, -lineLength * 3, largeCircleRadiu, strokePaint);
        canvas.drawText("Apple", 0, -lineLength * 3 - textOffsetY, textPaint);

        canvas.drawLine(0, -largeCircleRadiu * 4, 0, -lineLength * 5, strokePaint);
        canvas.drawCircle(0, -lineLength * 6, largeCircleRadiu, strokePaint);
        canvas.drawText("Orange", 0, -lineLength * 6 - textOffsetY, textPaint);

        //释放画布
        canvas.restore();

    }

    //----------------------------------------绘制右上方圆形-------------------------------------------

    /**
     * 绘制右上方图形
     */
    private void drawTopRight(Canvas canvas) {
        //锁定画布
        canvas.save();

        //平移和旋转画布
        canvas.translate(ccX, ccY);
        canvas.rotate(30);

        //依次画:线、圆
        canvas.drawLine(0, -largeCircleRadiu, 0, -lineLength * 2, strokePaint);
        canvas.drawCircle(0, -lineLength * 3, largeCircleRadiu, strokePaint);
        canvas.drawText("Tropical", 0, -lineLength * 3 - textOffsetY, textPaint);

        drawTopRightArc(canvas, -lineLength * 3);

        //释放画布
        canvas.restore();
    }

    //----------------------------------------绘制左下方圆形-------------------------------------------

    /**
     * 绘制左下方图形
     */
    private void drawBottomLeft(Canvas canvas) {
        //锁定画布
        canvas.save();

        //平移和旋转画布
        canvas.translate(ccX, ccY);
        canvas.rotate(-100);

        //依次画:线、圆
        canvas.drawLine(0, -largeCircleRadiu - space, 0, -lineLength * 2 - space, strokePaint);
        canvas.drawCircle(0, -lineLength * 2 - smallCircleRadiu - space * 2, smallCircleRadiu, strokePaint);

        canvas.save();
        canvas.translate(0, -lineLength * 2 - smallCircleRadiu - space * 2);
        canvas.rotate(100);
        canvas.drawText("Duck", 0, -textOffsetY, textPaint);
        canvas.restore();

        //释放画布
        canvas.restore();
    }

    //----------------------------------------绘制正下方圆形-------------------------------------------

    /**
     * 绘制正下方图形
     */
    private void drawBottom(Canvas canvas) {
        // 锁定画布
        canvas.save();

        // 平移和旋转画布
        canvas.translate(ccX, ccY);
        canvas.rotate(180);

        // 依次画:(间隔)线(间隔)-圈
        canvas.drawLine(0, -largeCircleRadiu - space, 0, -lineLength * 2 - space, strokePaint);
        canvas.drawCircle(0, -lineLength * 2 - smallCircleRadiu - space * 2, smallCircleRadiu, strokePaint);

        canvas.save();
        canvas.translate(0, -lineLength * 2 - smallCircleRadiu - space * 2);
        canvas.rotate(180);
        canvas.drawText("Cat", 0, -textOffsetY, textPaint);
        canvas.restore();

        // 释放画布
        canvas.restore();


    }

    //----------------------------------------绘制右下方圆形-------------------------------------------

    /**
     * 绘制右下方图形
     */
    private void drawBottomRight(Canvas canvas) {
        // 锁定画布
        canvas.save();

        // 平移和旋转画布
        canvas.translate(ccX, ccY);
        canvas.rotate(100);

        // 依次画:(间隔)线(间隔)-圈
        canvas.drawLine(0, -largeCircleRadiu - space, 0, -lineLength * 2 - space, strokePaint);
        canvas.drawCircle(0, -lineLength * 2 - smallCircleRadiu - space * 2, smallCircleRadiu, strokePaint);

        canvas.save();
        canvas.translate(0, -lineLength * 2 - smallCircleRadiu - space * 2);
        canvas.rotate(-100);
        canvas.drawText("Dog", 0, -textOffsetY, textPaint);
        canvas.restore();

        // 释放画布
        canvas.restore();
    }

    /**
     * 绘制右上角弧形
     */
    private void drawTopRightArc(Canvas canvas, float circleY) {
        canvas.save();

        canvas.translate(0, circleY);
        canvas.rotate(-30);

        float arcRadiu = size * ARC_RADIU;

        RectF oval = new RectF(-arcRadiu, -arcRadiu, arcRadiu, arcRadiu);

        arcPaint.setStyle(Paint.Style.FILL);
        arcPaint.setColor(0x55EC6941);
        canvas.drawArc(oval, -22.5F, -135, true, arcPaint);

        arcPaint.setStyle(Paint.Style.STROKE);
        arcPaint.setColor(Color.WHITE);
        canvas.drawArc(oval, -22.5F, -135, false, arcPaint);

        float arcTextRadiu = size * ARC_TEXT_RADIU;

        canvas.save();
        // 把画布旋转到扇形左端的方向
        canvas.rotate(-135F / 2F);

    /*
     * 每隔33.75度角画一次文本
     */
        for (float i = 0; i < 5 * 33.75F; i += 33.75F) {
            canvas.save();
            canvas.rotate(i);

            canvas.drawText("Aige", 0, -arcTextRadiu, textPaint);

            canvas.restore();
        }

        canvas.restore();

        canvas.restore();
    }

}

再看一眼,还好没放弃!!!

这里写图片描述

根据爱哥留下的问题,并在看过评论区和自己思考之后,修改了一下。

这个画布的旋转差点让我疯了,开始想着到底是怎么转的,然后跟着爱哥的思路走,但是也只能跟着别人的思路走,自己大部分还是转不动的,然后在评论中看到那个转文字之后,自己尝试了一下,然后抠破了一张纸,自己就用这两张纸转来转去,终于转出点思路来:
当save的时候,保留了canvas的位置信息(只说位置);
平移:先把要绘制的图形起始位置拽到原点,便于计算;
旋转:光平移还不行,可能相应的角度并不是针对的xy坐标系的正(负)方向,毕竟在xy轴上的数字才比较好计算,不然还得去用三角函数计算位置,把方向也转过来就方便多了。
restore,这个就是相当于画布就是一个弹簧,之前我们又是拽又是转的,目的达到之后,当然得让他弹回去,便于下一次的操作。

难一点的就是那个字体的旋转和平移:

就说左下方的那个小圆中的文字;
save:记录位置(原始位置);
平移:拽到原点,便于计算;
旋转:摆正方向,便于计算;
画线、画圆
画字:字的位置应该在线的延长线上看起来才算是合理、美观;
但是,这时候,线是竖着的,难道字要竖着写? 何必呢,老规矩嘛:
要画字是吧!更简单一点,一样可以把画布在此基础上再拉扯拉扯嘛!

save:记录当前位置信息(第二个位置);
平移:先把画布拽下来,拽到原点;
旋转,要想达到那种效果,最后我们反着角度转回去,(就当前的位置而言,和我们最终的位置尤其是那个线应该是平行的,这样才能达到效果,所以反着转当初转过来的角度)
然后开始画字
restore:画完得弹到第二个位置上;
……
其他操作
……
restore:全部画完了弹到起始位置。

个人见解,不对求教!

参考1–Android canvas.drawArc() 画圆弧
参考2–安卓开发——详解camera.rotate(x,y,z);的旋转方向
参考3–Android Graphics专题(1)— Canvas基础
参考4–canvas.draw(bitmap,matrix,paint)有什么区别呢?
参考5–Canvas 中 concat 与 setMatrix


1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值