Android 自定义图片裁剪框功能

Android自定义图片裁剪框功能

大体的功能如上gif所示,最后蓝色裁剪框中的矩形图片区域可以进行截取并返回一个Bitmap对象。整个裁剪功能由两个自定义的View组件完成,首先是图片显示控件DragScaleView,主要完成图片的双指触碰缩放和单指触碰滑动,以及根据裁剪框的变化对图片进行对应的平移和缩放;然后就是裁剪框控件DrawBoxView,他俩的父布局是FrameLayout,所以DrawBoxView是覆盖在DragScaleView组件上的,点击事件的处理优先级DrawBoxView>DragScaleView。
 

1.DrawBoxView

绘制裁剪框的代码:

paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);//设置填充样式
paint.setStrokeWidth(4);
canvas.drawRect(mRect, paint);
float midHorizontal = (mRect.left + mRect.right) / 2;//水平方向中点
float midVertical = (mRect.top + mRect.bottom) / 2;//垂直方向中点
//绘制八个控制点
paint.setStrokeWidth(10);
float[] pts = {mRect.left - 5, mRect.top, mRect.left + 40, mRect.top,
        midHorizontal - 20, mRect.top, midHorizontal + 20, mRect.top,
        mRect.right - 40, mRect.top, mRect.right + 5, mRect.top,

        mRect.left, mRect.top, mRect.left, mRect.top + 40,
        mRect.right, mRect.top, mRect.right, mRect.top + 40,
        mRect.left, midVertical - 20, mRect.left, midVertical + 20,
        mRect.right, midVertical - 20, mRect.right, midVertical + 20,
        mRect.left, mRect.bottom, mRect.left, mRect.bottom - 40,
        mRect.right, mRect.bottom, mRect.right, mRect.bottom - 40,

        mRect.left - 5, mRect.bottom, mRect.left + 40, mRect.bottom,
        midHorizontal - 20, mRect.bottom, midHorizontal + 20, mRect.bottom,
        mRect.right - 40, mRect.bottom, mRect.right + 5, mRect.bottom};
canvas.drawLines(pts, paint);

mRect代表的就是整个裁剪框所表示的矩形区域,八个控制点的绘制就是在mRect对应的位置上用更粗的画笔绘制指定长度即可。这个mRect的初值应等于图片的显示区域大小,例如DragScaleView宽高指定为300dp和500dp,DrawBoxView宽高设置为310dp和510dp,那么DrawBoxView在上下左右四个方向上都会多出5dp的空间来确保八个控制点加粗区域不会被遮挡,所以mRect的初始值即为new RectF(dp2px(5), dp2px(5), dp2px(300)+ dp2px(5), dp2px(500)+ dp2px(5)),dp2px()方法是把dp单位的值换算为像素单位,如图:

图中红色框体就是图片的显示区域即DragScaleView组件的大小,也就是裁剪框的位置大小,可以很清楚的看到DrawBoxView之所以要大一些的原因就是为了裁剪框的八个控制区域可以完整的显示出来。下面就是裁剪框组件和图片组件的点击事件分发,如果触点是在八个控制点上即需要改变裁剪框的大小,那么DrawBoxView就会消耗掉点击事件,如果触点不在八个控制点上即需要对图片直接进行操作,那么DrawBoxView就不消耗该事件,然后点击事件就会按照View树中的顺序传递给DragScaleView,接着就是双指缩放和单指滑动的处理。先来看DrawBoxView中对于触点位置的判断:

@Override
public boolean onTouchEvent(MotionEvent event) {
    mActionMovePoint.x = (int) event.getX();
    mActionMovePoint.y = (int) event.getY();
switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
	… …
if (getPosition(mActionMovePoint.x, mActionMovePoint.y) != -1) {
                return true;//触点在某个控制点上就消耗掉事件
            } else {
                return false;//触点在其他位置就不消耗
            }
        case MotionEvent.ACTION_MOVE:
	… …

先获取手指按下时触点位置,再传递给getPosition方法进行判断,八个点的标号为0-7,其他位置的标号为-1:

private int getPosition(float x, float y) {
if (mPressPointIndex > -1 && mPressPointIndex < 8) {
        return mPressPointIndex;
    }

    int POINT_RADIUS = 2500;//触点可响应区域的半径平方
    if ((x - mRect.left) * (x - mRect.left) + (y - mRect.top) * (y - mRect.top) < POINT_RADIUS) {
        return 0;
    } else if ((x - (mRect.left + mRect.right) / 2) * (x - (mRect.left + mRect.right) / 2) +
            (y - mRect.top) * (y - mRect.top) < POINT_RADIUS) {
        return 1;
    } else if ((x - mRect.right) * (x - mRect.right) + (y - mRect.top) * (y - mRect.top) < POINT_RADIUS) {
        return 2;
    } else if ((x - mRect.left) * (x - mRect.left) + (y - (mRect.top + mRect.bottom) / 2) *
            (y - (mRect.top + mRect.bottom) / 2) < POINT_RADIUS) {
        return 3;
    } else if ((x - mRect.right) * (x - mRect.right) + (y - (mRect.top + mRect.bottom) / 2) *
            (y - (mRect.top + mRect.bottom) / 2) < POINT_RADIUS) {
        return 4;
    } else if ((x - mRect.left) * (x - mRect.left) + (y - mRect.bottom) * (y - mRect.bottom) < POINT_RADIUS) {
        return 5;
    } else if ((x - (mRect.left + mRect.right) / 2) * (x - (mRect.left + mRect.right) / 2) +
            (y - mRect.bottom) * (y - mRect.bottom) < POINT_RADIUS) {
        return 6;
    } else if ((x - mRect.right) * (x - mRect.right) + (y - mRect.bottom) * (y - mRect.bottom) < POINT_RADIUS) {
        return 7;
    }
    return -1;
}

如果触点是八个点中的一个,就要在ACTION_MOVE中去改变mRect的大小并实时重绘来实现裁剪框在跟随手指移动的效果:

case MotionEvent.ACTION_MOVE:
switch (getPosition(mActionMovePoint.x, mActionMovePoint.y)) {
        case 0:
            mRect.left = mActionMovePoint.x;
            mRect.top = mActionMovePoint.y; 
            mPressPointIndex = 0;
            break;
case 1:
            mRect.top = mActionMovePoint.y; 
            mPressPointIndex = 1;
            break;
	… …
}
invalidate();
case MotionEvent.ACTION_UP: 
… …
    mPressPointIndex = -1;
    break;
… …

在按下控制点调整裁剪框的过程中,ACTION_MOVE中的逻辑一直在被重复执行,即从按下左上角控制点后开始滑动以调整裁剪框大小到手指抬起的整个过程中mPressPointIndex的值都是0,手指抬起时再置为-1,所以在getPosition方法的最开始有这个if判断:

if (mPressPointIndex > -1 && mPressPointIndex < 8) {
	return mPressPointIndex;
}

只有当mPressPointIndex的值为-1时才会走后面的逻辑来重新判断触点的位置,如果mPressPointIndex的值为0-7则说明此时手指还在滑动尚未抬起,mPressPointIndex的值就一定是当前值不用再走后面的一大串if判断直到手指抬起mPressPointIndex重新置为-1,然后手指再次按下,那么就重新进行判断。

手指抬起后需要做的是将裁剪框进行放大,直到宽或高达到最大值,这个最大值就是mRect的初始宽高

case MotionEvent.ACTION_UP:
if (mPressPointIndex != -1) {
        float xScale = mInitRect.width() / mRect.width();
        float yScale = mInitRect.height() / mRect.height();
        float scale = xScale > yScale ? yScale : xScale;
        if (mCropListener!=null) {
            mCropListener.onUp(mPressPointIndex, scale, xOffset, yOffset, mRect);
        }
        doAnimation(scale);
    }
    mPressPointIndex = -1;
    break;

这个CropListener接口的onUp方法就是在手指抬起时把裁剪框的缩放比例scale传给DragScaleView,然后图片控件会根据这个scale对图片进行等比缩放,裁剪框的缩放动画都在doAnimation方法中,其实就是动态设置mRect的left、top、right、bottom四个属性值,要注意的是这四个值还记得要加上dp2px(5):

private void doAnimation(float scale) {
float left = (mInitRect.width() - mRect.width() * scale) / 2 + dp2px(5);//动画结束后left的值
    float right = left + mRect.width() * scale;
    float top = (mInitRect.height() - mRect.height() * scale) / 2 + dp2px(5);
    float bottom = top + mRect.height() * scale;
    ValueAnimator rectAnimator = ValueAnimator.ofObject(new RectEvaluator(), mRect, new RectF(left, top, right, bottom));
    rectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mRect = (RectF) animation.getAnimatedValue();
            invalidate();
        }
    });
    rectAnimator.setDuration(500);
    rectAnimator.start();
}

class RectEvaluator implements TypeEvaluator<RectF> {
    @Override
    public RectF evaluate(float fraction, RectF startValue, RectF endValue) {
        RectF rectf = new RectF();
        rectf.left = startValue.left + (endValue.left - startValue.left) * fraction;
        rectf.right = startValue.right + (endValue.right - startValue.right) * fraction;
        rectf.top = startValue.top + (endValue.top - startValue.top) * fraction;
        rectf.bottom = startValue.bottom + (endValue.bottom - startValue.bottom) * fraction;
        return rectf;
    }
}

这里的属性动画如果四个属性一个一个去设置我觉得太麻烦,就自定义了一个估值器RectEvaluator,这样四个属性一起弄就挺方便的,关于估值器有不懂的可以去看看启舰大神的文章。DrawBoxView里面的主要逻辑就这些,然后裁剪框其实是有一个最大和最小范围,这个范围的设置是跟图片的缩放平移有密切关系,这个比较麻烦,后面会详细说。

2.DragScaleView

在DragScaleView中双指缩放和单指滑动是分别托管给了ScaleGestureDetector mScaleDetector和GestureDetector mGestureDetector两个监听器,这两个监听的响应回调如下:

/缩放
private class SimpleScaleListenerImpl extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScaleFactor *= detector.getScaleFactor();
        mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 4f));//缩放倍数范围:1~4
        invalidate();
        return true;
    }
}

//移动
private class SimpleGestureListenerImpl extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        mPosX -= distanceX;
        mPosY -= distanceY;
        invalidate();
        return true;
    }
}

mScaleFactor是当前图片的缩放倍数,范围是100%-400%,mPosX和mPosY是平移之后画布的坐标,这俩监听器的初始化逻辑如下:

mScaleDetector = new ScaleGestureDetector(context, new SimpleScaleListenerImpl());
mGestureDetector = new GestureDetector(context, new SimpleGestureListenerImpl());

onTouchEvent方法中的逻辑也很简单,直接把event托管过去就可以了:

@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
    //双指缩放
    mScaleDetector.onTouchEvent(event);
    //单指移动
    mGestureDetector.onTouchEvent(event);
    return true;
}

对图片进行缩放后,图片的可平移范围也要动态设置,图片的显示范围以裁剪框为界,如图所示:

这个范围的计算牵扯到一堆变量,我自己也不是很精通算法,在每次重绘调用onDraw方法时都会重新判定图片可以平移的范围,就是这个checkBounds方法:

private void checkBounds() {
if (mScaleFactor >= getWidth() / mNewWidth) {
        //宽度方向已经填满
        mPosX = Math.min(mPosX, (mScaleFactor - 1) * (mNewWidth / 2) + mMaxRight);//最右 mPosX<=
        mPosX = Math.max(mPosX, getWidth() - mNewWidth - (mScaleFactor - 1) * (mNewWidth / 2) + mMaxLeft);//最左 mPosX>=
    }
    if (mScaleFactor >= getHeight() / mNewHeight) {
        //高度方向已经填满
        mPosY = Math.min(mPosY, (mScaleFactor - 1) * (mNewHeight / 2) + mMaxBottom);//最下 mPosY<=
        mPosY = Math.max(mPosY, getHeight() - mNewHeight - (mScaleFactor - 1) * (mNewHeight / 2) + mMaxTop);//最上 mPosY>=
    }
}

如上图中所示,以向右平移为例,图中蓝色和绿色的线段长度分别代表(mScaleFactor - 1) * (mNewWidth / 2)和mMaxRight的值,mNewWidth是显示区域的宽度(图片初始宽度),至于mMaxRight的计算很麻烦,并不是简单的用显示区域的宽度减去当前裁剪框宽度除以2就可以了,这个值会影响整个缩放动画的执行,必须以动效的形式指定这个值不然会有问题:

float dx = x / 2 - rectF.width() * (scale - 1) / 2;
float dy = y / 2 - rectF.height() * (scale - 1) / 2;

这个rectF是手指抬起那一瞬间的裁剪框对应的矩形区域,x、y是手指按下到手指抬起两点间的x、y方向的偏移,计算出来的dx和dy就是单次滑动裁剪框之后mMaxRight、mMaxLeft、mMaxBottom、mMaxTop的增量或者减量,然后得添加为属性动画才可以保证在执行动画期间图片不会超出四个方向上的边界,否则可能出现图片闪烁的问题,这里面的原因牵扯太多不再赘述了:

ValueAnimator maxPosAnimator = ValueAnimator.ofObject(new MaxPosEvaluator(), new MaxPosition(mMaxLeft, mMaxRight, mMaxTop, 
mMaxBottom), new MaxPosition(mMaxLeft - dx, mMaxRight + dx, mMaxTop - dy, mMaxBottom + dy));

同样的,图片平移有范围限制,那么裁剪框的滑动也是被限制的,裁剪框以图片边界为界,最大只能滑动到显示区域的边界,如下图所示:

这个范围的确定必须根据mPosX和mPosY两个值的变化量来确定,DragScaleView的onDraw方法中会去调UpdateListener的onChange方法,把实时的mPosX、mPosY、mScaleFactor三个值传给DrawBoxView,然后在DrawBoxView的ACTION_MOVE中实时判断可滑动区域,这个算法逻辑也挺复杂,写到最后我自己也有点懵,代码如下:

float xMaxLeftOffset = 0f;//裁剪框往左方向(包括左上、左、左下)可滑动到的最大偏移量
float yMaxTopOffset = 0f;//裁剪框往上方向(包括左上、上、右上)可滑动到的最大偏移量
float xMaxRightOffset = 0f;//裁剪框往右方向(包括右上、右、右下)可滑动到的最大偏移量
float yMaxBottomOffset = 0f;//裁剪框往下方向(包括左下、下、右下)可滑动到的最大偏移量
@Override
public boolean onTouchEvent(MotionEvent event) {
    mActionMovePoint.x = (int) event.getX();
    mActionMovePoint.y = (int) event.getY();
    switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            Toast.makeText(getContext(), "" + getPosition(event.getX(), event.getY()), Toast.LENGTH_SHORT).show();
            actionDownRectLeft = mRect.left;
            actionDownRectTop = mRect.top;
            actionDownRectRight = mRect.right;
            actionDownRectBottom = mRect.bottom;
            xMaxLeftOffset = mRect.width() == mInitRect.width() ? 0 : (mPosX - (mScaleFactor - 1) * (mInitRect.width() / 2));
            yMaxTopOffset = mRect.height() == mInitRect.height() ? 0 : (mPosY - (mScaleFactor - 1) * (mInitRect.height() / 2));
            xMaxRightOffset = mRect.width() == mInitRect.width() ? 0 : (mPosX + (mScaleFactor - 1) * (mInitRect.width() / 2));
            yMaxBottomOffset = mRect.height() == mInitRect.height() ? 0 : (mPosY + (mScaleFactor - 1) * (mInitRect.height() / 2));
            if (getPosition(mActionMovePoint.x, mActionMovePoint.y) != -1) {
                return true;
            } else {
                return false;
            }
        case MotionEvent.ACTION_MOVE:
            switch (getPosition(mActionMovePoint.x, mActionMovePoint.y)) {
                case 0:
                    mRect.left = mActionMovePoint.x;
                    mRect.top = mActionMovePoint.y;
                    xOffset = mActionMovePoint.x - actionDownRectLeft;
                    yOffset = mActionMovePoint.y - actionDownRectTop;
                    //设置裁剪框可缩放到的最小区域大小以及裁剪之后x、y方向的偏移值
                    if ((actionDownRectRight - mActionMovePoint.x) < RECT_MIN_WIDTH) {
                        mRect.left = actionDownRectRight - RECT_MIN_WIDTH;
                        xOffset = mRect.left - actionDownRectLeft;
                    }
                    if ((actionDownRectBottom - mActionMovePoint.y) < RECT_MIN_HEIGHT) {
                        mRect.top = actionDownRectBottom - RECT_MIN_HEIGHT;
                        yOffset = mRect.top - actionDownRectTop;
                    }
                    //设置裁剪框可缩放到的最大区域大小以及裁剪之后x、y方向的偏移值
                    float xMaxLeft = mInitRect.left + xMaxLeftOffset < 0 ? 0 : mInitRect.left + xMaxLeftOffset;
                    float yMaxTop = mInitRect.top + yMaxTopOffset < 0 ? 0 : mInitRect.top + yMaxTopOffset;
                    if (mActionMovePoint.x < xMaxLeft) {
                        mRect.left = xMaxLeft;
                        xOffset = mRect.left - actionDownRectLeft;
                    }
                    if (mActionMovePoint.y < yMaxTop) {
                        mRect.top = yMaxTop;
                        yOffset = mRect.top - actionDownRectTop;
                    }
                    mPressPointIndex = 0;
                    break;
	… …

最后的一个主要功能就是需要根据裁剪框的位置和图片的大小位置截取出相应的图片区域,下面的方法会返回一个Bitmap对象,这里的逻辑就是要获取到当前裁剪框包裹住的图片区域的左上角坐标,截取出的图片宽高就是当前裁剪框区域的宽高mRect.width()和mRect.height(),其实要计算这个左上角的坐标值,以横坐标为例:需要算出当前图片和原图的宽度差的一半再减去图片的x方向偏移量mPosX(往右移mPosX为正,往左移mPosX为负),然后加上裁剪框的左边距,最后还要记得减去多算进来的那截5dp:

public Bitmap getImage () {
Matrix matrix = new Matrix();
    matrix.postScale(mScaleFactor, mScaleFactor);
    Bitmap bitmap = null;
    int ox = 0, oy = 0;
    if (mNewBitmap != null && !mNewBitmap.isRecycled()) {
        bitmap = Bitmap.createBitmap(mNewBitmap, 0, 0, (int) mNewWidth, (int) mNewHeight, matrix, true);
        ox = (int) ((bitmap.getWidth() - getWidth()) * 0.5f - mPosX + mRect.left + 0.5f) – dp2px(5);
        oy = (int) ((bitmap.getHeight() - getHeight()) * 0.5f - mPosY + mRect.top + 0.5f) - dp2px(5);
    }
    if (bitmap != null && !bitmap.isRecycled() && ox >= 0 && oy >= 0) {
        Bitmap cutBitmap = Bitmap.createBitmap(bitmap, ox, oy, (int) mRect.width(), (int) mRect.height());
	return cutBitmap;
    } else {
        return null;
    }
}

3.使用示例

首先是在xml中的布局,他俩的父布局是FrameLayout

<com.example.myapplication.DragScaleView
    android:id="@+id/drag_scale_view"
    android:layout_width="300dp"
    android:layout_height="500dp"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    android:background="#00000000"/>
<com.example.myapplication.DrawBoxView
    android:id="@+id/draw_line_view"
    android:layout_width="310dp"
    android:layout_height="510dp"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    android:background="#00000000" />

在MainActivity中除了初始化两个矩形区域外还要设置二者相互传递数据的接口:

dragScaleView.setRect(new RectF(0, 0, dp2px(300), dp2px(500))); 
dragScaleView.setImageResource(R.mipmap.test);
dragScaleView.setUpdateListener(new DragScaleView.UpdateListener() {
    @Override
    public void onChange(float posX, float posY, float scale) {
        drawBoxView.setValues(posX, posY, scale);
    }
});
drawBoxView.setRect(new RectF(dp2px(5), dp2px(5), dp2px(300) + dp2px(5), dp2px(500) + dp2px(5)));
drawBoxView.setCropListener(new DrawBoxView.CropListener() {
    @Override
    public void onUp(int positionIndex, float scale, float xOffset, float yOffset, RectF rectF) {
        dragScaleView.doAnimation(positionIndex, scale, xOffset, yOffset, rectF);
    }
});

源码:fjm19960930/AndroidDemo (github.com)

  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
Android自定义图片裁剪可以通过使用自定义View和Bitmap对象来实现。以下是一些步骤: 1. 创建自定义View类,继承自View类。在onDraw()方法中绘制Bitmap对象。 2. 设置触摸事件,获取用户手指操作的坐标,计算出裁剪区域的左上角和右下角坐标。 3. 在onTouchEvent()方法中重绘View,绘制裁剪后的Bitmap对象。 4. 使用Matrix对象进行缩放和旋转操作。 下面是一个简单的示例代码: ``` public class CropImageView extends View { private Bitmap mBitmap; private Paint mPaint; private Rect mRect; private Matrix mMatrix; private float mStartX; private float mStartY; private float mEndX; private float mEndY; public CropImageView(Context context) { super(context); init(); } public CropImageView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(5); mPaint.setColor(Color.RED); mRect = new Rect(); mMatrix = new Matrix(); } public void setImageBitmap(Bitmap bitmap) { mBitmap = bitmap; invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mBitmap != null) { canvas.drawBitmap(mBitmap, mMatrix, null); } canvas.drawRect(mRect, mPaint); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mStartX = event.getX(); mStartY = event.getY(); mEndX = mStartX; mEndY = mStartY; break; case MotionEvent.ACTION_MOVE: mEndX = event.getX(); mEndY = event.getY(); break; case MotionEvent.ACTION_UP: mEndX = event.getX(); mEndY = event.getY(); break; } calculateRect(); invalidate(); return true; } private void calculateRect() { float left = Math.min(mStartX, mEndX); float top = Math.min(mStartY, mEndY); float right = Math.max(mStartX, mEndX); float bottom = Math.max(mStartY, mEndY); mRect.set((int) left, (int) top, (int) right, (int) bottom); float centerX = (left + right) / 2; float centerY = (top + bottom) / 2; mMatrix.reset(); mMatrix.postTranslate(-centerX, -centerY); mMatrix.postRotate(45); mMatrix.postScale(0.5f, 0.5f); mMatrix.postTranslate(centerX, centerY); } } ``` 这个示例代码实现了一个简单的图片裁剪功能,并在裁剪后对图片进行了缩放和旋转。你可以根据自己的需求修改代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我们间的空白格

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值