笔记106--摘自爱哥05

原文地址:http://blog.csdn.net/aigestudio/article/details/41960507

一、Canvas方法简介


Canvas真正屌的不是它能画些什么,而是对画布的各种活用。

二、drawBitmapMesh()
1、简介

优点:它可以对Bitmap做几乎任何改变。

缺点:计算复杂是个鸡肋,这么屌的方法被埋没就因为其高不成低不就,有些变换我们可以用Matrix等其他方法简单实现,但是drawBitmapMesh()就要通过一些列计算,太复杂;那如果要做复杂图形效果呢,考虑到效率我们又会首选OpenGL。

2、构造函数

drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, int colorOffset, Paint paint)
verts存储改变后的坐标,drawBitmapMesh依据这些坐标来改变图像;drawBitmapMesh不能存储计算后点的值,每次调用drawBitmapMesh都是以基准点坐标为参考的,也就是说,不管你执行drawBitmapMesh几次,只要参数没改变,效果不累加。

verOffset是verts数组的偏移值,意为从第一个元素开始才对位图进行变化。

3、应用

如果可以做一个放大镜,实现错切效果等。实例来更好的理解下drawBitmapMesh:

public class BitmapMeshView2 extends View {  
    private static final int WIDTH = 9, HEIGHT = 9;// 分割数  
    private static final int COUNT = (WIDTH + 1) * (HEIGHT + 1);// 交点数  
  
    private Bitmap mBitmap;// 位图对象  
  
    private float[] matrixOriganal = new float[COUNT * 2];// 基准点坐标数组  
    private float[] matrixMoved = new float[COUNT * 2];// 变换后点坐标数组  
  
    private float clickX, clickY;// 触摸屏幕时手指的xy坐标  
  
    private Paint origPaint, movePaint, linePaint;// 基准点、变换点和线段的绘制Paint  
  
    public BitmapMeshView2(Context context, AttributeSet set) {  
        super(context, set);  
        setFocusable(true);  
  
        // 实例画笔并设置颜色  
        origPaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
        origPaint.setColor(0x660000FF);  
        movePaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
        movePaint.setColor(0x99FF0000);  
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);  
        linePaint.setColor(0xFFFFFB00);  
  
        // 获取位图资源  
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bt);  
  
        // 初始化坐标数组  
        int index = 0;  
        for (int y = 0; y <= HEIGHT; y++) {  
            float fy = mBitmap.getHeight() * y / HEIGHT;  
  
            for (int x = 0; x <= WIDTH; x++) {  
                float fx = mBitmap.getWidth() * x / WIDTH;  
                setXY(matrixMoved, index, fx, fy);  
                setXY(matrixOriganal, index, fx, fy);  
                index += 1;  
            }  
        }  
    }  
  
    /** 
     * 设置坐标数组 
     *  
     * @param array 
     *            坐标数组 
     * @param index 
     *            标识值 
     * @param x 
     *            x坐标 
     * @param y 
     *            y坐标 
     */  
    private void setXY(float[] array, int index, float x, float y) {  
        array[index * 2 + 0] = x;  
        array[index * 2 + 1] = y;  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
  
        // 绘制网格位图  
        canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, matrixMoved, 0, null, 0, null);  
  
        // 绘制参考元素  
        drawGuide(canvas);  
    }  
  
    /** 
     * 绘制参考元素 
     *  
     * @param canvas 
     *            画布 
     */  
    private void drawGuide(Canvas canvas) {  
        for (int i = 0; i < COUNT * 2; i += 2) {  
            float x = matrixOriganal[i + 0];  
            float y = matrixOriganal[i + 1];  
            canvas.drawCircle(x, y, 4, origPaint);  
  
            float x1 = matrixOriganal[i + 0];  
            float y1 = matrixOriganal[i + 1];  
            float x2 = matrixMoved[i + 0];  
            float y2 = matrixMoved[i + 1];  
            canvas.drawLine(x1, y1, x2, y2, origPaint);  
        }  
  
        for (int i = 0; i < COUNT * 2; i += 2) {  
            float x = matrixMoved[i + 0];  
            float y = matrixMoved[i + 1];  
            canvas.drawCircle(x, y, 4, movePaint);  
        }  
  
        canvas.drawCircle(clickX, clickY, 6, linePaint);  
    }  
  
    /** 
     * 计算变换数组坐标 
     */  
    private void smudge() {  
        for (int i = 0; i < COUNT * 2; i += 2) {  
  
            float xOriginal = matrixOriganal[i + 0];  
            float yOriginal = matrixOriganal[i + 1];  
  
            float dist_click_to_origin_x = clickX - xOriginal;  
            float dist_click_to_origin_y = clickY - yOriginal;  
  
            float kv_kat = dist_click_to_origin_x * dist_click_to_origin_x + dist_click_to_origin_y * dist_click_to_origin_y;  
  
            float pull = (float) (1000000 / kv_kat / Math.sqrt(kv_kat));  
  
            if (pull >= 1) {  
                matrixMoved[i + 0] = clickX;  
                matrixMoved[i + 1] = clickY;  
            } else {  
                matrixMoved[i + 0] = xOriginal + dist_click_to_origin_x * pull;  
                matrixMoved[i + 1] = yOriginal + dist_click_to_origin_y * pull;  
            }  
        }  
    }  
  
    @Override  
    public boolean onTouchEvent(MotionEvent event) {  
        clickX = event.getX();  
        clickY = event.getY();  
        smudge();  
        invalidate();  
        return true;  
    }  
} 
运行后效果图:

蓝点表示基准点的坐标,红点表示当前变换后的坐标,所有的变换都是参照蓝点进行的。默认状态,蓝点和红点重合。黄色的点代表我们当前触摸的点:


三、Canvas构造函数

两个构造函数:

Canvas canvas = new Canvas();
Canvas canvas = new Canvas(bitmap);
虽然无参的构造方法没有传入Bitmap对象,但是Android还是要求我们使用Canvas的setBitmap()去为Canvas指定一个Bitmap对象。为什么Canvas非要一个Bitmap对象呢?原因很简单,Canvas需要一个Bitmap对象来保存像素信息。不要的话就画的东西没法保存了,那画了有何意义。

四、Canvas方法分类

1、以drawXXX为主的绘制方法

2、以clipXXX为主的裁剪方法

3、以scale、skew、translate和rotate组成的Canvas变换方法

4、以saveXXX和restoreXXX构成的画布锁定和还原
5、其他渣渣方法

1、裁剪画布

public class CanvasView extends View {  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.drawColor(Color.BLUE);  
        canvas.clipRect(0, 0, 500, 500);  
        canvas.drawColor(Color.RED);  
    }  
}  
效果图:

clipRect(int left, int top, int right, int bottom)
与上面方法类似的方法:

clipRect(float left, float top, float right, float bottom)
还有两个方法与此对应:

clipRect(Rect rect)  
clipRect(RectF rect) 
2、Rect类

public class CanvasView extends View {  
    private Rect mRect;  
  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        mRect = new Rect(0, 0, 500, 500);  
  
        mRect.intersect(250, 250, 750, 750);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.drawColor(Color.BLUE);  
  
        canvas.clipRect(mRect);  
  
        canvas.drawColor(Color.RED);  
    }  
}
效果图:

PS:黄色辅助线为后期加上,非程序生成。
intersect作用跟我们之前学到的图形混合模式有点类似,它会取两个区域的最终区域作为最终区域,intersect的计算方式特别有趣,它不是单纯计算相交区域最近的左上端点和最近的右下端点。Rect中的另一个方法:union,它与intersect刚好相反,取得是相交区域最远的左上端点作为新区域的左上端点,而取最远的右下端点作为新区域的右下端点。比如:

mRect.union(250, 250, 750, 750);  
效果图:

对于不规则的裁剪区域,用:clipPath(Path path)

3、Path类

a.简介

是封装Android中几何路径的一个类。

b.方法介绍

1)

// 实例化路径  
mPath = new Path();  
  
// 移动点至[300,300]  
mPath.moveTo(100, 100);  
  
// 连接路径到点  
mPath.lineTo(300, 100);  
mPath.lineTo(400, 200);  
mPath.lineTo(200, 200); 
效果图:

如果此时想闭合该曲线让它形成一个形状该怎么做呢?mPath.lineTo(100, 100)然而Path给了我们更便捷的方法:close()去闭合曲线。

2)Path在绘制方法中还提供了许多XXXTo的方法来帮助我们绘制各种直线、曲线。例如:

quadTo(float x1, float y1, float x2, float y2)
可让我们绘制二阶贝塞尔曲线。什么叫贝塞尔曲线?很简单,使用三个或多个点来确定的一条曲线,贝塞尔曲线在图形图像中有相当重要的地位,Path中也提供了一些方法来给我们模拟低阶贝塞尔曲线。贝塞尔曲线定义也很简单:你只需一个起点、一个终点和至少零个控制点则可定义一个贝塞尔曲线,当控制点为零时,只有起点和终点,此时就是一条线段。利用quadTo来绘制一条曲线:

// 实例化路径  
mPath = new Path();  
  
// 移动点至[100,100]  
mPath.moveTo(100, 100);  
  
// 连接路径到点  
mPath.quadTo(200, 200, 300, 100);  
效果图:

quadTo前两个参数表示控制点的坐标,后两个参数表示终点坐标。看看三阶贝赛尔曲线:

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 
// 实例化路径  
mPath = new Path();  
  
// 移动点至[100,100]  
mPath.moveTo(100, 100);  
  
// 连接路径到点  
mPath.cubicTo(200, 200, 300, 0, 400, 100);  
效果图:

活用贝塞尔曲线可得到很多有意思的效果,比如,模拟下被子中水消匿的效果:

public class WaveView extends View {  
    private Path mPath;// 路径对象  
    private Paint mPaint;// 画笔对象  
  
    private int vWidth, vHeight;// 控件宽高  
    private float ctrX, ctrY;// 控制点的xy坐标  
    private float waveY;// 整个Wave顶部两端点的Y坐标,该坐标与控制点的Y坐标增减幅一致  
  
    private boolean isInc;// 判断控制点是该右移还是左移  
  
    public WaveView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
  
        // 实例化画笔并设置参数  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
        mPaint.setColor(0xFFA2D6AE);  
  
        // 实例化路径对象  
        mPath = new Path();  
    }  
  
    @Override  
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
        // 获取控件宽高  
        vWidth = w;  
        vHeight = h;  
  
        // 计算控制点Y坐标  
        waveY = 1 / 8F * vHeight;  
  
        // 计算端点Y坐标  
        ctrY = -1 / 16F * vHeight;  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        /* 
         * 设置Path起点 
         * 注意我将Path的起点设置在了控件的外部看不到的区域 
         * 如果我们将起点设置在控件左端x=0的位置会使得贝塞尔曲线变得生硬 
         * 至于为什么刚才我已经说了 
         * 所以我们稍微让起点往“外”走点 
         */  
        mPath.moveTo(-1 / 4F * vWidth, waveY);  
  
        /* 
         * 以二阶曲线的方式通过控制点连接位于控件右边的终点 
         * 终点的位置也是在控件外部 
         * 我们只需不断让ctrX的大小变化即可实现“浪”的效果 
         */  
        mPath.quadTo(ctrX, ctrY, vWidth + 1 / 4F * vWidth, waveY);  
  
        // 围绕控件闭合曲线  
        mPath.lineTo(vWidth + 1 / 4F * vWidth, vHeight);  
        mPath.lineTo(-1 / 4F * vWidth, vHeight);  
        mPath.close();  
  
        canvas.drawPath(mPath, mPaint);  
  
        /* 
         * 当控制点的x坐标大于或等于终点x坐标时更改标识值 
         */  
        if (ctrX >= vWidth + 1 / 4F * vWidth) {  
            isInc = false;  
        }  
        /* 
         * 当控制点的x坐标小于或等于起点x坐标时更改标识值 
         */  
        else if (ctrX <= -1 / 4F * vWidth) {  
            isInc = true;  
        }  
  
        // 根据标识值判断当前的控制点x坐标是该加还是减  
        ctrX = isInc ? ctrX + 20 : ctrX - 20;  
  
        /* 
         * 让“水”不断减少 
         */  
        if (ctrY <= vHeight) {  
            ctrY += 2;  
            waveY += 2;  
        }  
  
        mPath.reset();  
  
        // 重绘  
        invalidate();  
    }  
}  
3)arcTo方法用来生成弧线:

arcTo (RectF oval, float startAngle, float sweepAngle)
// 实例化路径  
mPath = new Path();  
  
// 移动点至[100,100]  
mPath.moveTo(100, 100);  
  
// 连接路径到点  
RectF oval = new RectF(100, 100, 200, 200);  
mPath.arcTo(oval, 0, 90);
效果图:
注意:使用Path生成的路径必定是连贯的虽然我们使用arcTo绘制的是一段弧,但其最终都会与我们的起始点[100, 100]连接起来,如果你不想连怎么办?简单,强制让arcTo绘制的起点作为Path的起点不就是了?Path也提供了另外一个重载方法:

arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
最后一个参数值为true时将会把弧的起点作为Path的起点: mPath.arcTo(oval,  0 90 true );

效果图:

4)rXXXTo方法

rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3)  
rLineTo(float dx, float dy)  
rMoveTo(float dx, float dy)  
rQuadTo(float dx1, float dy1, float dx2, float dy2)  

其实跟上面的XXXTo方法差不多,唯一不同的是rXXXTo方法的参考坐标是相对的,而XXXTo的参考坐标始终是参照画布原点坐标。举例说明下:

// 实例化路径  
mPath = new Path();  
  
// 移动点至[100,100]  
mPath.moveTo(100, 100);  
  
// 连接路径到点  
mPath.lineTo(200, 200);
从[100, 100]开始连接点[200, 200]构成一条直线: 这个点[200,  200]是相对于画布原点坐标的。如果换成: mPath.rLineTo( 200 200 );它的意思就是将会以[100,  100]作为原点坐标,连接以其为原点坐标的坐标点[200,  200],如果换成画布原点的话,实际上现在的[200,  200]是[300,  300]了: 这个前缀r就是relative的简写。

5)addXXX

XXXTo方法可连接Path中的曲线而Path提供的另一系列addXXX方法则可让我们直接往Path中添加一些曲线,比如:

addArc(RectF oval, float startAngle, float sweepAngle)
允许我们将一段弧形添加到Path。添加也就是说通过addXXX方法添加到Path中的曲线是不会和上一次的曲线进行连接的:

// 实例化路径  
        mPath = new Path();  
  
        // 移动点至[100,100]  
        mPath.moveTo(100, 100);  
  
        // 连接路径到点  
        mPath.lineTo(200, 200);  
  
        // 添加一条弧线到Path中  
        RectF oval = new RectF(100, 100, 300, 400);  
        mPath.addArc(oval, 0, 90);  

其他add方法:

addCircle(float x, float y, float radius, Path.Direction dir)  
addOval(float left, float top, float right, float bottom, Path.Direction dir)  
addRect(float left, float top, float right, float bottom, Path.Direction dir)  
addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Path.Direction dir) 
addArc是往Path中添加一段弧,说白了是一条开放的曲线,而上述几种方法都是一个具体的图形,或者说是一条闭合的曲线,Path.Direction就是标识这些闭合曲线的闭合方向,只有两个常量值CCW和CW分别表示逆时针闭合和顺时针闭合。我们借助 drawTextOnPath(String text, Path path,  float  hOffset,  float  vOffset, Paint paint)看下效果:

public class PathView extends View {  
    private Path mPath;// 路径对象  
    private Paint mPaint;// 路径画笔对象  
    private TextPaint mTextPaint;// 文本画笔对象  
  
    public PathView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
  
        /* 
         * 实例化画笔并设置属性 
         */  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
        mPaint.setStyle(Paint.Style.STROKE);  
        mPaint.setColor(Color.CYAN);  
        mPaint.setStrokeWidth(5);  
  
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);  
        mTextPaint.setColor(Color.DKGRAY);  
        mTextPaint.setTextSize(20);  
  
        // 实例化路径  
        mPath = new Path();  
  
        // 添加一条弧线到Path中  
        RectF oval = new RectF(100, 100, 300, 400);  
        mPath.addOval(oval, Path.Direction.CW);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        // 绘制路径  
        canvas.drawPath(mPath, mPaint);  
  
        // 绘制路径上的文字  
        canvas.drawTextOnPath("ad撒发射点发放士大夫斯蒂芬斯蒂芬森啊打扫打扫打扫达发达省份撒旦发射的", mPath, 0, 0, mTextPaint);  
    }  
}  
效果图:

如果我们把闭合方向改为CCW会发生什么呢?效果图:

沿着Path的文字全都在闭合曲线的“内部”了,Path.Direction闭合方向大概是这个意思。

Path结合PathEffect可得到很多很酷的效果。在众多用途中,用Path做折线图算是最常见的了。






6)clipPath(Path path)

public class CanvasView extends View {  
    private Path mPath;  
  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
  
        mPath = new Path();  
        mPath.moveTo(50, 50);  
        mPath.lineTo(75, 23);  
        mPath.lineTo(150, 100);  
        mPath.lineTo(80, 110);  
        mPath.close();  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.drawColor(Color.BLUE);  
        canvas.clipPath(mPath);   
        canvas.drawColor(Color.RED);  
    }  
} 

其他裁剪方法:

clipPath(Path path, Region.Op op)  
clipRect(Rect rect, Region.Op op)  
clipRect(RectF rect, Region.Op op)  
clipRect(float left, float top, float right, float bottom, Region.Op op)  
clipRegion(Region region, Region.Op op) 
Region.Op参数,Region表示一块封闭的区域。Op是Region的一个枚举类,里面有六个枚举常量:


Region.Op其实就是个组合模式,在1/6中我们曾学过一个叫图形混合模式的。

public class CanvasView extends View {  
    private Region mRegionA, mRegionB;// 区域A和区域B对象  
    private Paint mPaint;// 绘制边框的Paint  
  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
  
        // 实例化画笔并设置属性  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
        mPaint.setStyle(Paint.Style.STROKE);  
        mPaint.setColor(Color.WHITE);  
        mPaint.setStrokeWidth(2);  
  
        // 实例化区域A和区域B  
        mRegionA = new Region(100, 100, 300, 300);  
        mRegionB = new Region(200, 200, 400, 400);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        // 填充颜色  
        canvas.drawColor(Color.BLUE);  
  
        canvas.save();  
  
        // 裁剪区域A  
        canvas.clipRegion(mRegionA);  
  
        // 再通过组合方式裁剪区域B  
        canvas.clipRegion(mRegionB, Region.Op.DIFFERENCE);  
  
        // 填充颜色  
        canvas.drawColor(Color.RED);  
  
        canvas.restore();  
  
        // 绘制框框帮助我们观察  
        canvas.drawRect(100, 100, 300, 300, mPaint);  
        canvas.drawRect(200, 200, 400, 400, mPaint);  
    }  
}  
以下是各种组合模式的效果:

DIFFERENCE:最终区域为第一个区域与第二个区域不同的区域。


INTERSECT:最终区域为第一个区域与第二个区域相交的区域。


REPLACE:最终区域为第二个区域。


REVERSE_DIFFERENCE:最终区域为第二个区域与第一个区域不同的区域。


UNION:最终区域为第一个区域加第二个区域。


XOR:最终区域为第一个区域加第二个区域并减去两者相交的区域。


实际上Rect、Circle、Ovel等封闭的曲线都可使用Regina.Op。

Region和Path区别:首先,Region表示一个区域,而Rect表示一个矩形;其次,Region有个特别的地方是不受Canvas的变换影响,Canvas的位置不会直接影响到Region本身,举个simple:

public class CanvasView extends View {  
    private Region mRegion;// 区域对象  
    private Rect mRect;// 矩形对象  
    private Paint mPaint;// 绘制边框的Paint  
  
    public CanvasView(Context context, AttributeSet attrs) {  
        super(context, attrs);  
  
        // 实例化画笔并设置属性  
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);  
        mPaint.setStyle(Paint.Style.STROKE);  
        mPaint.setColor(Color.DKGRAY);  
        mPaint.setStrokeWidth(2);  
  
        // 实例化矩形对象  
        mRect = new Rect(0, 0, 200, 200);  
  
        // 实例化区域对象  
        mRegion = new Region(200, 200, 400, 400);  
    }  
  
    @Override  
    protected void onDraw(Canvas canvas) {  
        canvas.save();  
  
        // 裁剪矩形  
        canvas.clipRect(mRect);  
        canvas.drawColor(Color.RED);  
  
        canvas.restore();  
  
        canvas.save();  
  
        // 裁剪区域  
        canvas.clipRegion(mRegion);  
        canvas.drawColor(Color.RED);  
  
        canvas.restore();  
  
        // 为画布绘制一个边框便于观察  
        canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);  
    }  
}  

画布因为和屏幕一样大,所以我们看不出描边的效果,这时,我们将Canvas缩放至75%大小,看看会发生什么:

@Override  
protected void onDraw(Canvas canvas) {  
    // 缩放画布  
    canvas.scale(0.75F, 0.75F);  
  
    canvas.save();  
  
    // 裁剪矩形  
    canvas.clipRect(mRect);  
    canvas.drawColor(Color.RED);  
  
    canvas.restore();  
  
    canvas.save();  
  
    // 裁剪区域  
    canvas.clipRegion(mRegion);  
    canvas.drawColor(Color.RED);  
  
    canvas.restore();  
  
    // 为画布绘制一个边框便于观察  
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mPaint);  
}  
这时我们看到Rect随着Canvas的缩放一起缩放了,但是Region依旧泰山不动的淡定:


canvas.restore()和canvas.save():这两个方法相互匹配出现,作用是用来保存画布的状态和取出保存的状态的。当我们对画布进行旋转、缩放、平移等操作时其实我们是想对特定的元素进行操作,比如图片,一个矩形等,但是当你用canvas()的方法来进行这些操作时,其实是对整个画布进行了操作,那么之后在画布上的元素都会受到影响,所以我们在操作前调用canvas.save()来保存画布当前的状态,当操作之后取出之前保存过的状态,这样就不会对其他元素有影响了。

注意:save()必须在restore()之前调用,且save()方法的调用次数必须大于等于restore()调用的次数,否则会出错。



















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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值