自定义ZoomDragImageView

今天主要讲通过Matrix来更改ImageView图片的大小和拖拽。

使用Matrix首先应该在自定义控件的构造器中写上setScaleType(ScaleType.MATRIX);

Matrix使用起来很简单。1、设置matrix的值,比如缩放图片,matrix。postScale(scaleX,scaleY,x,y)。后面的两个值可以不填默认的是0,0是以该位置为中心进行缩放。2.直接将修改过的matrix设置到图片的matrix中即可。setImageMatrix(setMatrix)关于Matrix的具体详解https://blog.csdn.net/cquwentao/article/details/51445269这篇文章介绍的很详细大家可以看一下。至于postScale和setScale、preScale的区别如下

1、setScale(scaleX,scaleY),首先会将Matrix设置为对角矩阵,即相当于调用reset()方法,然后在这支Matrix的MSCALE_X和MSCALE_Y直接设置为scaleX,scaleY的值。显得会生硬又直接了当。

2、preScale(scaleX,scaleY),不会重置Matrix,而是直接与Matrix之前的MSCALE_X和MSCALE_Y值结合起来(相乘),M' = M *S(scaleX,scaleY);

3、postScale(scaleX,scaleY),不会重置Matrix,而是直接与Matrix之前的MSCALE_X和MSCALE_Y值结合起来(相乘),M' = S(scaleX,scaleY)* M;

pre....和post....的区别就是执行顺序问题。

1、pre...是向后生长的,即对于一个Matrix的设置中,所有pre....是倒着向后执行的

matrix.preScale(2.0f, 3.0f);//  1

matrix.preTranslate(8.0f,7.0f);//  2

执行顺序是先执行preTranslate后执行Scale

2、post...是向前生长的,即对于一个Matrix的设置中,所有post....是顺着向前执行的

 matrix.postScale(2.0f, 3.0f);//  1

matrix.postTranslate(8.0f,7.0f);// 2 

先执行postScale后执行postTranslate

3、当pre和post交替出现的执行顺序

matrix.preRotate(90);  //1    matrix.postScale(2.0f, 3.0f);  //2

matrix.postScale(2.0f, 3.0f);  //1   matrix.preRotate(90);  //2

总是先执行  preRotate。这是重点记住。/

4、混合式使用

 matrix.postScale(2.0f, 3.0f);// 第1步  

matrix.preRotate(90);// 第2步  

matrix.postTranslate(8.0f, 7.0f);// 第3步  

matrix.preScale(1.5f, 2.5f);// 第4步  

发现执行的顺序是   4----2----1---3

matrix.postScale(2.0f, 3.0f);// 第1步  

matrix.preRotate(90);// 第2步  

matrix.setScale(1.4f, 2.6f);// 第3步  

matrix.postTranslate(8.0f, 7.0f);// 第4步  

matrix.preScale(1.5f, 2.5f);// 第5步 

发现setScale之前的都无效。是在这之后执行。执行顺序是3---5---4

按照编程的习惯和要还渐进的效果最好选择post使用。

介绍完Matrix的基本用法再来说一下手势的问题。手势有ScaleGestureDetector识别缩放的手势,方便计算双触摸点的缩放问题,GestureDetector里面有很多触摸的效果,有长按,双击,单击滑动等等。

做一个缩放一般采用的就是双触摸点,手势可以直接使用ScaleGestureDector.OnScaleGestureListener.里面有三个方法主要是onScaleBegin、onScale、onScaleEnd可以做相应的处理。缩放需要随着手指的变动而变化,所以在onScale进行相关处理。OnScale里有个参数ScaleGestureDectector,获取这个值的detector.getScaleFactor既可以得到放大缩小的比例。该值>1即为放大,该值<1即为缩小。缩放会有个界限,如最小不能小于本身,最大不能大于其三倍。先附上onScale代码:

@Override
public boolean onScale(ScaleGestureDetector detector) {
    float scale = getScale();
    float scaleFactor = detector.getScaleFactor();

    if (getDrawable() == null) {
        return true;
    }
    /**
     * 缩放的范围控制
     */
    if ((scale < SCALE_MAX && scaleFactor > 1.0f) || (scale > initScale && scaleFactor < 1.0f)) {
        /**
         * 最大值最小值判断
         */
        if (scaleFactor * scale < initScale) {
            //依次减小缩小比例
            scaleFactor = initScale / scale;
        }
        if (scaleFactor * scale > SCALE_MAX) {
            //依次减小放大比例
            scaleFactor = SCALE_MAX / scale;
        }
        //Log.e(TAG,"scaleFactor:"+scaleFactor);
        /**
         * 设置缩放比例
         */
        mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());

        checkBorderAndCenterWhenScale();
        setImageMatrix(mScaleMatrix);
    }
    return true;
}
/**
 * 获得当前的缩放比例
 * @return
 */
public final float getScale() {
    mScaleMatrix.getValues(matrixValues);
    return matrixValues[Matrix.MSCALE_X];
} 

依次来解释下onScale里面执行的内容。先获取当前matrix的值,判断当前缩放的比例是否越界。如果不是则进行下一步判断。判断缩放后的大小是否越界。如果越界则修改缩放比例

if (scaleFactor * scale < initScale) {
    //依次减小缩小比例,直至最后接近1
    scaleFactor = initScale / scale;
}
if (scaleFactor * scale > SCALE_MAX) {
    //依次减小放大比例,直至最后接近1
    scaleFactor = SCALE_MAX / scale;
}

设置完缩放比例之后要在setImageMatrix之前判断图片范围。

/**
 * 在缩放时,进行图片显示范围的控制
 */
private void checkBorderAndCenterWhenScale() {
    RectF rect = getMatrixRectF();
    float deltaX = 0;
    float deltaY = 0;

    int width = getWidth();
    int height = getHeight();

    // 如果宽或高大于屏幕,则控制范围
    if (rect.width() >= width) {
        if (rect.left > 0) {
            //向左移动的距离为负值
            deltaX = -rect.left;
        }
        if (rect.right < width) {
            //向右移动的距离为正值
            deltaX = width - rect.right;
        }
    }
    if (rect.height() >= height) {
        if (rect.top > 0) {
            //向上移动的距离为负值
            deltaY = -rect.top;
        }
        if (rect.bottom < height) {
            //向下移动的距离为正值
            deltaY = height - rect.bottom;
        }
    }
    // 如果宽或高小于屏幕,则让其居中
    if (rect.width() < width) {
        //这两个式子同种理论
        //deltaX = width * 0.5f - rect.right + 0.5f * rect.width();
        deltaX = width * 0.5f - rect.left - 0.5f * rect.width();
    }
    if (rect.height() < height) {
        //这两个式子同种理论
        //deltaY = height * 0.5f - rect.bottom + 0.5f * rect.height();
        deltaY = height * 0.5f - rect.top - 0.5f * rect.height();
    }
    Log.e(TAG, "deltaX = " + deltaX + " , deltaY = " + deltaY);
    mScaleMatrix.postTranslate(deltaX, deltaY);
}

获取当前matrix的rect

/**
 * 根据当前图片的Matrix获得图片的范围
 * @return
 */
private RectF getMatrixRectF() {
    Matrix matrix = mScaleMatrix;
    RectF rect = new RectF();
    Drawable d = getDrawable();
    if (null != d) {
        rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
        matrix.mapRect(rect);
    }
    return rect;
}

将rectF设置Wie图片的四个顶点。然后判断当前的宽高与View的宽高问题。缩放功能主要是以上实现代码。

接下来说拖拽功能。

在View的onTouchEvent里进行处理。获得触点的个数。允许多触点也可以滑动,所以取多触点的平均值。

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (mGestureDetector.onTouchEvent(event)) {
        return true;
    }
    mScaleGestureDetector.onTouchEvent(event);

    float x = 0, y = 0;
    // 拿到触摸点的个数
    final int pointerCount = event.getPointerCount();
    // 得到多个触摸点的x与y均值
    for (int i = 0; i < pointerCount; i++) {
        x += event.getX(i);
        y += event.getY(i);
    }
    x = x / pointerCount;
    y = y / pointerCount;

    /**
     * 每当触摸点发生变化时,重置mLasX , mLastY
     */
    if (pointerCount != lastPointerCount) {
        isCanDrag = false;
        mLastX = x;
        mLastY = y;
    }

    lastPointerCount = pointerCount;
    RectF rectF = getMatrixRectF();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            float dx = x - mLastX;
            float dy = y - mLastY;

            if (!isCanDrag) {
                isCanDrag = isCanDrag(dx, dy);
            }
            if (isCanDrag) {
                if (getDrawable() != null) {
                    isCheckLeftAndRight = isCheckTopAndBottom = true;
                    // 如果宽度小于屏幕宽度,则禁止左右移动
                    if (rectF.width() < getWidth()) {
                        dx = 0;
                        isCheckLeftAndRight = false;
                    }
                    // 如果高度小于屏幕高度,则禁止上下移动
                    if (rectF.height() < getHeight()) {
                        dy = 0;
                        isCheckTopAndBottom = false;
                    }

                    //设置偏移量
                    mScaleMatrix.postTranslate(dx, dy);
                    //再次校验
                    checkMatrixBounds();
                    setImageMatrix(mScaleMatrix);
                }
            }
            mLastX = x;
            mLastY = y;
            break;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            lastPointerCount = 0;
            break;
        default:
            break;
    }
    return true;
}

这里的主要难度就是执行setImageViewMatrix之前要进行校验,校验图的边界是否越界。

/**
 * 移动时,进行边界判断,主要判断宽或高大于屏幕的
 */
private void checkMatrixBounds() {
    RectF rect = getMatrixRectF();

    float deltaX = 0, deltaY = 0;
    final float viewWidth = getWidth();
    final float viewHeight = getHeight();
    // 判断移动或缩放后,图片显示是否超出屏幕边界
    if (rect.top > 0 && isCheckTopAndBottom) {
        deltaY = -rect.top;
    }
    if (rect.bottom < viewHeight && isCheckTopAndBottom) {
        deltaY = viewHeight - rect.bottom;
    }
    if (rect.left > 0 && isCheckLeftAndRight) {
        deltaX = -rect.left;
    }
    if (rect.right < viewWidth && isCheckLeftAndRight) {
        deltaX = viewWidth - rect.right;
    }
    mScaleMatrix.postTranslate(deltaX, deltaY);
}
/**
 * 是否是推动行为
 *
 * @param dx
 * @param dy
 * @return
 */
private boolean isCanDrag(float dx, float dy) {
    return Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
}

还有一些其他的功能,如双击可放大双击的区域。再次双击可缩小。再就是方便缩放拖拽等问题需要将图置于控件中心。demo代码如下。

public class ZoomImageView extends AppCompatImageView
        implements ScaleGestureDetector.OnScaleGestureListener,
        ViewTreeObserver.OnGlobalLayoutListener {

    private static final String TAG = ZoomImageView.class.getSimpleName();
    public static final float SCALE_MAX = 3.0f;
    private static final float SCALE_MIN = 1.5f;

    /**
     * 初始化时的缩放比例,如果图片宽或高大于屏幕,此值将小于0
     */
    private float initScale = 1.0f;
    private boolean once = true;

    /**
     * 用于存放矩阵的9个值
     */
    private final float[] matrixValues = new float[9];

    /**
     * 缩放的手势检测
     */
    private ScaleGestureDetector mScaleGestureDetector = null;
    private final Matrix mScaleMatrix = new Matrix();

    /**
     * 用于双击检测
     */
    private GestureDetector mGestureDetector;
    private boolean isAutoScale;

    private int mTouchSlop = 10;

    private float mLastX;
    private float mLastY;

    private boolean isCanDrag;
    private int lastPointerCount;

    private boolean isCheckTopAndBottom = true;
    private boolean isCheckLeftAndRight = true;

    public ZoomImageView(Context context) {
        this(context, null);
    }

    public ZoomImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        super.setScaleType(ScaleType.MATRIX);
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                if (isAutoScale == true) {
                    return true;
                }
                float x = e.getX();
                float y = e.getY();
                Log.e("DoubleTap", getScale() + " , " + initScale);
                if (getScale() < SCALE_MIN) {
                    ZoomImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MIN, x, y), 16);
                    isAutoScale = true;
                } else if (getScale() >= SCALE_MIN && getScale() < SCALE_MAX) {
                    ZoomImageView.this.postDelayed(new AutoScaleRunnable(SCALE_MAX, x, y), 16);
                    isAutoScale = true;
                } else {
                    ZoomImageView.this.postDelayed(new AutoScaleRunnable(initScale, x, y), 16);
                    isAutoScale = true;
                }
                return true;
            }
        });
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
    }

    /**
     * 自动缩放的任务
     * @author zhy
     */
    private class AutoScaleRunnable implements Runnable {
        static final float BIGGER = 1.07f;
        static final float SMALLER = 0.93f;
        private float mTargetScale;
        private float tmpScale;

        /**
         * 缩放的中心
         */
        private float x;
        private float y;

        /**
         * 传入目标缩放值,根据目标值与当前值,判断应该放大还是缩小
         * @param targetScale
         */
        public AutoScaleRunnable(float targetScale, float x, float y) {
            this.mTargetScale = targetScale;
            this.x = x;
            this.y = y;
            if (getScale() < mTargetScale) {
                tmpScale = BIGGER;
            } else {
                tmpScale = SMALLER;
            }
        }

        @Override
        public void run() {
            // 进行缩放
            mScaleMatrix.postScale(tmpScale, tmpScale, x, y);
            checkBorderAndCenterWhenScale();
            setImageMatrix(mScaleMatrix);

            final float currentScale = getScale();
            // 如果值在合法范围内,继续缩放
            if (((tmpScale > 1f) && (currentScale < mTargetScale)) || ((tmpScale < 1f) && (mTargetScale < currentScale))) {

                ZoomImageView.this.postDelayed(this, 16);
            } else {
                // 设置为目标的缩放比例
                final float deltaScale = mTargetScale / currentScale;
                mScaleMatrix.postScale(deltaScale, deltaScale, x, y);
                checkBorderAndCenterWhenScale();
                setImageMatrix(mScaleMatrix);
                isAutoScale = false;
            }
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }

    /**
     * 根据图片的宽和高以及屏幕的宽和高,对图片进行缩放以及移动至屏幕的中心。
     * 如果图片很小,那就正常显示,不放大了~
     */
    @Override
    public void onGlobalLayout() {
        if (once) {
            Drawable d = getDrawable();
            if (d == null) {
                return;
            }
            //Log.e(TAG, d.getIntrinsicWidth() + " , " + d.getIntrinsicHeight());
            int width = getWidth();
            int height = getHeight();
            // 拿到图片的宽和高
            int dw = d.getIntrinsicWidth();
            int dh = d.getIntrinsicHeight();
            float scale = 1.0f;
            // 如果图片的宽或者高大于屏幕,则缩放至屏幕的宽或者高
            if (dw > width && dh <= height) {
                scale = width * 1.0f / dw;
            }
            if (dh > height && dw <= width) {
                scale = height * 1.0f / dh;
            }
            // 如果宽和高都大于屏幕,则让其按按比例适应屏幕大小
            if (dw > width && dh > height) {
                scale = Math.min(width * 1.0f / dw, height * 1.0f / dh);
            }
            initScale = scale;

            //Log.e(TAG, "initScale = " + initScale);
            mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) / 2);
            mScaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
            // 图片移动至屏幕中心
            setImageMatrix(mScaleMatrix);
            once = false;
        }
    }

    /**
     * 在缩放时,进行图片显示范围的控制
     */
    private void checkBorderAndCenterWhenScale() {

        RectF rect = getMatrixRectF();
        float deltaX = 0;
        float deltaY = 0;

        int width = getWidth();
        int height = getHeight();

        // 如果宽或高大于屏幕,则控制范围
        if (rect.width() >= width) {
            if (rect.left > 0) {
                //向左移动的距离为负值
                deltaX = -rect.left;
            }
            if (rect.right < width) {
                //向右移动的距离为正值
                deltaX = width - rect.right;
            }
        }
        if (rect.height() >= height) {
            if (rect.top > 0) {
                //向上移动的距离为负值
                deltaY = -rect.top;
            }
            if (rect.bottom < height) {
                //向下移动的距离为正值
                deltaY = height - rect.bottom;
            }
        }
        // 如果宽或高小于屏幕,则让其居中
        if (rect.width() < width) {
            //这两个式子同种理论
            //deltaX = width * 0.5f - rect.right + 0.5f * rect.width();
            deltaX = width * 0.5f - rect.left - 0.5f * rect.width();
        }
        if (rect.height() < height) {
            //这两个式子同种理论
            //deltaY = height * 0.5f - rect.bottom + 0.5f * rect.height();
            deltaY = height * 0.5f - rect.top - 0.5f * rect.height();
        }
        //Log.e(TAG, "deltaX = " + deltaX + " , deltaY = " + deltaY);
        mScaleMatrix.postTranslate(deltaX, deltaY);
    }

    /**
     * 根据当前图片的Matrix获得图片的范围
     * @return
     */
    private RectF getMatrixRectF() {
        Matrix matrix = mScaleMatrix;
        RectF rect = new RectF();
        Drawable d = getDrawable();
        if (null != d) {
            rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
            matrix.mapRect(rect);
        }
        return rect;
    }
    /**
     * 对图片进行缩放的控制,首先进行缩放范围的判断,然后设置mScaleMatrix的scale值
     * @param detector
     * @return
     */
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();

        if (getDrawable() == null) {
            return true;
        }
        /**
         * 缩放的范围控制
         */
        if ((scale < SCALE_MAX && scaleFactor > 1.0f) || (scale > initScale && scaleFactor < 1.0f)) {
            /**
             * 最大值最小值判断
             */
            if (scaleFactor * scale < initScale) {
                //依次减小缩小比例
                scaleFactor = initScale / scale;
            }
            if (scaleFactor * scale > SCALE_MAX) {
                //依次减小放大比例
                scaleFactor = SCALE_MAX / scale;
            }
            //Log.e(TAG,"scaleFactor:"+scaleFactor);
            /**
             * 设置缩放比例
             */
            mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());

            checkBorderAndCenterWhenScale();
            setImageMatrix(mScaleMatrix);
        }
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
    }
    /**
     * 我们让OnTouchListener的MotionEvent交给ScaleGestureDetector进行处理
     * public boolean onTouch(View v, MotionEvent event){
     * return mScaleGestureDetector.onTouchEvent(event);
     * }
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mGestureDetector.onTouchEvent(event)) {
            return true;
        }
        mScaleGestureDetector.onTouchEvent(event);

        float x = 0, y = 0;
        // 拿到触摸点的个数
        final int pointerCount = event.getPointerCount();
        // 得到多个触摸点的x与y均值
        for (int i = 0; i < pointerCount; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }
        x = x / pointerCount;
        y = y / pointerCount;

        /**
         * 每当触摸点发生变化时,重置mLasX , mLastY
         */
        if (pointerCount != lastPointerCount) {
            isCanDrag = false;
            mLastX = x;
            mLastY = y;
        }

        lastPointerCount = pointerCount;
        RectF rectF = getMatrixRectF();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (rectF.width() > getWidth() || rectF.height() > getHeight()) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                float dx = x - mLastX;
                float dy = y - mLastY;

                if (!isCanDrag) {
                    isCanDrag = isCanDrag(dx, dy);
                }
                if (isCanDrag) {
                    if (getDrawable() != null) {
                        isCheckLeftAndRight = isCheckTopAndBottom = true;
                        // 如果宽度小于屏幕宽度,则禁止左右移动
                        if (rectF.width() < getWidth()) {
                            dx = 0;
                            isCheckLeftAndRight = false;
                        }
                        // 如果高度小于屏幕高度,则禁止上下移动
                        if (rectF.height() < getHeight()) {
                            dy = 0;
                            isCheckTopAndBottom = false;
                        }

                        //设置偏移量
                        mScaleMatrix.postTranslate(dx, dy);
                        //再次校验
                        checkMatrixBounds();
                        setImageMatrix(mScaleMatrix);
                    }
                }
                mLastX = x;
                mLastY = y;
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                lastPointerCount = 0;
                break;
            default:
                break;
        }
        return true;
    }

    /**
     * 获得当前的缩放比例
     * @return
     */
    public final float getScale() {
        mScaleMatrix.getValues(matrixValues);
        return matrixValues[Matrix.MSCALE_X];
    }

    /**
     * 移动时,进行边界判断,主要判断宽或高大于屏幕的
     */
    private void checkMatrixBounds() {
        RectF rect = getMatrixRectF();

        float deltaX = 0, deltaY = 0;
        final float viewWidth = getWidth();
        final float viewHeight = getHeight();
        // 判断移动或缩放后,图片显示是否超出屏幕边界
        if (rect.top > 0 && isCheckTopAndBottom) {
            deltaY = -rect.top;
        }
        if (rect.bottom < viewHeight && isCheckTopAndBottom) {
            deltaY = viewHeight - rect.bottom;
        }
        if (rect.left > 0 && isCheckLeftAndRight) {
            deltaX = -rect.left;
        }
        if (rect.right < viewWidth && isCheckLeftAndRight) {
            deltaX = viewWidth - rect.right;
        }
        mScaleMatrix.postTranslate(deltaX, deltaY);
    }

    /**
     * 是否是推动行为
     *
     * @param dx
     * @param dy
     * @return
     */
    private boolean isCanDrag(float dx, float dy) {
        return Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
    }
}

详细demo链接:https://github.com/Maliola/Martix.git

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值