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

来源:互联网 发布:c4d r16 mac 注册机 编辑:程序博客网 时间:2024/05/16 16:19

前言
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/10432935,http://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之后画的那张图片

0 0
原创粉丝点击