深入理解Xfermode,使用时要注意以及顺便膜拜下saveLayer的强大

前言
Android的Xfermode可以做出很多神奇的效果,例如ios锁屏的扫光效果,刮奖卡刮开的效果,相框相片合成效果等等。相信很多人都用过Xfermode,网上也有很多现成的效果实例,但是我们真的了解它吗?

基本用法
关于Xfermode的使用可以看看Android官方提供的ApiDemos工程看看源码,如何创建并运行ApiDemos可看这:http://my.oschina.net/libralzy/blog/151856或者http://blog.csdn.net/liu_zhen_wei/article/details/6924017

它的基本用法看下ApiDemos的源码就懂了,源码就一百多行,其中核心代码就几行,实现上手比较容易,或者也可以看看这两篇文章:http://blog.csdn.net/t12x3456/article/details/10432935http://blog.csdn.net/lmj623565791/article/details/42094215

下面的ApiDemos中Xfermode的运行截图,我借用下上面文章博主的图:这里写图片描述

从图片我们可以看到,通过Xfermode我们可以把Src和Dst两张图片做一定的合成渲染效果处理,用到实例上会更加神奇。

简单例子:文字上部分区域加上光效
下面我先写一个简单的例子,后面会用到。该例子实现的效果就是仿ios锁屏文字的扫光效果,只不过光不会动,加上动画修改样式就会跟ios十分类似。此处写的是其重要原理。

直接上代码,我写得比较简单:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new MainView(this), 
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    }

    class MainView extends View {

        /**
         * 文字图片
         */
        private Bitmap mTextBitmap = null;
        /**
         * 文字Canvas
         */
        private Canvas mTextCanvas = null;

        /**
         * 光效图片
         */
        private Bitmap mLightBitmap = null;
        /**
         * 光效Canvas
         */
        private Canvas mLightCanvas = null;

        private boolean mHasCreated = false;
        private Paint mTextPaint = null;
        private Paint mLightPaint = null;
        private Paint mPaint = null;
        private  Xfermode mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

        public MainView(Context context) {
            super(context);
            mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mTextPaint.setTextSize(40);
            mTextPaint.setColor(Color.BLACK);       // 文字是黑色的
            mLightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mLightPaint.setColor(Color.RED);        // 光是红色的
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        }

        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            if (!mHasCreated) {
                // 为了简单,这里创建的图片都是整个屏幕那么大
                mTextBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
                mTextCanvas = new Canvas(mTextBitmap);
                // 在中间画一段文字
                String text = "红红火火恍恍惚惚";
                float textSize = mTextPaint.measureText(text);
                mTextCanvas.drawText(text, (w - textSize) / 2, h / 2, mTextPaint);
                mLightBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
                mLightCanvas = new Canvas(mLightBitmap);
                // 画光效,其实就是一个红色的圆
                mLightCanvas.drawCircle(w / 2, h / 2, 70, mLightPaint);
                mHasCreated = true;
            }
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            // 先画一次原文字
            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
            // 保存画布
            int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null,
                    Canvas.MATRIX_SAVE_FLAG |
                    Canvas.CLIP_SAVE_FLAG |
                    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
                    Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
                    Canvas.CLIP_TO_LAYER_SAVE_FLAG);

            // 画光效的文字
            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
//          mPaint.setXfermode(mXfermode);
            canvas.drawBitmap(mLightBitmap, 0, 0, mPaint);
//          mPaint.setXfermode(null);

            canvas.restoreToCount(sc);
        }
    }
}

直接看效果图,先看看没有用Xfermode时的效果,上面的代码已经把Xfermode注释了:
没用Xfermode时

然后我们看看用了Xfermode的效果,需要把下面注释的代码打开:

            // 画光效的文字
            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
//          mPaint.setXfermode(mXfermode);
            canvas.drawBitmap(mLightBitmap, 0, 0, mPaint);
//          mPaint.setXfermode(null);

效果图:
用了Xfermode

只要上面红色区域慢慢左右移动,最后形成的效果就是类似ios锁屏文字的效果了。

问题出现
上面的代码很简单,其核心代码就是onDraw方法里面的代码,其中

            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
            mPaint.setXfermode(mXfermode);
            canvas.drawBitmap(mLightBitmap, 0, 0, mPaint);
            mPaint.setXfermode(null);

就是使用Xfermode的地方。

现在如果想换个颜色背景,然后我在这代码上面加一行画背景色的代码,就是如下:

canvas.drawColor(Color.BLUE);           // 画一个蓝色的背景色
canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
mPaint.setXfermode(mXfermode);
canvas.drawBitmap(mLightBitmap, 0, 0, mPaint);
mPaint.setXfermode(null);

然后问题就出现了,请看效果图:
蓝色背景呢?

咦?说好的蓝色背景呢?怎么不见了?再看看代码明明是已经把蓝色画在画布上,怎么一点蓝色都没有呢?
此时确实很有疑惑,一时也摸不着头脑。我们尝试下把mXfermode换个相反的模式,把原本的PorterDuff.Mode.SRC_IN改成PorterDuff.Mode.DST_IN,也就是:

private  Xfermode mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);

看看效果图:
这里写图片描述

蓝色终于出现了!不过为啥不是整个屏幕呢?!而且文字效果也不对!好有疑惑。

我的猜想

以前我一直以为Xfermode合成的是使用Xfermode前后的两个图片,也就是mTextBitmap和mLightBitmap这两张图片:

            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
            mPaint.setXfermode(mXfermode);
            canvas.drawBitmap(mLightBitmap, 0, 0, mPaint);
            mPaint.setXfermode(null);

但是现在效果明显告诉我不是这样的。

我认为Xfermode合成的应该是当前Canvas与setXfermode之后画的那张图片。回到上面的第一次画蓝色背景的例子:

canvas.drawColor(Color.BLUE);           // 画一个蓝色的背景色       ①
canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);        ②
mPaint.setXfermode(mXfermode);
canvas.drawBitmap(mLightBitmap, 0, 0, mPaint);        ③
mPaint.setXfermode(null);

假如我们不画蓝色背景,跳过①,我们来到②的位置,此处画了整个文字,此时Canvas上的有像素值的地方仅仅是文字的地方;然后执行③后,将Canvas上的像素和mLightBitmap的像素合成,因此就会形成正确的效果,就是部分文字出现红色;

但是如果我们先执行了①,由于画了整个Canvas,此时整个Canvas都有像素值,所以执行③,将Canvas上的像素和mLightBitmap的像素合成后,形成的效果就是如上面的图所示。

以上是我的猜想,由于Canvas的源码都是调用Native层的代码实现,最终是调用Skia图库实现,这部分我不熟悉,所以无法从代码上验证。但是对于该猜想把握十足。

侧面验证,问题的解决方法
解决方法一:
我们可以从该解决方法侧面验证我的猜想,这个解决方法比较简单,就是把画蓝色背景色的部分移到saveLayer之前,也就是:

@Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.drawColor(Color.BLUE);           // 画一个蓝色的背景色
            // 先画一次原文字
            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
            // 保存画布
            int sc = canvas.saveLayer(0, 0, getWidth(), getHeight(), null,
                    Canvas.MATRIX_SAVE_FLAG |
                    Canvas.CLIP_SAVE_FLAG |
                    Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
                    Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
                    Canvas.CLIP_TO_LAYER_SAVE_FLAG);

            // 画光效的文字
            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
            mPaint.setXfermode(mXfermode);
            canvas.drawBitmap(mLightBitmap, 0, 0, mPaint);
            mPaint.setXfermode(null);

            canvas.restoreToCount(sc);
        }

先看效果图:
这里写图片描述

效果非常正确,这正是我想要的。

上面的代码改了之后,因为在使用Xfermode已经saveLayer了,导致后面所有操作都是在另一个图层所做的,因此此时Canvas非常干净,所以该Layer层上用Xfermode合成时就是文字和圆形红光,然后在restoreToCount之后,该Layer就会绘制在原有的Canvas上,因此效果就是上图,非常正确。这也侧面验证了猜想。

解决方法二:
将所有操作都放在mTextCanvas上,也就是:

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.drawColor(Color.BLUE);           // 画一个蓝色的背景色
            // 先画一次原文字
            String text = "红红火火恍恍惚惚";
            float textSize = mTextPaint.measureText(text);
            canvas.drawText(text, (getWidth() - textSize) / 2, getHeight() / 2, mTextPaint);
//          canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
            // 画光效的文字
            mPaint.setXfermode(mXfermode);
            mTextCanvas.drawBitmap(mLightBitmap, 0, 0, mPaint);
            mPaint.setXfermode(null);
            canvas.drawBitmap(mTextBitmap, 0, 0, mPaint);
        }

这个解决方法就是把所有操作都放到了画Text的Canvas上了,因为mTextCanvas没有背景色像素干扰,所以同样十分干净,有像素值的地方仅仅是文本的地方,所以合成效果也十分正确。

延伸理解
上面的代码里用到了canvas.saveLayer的方法,此处也是多亏该方法,才能让效果完全实现,当然解决方法二不需要如此。

以前不怎么理解saveLayer,一直觉得跟save好像,但是现在来看两者差远了,saveLayer强大很多。

我们看看源码对两者的注释:

 /**
     * Saves the current matrix and clip onto a private stack.
     * <p>
     * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
     * clipPath will all operate as usual, but when the balancing call to
     * restore() is made, those calls will be forgotten, and the settings that
     * existed before the save() will be reinstated.
     *
     * @return The value to pass to restoreToCount() to balance this save()
     */
    public int save() {
        return native_save(mNativeCanvasWrapper, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);
    }


    /**
     * This behaves the same as save(), but in addition it allocates and
     * redirects drawing to an offscreen bitmap.
     * <p class="note"><strong>Note:</strong> this method is very expensive,
     * incurring more than double rendering cost for contained content. Avoid
     * using this method, especially if the bounds provided are large, or if
     * the {@link #CLIP_TO_LAYER_SAVE_FLAG} is omitted from the
     * {@code saveFlags} parameter. It is recommended to use a
     * {@link android.view.View#LAYER_TYPE_HARDWARE hardware layer} on a View
     * to apply an xfermode, color filter, or alpha, as it will perform much
     * better than this method.
     * <p>
     * All drawing calls are directed to a newly allocated offscreen bitmap.
     * Only when the balancing call to restore() is made, is that offscreen
     * buffer drawn back to the current target of the Canvas (either the
     * screen, it's target Bitmap, or the previous layer).
     * <p>
     * Attributes of the Paint - {@link Paint#getAlpha() alpha},
     * {@link Paint#getXfermode() Xfermode}, and
     * {@link Paint#getColorFilter() ColorFilter} are applied when the
     * offscreen bitmap is drawn back when restore() is called.
     *
     * @param bounds May be null. The maximum size the offscreen bitmap
     *               needs to be (in local coordinates)
     * @param paint  This is copied, and is applied to the offscreen when
     *               restore() is called.
     * @param saveFlags see _SAVE_FLAG constants, generally {@link #ALL_SAVE_FLAG} is recommended
     *               for performance reasons.
     * @return       value to pass to restoreToCount() to balance this save()
     */
    public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
        if (bounds == null) {
            bounds = new RectF(getClipBounds());
        }
        return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint, saveFlags);
    }

save方法可以保存当前的matrix and clip,并且在restore把它恢复,一些平移,旋转,缩放等操作都会影响Canvas的matrix,所以save操作一般可以保存这些信息以及clip信息;

而saveLayer则强大很多,它相当于另外起一张干净图层,并在上面进行绘制操作,然后在restoreToCount的时候,把刚才所绘制的重新绘制在原本的Canvas上。当时正如所知的那样,它会绘制两次,所以消耗是十分巨大,对此,官方注释也进行了很长的说明和建议,请自行翻译。

小结
就是上面的猜想:我认为Xfermode合成的是当前Canvas与setXfermode之后画的那张图片

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值