Xfermode原理与案例

Xfermode原理与案例

目录:

  • Xfermode的基本原理
  • Xfermode的多种模式
  • Xfermode的使用案例
Xfermode的基本原理
  1. Xfermode是什么?
    在Android绘制中,通过使用Xfermode将绘制的图形的像素和Canvas上对应位置的像素按照一定的规则进行混合,形成新的像素,再更新到Canvas中形成最终的图形。

  2. 像素组成的4元素:ARGB
    我们一个像素的颜色都是由四个分量组成,即ARGB,A表示的是我们Alpha值,RGB表示的是颜色
    S表示的是源像素,源像素的值表示[Sa,Sc] Sa表示的就是源像素的Alpha值,Sc表示源像素的颜色值
    D表示的是目标像素,目标像素的值表示[Da,Dc] Da表示的就是目标像素的Alpha值

PorterDuffXfermode

Xfermode有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode。其中AvoidXfermode, PixelXorXfermode已经过时不推荐使用。那么PorterDuffXfermode则是需要了解的东西。

  1. 以下的一张图和一段伪代码可以理解PorterDuffXfermode的基本概念

在这里插入图片描述
2. Xfermode理解起来并不是很难,根据上面的图可以理解为,两个不同的像素点。通过Xfermode的不同的混合模式混合之后展示出来的新的像素点效果。(注意这里是针对每一个像素的混合效果。而且这两个像素点需要是在画布上的同一位置,可以理解为重叠)

  1. 伪代码可以这样表示:
// 初始化PorterDuffXfermode
private PorterDuffXfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

// 在ondraw中使用PorterDuffXfermode
 protected void onDraw(Canvas canvas) {

        // DstBitmap SRCBitmap 为两个不同的bitmap
        canvas.drawBitmap(SrcBitmap,0,0,mPaint);
        // PorterDuffXfermode和paint联用
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(DstBitmap,0,0,mPaint);
        
        // 将xfermode制空
        mPaint.setXfermode(null);
    }

以上的代码也比较简单理解: 先draw一个bitmap,然后设置paint的xfermode,然后在画第二个bitmap。这样他们重叠的部分就会出现不通过的UI效果了。

Xfermode的多种混合模式

在这里插入图片描述
官方的贴图非常形象的展示出各种混合模式使用后展示的效果。

接下来挑出一个常用的例子SRC_IN来解释下这些算法的基本应用。
圆形头像实现的方式可能有很多。比如用bitmapshader等等。使用xfermode同样能实现。

SRC_IN      (5),
/** [Sa * Da, Sa * Dc] */

SRC_IN的算法是这样的:
(a)Sa * Da:源图(S)像素透明度和目标图片(D)像素的透明的决定混合后像素的透明度
(b)Sa * Dc:源图(S)像素透明度和目标图片(D)像素的颜色决定混合后像素的颜色

那么混合的图解:

在这里插入图片描述

从(a)(b)可以看出,源图片只采用了透明度的变化。混合后图像像素的透明度和颜色都和源图的像素的透明度的有关。如果源图的像素是透明的,那么混合后的像素为透明。反之不透明。所以源图为:
在这里插入图片描述
从(b)可以看出,决定混合后图像素颜色是由目标图片(D)决定的。所以目标图片是:
在这里插入图片描述

这里主要是理解算法:[Sa * Da, Sa * Dc]
在这里插入图片描述

示例代码:

package com.hdp.testvie.xformode;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

/**
 * 使用xfermode中的src_in模式叠加做头像。
 */
public class CustomHeadView extends View {

    private Bitmap DBitmap;

    private Bitmap SBitmap;

    private Paint mPaint;

    private PorterDuffXfermode xfermode;

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

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

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

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

    public void drawHead(int src, int dst) {
        SBitmap = BitmapFactory.decodeResource(getResources(), src, null);
        DBitmap = BitmapFactory.decodeResource(getResources(), dst, null);
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(SBitmap.getWidth(), SBitmap.getHeight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);

        canvas.drawBitmap(SBitmap, 0, 0, mPaint);
        mPaint.setXfermode(xfermode);
        canvas.drawBitmap(DBitmap, 0, 0, mPaint);

        mPaint.setXfermode(null);
        canvas.restoreToCount(layerId);
    }
}

上述例子已经非常清晰的说明了xfermode的算法:
源图(S)和目标图(D)像素的透明度和颜色,通过特定的算法来算出混合后新图的透明度和颜色。(注意这里是对每个像素进行操作)

一些常用PorterDuffXfermode的例子
各种形状的图形

使用xfermode来完成圆形头像只是其中之一。如果有特殊要求,想弄成其他的形状都是可以的。 如果我上面写的圆形图片的例子能够理解,那么其他的各种形状的例子使用的方法是一样的。
在这里插入图片描述

刮刮卡效果

在这里插入图片描述
实际上实现一个效果并不是说只能采用一种叠加模式。用不同的模式也能做到相同的效果。 这里展示的刮刮卡效果,采用DST_OUT模式。

实现源码如下:
package com.hdp.testvie.xformode;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;

import com.hdp.testvie.R;

public class GuaGuaCard extends View implements View.OnTouchListener {
/**
* 原图
*/
private Bitmap SBitmap;

/**
 * 目标图片
 */
private Bitmap DBitmap;

private Bitmap bitmap;

private Paint mPaint;

private PorterDuffXfermode xfermode;

/**
 * 记录手指划过的路劲
 */
private Path mPath = new Path();

private float startX;
private float startY;

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

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

public GuaGuaCard(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    // 去掉硬件加速
    setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    init(context);
    setOnTouchListener(this);
}

private void init(Context context) {
    mPaint = new Paint();
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(45);

    SBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.one);
    bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.two);
    DBitmap = Bitmap.createBitmap(SBitmap.getWidth(), SBitmap.getHeight(), Bitmap.Config.ARGB_8888);
    bitCanvas = new Canvas(DBitmap);
    xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
}

private Canvas bitCanvas;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(bitmap.getWidth(), bitmap.getHeight());
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawBitmap(bitmap, 0, 0, mPaint);
    int layerId = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);

    bitCanvas.drawPath(mPath, mPaint);
    canvas.drawBitmap(SBitmap, 0, 0, mPaint);
    // 叠加模式
    mPaint.setXfermode(xfermode);
    canvas.drawBitmap(DBitmap, 0, 0, mPaint);
    // 记得重置xfermode模式
    mPaint.setXfermode(null);
    canvas.restoreToCount(layerId);
}

// 移动的时候记录移动的路线
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
    switch (motionEvent.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = motionEvent.getX();
            startY = motionEvent.getY();
            mPath.moveTo(startX, startY);
            break;
        case MotionEvent.ACTION_MOVE:
            float endx = motionEvent.getX();
            float endY = motionEvent.getY();
            mPath.quadTo(startX, startY, endx, endY);
            startX = motionEvent.getX();
            startY = motionEvent.getY();
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    postInvalidate();
    return true;
}

}

draw方法分析

  1. 首先绘制底部刮开后显示的图片(这个实际和混合没有关系)
  2. 保存现场,这里是混合的开始
  3. bitCanvas.drawPath(mPath, mPaint);通过canvas,将手指移动的path绘制到目标Bitmap(一段透明的path)
  4. 绘制遮罩,就是需要擦掉的源Bitmap
  5. 设置叠加模式DST_OUT
  6. 绘制目标Bitmap(),这里由于是DST_OUT,相交的部分变成透明了,故实现了擦除效果
  7. 重制Xformode,恢复现场
给美女脱衣效果

这里使用SRC_OUT的混合模式,和上面刮刮乐相反,我们使用手指移动path生成的Bitmap作为源图,使用遮罩层作为目标图,这样混合后,源图和目标图相交的部分会变成透明,露出了底图,也就是实现了我们的效果

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

带圆角和阴影的布局

圆角和阴影的实现方式:

  • 通过定义XML文件实现
  • 通过自定义View实现,给自己添加圆角和阴影

这里,我们另辟蹊径,通过统一的一个父布局,通过控制子布局的位置,来进行子布局圆角和阴影的控制

圆角的实现方式:

  1. 通过Canvas.clipPath来实现,这个可以实现效果,但是有一个问题,就是当圆角比较大,布局较大的时候,会有明显的锯齿效果,这是clip方式裁剪的硬伤,无法避免
  2. 第二种就是我们要用到的通过paint的Xformode来实现,给paint添加抗锯齿即可
圆角阴影父布局实现

注:这里不是给父布局添加阴影,是通过父布局的drawChild来控制子布局的阴影,这样实现的好处就是:父布局里面的任意子布局都可以很方便的添加阴影和圆角效果

1、定义HLayoutParamsData类

作用:

  1. 解析自定义的属性
  2. 保存单个子View的布局信息,生成对应的path

源代码如下:

package com.hdp.testvie.roundview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

import com.hdp.testvie.R;

public class HLayoutParamsData {
    int radius;
    int shadowColor;
    int shadowDx;
    int shadowDy;
    int shadowEvaluation;
    RectF widgetRect;
    Path widgetPath;
    Path clipPath;
    boolean needClip;
    boolean hasShadow;

    public HLayoutParamsData(Context context, AttributeSet attrs) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HLayout);
        radius = a.getDimensionPixelOffset(R.styleable.HLayout_layout_radius, 0);
        shadowDx = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowDx, 0);
        shadowDy = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowDy, 0);
        shadowColor = a.getColor(R.styleable.HLayout_layout_shadowColor, 0x99999999);
        shadowEvaluation = a.getDimensionPixelOffset(R.styleable.HLayout_layout_shadowEvaluation, 0);
        a.recycle();
        needClip = radius > 0;
        hasShadow = shadowEvaluation > 0;
    }


    public void initPaths(View v) {
        widgetRect = new RectF(v.getLeft(),
                v.getTop(),
                v.getRight(),
                v.getBottom());

        widgetPath = new Path();
        clipPath = new Path();
        clipPath.addRect(widgetRect, Path.Direction.CCW);
        clipPath.addRoundRect(
                widgetRect,
                radius,
                radius,
                Path.Direction.CW
        );
        widgetPath.addRoundRect(
                widgetRect,
                radius,
                radius,
                Path.Direction.CW
        );
    }
}
2、定义HLayoutParams类

方便我们自定义的父布局对子类参数的解析,自定义LayoutParam实现该接口

package com.hdp.testvie.roundview;

public interface HLayoutParams {
    HLayoutParamsData getData();
}

3、定义HConstraintLayout类

现在最常用的外层布局就是约束布局,实现给子布局添加圆角和阴影

  • 自定义LayoutParams,继承ConstraintLayout.LayoutParams,并实现接口HLayoutParams
  • 在构造函数中,初始化圆角和阴影画笔
  • 重写generateLayoutParams返回自定义的LayoutParams
  • onLayout方法中初始化每个子view相关的额path,为混合成圆角和阴影做准备
  • 重写drawChild方法,实现圆角和阴影,使用Xformode实现

源代码如下:

package com.hdp.testvie.roundview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import androidx.constraintlayout.widget.ConstraintLayout;

public class HConstraintLayout extends ConstraintLayout {
    private Paint shadowPaint;
    private Paint clipPaint;

    public HConstraintLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        //初始化沪指阴影的paint
        shadowPaint = new Paint();
        shadowPaint.setAntiAlias(true);
        shadowPaint.setDither(true);
        shadowPaint.setFilterBitmap(true);
        shadowPaint.setStyle(Paint.Style.FILL);

        //初始化绘制圆角的paint
        clipPaint = new Paint();
        clipPaint.setAntiAlias(true);
        clipPaint.setDither(true);
        clipPaint.setFilterBitmap(true);
        clipPaint.setStyle(Paint.Style.FILL);
        //关闭硬件加速
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

    /**
     * 重写generateLayoutParams
     * @param attrs
     * @return
     */
    @Override
    public ConstraintLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    /**
     * 布局时候,便利所有子view,初始化每个子view相关的额path,为混合成圆角和阴影做准备
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        for (int i = 0, size = getChildCount(); i < size; i++) {
            View v = getChildAt(i);
            ViewGroup.LayoutParams lp = v.getLayoutParams();
            if (lp instanceof HLayoutParams) {
                HLayoutParams Hlp = (HLayoutParams) lp;
                Hlp.getData().initPaths(v);
            }
        }
    }


    /**
     * 重写drawChild方法,实现圆角和阴影
     * @param canvas
     * @param child
     * @param drawingTime
     * @return
     */
    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        ViewGroup.LayoutParams lp = child.getLayoutParams();
        boolean ret = false;
        if (lp instanceof HLayoutParams) {
            HLayoutParams elp = (HLayoutParams) lp;
            HLayoutParamsData data = elp.getData();
            if (isInEditMode()) {//预览模式采用裁剪
                Log.d("TAG", "isInEditMode");
                canvas.save();
                canvas.clipPath(data.widgetPath);
                ret = super.drawChild(canvas, child, drawingTime);
                canvas.restore();
                return ret;
            }
            if (!data.hasShadow && !data.needClip)
                return super.drawChild(canvas, child, drawingTime);
            //为解决锯齿问题,正式环境采用xfermode
            if (data.hasShadow) {
                int count = canvas.saveLayer(null, null, Canvas.ALL_SAVE_FLAG);
                shadowPaint.setShadowLayer(data.shadowEvaluation, data.shadowDx, data.shadowDy, data.shadowColor);
                shadowPaint.setColor(data.shadowColor);
                canvas.drawPath(data.widgetPath, shadowPaint);
                shadowPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
                shadowPaint.setColor(Color.WHITE);
                canvas.drawPath(data.widgetPath, shadowPaint);
                shadowPaint.setXfermode(null);
                canvas.restoreToCount(count);

            }
            if (data.needClip) {
                Log.d("TAG", "PorterDuffXfermode CLEAR");
                int count = canvas.saveLayer(child.getLeft(), child.getTop(), child.getRight(), child.getBottom(), null, Canvas.ALL_SAVE_FLAG);
                ret = super.drawChild(canvas, child, drawingTime);
                clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
                clipPaint.setColor(Color.WHITE);
                canvas.drawPath(data.clipPath, clipPaint);
                clipPaint.setXfermode(null);
                canvas.restoreToCount(count);
            }
        }
        return ret;
    }

    /**
     * 自定义LayoutParams
     */
    static class LayoutParams extends ConstraintLayout.LayoutParams implements HLayoutParams {

        private HLayoutParamsData data;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            data = new HLayoutParamsData(c, attrs);
        }

        @Override
        public HLayoutParamsData getData() {
            return data;
        }
    }
}

style文件:


    <!--为了方便扩展其他layout,定义在外层,命名以layout_开头,否则lint会报红警告-->
    <!--为了方便扩展其他layout,定义在外层,命名以layout_开头,否则lint会报红警告-->
    <attr name="layout_radius" format="dimension" />
    <attr name="layout_shadowColor" format="color" />
    <attr name="layout_shadowEvaluation" format="dimension" />
    <attr name="layout_shadowDx" format="dimension" />
    <attr name="layout_shadowDy" format="dimension" />
    <!--用统一一个EasyLayout,用于封装读取自定义属性-->
    <declare-styleable name="HLayout">
        <attr name="layout_radius" />
        <attr name="layout_shadowColor" />
        <attr name="layout_shadowEvaluation" />
        <attr name="layout_shadowDx" />
        <attr name="layout_shadowDy" />
    </declare-styleable>
    <!--和EasyLayout属性列表一样,但是命名要以XXX_Layout格式,这样开发工具会提示自定义属性-->
    <declare-styleable name="HConstraintLayout_Layout">
        <attr name="layout_radius" />
        <attr name="layout_shadowColor" />
        <attr name="layout_shadowEvaluation" />
        <attr name="layout_shadowDx" />
        <attr name="layout_shadowDy" />
    </declare-styleable>

注意这里,为了在所以子View中使用自定义属性,我们需要将所有的属性定义成layout开头

使用效果如下:最外层使用HConstraintLayout
在这里插入图片描述

Thinks
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值