自定义控件三部曲之绘图篇(二十)——RadialGradient与水波纹按钮效果

前言:每当感叹自己的失败时,那我就问你,如果让你重新来一次,你会不会成功?如果会,那说明并没有拼尽全力。

系列文章:

Android自定义控件三部曲文章索引:http://blog.csdn.net/harvic880925/article/details/50995268

最近博主实在是太忙了,博客更新实在是太慢了,真是有愧大家。

这篇将是Shader的最后一篇,下部分,我们将讲述Canvas变换的知识。在讲完Canvas变换以后,就正式进入第三部曲啦,是不是有点小激动呢……

今天给大家讲的效果是使用RadialGradient来实现水波纹按钮效果,水波纹效果是Android L平台上自带的效果,这里我们就看看它是如何实现的,本篇的最终效果图如下
在这里插入图片描述

###一、RadialGradient详解
RadialGradient的意思是放射渐变,即它会向一个放射源一样,从一个点开始向外从一个颜色渐变成另一种颜色;

一、构造函数

RadialGradient有两个构造函数

//两色渐变
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
//多色渐变
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)

(1)、两色渐变构造函数使用实例
下面我们来看一下两色渐变构造函数的使用方法。

RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)

这个两色渐变的构造函数的各项参数意义如下:

  • centerX:渐变中心点X坐标
  • centerY:渐变中心点Y坐标
  • radius:渐变半径
  • centerColor:渐变的起始颜色,即渐变中心点的颜色,取值类型必须是八位的0xAARRGGBB色值!透明底Alpha值不能省略,不然不会显示出颜色。
  • edgeColor:渐变结束时的颜色,即渐变圆边缘的颜色,同样,取值类型必须是八位的0xAARRGGBB色值!
  • TileMode:与我们前面讲的各个Shader一样,用于指定当控件区域大于指定的渐变区域时,空白区域的颜色填充方式。

下面我们举个例子来看下用法:

public class DoubleColorRadialGradientView extends View {
    private Paint mPaint;
    private RadialGradient mRadialGradient;
    private int mRadius = 100;
    public DoubleColorRadialGradientView(Context context) {
        super(context);
        init();
    }

    public DoubleColorRadialGradientView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DoubleColorRadialGradientView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
	    setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
        mPaint.setShader(mRadialGradient);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
    }
}

代码量不大,这里首先在onSizeChange中,创建RadialGradient实例。onSizeChange会在布局发生改变时调用,onSizeChanged(int w, int h, int oldw, int oldh)传过来四个参数,前两个参数就代表当前控件所应显示的宽和高。有关onSizeChange的具体意义,我们会在第三部曲讲解回调函数流程中具体讲到,这里大家就先理解到这吧。
在onSizeChange中,我们创建了一个RadialGradient,以控件的中心点为圆点,创建一个半径为mRadius的,从0xffff0000到0xff00ff00的放射渐变。我们这里指定的空白填充方式为Shader.TileMode.REPEAT。
然后在绘图的时候:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
}

在绘图时,依然是以控件中心点为圆心,画一个半径为mRadius的圆;注意我们画的圆的大小与所构造的放射渐变的大小是一样的,所以不存在空白区域的填充问题。
效果图如下:
在这里插入图片描述

(2)、多色渐变构造函数使用实例
多色渐变的构造函数如下:

RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)

这里与两色渐变不同的是两个函数:

  • int[] colors:表示所需要的渐变颜色数组
  • float[] stops:表示每个渐变颜色所在的位置百分点,取值0-1,数量必须与colors数组保持一致,不然直接crash,一般第一个数值取0,最后一个数值取1;如果第一个数值和最后一个数值并没有取0和1,比如我们这里取一个位置数组:{0.2,0.5,0.8},起始点是0.2百分比位置,结束点是0.8百分比位置,而0-0.2百分比位置和0.8-1.0百分比的位置都是没有指定颜色的。而这些位置的颜色就是根据我们指定的TileMode空白区域填充模式来自行填充!!!有时效果我们是不可控的。所以为了方便起见,建议大家stop数组的起始和终止数值设为0和1.

下面我们举个例子来看下用法:

public class MultiColorRadialGradientView extends View {
    private Paint mPaint;
    private RadialGradient mRadialGradient;
    private int mRadius = 100;
    public MultiColorRadialGradientView(Context context) {
        super(context);
        init();
    }

    public MultiColorRadialGradientView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MultiColorRadialGradientView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        int[]   colors = new int[]{0xffff0000,0xff00ff00,0xff0000ff,0xffffff00};
        float[] stops  = new float[]{0f,0.2f,0.5f,1f};
        mRadialGradient = new RadialGradient(w/2,h/2,mRadius,colors,stops, Shader.TileMode.REPEAT);
        mPaint.setShader(mRadialGradient);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawCircle(getWidth()/2,getHeight()/2,mRadius,mPaint);
    }
}

这里主要看下多色渐变的构造方法:

int[]   colors = new int[]{0xffff0000,0xff00ff00,0xff0000ff,0xffffff00};
float[] stops  = new float[]{0f,0.2f,0.5f,1f};
mRadialGradient = new RadialGradient(w/2,h/2,mRadius,colors,stops, Shader.TileMode.REPEAT);

这里构造了一个四色颜色数组,渐变位置对应{0f,0.2f,0.5f,1f},然后创建RadialGradient实例。没什么难度。
然后在绘画的时候,同样以控件中心为半径,以放射渐变的半径为半径画圆。由于画的圆半径与放射渐变的大小相同,所以不存在空白位置填充的问题,所以TileMode.REPEAT并没有用到。
效果图如下:
在这里插入图片描述

###二、TileMode重复方式
TileMode的问题,已经重复讲了几篇文章了,其实虽然每种Shader所表现出来的效果不一样,但是形成原理都是相同的。下面我们再来看一下RadialGradient在不同的TileMode下的具体表现。

(1)、X、Y轴共用填充参数

与LinearGradient一样,从构造函数中,可以明显看出RadialGradient只有一个填充模式:

//两色渐变
RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, Shader.TileMode tileMode)
//多色渐变
RadialGradient(float centerX, float centerY, float radius, int[] colors, float[] stops, Shader.TileMode tileMode)

这就说明了,当填充空白区域时,X轴和Y轴使用同一种填充模式。而不能像BitmapShader那样分别指定X轴与Y轴的填充参数。

(2)、TileMode.CLAMP——边缘填充

我们依然使用双色渐变的示例来看下效果,为了显示填充效果,我们这次画一个屏幕大小的矩形:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.CLAMP);
    mPaint.setShader(mRadialGradient);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}

效果图如下:
在这里插入图片描述
从效果图中可以明显看出,除了放渐渐变以外的空白区域都被边缘填充成为了绿色;

(3)、TileMode.REPEAT——重复填充

我们仍使用上面的代码,只是将填充模式改为重复填充:

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
    mPaint.setShader(mRadialGradient);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}

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

这个图像乍看去有点辣眼睛,花花绿绿的……从效果图中可以看出,最内部的圆形是红到绿的原始放射渐变。其外面的圆就是空白区域填充模式了,在它的外围,从红到绿渐变。

(4)、TileMode.MIRROR—镜像填充

同样是使用上面的代码,只是将填充模式改为镜像填充:

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.MIRROR);
    mPaint.setShader(mRadialGradient);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawRect(0,0,getWidth(),getHeight(),mPaint);
}

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

有些同学第一下看到这个图可能有点懵,所谓镜像,就是把原来的颜色的倒过来填充。即原始是红到绿渐变,第二圈就变成了绿到红渐变,第三圈就又是红到绿渐变,如此往复。
如果我把每一圈渐变的界限标出来,大家可能就容易看懂了:
在这里插入图片描述

图中白色线就是每一圈渐变的边界线,一次完整的填充就是两个白色圈中的部分。

(5)、填充方式:从控件左上角开始填充

在讲BitmapShader和LinearShader时,我们就一再强调一个点:无论哪种Shader,都是从控件的左上角开始填充的,利用canvas.drawXXX系列函数只是用来指定显示哪一块;
我们在RadialGradient中也做下测试:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    mRadialGradient = new RadialGradient(w/2,h/2,mRadius,0xffff0000,0xff00ff00, Shader.TileMode.REPEAT);
    mPaint.setShader(mRadialGradient);
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawRect(0,0,200,200,mPaint);
}

我们这里使用TileMode.REPEAT来填充空白区域,在绘图时,我们只画出左上角的一部分;
效果图如下:
在这里插入图片描述

从效果图中明显可以看出结论了:
无论哪种Shader,都是从控件的左上角开始填充的,利用canvas.drawXXX系列函数只是用来指定显示哪一块

###二、水波纹按钮效果

这部分就要利用RadialGradient来实现水波纹效果了,我们这里直接继承自Button类,做一个按钮的水波纹效果,其实这里继承自任何一个类都是可以在这个类原本的显示内容上显示水波纹效果的,比如,大家可以试验下在源码的基础上,将其改为派生自ImageView,当然要记得给它添加上src属性,是一样会有水波纹效果的。

1、概述

根据上面的的对RadialGradient的讲解,大家第一反应应该是,水波纹很好实现啊:不就是,画一个带有渐变效果的逐渐放大的圆不就得了。不错,思想确实就是这么简单。

(1)、不过,第一个问题来了,从哪个颜色,渐变到哪个颜色呢?

最理想的状态是,从按钮的背景色渐变到天蓝色(开篇效果图中颜色)。但是,怎么拿到按钮的背景色呢?因为按钮的android:background属性填充不一定是颜色,有可能是一个drawable,而这个drawable可以是图片,也可能是selector文件等,所以这条路根本走不通。
而我们讲过,RadialGradient中填充的渐变色彩必须是AARRGGBB形式的,所以我们只需要讲初始颜色的透明度设为0,不就露出了按钮的背景色了么。即类似下面的代码:

 mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);

在这里我们将初始的渐变色改为0x00FFFFFF,由于透明度部分全部设置为0,所以整个颜色就是透明的。所以整个渐变过程就变为从零透明度逐渐变为纯天蓝色(0xFF58FAAC)。

(2)、第二个问题,我们应该怎么安排RadialGradient的填充模式

从效果图中是可以明显看出我们会逐渐放大绘制RadialGradient的圆的,那么,我们是让RadialGradient的渐变变径随着绘制的圆增大而增大,还是不改变RadialGradient的初始半径,空余部分使用Shader.TileMode.CLAMP填充来实现水波纹呢。

答案是让RadialGradient的渐变变径随着绘制的圆增大而增大;下面我们分别举个例子来看下效果就知道区别了:

我们将RadialGradient的初始半径设置为20,而假设当前绘制圆的半径是150,分别用模拟代码来展示在不同代码处理下的效果,以最终决定选用哪种方式来绘制RadialGradient渐变。

如果使用空余部分使用Shader.TileMode.CLAMP填充:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    if (mRadialGradient == null) {
        int x = getWidth()/2;
        int y = getHeight()/2;
        mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
        mPaint.setShader(mRadialGradient);
        canvas.drawCircle(x, y, 150, mPaint);
    }
}

这里以控件中心为圆心,构造一个RadialGradient,这个RadialGradient的半径是20,从透明色,渐变到天蓝色

mRadialGradient = new RadialGradient(x, y,20 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);

而在canvas画圆时,仍然以控件中心为圆心,但圆的半径却是150,明显要超出RadialGradient的半径,空白部分使用Shader.TileMode.CLAMP边缘模式填充

canvas.drawCircle(x, y, 150, mPaint);

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

从效果图中可以看出,在0-20的部分是从透明色到天蓝色的渐变,但是超出半径20的部分,都以边缘模式填充为完全不透明的天蓝色,感觉跟按钮完全没有融合在一起有没有

如果让RadialGradient的渐变变径随着绘制的圆增大而增大

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    if (mRadialGradient == null) {
        int x = getWidth()/2;
        int y = getHeight()/2;
        mRadialGradient = new RadialGradient(x, y,150 , 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
        mPaint.setShader(mRadialGradient);
        canvas.drawCircle(x, y, 150, mPaint);
    }
}

这里的代码跟上面的一样,唯一不同的是,构造的RadialGradient的渐变半径与canvas.drawCircle所画的圆的半径是一样的,都是150;这就演示了让RadialGradient的渐变变径随着绘制的圆增大而增大的效果

效果图如下:
在这里插入图片描述
很明显,这是我们想要的结果,渐变色与按钮的背景完全融合。

2、代码实现

上面在讲解了解决了核心问题,以后,下面我们就开始正式实战了

我们再来看下效果图:
在这里插入图片描述

从效果图中,可以看到我们所需要完成的功能:

  • 在手指按下时,绘制一个默认大小的圆
  • 在手指移动时,所绘制的默认圆的位置需要跟随手指移动
  • 在手指放开时,圆逐渐变大
  • 在动画结束时,波纹效果消失

按下和移动

首先,我们来完成前两个功能:当首先按下时,绘制一个默认大小的圆,而且当手指移动时,可以跟随移动:

private int mX, mY;
private int DEFAULT_RADIUS = 50;
public boolean onTouchEvent(MotionEvent event) {

    if (mX != event.getX() || mY != mY) {
        mX = (int) event.getX();
        mY = (int) event.getY();
        setRadius(DEFAULT_RADIUS);
    }

    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        return true;
    } 
    return super.onTouchEvent(event);
}   

首先,我们这里并没区分MotionEvent.ACTION_DOWNMotionEvent.ACTION_UP的绘图操作,只是统一在当前手指位置与上次的不一样时,就调用setRadius(DEFAULT_RADIUS);重绘RadialGradient;很明显,mX、mY变量表示当前手指的位置,而DEFAULT_RADIUS变量表示默认的RadialGradient的渐变尺寸。但是必须在 MotionEvent.ACTION_DOWN时return true,因为如果不return true,就表示当前控件并不需要下按之后的消息,所以ACTION_MOVE、ACTION_UP消息都不会再传到这个控件里来了,有关这个问题,在前面的文章中已经不只一次提到,这里就不再缀述了。

其中,setRadius(DEFAULT_RADIUS)函数的定义如下:

//表示当前渐变半径
private int mCurRadius = 0;
public void setRadius(final int radius) {
    mCurRadius = radius;
    if (mCurRadius > 0) {
        mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
        mPaint.setShader(mRadialGradient);
    }
    postInvalidate();
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawCircle(mX, mY, mCurRadius, mPaint);
}

在setRadius中主要负责在手指位置和渐变半径改变时,重新创建RadialGradient,然后重绘。很明显mCurRadius变量表示当前的渐变半径。最后在OnDraw函数中重绘时,画一个跟渐变半径同样大小的圆即可。

手指放开

在手指放开时,主要是开始逐渐放大放射半径的动画,然后在动画结束的时候,清除RadialGradient。代码如下:

private ObjectAnimator mAnimator;
@Override
public boolean onTouchEvent(MotionEvent event) {
	…………
    f (event.getAction() == MotionEvent.ACTION_UP) {
        if (mAnimator != null && mAnimator.isRunning()) {
            mAnimator.cancel();
        }
        if (mAnimator == null) {
            mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
        }
        mAnimator.setInterpolator(new AccelerateInterpolator());
        mAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                setRadius(0);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        mAnimator.start();
    }

    return super.onTouchEvent(event);
}

在这段代码中,首先是在开始下一个动画前,先判断当前mAnimator是不是还在动画中,如果是正在动画就先取消:

if (mAnimator != null && mAnimator.isRunning()) {
    mAnimator.cancel();
}

这是为了避免当用户连续点击多次的时候,下一次开始动画时,上一次动画还没结束,这样两次动画就会造成冲突,应该先把上次的动画取消掉,然后再重新开始这次的动画:

mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
}
mAnimator.setInterpolator(new AccelerateInterpolator());

然后创建一个ObjectAnimator对象,这里动画操作的函数是setRadius(final int radius)函数,动画的区间是从默认半径到整个控件的宽度,之所以用当前控件的宽度来做为最大动画值,是因为,我们必须指定一个足够大的值,足以让波纹能够覆盖整个控件以后再结束。从效果图中可以看出,在这里控件的宽度是整个控件长度的最大值,所以,我们就以用户点击控件最边缘来算,当用户点击最左或最右边缘时,整个RadialGradient的半径是最大的,此时的最大值是控件宽度,所以我们就用控件宽度来做为动画的最大值即可。

其实这里还是不够严谨,因为在实际应用中,控件的宽度并不是整个控件的最大值,也有可能是控件的高度是最大的,所以最严谨的做法就是先判断控件的高度和宽度哪个最大,然后将最大值做为动画的半径。这里为了简化代码可读性,就不再对比了。

有关ObjectAnimation的知识可以参考:《自定义控件三部曲之动画篇(七)——ObjectAnimator基本使用》

然后给mAnimator设置AccelerateInterpolator()插值器,因为我们需要让波纹的速度逐渐加快,如果不设置插值器的话,默认是使用LinearInterpolator插值器的,这样出来的效果是波纹的变大速度将是匀速的。

mAnimator.setInterpolator(new AccelerateInterpolator());

最后我们需要监听mAnimator结束的动作,当动画结束时,我们需要让RadialGradient消失,最简单的消失办法就是将所画圆的半径设置为0。

mAnimator.addListener(new Animator.AnimatorListener() {
    …………
    @Override
    public void onAnimationEnd(Animator animation) {
        setRadius(0);
    }
	…………
});

到这里所有的代码就讲完了,完整的代码如下:

public class RippleView extends Button {
    private int mX, mY;
    private ObjectAnimator mAnimator;
    private int DEFAULT_RADIUS = 50;
    private int mCurRadius = 0;
    private RadialGradient mRadialGradient;
    private Paint mPaint;

    public RippleView(Context context) {
        super(context);
        init();
    }

    public RippleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RippleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if (mX != event.getX() || mY != mY) {
            mX = (int) event.getX();
            mY = (int) event.getY();

            setRadius(DEFAULT_RADIUS);
        }

        if (event.getAction() == MotionEvent.ACTION_DOWN) {

            return true;
        } else if (event.getAction() == MotionEvent.ACTION_UP) {

            if (mAnimator != null && mAnimator.isRunning()) {
                mAnimator.cancel();
            }

            if (mAnimator == null) {
                mAnimator = ObjectAnimator.ofInt(this,"radius",DEFAULT_RADIUS, getWidth());
            }

            mAnimator.setInterpolator(new AccelerateInterpolator());
            mAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    setRadius(0);
                }

                @Override
                public void onAnimationCancel(Animator animation) {

                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            mAnimator.start();
        }

        return super.onTouchEvent(event);
    }

    public void setRadius(final int radius) {
        mCurRadius = radius;
        if (mCurRadius > 0) {
            mRadialGradient = new RadialGradient(mX, mY, mCurRadius, 0x00FFFFFF, 0xFF58FAAC, Shader.TileMode.CLAMP);
            mPaint.setShader(mRadialGradient);
        }
        postInvalidate();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawCircle(mX, mY, mCurRadius, mPaint);
    }
}

如果本文有帮到你,记得加关注哦
源码下载地址:http://download.csdn.net/detail/harvic880925/9639134
转载请标明出处,http://blog.csdn.net/harvic880925/article/details/52653811谢谢


如果你喜欢我的文章,你可能更喜欢我的公众号
在这里插入图片描述


  • 41
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 30
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值