Android 自定義 View 進(jìn)階 - Xfermode

在 Android 自定義控件中帜篇,Xfermode 知識(shí)點(diǎn)占有很重要的地位糙捺,它能幫助我們實(shí)現(xiàn)很多炫酷的效果。例如笙隙,實(shí)現(xiàn)各種形狀的圖片控件洪灯;結(jié)合屬性動(dòng)畫(huà)實(shí)現(xiàn)漸變效果。

highlight.gif

Xfermode 介紹

Xfermode 主要是通過(guò) paint.setXfermode(Xfermode xfermode) 方法進(jìn)行設(shè)置的竟痰,其中 在 API 28 中签钩, Xfermode 類只有一個(gè)子類 PorterDuffXfermode

PorterDuffXfermode 構(gòu)造函數(shù):

public PorterDuffXfermode(PorterDuff.Mode mode)

參數(shù) mode 設(shè)置不同的混合模式,取值有以下這幾種:

Xfermode 使用方法

通常情況下坏快,需要關(guān)閉硬件加速 setLayerType(View.LAYER_TYPE_SOFTWARE, null); 然后在自定義控件的 onDraw() 方法中铅檩,保存至新的圖層中,先繪制 dest 圖像莽鸿,然后再設(shè)置 paint.setXfermode(new PorterDuffXfermode(getMode(mode))); 接著繪制 src 圖像柠并,這樣畫(huà)筆就應(yīng)用上了指定的模式了。主要流程如下:

@Override
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //將繪制操作保存到新的圖層臼予,因?yàn)閳D像合成是很昂貴的操作,將用到硬件加速啃沪,這里將圖像合成的處理放到離屏緩存中進(jìn)行
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
        // 繪制 dest
        canvas.drawBitmap(destBitmap, 0, 0, paint);
        // 設(shè)置 xfermode
        if (mode != 0) {
            paint.setXfermode(new PorterDuffXfermode(getMode(mode)));
        }
        // 繪制 src
        canvas.drawBitmap(srcBitmap, 0, 0, paint);
        paint.setXfermode(null);
        canvas.restoreToCount(saveCount);
}

官方 Sample 測(cè)試

先準(zhǔn)備兩種圖片素材粘拾,dest 圖像為 紅色方塊 圖片,src 圖像為 藍(lán)色方塊 圖片(純?yōu)橥傅讏D)


dest.png
src.png

新建自定義控件類 XfermodeBitmapView 创千,繼承 View, 在 onDraw() 方法先繪制 dest 圖像缰雇,然后將 paint xfermode 設(shè)置為 指定的模式,再繪制 src 圖像追驴。

public class XfermodeBitmapView extends View {

    private Paint textPaint;
    private Paint paint;
    private int mode;
    private Bitmap destBitmap;
    private Bitmap srcBitmap;

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

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

    private void readAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.XfermodeView);
        mode = typedArray.getInt(R.styleable.XfermodeView_mode, 0);
        typedArray.recycle();
    }

    private void init() {
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(60);

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setStyle(Paint.Style.FILL);
        destBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.red);
        srcBitmap = BitmapFactory.decodeResource(getContext().getResources(), R.drawable.blue);

        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 設(shè)置背景
        canvas.drawColor(Color.DKGRAY);

        //將繪制操作保存到新的圖層械哟,因?yàn)閳D像合成是很昂貴的操作,將用到硬件加速殿雪,這里將圖像合成的處理放到離屏緩存中進(jìn)行
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), paint, Canvas.ALL_SAVE_FLAG);
        // 繪制 dest
        canvas.drawBitmap(destBitmap, 0, 0, paint);
        // 設(shè)置 xfermode
        if (mode != 0) {
            paint.setXfermode(new PorterDuffXfermode(getMode(mode)));
        }
        // 繪制 src
        canvas.drawBitmap(srcBitmap, 0, 0, paint);
        paint.setXfermode(null);
        canvas.restoreToCount(saveCount);
        canvas.drawText(getMode(mode).toString(), getWidth() - 300, getHeight() / 2f, textPaint);
    }

    private PorterDuff.Mode getMode(int value) {
        PorterDuff.Mode mode = null;
        switch (value) {
            case 1:
                mode = PorterDuff.Mode.CLEAR;
                break;
            case 2:
                mode = PorterDuff.Mode.SRC;
                break;
            case 3:
                mode = PorterDuff.Mode.DST;
                break;
            case 4:
                mode = PorterDuff.Mode.SRC_OVER;
                break;
            case 5:
                mode = PorterDuff.Mode.DST_OVER;
                break;
            case 6:
                mode = PorterDuff.Mode.SRC_IN;
                break;
            case 7:
                mode = PorterDuff.Mode.DST_IN;
                break;
            case 8:
                mode = PorterDuff.Mode.SRC_OUT;
                break;
            case 9:
                mode = PorterDuff.Mode.DST_OUT;
                break;
            case 10:
                mode = PorterDuff.Mode.SRC_ATOP;
                break;
            case 11:
                mode = PorterDuff.Mode.DST_ATOP;
                break;
            case 12:
                mode = PorterDuff.Mode.XOR;
                break;
            case 13:
                mode = PorterDuff.Mode.DARKEN;
                break;
            case 14:
                mode = PorterDuff.Mode.LIGHTEN;
                break;
            case 15:
                mode = PorterDuff.Mode.MULTIPLY;
                break;
            case 16:
                mode = PorterDuff.Mode.SCREEN;
                break;
        }
        return mode;
    }
}

效果:

1.png
2.png
3.png

Xfermode 實(shí)現(xiàn)高亮進(jìn)度 ImageView

實(shí)現(xiàn)思路:

(1) 顯示圖片暇咆,繼承 ImageView 類 ,更方便丙曙。

(2) 圓角矩形圖片爸业,通過(guò) canvas.clipPath() 裁剪 canvas 畫(huà)布(在 super.onDraw() 之前調(diào)用),繪制的圖片就會(huì)顯示成為圓角矩形亏镰。

(3) 圖片上的灰色蒙層和圓形鏤空扯旷,通過(guò) Xfermode 模式,先繪制 dst 鏤空?qǐng)A索抓,在繪制 src 灰色蒙層钧忽,并將 paint 設(shè)置為 srcOut , 這樣 dst 鏤空?qǐng)A和灰色蒙層重疊的部分就會(huì)變成透明了,顯示出了底層的圖片

/**
 * 高亮進(jìn)度 ImageView
 */
public class HighlightProgressImageView extends AppCompatImageView {

    private Paint backgroundPaint;
    private Paint circlePaint;
    private int radius;
    private int width;
    private int height;
    private int roundCorner;
    private Path clipPath;
    private RectF pathRectF;
    private RectF circleRectF;
    private RectF backgroundRectF;
    private PorterDuffXfermode porterDuffXfermode;
    private AnimatorSet animatorSet;
    private ValueAnimator angleAnimator;
    private ValueAnimator scaleAnimator;
    // 扇形角度
    private int angle;
    // 縮放半徑
    private float scaleRadius = radius;
    private boolean needDrawArc = true;


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

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

    private void init() {
        backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        backgroundPaint.setColor(getResources().getColor(R.color.translucentGray));
        backgroundPaint.setStyle(Paint.Style.FILL);

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setColor(getResources().getColor(android.R.color.white));
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setStrokeWidth(DensityUtil.dp2Px(getContext(), 8));

        radius = DensityUtil.dp2Px(getContext(), 40);
        roundCorner = DensityUtil.dp2Px(getContext(), 10);

        clipPath = new Path();
        porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        pathRectF = new RectF(0, 0, width, height);
        clipPath.addRoundRect(pathRectF, roundCorner, roundCorner, Path.Direction.CCW);
        circleRectF = new RectF(-radius, -radius, radius, radius);
        backgroundRectF = new RectF(-width / 2, -height / 2, width / 2f, height / 2f);
    }

    /**
     * 繪制步驟: 先繪制 圓逼肯, 再在圓上繪制灰色背景耸黑,繪制灰色背景時(shí),將 Xfermode 設(shè)置為 PorterDuff.Mode.SRC_OUT汉矿, 這樣重疊的部分就會(huì)變?yōu)橥该髌榉唬@示出正常的圖片
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        // 通過(guò) path, 裁剪 canvas 畫(huà)布
        canvas.clipPath(clipPath);
        // 繪制圖片
        super.onDraw(canvas);
        //將繪制操作保存到新的圖層,因?yàn)閳D像合成是很昂貴的操作洲拇,將用到硬件加速奈揍,這里將圖像合成的處理放到離屏緩存中進(jìn)行
        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), backgroundPaint, Canvas.ALL_SAVE_FLAG);
        canvas.translate(width / 2f, height / 2f);
//        if (needDrawArc) {
        // 繪制 dst 圓環(huán)
        circlePaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(0, 0, radius, circlePaint);
        // 繪制 dst 扇形
        circlePaint.setStyle(Paint.Style.FILL);
        canvas.drawArc(circleRectF, -90, angle, true, circlePaint);
//        }
        circlePaint.setStyle(Paint.Style.FILL);
        // 繪制 dst 圓
        canvas.drawCircle(0, 0, scaleRadius, circlePaint);
        // 設(shè)置 Xfermode 為 SRC_OUT
        backgroundPaint.setXfermode(porterDuffXfermode);
        // 繪制 src 圖片上層的灰色蒙層
        canvas.drawRoundRect(backgroundRectF, roundCorner, roundCorner, backgroundPaint);
        backgroundPaint.setXfermode(null);
        canvas.restoreToCount(saveCount);
    }

    /**
     * 開(kāi)啟動(dòng)畫(huà)
     */
    public void start() {
        startAnimator();
    }

    /**
     * 停止動(dòng)畫(huà)
     */
    public void stop() {
        if (animatorSet != null) {
            animatorSet.cancel();
            animatorSet = null;
        }
    }

    private void startAnimator() {
        // 扇形進(jìn)度動(dòng)畫(huà)
        if (angleAnimator == null) {
            angleAnimator = ValueAnimator.ofInt(0, 360);
//            angleAnimator.setDuration(2000);
            angleAnimator.setInterpolator(new LinearInterpolator());
            angleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    angle = (int) animation.getAnimatedValue();
                    invalidate();
                }
            });
            angleAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    needDrawArc = false;
                }
            });
        }

        if (scaleAnimator == null) {
            scaleAnimator = ValueAnimator.ofFloat(radius, width > height ? width : height);
//            scaleAnimator.setDuration(2000);
            scaleAnimator.setInterpolator(new LinearInterpolator());
            scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    scaleRadius = (float) animation.getAnimatedValue();
                    invalidate();
                }
            });
        }
        if (animatorSet == null) {
            animatorSet = new AnimatorSet();
            animatorSet.setDuration(2000);
            animatorSet.setInterpolator(new LinearInterpolator());
            animatorSet.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                }
            });
            animatorSet.playSequentially(angleAnimator, scaleAnimator);
        }
        animatorSet.start();
    }
}

效果:

highlight.gif

Xfermode 實(shí)現(xiàn)心狀圖片

實(shí)現(xiàn)思路:

(1) 繼承自 ImageView 類,先繪制圖片作為 dest 圖像赋续,此時(shí)的畫(huà)筆 paint 需要是 ImageView 圖片的畫(huà)筆男翰,不能是 重新創(chuàng)建的新畫(huà)筆;

(2) 設(shè)置畫(huà)筆 Xfermode 模式為 SRC_IN 纽乱,并利用 path 貝塞爾曲線繪制一個(gè)心形蛾绎,這樣心形和圖片重合的部分就保留顯示了心形部分的圖片。

/**
 * 心形圖片
 */
public class XfermodeHeartShapeImageView extends android.support.v7.widget.AppCompatImageView {

    private int mViewWidth;
    private int mViewHeight;
    private Paint paint;
    private PorterDuffXfermode xfermode;
    private float radius;
    private Path path;

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

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

    private void init() {
        xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        path = new Path();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
        radius = Math.min(mViewWidth, mViewHeight) / 3f;

        // 獲取繪制圖片對(duì)應(yīng)的 paint
        paint = ((BitmapDrawable) getDrawable()).getPaint();

        // 二階貝塞爾曲線
        path.moveTo(mViewWidth / 2f, mViewHeight / 4f);
        path.cubicTo(mViewWidth / 10f, mViewHeight / 12f,
                mViewWidth / 9f, (mViewHeight * 3) / 5f,
                mViewWidth / 2f, (mViewHeight * 5) / 6f);
        path.cubicTo(
                mViewWidth * 8 / 9f, (mViewHeight * 3) / 5f,
                mViewWidth * 9 / 10f, mViewHeight / 12f,
                mViewWidth / 2f, mViewHeight / 4f);

        setLayerType(LAYER_TYPE_SOFTWARE, null);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int saveCount = canvas.saveLayer(0, 0, mViewWidth, mViewHeight, null, Canvas.ALL_SAVE_FLAG);
        // 繪制 dst 心形
        /***為什么 paint.setStyle() 放在 onDraw() 中才生效,放在 onSizeChanged() 中進(jìn)行不能生效***/
        paint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, paint);
        // 將繪制圖片對(duì)應(yīng)的 paint Xfermode 設(shè)置為 SRC_IN , 重疊的部分顯示為 src 圖片租冠, dst 中不重疊的部分不變鹏倘, src 中不重疊的部分顯示為透明
        paint.setXfermode(xfermode);
        // 繪制 src 圖片
        super.onDraw(canvas);
        paint.setXfermode(null);
        canvas.restoreToCount(saveCount);
    }
}

同樣的實(shí)現(xiàn)方式可以實(shí)現(xiàn)各種形狀的圖片控件。

love
源碼地址:https://github.com/xing16/ProgressAndroid
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末顽爹,一起剝皮案震驚了整個(gè)濱河市纤泵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镜粤,老刑警劉巖捏题,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異肉渴,居然都是意外死亡公荧,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)同规,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)循狰,“玉大人,你說(shuō)我怎么就攤上這事捻浦∥畲В” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵朱灿,是天一觀的道長(zhǎng)昧识。 經(jīng)常有香客問(wèn)我,道長(zhǎng)盗扒,這世上最難降的妖魔是什么跪楞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮侣灶,結(jié)果婚禮上甸祭,老公的妹妹穿的比我還像新娘。我一直安慰自己褥影,他們只是感情好池户,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著凡怎,像睡著了一般校焦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上统倒,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天寨典,我揣著相機(jī)與錄音,去河邊找鬼房匆。 笑死耸成,一個(gè)胖子當(dāng)著我的面吹牛报亩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播井氢,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼弦追,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了毙沾?” 一聲冷哼從身側(cè)響起骗卜,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎左胞,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體举户,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡烤宙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俭嘁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躺枕。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖供填,靈堂內(nèi)的尸體忽然破棺而出拐云,到底是詐尸還是另有隱情,我是刑警寧澤近她,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布叉瘩,位于F島的核電站,受9級(jí)特大地震影響粘捎,放射性物質(zhì)發(fā)生泄漏薇缅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一攒磨、第九天 我趴在偏房一處隱蔽的房頂上張望泳桦。 院中可真熱鬧,春花似錦娩缰、人聲如沸灸撰。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)浮毯。三九已至,卻和暖如春演痒,著一層夾襖步出監(jiān)牢的瞬間亲轨,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工鸟顺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惦蚊,地道東北人器虾。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蹦锋,于是被迫代替她去往敵國(guó)和親兆沙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容