使用PorterDuff解决clipPath无法抗锯齿问题

一、简述

前段时间公司史无前例的接了一个大数据外包项目(哇~我们又不是外包公司(╯°Д°)╯︵ ┻━┻),要求搞很多图表方便观察运营的数据情况,图表当然要用到MPAndroidChart啦,但并不是所有的图表都可以用它用实现,这时就需要自定义View了,其中有一个要求,如下图所示,这就是本篇要实现的效果:

本篇全文适合像我一样的小白细细观看,如果你很赶时间,就只是进来看看标题上的解决方案,那么请直接看第二部分分析与实现的第5章节《优化解决抗锯齿问题》

二、分析与实现

最终效果上图就可以看到了,下面就来想想怎么实现从0实现这个自定义View吧。

1、分析

1)UI

可以看到这个View要根据进度,绘制对应长度的弧(或者说是不完整的环),因为环的颜色是渐变的,不能用程序来控制(或者说不好实现),所以向美术要了如下两张切图:

别看这两个环大小不一,实际上图片的整体尺寸是一样的,都是95*95。

那么接下来就是根据进度把图片的部分区域绘制出来就好了。

2)功能

  1. 可以自由设置总进度(maxProcess)与当前进度(process)。
  2. 可以执行绘制动画效果。

3)难点

  1. canvas绘制bitmap
  2. canvas裁切功能的使用
  3. 锯齿出现的原因与解决方法
  4. PorterDuff的理解与使用

2、实现

1)获取自定义控件宽高

这段可以说是自定义View的模板代码了,就不详细说明,基本上所有的自定义View都这样测量控件宽高,模板代码如下:

public class ArithmeticView extends View {
    private int mWidth  = 0;// 控件的宽度
    private int mHeight = 0;// 控件的高度
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        switch (widthSpecMode) {
            case MeasureSpec.AT_MOST:
                mWidth = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mWidth = widthSpecSize;
                break;
        }
        switch (heightSpecMode) {
            case MeasureSpec.AT_MOST:
                mHeight = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mHeight = heightSpecSize;
                break;
        }
        setMeasuredDimension(mWidth, mHeight);
    }
}

2)初始化加载图片资源

因为是项目特有的自定义View,不考虑通用问题,直接在View创建后加载需要用到的图片资源即可。

private Bitmap mBitmapInner;
private Bitmap mBitmapOuter;

public ArithmeticView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}    
private void init() {
    mBitmapInner = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_inner);
    mBitmapOuter = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_outer);
}

3)draw()方法实现

因为要绘制的环有2个,分为大小环,故绘制对应环的方法命名为:drawInnerBitmap()、drawOuterBitmap()。

@Override
protected void onDraw(Canvas canvas) {
    canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
    drawInnerBitmap(canvas);
    drawOuterBitmap(canvas);
}

private void drawInnerBitmap(Canvas canvas) {
}
private void drawOuterBitmap(Canvas canvas) {
}

可以看到在onDraw()方法中有这样一段代码:

canvas.setDrawFilter(new PaintFlagsDrawFilter(0, >Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));

这是为了抗锯齿的,但这里先透露一下,该方法对下面的实现方案一抗锯齿无效。

3、canvas绘制bitmap

1)canvas.drawBitmap()

canvas绘制bitmap用到的方法就是drawBitmap(),它的所有重载方法如下图所示

说明一下,如果你就单单只是把绘制Bitmap绘制出来,那么最后的paint参数可以传入null。

实际上,当你使用drawBitmap()绘制Bitmap时,画笔paint的作用并不大,可以认为无效。

那么多方法,用哪个呢?其实开发中常用的重载方法就如下两个:

public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint)
public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, @Nullable Paint paint)

第1个重载方法参数较少,其中left和top表示图片要绘制到canvas的起始位置,这个方法无法指定bitmap的图片要显示的区域(有时一张图片就只需要显示它的四分之一),这就是该重载方法的局限,而第2个重载方法则可以随便指定bitmap要显示的区域,而且是最常用的方法,功能相对更强,这里使用该方法来实现Bitmap的绘制。

2)绘制外环Bitmap

因为2个环的绘制原理一样,所以这里就以绘制外环为例:

private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);
    canvas.drawBitmap(mBitmapOuter, src, dsc, null);
}

其中,src表示要图片要被绘制区域(注意,是针对图片来说的),如果只绘制图片的四分之一,则代码如下:

int left = 0;
int top = 0;
int right = mBitmapOuter.getWidth() / 2;
int bottom = mBitmapOuter.getHeight() / 2;
Rect src = new Rect(left, top, right, bottom);

而dsc则表示要绘制到canvas上的哪个矩形区域(注意,是针对canvas来说的),前面的懂了,相信这个也不难理解。

这样,图片就绘制出来了,注意此时是没有锯齿的。

4、方案一:使用canvas裁切功能

环的完整绘制在上面已经用drawBitmap()方法实现,那么接下来就是绘制一个不完整的环了。我首先想到的方法就是使用canvas的裁切功能,将canvas的绘制区域先裁切出来,然后再在上面绘制图形,进而实现根据进度绘制图片的一部分。

针对该自定义View,需要说明一点,canvas的可绘制区域应该是一个从中上方开始逆时针打开的扇形(可以想象成一把扇子);用二维坐标系的方式来说的话,就从y轴开始,以原点为圆心,逆时针画圆。

1)canvas.clipXXX()

canvas的裁切功能需要用到clip开头的方法,canvas中所有的clipXXX()方法如下图所示:

绝大部分方法都是为了裁切出一个矩形,而我们这个不一样,它是要裁切出一个扇形!所以只有一个clipPath()方法可用,由我们来自定义裁切形状(同样的,除了矩形以外的任何形状都需要我们自己定义路径)。

2)使用clipPath()根据进度裁切扇形

不管是哪个clipPath()方法,都需要用到Path对象,该path对象就代表了canvas的裁切路径,因为大小环的进度可能不同,但原理一样,所以将path的设置代码抽出来作为一个共用方法,代码如下:

private Path   mPath;
private void init() {
    ...
    mPath = new Path();
}
/**
 * 设置裁切路径
 *
 * @param process    当前进度
 * @param maxProcess 总进度
 */
private void setClipPath(float process, float maxProcess) {
    mPath.reset();
    float ratio = process / maxProcess;
    if (ratio == 1) {
        // 当进度比例为1时,说明进度100%,要完整绘制Bitmap
        mPath.addCircle(mWidth / 2, mHeight / 2, mWidth / 2, Path.Direction.CCW);
    } else {
        float sweepAngle = ratio * -360;
        mPath.moveTo(mWidth / 2, mHeight / 2);// View的中心点位置
        mPath.lineTo(mWidth / 2, mHeight);// View的中心点上方位置
        mPath.arcTo(new RectF(0, 0, mWidth, mHeight), 270, sweepAngle * mProgressPercent, false);// 根据角度画弧线
        mPath.lineTo(mWidth / 2, mHeight / 2);// 最后再回到View的中心点位置,形成一个封闭路径
    }
    mPath.close();
}

关于Path的moveTo()、lineTo()、arcTo等方法在这里就是不详细科普了,因为方法比较多,要说明可能会花费不少篇幅,而且实际上这些方法顾名就可思义,如果不是很懂的同学自行百度查一下吧。

这里要着重说明一点,Android中的角度问题,如下图所示:

Android中的角度是以x轴为0°开始,以顺时针方向递增,而我们的自定义View要绘制的方向则是逆时针,所以要计算arcTo的sweepAngle时要注意乘以一个负值。

3)canvas的保存与复原

要注意一点,canvas的裁切功能会对后续的绘制产生影响,所以在裁切之前需要将Canvas的当前状态保存一下,在裁切绘制过后将Canvas的状态恢复回来。否则,之后的绘制结果可能并不是你想要的了。

如果不保存与恢复Canvas的状态,那么下次绘制只会在裁切出来的区域中进行。举个例子,假设内环的尺寸只有50*50,你先绘制了内环,但没有保存与恢复Canvas的状态,那么之后在绘制外环时(外环尺寸95*95),你就会发现外环看不到了。

保存Canvas的当前状态代码:

canvas.save(Canvas.CLIP_SAVE_FLAG);

恢复Canvas之前的状态代码:

canvas.restore();

所以外环裁切并绘制的完整代码如下:

private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);

    canvas.save(Canvas.CLIP_SAVE_FLAG);
    setClipPath(mOuterProcess, mOuterMaxProcess);
    canvas.clipPath(mPath);
    canvas.drawBitmap(mBitmapOuter, src, dsc, null);
    canvas.restore();
}

将进度设置为80%后,绘制出来的效果如下:

注意了,锯齿出现了!!!

5、优化解决抗锯齿问题

为什么?明明对canvas设置了抗锯齿了,怎么还这样?难道是因为在调用canvas.drawBitmap()时,没有传入抗锯齿画笔的原因?错,前面已经说过了,这个paint对drawBitmap()的作用并不大,就算你传入了可以抗锯齿的paint,锯齿依然存在。仔细想想,在使用裁切前后,锯齿的出现情况,你就能发现猫腻,百度及Google大法后,最终得出如下几点结论:

  • Android中,Canvas的抗锯齿功能必须使用到画笔Paint。
  • clipPath()会使drawBitmap()绘制出来的图像出现锯齿,而且没法解决方法,即使你使用了画笔。
  • 可以使用PorterDuff来实现同样的绘制效果。

那么,接下来就开始进行“曲线救国”路线。

6、方案二:使用PorterDuff

对于PorterDuff的介绍这里就不说了,百度吧,或者跳过,直接来看下面这图:

这图很好的表示了PorterDuff.Mode的各种效果,下面是对效果的详细说明。

Mode 说明
.CLEAR 所绘制不会提交到画布上
PorterDuff.Mode.SRC 显示上层绘制图片
PorterDuff.Mode.DST 显示下层绘制图片
PorterDuff.Mode.SRC_OVER 正常绘制显示,上下层绘制叠盖
PorterDuff.Mode.DST_OVER 上下层都显示。下层居上显示
PorterDuff.Mode.SRC_IN 取两层绘制交集。显示上层
PorterDuff.Mode.DST_IN 取两层绘制交集。显示下层
PorterDuff.Mode.SRC_OUT 取上层绘制非交集部分
PorterDuff.Mode.DST_OUT 取下层绘制非交集部分
PorterDuff.Mode.SRC_ATOP 取下层非交集部分与上层交集部分
PorterDuff.Mode.DST_ATOP 取上层非交集部分与下层交集部分
PorterDuff.Mode.XOR 异或:去除两图层交集部分
PorterDuff.Mode.DARKEN 取两图层全部区域,交集部分颜色加深
PorterDuff.Mode.LIGHTEN 取两图层全部,点亮交集部分颜色
PorterDuff.Mode.MULTIPLY 取两图层交集部分叠加后颜色
PorterDuff.Mode.SCREEN 取两图层全部区域,交集部分变为透明色

仔细看图中的Src、Dst、SrcIn,有没有什么想法呢?

如果你之前没用过PorterDuff.Mode也不慌,继续"听"我给你吹水,并不难理解。

1)原理与实现

这里要先理解2个单词:src与dst。通俗的说,dst是已经绘制在canvas上的图像,而src则是将要绘制到canvas上的图像(不得不说跟OpenGL的模板测试部分概念很像耶~)。那么,对于我们这个自定义View而言,dst就是那个扇形图像,src就是环,再结合PorterDuff.Mode的SrcIn模式,就可以让环的Bitmap只显示出扇形的部分了。

注意:使用PorterDuff需要禁止硬件加速。

代码很简单,不多废话,如下:

private Paint         mPaint;
private void init() {
    // 禁止硬件加速,硬件加速会有一些问题,这里禁用掉
    setLayerType(LAYER_TYPE_SOFTWARE, null);
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
}
private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);

    mPaint.reset();
    setClipPath(mOuterProcess, mOuterMaxProcess);
    canvas.drawPath(mPath, mPaint);// 绘制Dst
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 设置转换模式(显示Scr与Dst交接的区域)
    canvas.drawBitmap(mBitmapOuter, src, dsc, mPaint);// 绘制Src
}

效果很棒,没有锯齿了。

2)优化,实现双环无锯齿绘制

前面看似已经用PorterDuff实现了环的无锯齿绘制,但如果内环也是按照上面的代码来写,你就会发现,只显示后绘制的外环。具体原因嘛,我也不太懂,猜测是多次使用PorterDuff,会将之前绘制在Canvas上的图像清除吧。(如果有人知道真实原因,麻烦留言说一下,thx)。既然,直接对canvas操作不可取,那就换个思路吧。

我们要的不过是最后显示出来的不完整的环对吧,那我们可以在另一个Canvas上把这个不完整的环画出来,得到它的Bitmap,再在onDraw()的Canvas上对这个Bitmap进行绘制即可。所以,改进后的代码如下:

private void drawOuterBitmap(Canvas canvas) {
    int left = 0;
    int top = 0;
    int right = mBitmapOuter.getWidth();
    int bottom = mBitmapOuter.getHeight();
    Rect src = new Rect(left, top, right, bottom);
    RectF dsc = new RectF(0, 0, mWidth, mHeight);

    // 1. 在另一个Canvas中使用 path + mBitmapOuter 将最终图形finalBitmap绘制出来。
    mPaint.reset();
    setClipPath(mOuterProcess, mOuterMaxProcess);
    Bitmap finalBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);// 一张白纸位图
    Canvas mCanvas = new Canvas(finalBitmap);// 用指定的位图构造一个画布来绘制
    mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));// 画布绘制Bitmap时搞锯齿
    mCanvas.drawPath(mPath, mPaint);// 绘制Dst
    mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 设置转换模式(显示Scr与Dst交接的区域)
    mCanvas.drawBitmap(mBitmapOuter, src, dsc, mPaint);// 绘制Src
    // 2. 再在原来的Canvas中将finalBitmap绘制出来。
    canvas.drawBitmap(finalBitmap, 0, 0, null);
}

同样的,内环的代码基本一致,直接看效果吧。

三、完整代码

至于那个执行绘制动画效果的功能并非本文重点,实现上能见仁见智,下面贴出该自定义View的完整代码,当然执行绘制动画效果的功能也在里面(具体实现看animateStart()方法)。完整代码如下:

public class ArithmeticView extends View {

    private int mWidth  = 0;// 控件的宽度
    private int mHeight = 0;// 控件的高度
    private Bitmap mBitmapInner;
    private Bitmap mBitmapOuter;
    private Path   mPath;
    private float mInnerProcess    = 50;
    private float mInnerMaxProcess = 100;
    private float mOuterProcess    = 80;
    private float mOuterMaxProcess = 100;
    private float mProgressPercent = 1;// 当前进度百分比
    private ValueAnimator mValueAnimator;
    private Paint         mPaint;

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

    public ArithmeticView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ArithmeticView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        switch (widthSpecMode) {
            case MeasureSpec.AT_MOST:
                mWidth = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mWidth = widthSpecSize;
                break;
        }
        switch (heightSpecMode) {
            case MeasureSpec.AT_MOST:
                mHeight = dp2px(200);
                break;
            case MeasureSpec.EXACTLY:
            case MeasureSpec.UNSPECIFIED:
                mHeight = heightSpecSize;
                break;
        }
        setMeasuredDimension(mWidth, mHeight);
    }

    private void init() {
        // 禁止硬件加速,硬件加速会有一些问题,这里禁用掉
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        mBitmapInner = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_inner);
        mBitmapOuter = BitmapFactory.decodeResource(getResources(), R.mipmap.main_arithmetic_outer);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
        drawInnerBitmap(canvas);
        drawOuterBitmap(canvas);
    }

    private void drawInnerBitmap(Canvas canvas) {
        int left = 0;
        int top = 0;
        int right = mBitmapInner.getWidth();
        int bottom = mBitmapInner.getHeight();
        Rect src = new Rect(left, top, right, bottom);
        RectF dsc = new RectF(0, 0, mWidth, mHeight);

        mPaint.reset();
        setClipPath(mInnerProcess, mInnerMaxProcess);
        Bitmap finalBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);// 一张白纸位图
        Canvas mCanvas = new Canvas(finalBitmap);// 用指定的位图构造一个画布来绘制
        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));// 画布绘制Bitmap时搞锯齿
        mCanvas.drawPath(mPath, mPaint);// 绘制Dst
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 设置转换模式(显示Scr与Dst交接的区域)
        mCanvas.drawBitmap(mBitmapInner, src, dsc, mPaint);// 绘制Src
        canvas.drawBitmap(finalBitmap, 0, 0, null);
    }

    private void drawOuterBitmap(Canvas canvas) {
        int left = 0;
        int top = 0;
        int right = mBitmapOuter.getWidth();
        int bottom = mBitmapOuter.getHeight();
        Rect src = new Rect(left, top, right, bottom);
        RectF dsc = new RectF(0, 0, mWidth, mHeight);

        /*------------------ 方案1:使用clipPath方式绘制图片,但无法抗锯齿 ------------------*/
//        canvas.save(Canvas.CLIP_SAVE_FLAG);
//        setClipPath(mOuterProcess, mOuterMaxProcess);
//        canvas.clipPath(mPath);
//        canvas.drawBitmap(mBitmapOuter, src, dsc, null);
//        canvas.restore();

        /*------------------ 方案二:使用PorterDuff方式,可以抗锯齿 ------------------*/
        // 1. 在另一个Canvas中使用 path + mBitmapOuter 将最终图形finalBitmap绘制出来。
        mPaint.reset();
        setClipPath(mOuterProcess, mOuterMaxProcess);
        Bitmap finalBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);// 一张白纸位图
        Canvas mCanvas = new Canvas(finalBitmap);// 用指定的位图构造一个画布来绘制
        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));// 画布绘制Bitmap时搞锯齿
        mCanvas.drawPath(mPath, mPaint);// 绘制Dst
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));// 设置转换模式(显示Scr与Dst交接的区域)
        mCanvas.drawBitmap(mBitmapOuter, src, dsc, mPaint);// 绘制Src
        // 2. 再在原来的Canvas中将finalBitmap绘制出来。
        canvas.drawBitmap(finalBitmap, 0, 0, null);
    }

    /**
     * 设置裁切路径
     *
     * @param process    当前进度
     * @param maxProcess 总进度
     */
    private void setClipPath(float process, float maxProcess) {
        mPath.reset();
        float ratio = process / maxProcess;
        if (ratio == 1) {
            mPath.addCircle(mWidth / 2, mHeight / 2, mWidth / 2, Path.Direction.CCW);
        } else {
            float sweepAngle = ratio * -360;
            mPath.moveTo(mWidth / 2, mHeight / 2);
            mPath.lineTo(mWidth / 2, mHeight);
            mPath.arcTo(new RectF(0, 0, mWidth, mHeight), 270, sweepAngle * mProgressPercent, false);
            mPath.lineTo(mWidth / 2, mHeight / 2);
        }
        mPath.close();
    }

    public float getInnerProcess() {
        return mInnerProcess;
    }

    public void setInnerProcess(float innerProcess) {
        mInnerProcess = innerProcess;
        postInvalidate();
    }

    public float getInnerMaxProcess() {
        return mInnerMaxProcess;
    }

    public void setInnerMaxProcess(float innerMaxProcess) {
        mInnerMaxProcess = innerMaxProcess;
    }

    public float getOuterProcess() {
        return mOuterProcess;
    }

    public void setOuterProcess(float outerProcess) {
        mOuterProcess = outerProcess;
        postInvalidate();
    }

    public float getOuterMaxProcess() {
        return mOuterMaxProcess;
    }

    public void setOuterMaxProcess(float outerMaxProcess) {
        mOuterMaxProcess = outerMaxProcess;
    }

    /**
     * 开始动画
     *
     * @param duration 动画时长(毫秒)
     */
    public void animateStart(long duration) {
        if (mValueAnimator != null) {
            mValueAnimator.cancel();
            mValueAnimator = null;
        }
        mValueAnimator = ValueAnimator.ofFloat(0, 100);
        mValueAnimator.setDuration(duration).start();
        mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Float value = (Float) animation.getAnimatedValue();
                if (value == 100) {
                    mValueAnimator.cancel();
                    mValueAnimator = null;
                }
                mProgressPercent = value / 100;
                postInvalidate();
            }
        });
    }

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

推荐阅读更多精彩内容