您的位置:首页 > Web前端

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

2015-11-07 16:15 786 查看
前言

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的效果,需要把下面注释的代码打开:

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


效果图:



只要上面红色区域慢慢左右移动,最后形成的效果就是类似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之后画的那张图片
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: