Android—PorterDuffXfermode实现进度条

我的CSDN: ListerCi
我的简书: 东方未曦

周一上午,老王叼着包子,一进公司门就看到了产品在张望。老王暗道一声不好,正想溜之大吉,却已经被产品半路截下:“我们的进度条需要改版,我给你找了个样式,你就照这个做吧”。老王寻思不就一进度条吗,能有什么花头。伸手接过产品递来的手机一看,眼前的进度条长这个样子:

gif_效果.gif

老王眉头一皱,发现事情并不简单,但是作为一个沉着冷静的老开发,他从来不会说“应该”、“或许”这种有损他形象的词语。他转头对产品微微一笑,说道:“下班前给你”。

.......

一、进度条的背景与填充

看到这么一个需求,老王的第一反应就是先实现外部的背景与填充色。首先绘制外部的整体背景,然后根据进度绘制内部的填充。那么首先需要实现这样一个自定义View:

public class ProgressTestView extends View {
    private Context mContext;
    private int mWidth;
    private int mHeight;
    private float mRadius;

    private Paint mBackgroundPaint;
    private RectF mBackgroundRectF;

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

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

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

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null); // 取消硬件加速
        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setColor(Color.GRAY);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        mRadius = mHeight / 2.0f;
        mBackgroundRectF = new RectF(0, 0, mWidth, mHeight);
    }

    private void drawBackground(Canvas canvas) {
        canvas.drawRoundRect(mBackgroundRectF, mRadius, mRadius, mBackgroundPaint);
    }

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

这个自定义View很简单,在onSizeChanged()中获取到为该View设置的高度和宽度,由于需要绘制的是一个圆角矩形,这里将高度的一半设置为圆角矩形两端的半径。此时的效果如下:

进度条背景.png

有了背景之后就可以开始绘制内容了,背景中的填充也是一个圆角矩形。该圆角矩形的高度和弧度与背景矩形相同,宽度是当前进度(∈[0, 1])与背景宽度的乘积。根据该思路,需要定义一个值表示当前的进度并为外界提供修改该进度的方法。当外界每次改变进度时重绘当前View。代码如下:

public class ProgressTestView extends View {
    // ....
    private float mCurProgress = 0;
    private Paint mContentPaint;
    // ......

    public void setCurProgress(float progress) {
        if (progress > 1) mCurProgress = 1;
        else if (progress < 0) mCurProgress = 0;
        else mCurProgress = progress;
        invalidate();
    }

    private void init() {
        // ......
        mContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mContentPaint.setColor(mContext.getResources().getColor(R.color.color_progress_content));
    }

    // ......

    private void drawContent(Canvas canvas) {
        canvas.drawRoundRect(new RectF(0, 0, mCurProgress * mWidth, mHeight),
                mRadius, mRadius, mContentPaint);
    }

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

然后在使用该View的地方定义一个动画看看效果:

ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration(5000);
animator.addUpdateListener(animation -> {
    float percentage = (float) animation.getAnimatedValue();
    mProgressView.setCurProgress(percentage);
});
animator.start();

最终的效果如下所示:

gif_直接绘制内容.gif

这个效果是不是有哪里不对?老王也陷入了沉思,作为一个强迫症,根本无法接受这种丑陋的动画。

仔细观察发现,填充的内容只有在宽度>=高度的时候才有正确的显示效果。那么我们可不可以只显示背景与内容相交的部分呢?当然可以!这就涉及到图像合成——PorterDuffXfermode

二、PorterDuffXfermode的使用和避坑指南

PorterDuffXfermode用于两个图像的合成,API中定义了16种合成方式。图中蓝色的为Src图像,黄色的为Dst图像,通过不同的组合方式能够得到各种组合图形。那么要实现之前说的相交,把进度条的内容作为Dst,背景作为Src,通过DstIn这个合成方式不就能得到结果了吗?

PorterDuffXfermode合成方式.jpg

根据这个思路,再查一下PorterDuffXfermode的使用文档,就可以把原来的代码修改成下面这样。注意绘制时是先DST后SRC。

public class ProgressTestView extends View {
    
    private PorterDuffXfermode mPorterDuffXfermode;

    // ......

    private void init() {
        setLayerType(LAYER_TYPE_SOFTWARE, null);

        mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setColor(Color.GRAY);

        mContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mContentPaint.setColor(mContext.getResources().getColor(R.color.color_progress_content));
    }

    // ......

    private void drawContent(Canvas canvas) {
        canvas.drawRoundRect(new RectF(0, 0, mCurProgress * mWidth, mHeight),
                mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(mPorterDuffXfermode);
        canvas.drawRoundRect(mBackgroundRectF, mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(null);
    }
}

运行一下,发现效果并没有什么变化,这是因为这里没有考虑到图层(Layer)的问题。

Layer可以理解为画布Canvas的一个层级,默认情况下Canvas只有一个Layer,所有的绘制都在同一图层上。当需要绘制多层图像时,可以通过canvas.saveLayer(...)生成新的Layer,在新Layer上绘制的内容是独立的,不会影响到其他Layer的内容,调用canvas. restoreToCount(int sc)时将该Layer覆盖到Canvas现有的图像上。Canvas通过栈的形式管理Layer,示意图如下。

Layer.png

而在进行图像混合时,先绘制的内容是DST,后绘制的是SRC。如果不新建Layer的话,在绘制SRC时,Canvas上的所有内容都会被当作DST,所以背景等内容也会参与图像混合,很容易得到错误的效果。

public class ProgressTestView extends View {

    // ......

    private void drawContent(Canvas canvas) {
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);

        canvas.drawRoundRect(mRectFBackground, mRadius, mRadius, mBgPaint);
        mContentPaint.setXfermode(mContentMode);
        canvas.drawRoundRect(new RectF(0, 0, mWidth * mCurPercentage, mHeight),
                mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(null);

        canvas.restoreToCount(sc);
    }

    // ......
}

再来看看效果。三个字:舒服了~

gif_图像合成绘制内容.gif

三、文字绘制

有了上面的成功经验,绘制文字的思路就很清晰了。将文字作为DST,进度条的内容作为SRC,文字本身为白色,当两者相交时,相交部分的文字绘制进度条内容的颜色。查阅一下效果图,很显然这种合成方式是SRC_ATOP。这里不再单独介绍文字的绘制和基线的计算方式,整个进度条的全部代码如下所示。

public class SaleProgressView extends View {
    private int mWidth;
    private int mHeight;
    private float mRadius;
    private RectF mRectFBackground;

    private Paint mBgPaint;
    private Paint mContentPaint;
    private Paint mTextPaint;

    private float mBaseLineY;

    private PorterDuffXfermode mContentMode;
    private PorterDuffXfermode mTextMode;

    private Bitmap mTextBitmap;
    private Canvas mTextCanvas;

    private float mCurPercentage;

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

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

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

    private void init() {
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        mContentMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        mTextMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP);

        mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBgPaint.setColor(Color.GRAY);
        mContentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mContentPaint.setColor(Color.GREEN);
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(36);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    public void setCurPercentage(float curPercentage) {
        mCurPercentage = curPercentage;
        invalidate();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        // 圆的半径
        mRadius = mHeight / 2.0f;
        if (mRectFBackground == null) {
            mRectFBackground = new RectF(0, 0,
                    mWidth, mHeight);
        }
        if (mBaseLineY == 0.0f) {
            Paint.FontMetricsInt fm = mTextPaint.getFontMetricsInt();
            mBaseLineY = mHeight / 2.0f - (fm.descent / 2.0f + fm.ascent / 2.0f);
        }

        mTextBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        mTextCanvas = new Canvas(mTextBitmap);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        drawContent(canvas);
        drawText(canvas);
    }

    private void drawContent(Canvas canvas) {
        int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);

        canvas.drawRoundRect(mRectFBackground, mRadius, mRadius, mBgPaint);
        mContentPaint.setXfermode(mContentMode);
        canvas.drawRoundRect(new RectF(0, 0, mWidth * mCurPercentage, mHeight),
                mRadius, mRadius, mContentPaint);
        mContentPaint.setXfermode(null);

        canvas.restoreToCount(sc);
    }

    private void drawText(Canvas canvas) {
        String text = "下载中";
        mTextPaint.setColor(Color.GREEN);
        mTextCanvas.drawText(text, mWidth / 2.0f, mBaseLineY, mTextPaint);

        mTextPaint.setXfermode(mTextMode);

        mTextPaint.setColor(Color.WHITE);
        mTextCanvas.drawRoundRect(new RectF(0, 0, mWidth * mCurPercentage, mHeight),
                mRadius, mRadius, mTextPaint);

        canvas.drawBitmap(mTextBitmap, 0, 0, null);

        mTextPaint.setXfermode(null);
    }
}

......

当老王把这个需求完成的时候,天已经蒙蒙亮了,老王潇洒地把效果展示给了打着哈欠刚来上班的产品。产品看完以后疑惑地问:“你昨天不是说下班前给我吗?”,老王微微一笑:“我还没下班呢~”

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

推荐阅读更多精彩内容