6.7 Android图像处理之画笔特效处理" class="reference-link">6.7 Android图像处理之画笔特效处理
不管是在我们的世界里,还是在Android的世界里,要想向神笔马良一样画出各种精彩绝伦的画,就必须得有一个前提——要有一支神奇的画笔。在前面的学习中,我们已经初步了解了一些常用的画笔属性,比如普通的画笔(Paint),带边框、填充的style,颜色(Color),宽度(StrokeWidth),抗锯齿(ANTI_ALIAS_FLAG)等,这些都是最基本的画笔属性,就好像一个普通人所拥有的画笔工具。然而除此之外,还有各种各样专业的画笔工具,如记号笔、毛笔、蜡笔等,使用它们可以实现更加丰富的绘图效果。下面我们就来看看画笔的一些高级属性,帮助我们实现更丰富的绘图效果。
6.7.1 PorterDuffXfermode" class="reference-link">6.7.1 PorterDuffXfermode
在学习这个东西之前,先来看一张非常经典的图,出自API Demo,基本上所有讲PorterDuffXfermode的文章都会使用这张图做说明,如图6.63所示。
图6.63 PorterDuffXfermode
图6.63中列举了16种PorterDuffXfermode,有点像数学中集合的交集、并集这样的概念,相信大家配合图例应该很好理解,它控制的是两个图像间的混合显示模式。
这里要注意的是,PorterDuffXfermode设置的是两个图层交集区域的显示方式,dst是先画的图形,而src是后画的图形。
当然,这些模式也不是经常使用的,用的最多的是,使用一张图片作为另一张图片的遮罩层,通过控制遮罩层的图形,来控制下面被遮罩图形的显示效果。其中最常用的就是通过DST_IN、SRC_IN模式来实现将一个矩形图片变成圆角图片或者圆形图片的效果。
要使用PorterDuffXfermode非常简单,只需要让画面拥有这个属性就可以了,比如下面要实现如图6.64所示的实例。
图6.64 圆角图片
先用一个普通画笔画一个Mask遮罩层,再用带PorterDuffXfermode的画笔将图像画在遮罩层上,这样就可以通过上面所说的效果来混合两个图像了,代码如下所示。
- mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
- mOut = Bitmap.createBitmap(mBitmap.getWidth(),
- mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(mOut);
- mPaint = new Paint();
- mPaint.setAntiAlias(true);
- canvas.drawRoundRect(0, 0,
- mBitmap.getWidth(),
- mBitmap.getHeight(), 80, 80, mPaint);
- mPaint.setXfermode(new PorterDuffXfermode(
- PorterDuff.Mode.SRC_IN));
- canvas.drawBitmap(mBitmap, 0, 0, mPaint);
下面再来看一个稍微复杂点的效果——刮刮卡效果。我们都知道,刮刮卡一般有两个图层,即上面的用来被刮掉的图层和下面隐藏的图层。在初始状态下,上面的图层会将下面整个图层覆盖,当你用手刮上面的图层的时候,下面的图层会慢慢显示出来,这也类似很多画图工具中的橡皮擦效果。这个效果同样可以使用PorterDuffXfermode来实现。
首先需要做一些初始化工作,例如准备好图片,设置好Paint的一些属性,代码如下所示。
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setStrokeJoin(Paint.Join.ROUND);
- mPaint.setStrokeWidth(50);
- mPaint.setStrokeCap(Paint.Cap.ROUND);
- mPath = new Path();
- mBgBitmap = BitmapFactory.decodeResource(getResources(),
- R.drawable.test);
- mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(),
- mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
- mCanvas = new Canvas(mFgBitmap);
- mCanvas.drawColor(Color.GRAY);
在上面的代码中,给Paint设置一些属性,让它的笔触和连接处能更加圆滑一点,即Paint.Join.ROUND和Paint.Cap.ROUND属性。
接下来,看一下如何获取用户手指滑动所产生的路径,代码如下所示。使用Path保存用户手指划过的路径。当然,这里如果使用贝赛尔曲线来做优化则会得到更好的显示效果,这里为了简化演示功能,就不使用贝塞尔曲线了。
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mPath.reset();
- mPath.moveTo(event.getX(), event.getY());
- break;
- case MotionEvent.ACTION_MOVE:
- mPath.lineTo(event.getX(), event.getY());
- break;
- }
最后,只需要使用DST_IN模式将路径绘制到前面覆盖的图层上面即可。不过,还需要做最关键的一步,那就是将画笔的透明度设置为0,这样才能显示出擦除的效果。很多读者可能不太理解这里将透明度设置为0的原因,这是因为在使用PorterDuffXfermode进行图层混合时,并不是简单地只进行图层的计算,同时也会去计算透明通道的值。正是由于混合了透明通道,才形成了这样的效果,完整代码如下所示。
- public class XfermodeView extends View {
- private Bitmap mBgBitmap, mFgBitmap;
- private Paint mPaint;
- private Canvas mCanvas;
- private Path mPath;
- public XfermodeView(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
- private void init() {
- mPaint = new Paint();
- mPaint.setAlpha(0);
- mPaint.setXfermode(
- new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setStrokeJoin(Paint.Join.ROUND);
- mPaint.setStrokeWidth(50);
- mPaint.setStrokeCap(Paint.Cap.ROUND);
- mPath = new Path();
- mBgBitmap = BitmapFactory.decodeResource(getResources(),
- R.drawable.test);
- mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(),
- mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
- mCanvas = new Canvas(mFgBitmap);
- mCanvas.drawColor(Color.GRAY);
- }
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mPath.reset();
- mPath.moveTo(event.getX(), event.getY());
- break;
- case MotionEvent.ACTION_MOVE:
- mPath.lineTo(event.getX(), event.getY());
- break;
- }
- mCanvas.drawPath(mPath, mPaint);
- invalidate();
- return true;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawBitmap(mBgBitmap, 0, 0, null);
- canvas.drawBitmap(mFgBitmap, 0, 0, null);
- }
- }
程序运行效果如图6.65所示,当用户用手指滑动时,就会擦除上面的涂层,形成刮刮卡的效果。
图6.65 刮刮卡效果
在使用PorterDuffXfermode时还有一点需要注意,那就是最好在绘图时,将硬件加速关闭,因为有些模式并不支持硬件加速。
6.7.2 Shader" class="reference-link">6.7.2 Shader
Shader又被称之为着色器、渲染器,它用来实现一系列的渐变、渲染效果。Android中的Shader包括以下几种。
- BitmapShader——位图Shader
- LinearGradient——线性Shader
- RadialGradient——光束Shader
- SweepGradient——梯度Shader
- ComposeShader——混合Shader
除了第一个Shader以外,其他的Shader都比较正常,实现了名副其实的渐变、渲染效果。而与其他的Shader所产生的渐变不同,BitmapShader产生的是一个图像,这有点像Photoshop中的图像填充渐变。它的作用就是通过Paint对画布进行指定Bitmap的填充,填充时有以下几种模式可以选择。
- CLAMP拉伸——拉伸的是图片最后的那一个像素,不断重复
- REPEAT重复——横向、纵向不断重复
- MIRROR镜像——横向不断翻转重复,纵向不断翻转重复
这几种模式的含义都非常好理解,与字面意思基本相同。这里最常使用的就是CLAMP拉伸模式,虽然它会拉伸最后一个像素,但是只要将图像设置为一定的大小,就可以避免这种拉伸。下面来看一个跟上面有类似效果的例子,将一个矩形的图片变成一张圆形的图片。当然,通过绘制不同的图形,你也可以绘制出不同形状的图形,显示效果如图6.66所示。
图6.66 圆形图片
程序非常简单,无非就是使用BitmapShader来进行图形填充,代码如下所示。
- mBitmap = BitmapFactory.decodeResource(getResources(),
- R.drawable.test);
- mBitmapShader = new BitmapShader(mBitmap,
- Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
- mPaint = new Paint();
- mPaint.setShader(mBitmapShader);
- canvas.drawCircle(500, 250, 200, mPaint);
在以上代码中,用一张图片创建了一支具有图像填充功能的画笔,并使用这个画笔绘制了一个圆形。这样,我们就看见了一个圆形的图形。
这里大家可能还不能很好地体会BitmapShader,这是因为选择的Bitmap大小比较大,所以在设置BitmapShader的TileMode的时候,没有能够看出效果上的差别。下面把TileMode改为REPEAT,并将Bitmap改为比较小的ic_launcher图标,大家就可以看出他们的区别在哪里了,代码如下所示。
- mBitmap = BitmapFactory.decodeResource(getResources(),
- R.drawable.ic_launcher);
- mBitmapShader = new BitmapShader(mBitmap,
- Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
- mPaint = new Paint();
- mPaint.setShader(mBitmapShader);
- canvas.drawCircle(500, 250, 200, mPaint);
通过以上代码,再运行程序,显示效果如图6.67所示。
图6.67 TileMode效果
相信通过这样一幅图,大家应该可以理解使用CLAMP拉伸模式来创建圆形图像的原因了。
在讲完BitmapShader这个比较特殊的Shader后,再来看看其他比较类似的Shader。
先来看一个最简单,也是最常用的Shader——LinearGradient。直译过来就是线性渐变,没错,它就是一个简单的线性渐变,与Photoshop里面的渐变效果类似,如图6.68所示。
图6.68 Photoshop中的渐变
要使用LinearGradient也非常简单,只需要指定渐变起始的颜色就可以了,代码如下所示。
- Paint paint = new Paint();
- paint.setShader(new LinearGradient(0, 0, 400, 400,
- Color.BLUE, Color.YELLOW, Shader.TileMode.REPEAT));
- canvas.drawRect(0, 0, 400, 400, paint);
通过以上代码,画出一个LinearGradient,效果如图6.69所示,它是一个从(0,0)到(400, 400)的一个由蓝色到黄色的渐变效果。
图6.69 LinearGradient效果
LinearGradient方法参数中的TileMode与在BitmapShader中的含义基本相同,这里将绘制矩形的大小设置为与渐变图像大小相同,所以没有看见REPEAT的效果。如果将图形扩大,REPEAT的效果就出来了,如图6.70所示。
图6.70 REPEAT效果
其他几种渐变模式与LinearGradient基本相同,只是渐变的显示效果不同而已,这里就不做过多介绍了。
其实,这些渐变效果通常不会直接使用在程序里。通常情况下,把这种渐变效果作为一个遮罩层来使用,同时结合前面的PorterDuffXfermode。这样处理后,遮罩层就不再是一个生硬的图形,而是一个具有渐变效果的图层。这样处理的效果会更加柔和、更加自然。下面这个实例就演示了如何使用LinearGradient和PorterDuffXfermode来创建一个具有倒影效果的图片,效果如图6.71所示。
图6.71 图片倒影效果
要实现这个效果,首先需要把原图复制一份并进行翻转,代码如下所示。
- mSrcBitmap = BitmapFactory.decodeResource(getResources(),
- R.drawable.test);
- Matrix matrix = new Matrix();
- matrix.setScale(1F, -1F);
- mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0,
- mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);
需要注意的是,使用matrix.setScale(1F, -1F)方法来实现图片的垂直翻转,这是一个非常有用的技巧,避免了使用旋转变换的复杂计算。其原理相信大家只要将值带入前面讲解图像图形变换矩阵的计算公式中,就知道是为什么了。同理,还可以实现图像的水平翻转。
在onDraw()方法中,首先绘制两张图片即原图和倒影图,只是这个时候还未绘制渐变层,因此倒影图与原图的透明度相同。接下来,在倒影图上面绘制一个同样大小的渐变矩形,并通过Mode.DST_IN模式绘制到倒影图上,从而形成一个具有过渡效果的渐变层,完整代码如下所示。
- public class ReflectView extends View {
- private Bitmap mSrcBitmap, mRefBitmap;
- private Paint mPaint;
- private PorterDuffXfermode mXfermode;
- public ReflectView(Context context, AttributeSet attrs) {
- super(context, attrs);
- initRes(context);
- }
- private void initRes(Context context) {
- mSrcBitmap = BitmapFactory.decodeResource(getResources(),
- R.drawable.test);
- Matrix matrix = new Matrix();
- matrix.setScale(1F, -1F);
- mRefBitmap = Bitmap.createBitmap(mSrcBitmap, 0, 0,
- mSrcBitmap.getWidth(), mSrcBitmap.getHeight(), matrix, true);
- mPaint = new Paint();
- mPaint.setShader(new LinearGradient(0, mSrcBitmap.getHeight(), 0,
- mSrcBitmap.getHeight() + mSrcBitmap.getHeight() / 4,
- 0XDD000000, 0X10000000, Shader.TileMode.CLAMP));
- mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- canvas.drawColor(Color.BLACK);
- canvas.drawBitmap(mSrcBitmap, 0, 0, null);
- canvas.drawBitmap(mRefBitmap, 0, mSrcBitmap.getHeight(), null);
- mPaint.setXfermode(mXfermode);
- //绘制渐变效果矩形
- canvas.drawRect(0, mSrcBitmap.getHeight(),
- mRefBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint);
- mPaint.setXfermode(null);
- }
- }
6.7.3 PathEffect" class="reference-link">6.7.3 PathEffect
要理解PathEffect,先来看一张比较直观的图,来了解一下什么是PathEffect,如图6.72所示。
图6.72 PathEffect
PathEffect就是指,用各种笔触效果来绘制一个路径。Android系统提供了如图6.72中展示的几种绘制PathEffect的方式,从上到下依次是:
没效果、CornerPathEffect、DiscretePathEffect、DashPathEffect、PathDashPathEffect、ComposePathEffect。
- CornerPathEffect
非常好理解,就是将拐角处变得圆滑,具体圆滑的程度,则由参数决定。
- DiscretePathEffect
使用这个效果之后,线段上就会产生许多杂点。
- DashPathEffect
显然,这个效果可以用来绘制虚线,用一个数组来设置各个点之间的间隔。此后绘制虚线时就重复这样的间隔进行绘制,另一个参数phase则用来控制绘制时数组的一个偏移量,通常可以通过设置值来实现路径的动态效果。
- PathDashPathEffect
这个效果与前面的DashPathEffect类似,只不过它的功能更加强大,可以设置显示点的图形,即方形点的虚线、圆形点的虚线。
- ComposePathEffect
如果每次都只能实现一种路径的特殊效果,那就显得太单调了。Android提供了一种更加灵活的方式——通过ComposePathEffect来组合PathEffect,这个方法的功能就是将任意两种路径特性组合起来形成一个新的效果。
有了以上的了解,来看看图6.72中的效果是如何实现的。
首先,需要生成一个Path,这里使用随机数来生成一些随机的点并形成一条路径,代码如下所示。
- mPath = new Path();
- mPath.moveTo(0, 0);
- for (int i = 0; i <= 30; i++) {
- mPath.lineTo(i * 35, (float) (Math.random() * 100));
- }
接下来,可以在onDraw()方法中通过不同的PathEffect来绘制这些Path了,代码如下所示。
- mEffects[0] = null;
- mEffects[1] = new CornerPathEffect(30);
- mEffects[2] = new DiscretePathEffect(3.0F, 5.0F);
- mEffects[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, 0);
- Path path = new Path();
- path.addRect(0, 0, 8, 8, Path.Direction.CCW);
- mEffects[4] =
- new PathDashPathEffect(path, 12, 0, PathDashPathEffect.Style.ROTATE);
- mEffects[5] = new ComposePathEffect(mEffects[3], mEffects[1]);
- for (int i = 0; i < mEffects.length; i++) {
- mPaint.setPathEffect(mEffects[i]);
- canvas.drawPath(mPath, mPaint);
- canvas.translate(0, 200);
- }
每绘制一个Path,就将画布平移,从而让各种PathEffect依次绘制出来。
