赞
踩
前言
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的效果,需要把下面注释的代码打开:
// 画光效的文字
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之后画的那张图片。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。