教你用Paint给女朋友化妆(上)!

1.前言

正在实习的公司最近特别忙,加上内分泌失调,顺便TI7也开始了(唯一指定冠军!LGD是不可战胜的!),以上种种,导致断更了……下面进入正文。

学会给女孩子化妆,是你走向人生巅峰的第一步。今天我们就走进堪称亚洲四大邪术之首的化妆术,一起来剖析这门技术的幕后原理。
但是,不要跟我说什么唇膏唇蜜唇釉唇彩!这种东西我不懂!也不需要!作为一名Android程序员,老夫化妆向来都是一把梭!Paint!Martrix!合二为一!变白就走!

2.Paint基本使用

化妆属于Paint的高级技巧,那么在学习之前,我们肯定是要把基础都给夯扎实了。

之前写过一篇Paint基础,内容大多数是枯燥的Api使用,考虑再三就给删了。这些内容完全可以参考官方文档,因此我只选出一些有意思的Api。

Paint的使用主要可以分为两大类,第一是图形、路径的绘制,第二是文字的绘制。我们开始吧~

2.1 绘制图形与路径

1.setStyle(Paint.Style style):设置画笔样式

Paint.Style.FILL :填充内部
Paint.Style.FILL_AND_STROKE :填充内部和描边
Paint.Style.STROKE :仅描边
这些样式不难理解,顾名思义即可

2.setStrokeCap(Paint.Cap cap):设置线冒样式

Paint.Cap.ROUND:圆形线冒
Paint.Cap.SQUARE:方形线冒
Paint.Cap.BUTT:无线冒)

这个不太好理解,我们用代码和图片来说明。

 private void drawStrokeCap(Canvas canvas){
        Paint paint = new Paint();
        paint.setStrokeWidth(80);
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);

        paint.setStrokeCap(Paint.Cap.BUTT);// 无线帽
        canvas.drawLine(100,200,400,200,paint);

        paint.setStrokeCap(Paint.Cap.SQUARE);// 方形线帽
        canvas.drawLine(100,400,400,400,paint);

        paint.setStrokeCap(Paint.Cap.ROUND);// 圆形线帽
        canvas.drawLine(100,600,400,600,paint);

    }

效果图:

线帽类型

冒多出来的那块区域就是线帽,就相当于给原来的直线加上一个帽子一样。

3.setStrokeJoin(Paint.Join join):设置线段连接处样式

Join.MITER:结合处为锐角
Join.ROUND:结合处为圆弧
Join.BEVEL:结合处为直线

代码如下:

 private void drawStrokeJoin(Canvas canvas){
        ...
        Path path  = new Path();
        path.moveTo(100,100);
        path.lineTo(450,100);
        path.lineTo(100,300);
        paint.setStrokeJoin(Paint.Join.MITER);//锐角
        canvas.drawPath(path,paint);

        path.moveTo(100,400);
        path.lineTo(450,400);
        path.lineTo(100,600);
        paint.setStrokeJoin(Paint.Join.BEVEL);//直线
        canvas.drawPath(path,paint);

        path.moveTo(100,700);
        path.lineTo(450,700);
        path.lineTo(100,900);
        paint.setStrokeJoin(Paint.Join.ROUND);//圆弧
        canvas.drawPath(path,paint);
    }

效果图:

连接样式

4.setPathEffect(PathEffect effect):设置绘制路径的效果
这个方法很牛逼,根据PathEffect的不同,可以实现不同的效果,这里我举两个例子。

1.CornerPathEffect:圆形拐角效果 
paint.setPathEffect(new CornerPathEffect(100));
利用半径R=50的圆来代替原来两条直线间的夹角

2.DashPathEffect:虚线效果 
paint.setPathEffect(new DashPathEffect(new float[]{20,10,50,100},15));
intervals[]:表示组成虚线的各个线段的长度;整条虚线就是由intervals[]中这些基本线段循环组成的。
phase:表示开始绘制的偏移值 

代码如下:

private void drawComposePathEffectDemo(Canvas canvas){
        //画原始路径
        Paint paint = getPaint();
        Path path = getPath();
        canvas.drawPath(path,paint);

        //仅应用圆角特效的路径
        canvas.translate(0,300);
        CornerPathEffect cornerPathEffect = new CornerPathEffect(100);
        paint.setPathEffect(cornerPathEffect);
        canvas.drawPath(path,paint);

        //仅应用虚线特效的路径
        canvas.translate(0,300);
        DashPathEffect dashPathEffect = new DashPathEffect(new float[]{2,5,10,10},0);
        paint.setPathEffect(dashPathEffect);
        canvas.drawPath(path,paint);

        //利用ComposePathEffect先应用圆角特效,再应用虚线特效
        canvas.translate(0,300);
        ComposePathEffect composePathEffect = new ComposePathEffect(dashPathEffect,cornerPathEffect);
        paint.setPathEffect(composePathEffect);
        canvas.drawPath(path,paint);

        //利用SumPathEffect,分别将圆角特效应用于原始路径,然后将生成的两条特效路径合并
        canvas.translate(0,300);
        paint.setStyle(Paint.Style.STROKE);
        SumPathEffect sumPathEffect = new SumPathEffect(cornerPathEffect,dashPathEffect);
        paint.setPathEffect(sumPathEffect);
        canvas.drawPath(path,paint);

    }

效果图:


PathEffect

(5-8就是我们真正的化妆技术了!等会一个个详细介绍的!这里先留个印象吧)

5.setXfermode(Xfermode xfermode):设置图形重叠时的处理方式
如合并,取交集或并集,经常用来制作橡皮的擦除效果

6.setMaskFilter(MaskFilter maskfilter):实现滤镜的效果
如滤化,立体等

7.setColorFilter(ColorFilter colorfilter):设置颜色过滤器
可以在绘制颜色时实现不用颜色的变换效果

8.setShader(Shader shader):设置图像效果
使用Shader可以绘制出各种渐变效果

2.2 绘制文字

如果有的时候,你想在化妆的同时在你女朋友脸上写字,那么下面的知识点是一定要重点掌握的。

1.Typeface getTypeface() 、Typeface setTypeface(Typeface typeface):获取与设置字体类型。
Android默认有四种字体样式:BOLD(加粗)、BOLD_ITALIC(加粗并倾斜)、ITALIC(倾斜)、NORMAL(正常)。
我们也可以通过Typeface类来自定义个性化字体。这里有一个骚操作,就是将图标直接放到ttf中,用起来很舒服。

2.Paint.Align getTextAlign() 、void setTextAlign(Paint.Align align):获取与设置文本对齐方式
取值为CENTER、LEFT、RIGHT,也就是文字绘制是左边对齐、右边还是局中的。

3.int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth):判断能显示多少文字

private static final String STR = "ABCDEF";
mPaint.setTextSize(50);
float[] value = new float[1];
int ret = mPaint.breakText(STR, true, 200, value);
Log.i(TAG, "breakText="+ret+", STR="+STR.length()+", value="+value[1]);
//输出结果如下:
//breakText=5, STR=8, value=195.0

2.3 绘制文字高级技巧

现在考虑这样一种场景,自定义一个圆形的进度条,在圆形中间显示下载进度。
乍一看感觉很简单,但是动手之后就发现这里面有一个坑——下载进度无法显示在圆心。

为什么呢?这儿就涉及到Android绘制文字的原理了。在Paint中,我们可以找到一个叫FontMetrics的内部类,从名字上看,它和文字的测量时有关系的,进去看看

public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }

这里一共定义了5个float类型的变量,我们用图片来直观的体现下他们的关系。

FontMetrics

top = top线的y坐标 - baseline线的y坐标,不能超过
bottom = bottom线的y坐标 - baseline线的y坐标,不能超过
ascent = ascent线的y坐标 - baseline线的y坐标,是系统建议的高度,字母上有音标符号时,会超过
desent = desent线的y坐标 - baseline线的y坐标,可能超过
最坑的就是这个baseline,由于它不在文字的中心,而drawText()中传入的参数是以baseline为基准的,因此在绘制时就会出现文字不在中心的情况。

那么要如何解决呢?拥有数学头脑的同学肯定一眼就看穿,中心点是可以计算出来的!

解:设文字中心到top、bottom的距离分别为A、B,其中A=B;
设文字中心到baseLine的距离为C;
由图可知
A=B = (bottom - top)/2
因为bottom = baseline + FontMetrics.bottom
且top = baseline + FontMetrics.top
所以A = B = (FontMetrics.bottom - FontMetrics.top)/ 2

又因为C =  B - (bottom - baseline)=  B - FontMetrics.bottom
且C = baseline - center      
所以baseline - center = (FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom

综上所述
baseline = center +(FontMetrics.bottom - FontMetrics.top)/2 - FontMetrics.bottom        

计算完毕,是不是有点怀念自己的高中数学老师了?

2.4 实践是检验真理的唯一标准

现在我们知道了文字绘制的原理,又得到了中心点的计算公式,那么就来手写一个圆形的进度条吧。

首先需要在创建attrs资源文件,并且在其中自定义资源类型

 <declare-styleable name="CircleProgressBar">
        <attr name="progressColor" format="color"/>
        <attr name="progressBackgroundColor" format="color"/>
        <attr name="progressMax" format="float"/>
        <attr name="circleWidth" format="dimension"/>
        <attr name="textSize" format="dimension"/>
        <attr name="textColor" format="color"/>
    </declare-styleable>

接着自定义控件,由于有自定义的资源类型,所以要在构造方法中获取他们,注意最后要typedArray.recycle()

 public CircleProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressBar);
        mProgressBackgroundColor = typedArray.getColor(R.styleable.CircleProgressBar_progressBackgroundColor, Color.GRAY);
        mProgressColor = typedArray.getColor(R.styleable.CircleProgressBar_progressColor, Color.BLUE);
        mProgressMax = typedArray.getFloat(R.styleable.CircleProgressBar_progressMax, 100);
        mCircleWidth = typedArray.getDimension(R.styleable.CircleProgressBar_circleWidth, 20);
        mTextSize = typedArray.getDimension(R.styleable.CircleProgressBar_textSize, 60);
        mTextColor = typedArray.getColor(R.styleable.CircleProgressBar_textColor, Color.RED);
        typedArray.recycle();
    }

代码主要是重写onDraw()

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画圆环
        mPaint = new Paint();
        mPaint.setColor(mProgressBackgroundColor);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        int center = getWidth() / 2;
        float radius = center - mCircleWidth / 2;
        canvas.drawCircle(center, center, radius, mPaint);
        //画文字
        mPaint = new Paint();
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        mPaint.setStrokeWidth(0);
        mPaint.setTypeface(Typeface.DEFAULT_BOLD);
        int percent = (int) (mProgress / mProgressMax* 100) ;
        String percentStr = percent + "%";
        Paint.FontMetricsInt fm = mPaint.getFontMetricsInt();
        canvas.drawText(percentStr,
                center - mPaint.measureText(percentStr) / 2,
                center + (fm.bottom - fm.top) / 2 - fm.bottom,
                mPaint);
        //画圆弧
        mPaint=new Paint();
        mPaint.setColor(mProgressColor);
        mPaint.setStrokeWidth(mCircleWidth);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        RectF oval=new RectF(center-radius,center-radius,center+radius,center+radius);
        canvas.drawArc(oval,0,mProgress/mProgressMax*360,false,mPaint);
    }

重写onMeasure()是为了解决wrap_content的问题。如果没有加上这一段,那么使用wrap_contentmatch_parent就没有区别。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
        if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(300,300);
        }else if(widthSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(300,heightSpecSize);
        }else if(heightSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSpecSize,300);
        }
    }

再给调用者提供一个设置进度的方式,万事大吉!

public void setProgress(int progress){
        if(progress<0){
            throw new IllegalArgumentException("进度不能小于0!");
        }
        if(progress>mProgressMax){
            mProgress= (int) mProgressMax;
        }
        if(progress<mProgressMax){
            mProgress=progress;
            //不知道应用层在哪个线程调用
            postInvalidate();
        }
    }

值得注意的是,在这里我们还没有对padding进行处理。认真阅读奶奶系列的同学,自己解决一定是没有问题的,这里给出一个解决思路,只要在onDraw()中获取这些值,并在canvas.drawXXX()时加入即可。

int paddingLeft=getPaddingLeft();
int paddingRight=getPaddingRight();
int paddingTop=getPaddingTop();
int paddingBottom=getPaddingBottom();

最后来看看效果图(挺丑的= =)


圆形进度条

3.高级渲染

基础的知识点大家应该了解了不少,下面就一起来学习真正的化妆技巧吧。

首先是高级渲染部分。所谓高级渲染,其实就是通过setShader(Shader shader)方法来设置图像效果。
Shader是着色器的意思,canvas.drawXXX()画出了具体的形状,而画笔的shader则定义了图形的着色和外观。

3.1.BitmapShader

BitmapShader是指位图图像渲染,即用BitMap对绘制的图形进行渲染着色,简单来说是用图片对图形进行贴图。

在使用Shader时,涉及到一个TileMode的概念,从名字上可以看出,这个参数就是用来设置拉伸模式的。也就是说,当图片小于容器的大小时,多出来的那部分要怎么画。TileMode一共有三种类型:

TileMode.CLAMP 拉伸最后一个像素去铺满剩下的地方
TileMode.MIRROR 通过镜像翻转铺满剩下的地方。
TileMode.REPEAT 重复图片平铺整个画面

具体该怎么使用就直接上代码吧

private void BitmapShaderTest(Canvas canvas) {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.xyjy2);
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        float scale = Math.max(width, height) / Math.min(width, height);
        BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        android.graphics.Matrix matrix = new android.graphics.Matrix();
        matrix.setScale(scale, scale);
        bitmapShader.setLocalMatrix(matrix);
        mPaint.setShader(bitmapShader);
        canvas.drawRect(0,0,800,800,mPaint);
    }

都是一些基本的套路,接着看效果图


BitmapShader_Rectangle

注意观察左侧被拉伸的区域,这就是TileMode.CLAMP的作用。剩下2个类型大家可以自己去玩玩看。

那么这玩意儿有什么用处呢?你可以和女朋友装逼说“用什么CircleImageView,直接用Paint画一个不就好了?”。
只要把上面代码最后一行canvas.drawRect(0,0,800,800,mPaint);替换成canvas.drawCircle(height / 2, height / 2, height / 2, mPaint);即可。

BitmapShader_Cirlce

遗憾的是,如果你女朋友是会玩的并且学习过反装逼,那她就会说,“这算什么,老娘不用你的Paint照样能画出来!”。于是唰唰唰一顿操作,你的代码变成了这样:

ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
shapeDrawable.getPaint().setShader(bitmapShader);
shapeDrawable.setBounds(0,0,width,height);
shapeDrawable.draw(canvas);

效果图就不放了,和上面的类似,只不过这次是个椭圆。

3.1.1.BitmapShader放大镜效果

结合上面的知识点,我们来学习第一个化妆技巧——放大妆!
首先,我们需要原图和放大图两张图片。原图直接展示在界面上,放大图通过ShapeDrawable只展示出一块圆形的区域。

public ZoomImageView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //原图
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.xyjy3);
        //缩放图
        mBitmapScale = Bitmap.createScaledBitmap(mBitmap, mBitmap.getWidth() * FACTOR
                , mBitmap.getHeight() * FACTOR, true);
        mBitmapShader = new BitmapShader(mBitmapScale, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mShapeDrawable = new ShapeDrawable(new OvalShape());
        mShapeDrawable.getPaint().setShader(mBitmapShader);
        mShapeDrawable.setBounds(0,0,RADIUS*2,RADIUS*2);
        mMatrix = new Matrix();
    }

接着,在ondraw()方法中进行绘制

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(mBitmap,0,0,null);
        mShapeDrawable.draw(canvas);
    }

最后,重写onTouchEvent()实现自由放大。需要理解的是,当我的手指向右下方移动时,放大图片本身是要向左上移动的,而放大区域则是一直以手指触摸点为圆心。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x= (int) event.getX();
        int y= (int) event.getY();
        //将放大的图片往相反的方向移动
        mMatrix.setTranslate(RADIUS-x*FACTOR,RADIUS-y*FACTOR);
        mBitmapShader.setLocalMatrix(mMatrix);
        //手指触摸点为圆心
        mShapeDrawable.setBounds(x-RADIUS,y-RADIUS,x+RADIUS,y+RADIUS);
        invalidate();
        return true;
    }

放大妆的作用很明显,比如你女朋友牙齿特别好看,那么出门的时候就可以给她来上这个一下,把美丽的牙齿大方的展示出来!我们来看看效果图

放大镜效果

3.2.LinearGradient

LinearGradient是线性渲染, 主要通过颜色的变化来进行渲染。
关于基本使用,我们直接上代码来看

private int[] mColors = {Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW};
 /**线性渐变
     * x0, y0, 起始点
     * x1, y1, 结束点
     * int[]  mColors, 中间依次要出现的几个颜色
     * float[] positions,数组大小跟colors数组一样大,中间依次摆放的几个颜色分别放置在那个位置上(参考比例从左往右)
     * tile
     */
    private void LinearGradientTest(Canvas canvas) {
        LinearGradient linearGradient = new LinearGradient(0, 0, 800, 800, mColors, null, Shader.TileMode.CLAMP);
        mPaint.setShader(linearGradient);
        canvas.drawRect(0, 0, 800, 800, mPaint);
    }

效果图这样

LinearGradient基本使用.png

3.2.1.LinearGradient霓虹灯文字

下面这个妆就厉害了,是动态的,还会来回转,我们看代码
首先,这个自定义View要继承TextView,这样比较方便

public class LinearGradientTextView extends TextView

接着,通过getPaint()获取到画笔,并为其设置LinearGradient

public LinearGradientTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //获取屏幕宽度
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mPoint = new Point();
        windowManager.getDefaultDisplay().getSize(mPoint);
        String text = getText().toString();
        //拿到TextView的画笔
        mPaint = getPaint();
        mTextWidth = (int) mPaint.measureText(text);
        int gradientTextSize = mTextWidth / text.length() * 3;
        mLinearGradient = new LinearGradient(0, 0, gradientTextSize, 0, mColors, null, Shader.TileMode.CLAMP);
        mPaint.setShader(mLinearGradient);
    }

此时静态的效果已经出来了,剩下的就是重写onDraw()实现动态变换

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Matrix matrix = new Matrix();
        mTranslate += DELTAX;
        //反复循环
        if (mTranslate > mPoint.x || mTranslate < 1) {
            DELTAX = -DELTAX;
        }
        matrix.setTranslate(mTranslate, 0);
        mLinearGradient.setLocalMatrix(matrix);
        postInvalidate();
    }

国际惯例,看一张效果图(gif什么的看上去好麻烦…大家根据代码自己脑补吧)

文字霓虹灯.png

3.3.SweepGradient

SweepGradient叫渐变渲染或者梯度渲染,其效果与用法都和LinearGradient类似,还是先介绍基本用法

private void SweepGradientTest(Canvas canvas) {
        SweepGradient mSweepGradient = new SweepGradient(300, 300, mColors, null);
        mPaint.setShader(mSweepGradient);
        canvas.drawRect(0, 0, 800,800, mPaint);
    }

效果图

SweepGradient基本使用.png

3.3.1 SweepGradient雷达扫描

那么这倒霉玩意儿又能干啥呢?SweepGradient经典的应用场景就是实现雷达扫描效果。
来看代码,首先在构造方法中初始化Paint

public RadarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);

        mRadarBg = new Paint(Paint.ANTI_ALIAS_FLAG);     //设置抗锯齿
        mRadarBg.setColor(mRadarBgColor);                  //画笔颜色
        mRadarBg.setStyle(Paint.Style.FILL);           //画实心圆

        mRadarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);     //设置抗锯齿
        mRadarPaint.setColor(mCircleColor);                  //画笔颜色
        mRadarPaint.setStyle(Paint.Style.STROKE);           //设置空心的画笔,只画圆边
        mRadarPaint.setStrokeWidth(2);                      //画笔宽度
        mRadarShader = new SweepGradient(0, 0, mStartColor, mEndColor);
        mMatrix = new Matrix();
    }

接着在onDraw()方法中进行绘制

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mRadarBg.setShader(null);
        //将画板移动到屏幕的中心点
        canvas.translate(mRadarRadius, mRadarRadius);
        //绘制底色,让雷达的线看起来更清晰
        canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);
        //画圆圈
        for (int i = 1; i <= mCircleNum; i++) {
            canvas.drawCircle(0, 0, (float) (i * 1.0 / mCircleNum * mRadarRadius), mRadarPaint);
        }
        //绘制雷达基线 x轴
        canvas.drawLine(-mRadarRadius, 0, mRadarRadius, 0, mRadarPaint);
        //绘制雷达基线 y轴
        canvas.drawLine(0, mRadarRadius, 0, -mRadarRadius, mRadarPaint);
        //设置颜色渐变从透明到不透明
        mRadarBg.setShader(mRadarShader);
        canvas.concat(mMatrix);
        canvas.drawCircle(0, 0, mRadarRadius, mRadarBg);
    }

最后来个handler让雷达动起来吧

private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            mRotate += 3;
            postInvalidate();
            mMatrix.reset();
            mMatrix.preRotate(mRotate, 0, 0);
            mHandler.sendEmptyMessageDelayed(MSG_WHAT, DELAY_TIME);
        }
    };

效果图长这样

雷达扫描.png

3.4.RadialGradient

RadialGradient表示环形渲染,先来看下基本用法

 private void RadialGradientTest(Canvas canvas) {
        RadialGradient mRadialGradient = new RadialGradient(300, 300, 100, mColors, null, Shader.TileMode.REPEAT);
        mPaint.setShader(mRadialGradient);
        canvas.drawCircle(300, 300, 300, mPaint);
    }

效果图,正常人应该是会晕的

RadialGradient基本使用.png

3.4.1 RadialGradient水波纹效果

文章的最后,终于轮到大名鼎鼎的水波纹效果了。
思路大致如下:使用RadialGradient响应触摸事件绘制出初始的圆形波纹,接着使用属性动画将圆形波纹变大。
由于属性动画涉及到对属性的设置,这里我们提供一个setRadius()方法

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();
    }

重点都在onTouch()方法中

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if (mX != event.getX() || mY != mY) {
            mX = (int) event.getX();
            mY = (int) event.getY();
            setRadius(DEFAULT_RADIUS);
        }
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                return true;
            case 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 onAnimationEnd(Animator animation) {
                        setRadius(0);
                    }
                    ...
                });
                mAnimator.start();
            }
        }
        return super.onTouchEvent(event);
    }

这里就通过属性动画对radius值进行了改变,关于属性动画,以后会从源码入手好好讲一讲。最后只要在onDraw()里进行简单的绘制即可。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mX, mY, mCurRadius, mPaint);
    }

效果光看图片不容易感受,大伙将就着看。


水波纹效果.png

4.总结

这篇文章首先介绍了Paint的一些基础用法,包括图形、路径、文字的绘制。接着将高级渲染进行了细致讲解。其实还有最后一个ComposeShader没有提,这是组合渲染,可以将前面说的4种渲染方式随意组合,大家自由发挥即可。

Paint高级化妆技巧还包括Xfermode以及滤镜的使用,下篇文章咱们就来怼他们俩。

完结撒花~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容