除了常用的画笔属性,比如普通的画笔(Paint),带边框、填充的style,颜色(Color),宽度(StrokeWidth),抗锯齿(ANTI_ALIAS_FLAG)等,Android还提供了各种各样专业的画笔工具,如记号笔、毛笔、蜡笔等,使用它们可以实现更加丰富的效果。
PorterDuffXfermode
下图中列举了16种PorterDuffXfermode,有点像数学中集合的交集、并集这样的概念,它控制的是两个图像间的混合显示模式。
PorterDuffXfermode设置的是两个图层交集区域的显示方式,dst是先画的图形,而src是后画的图形。
这些模式也不是经常使用,用的最多的是,使用一张图片作为另一张图片的遮罩层,通过控制遮罩层的图形,来控制下面被遮罩图形的显示效果。其中最常用的就是通过DST_IN、SRC_IN模式来实现将一个矩形图片变成圆角图片或者圆形图片的效果。
- 圆角矩形
要使用PorterDuffXfermode非常简单,只需要让画面拥有这个属性就可以了。比如要实现下面圆角矩形的效果:
先用一个普通画笔画一个Mask遮罩层,再用带PorterDuffXfermode的画笔将图像画在遮罩层上,这样就可以通过上面所说的效果来混合两个图像了,代码如下:
mPaint=new Paint();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.iu);
mBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(mBitmap);
mPaint.setAntiAlias(true);
canvas.drawRoundRect(0,0,bitmap.getWidth(),bitmap.getHeight(),80,80,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap,0,0,mPaint);
mImageView= (ImageView) findViewById(R.id.img);
mImageView.setImageBitmap(mBitmap);
- 刮刮卡效果
若要实现刮刮卡效果(刮刮卡一般有两个图层,即上面的用来被刮掉的图层和下面隐藏的图层)。在初始状态下,上面的图层会将下面整个图层覆盖,当你用手刮上面的图层的时候,下面的图层会慢慢显示出来,类似于画图工具中的橡皮擦效果。
首先要做一些初始化工作,例如准备好图片,设置好Paint的一些属性,代码如下:
mPaint = new Paint();
/**
* 将画笔的透明度设置为0,这样才能显示出擦除效果。
* 在使用PorterDuffXfermode进行图层混合时,并不是简单地只进行图层的计算,同时也会计算透明通道的值。
* 正是由于混合了透明通道,才形成了这样的效果。
*/
mPaint.setAlpha(0);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
mPaint.setStyle(Paint.Style.STROKE);
//使Paint的笔触更加圆滑一点
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeWidth(50);
//使Paint的连接处更加圆滑一点
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPath = new Path();
mBgBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
mFgBitmap = Bitmap.createBitmap(mBgBitmap.getWidth(), mBgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mFgBitmap);
mCanvas.drawColor(Color.GRAY);
获取用户手指滑动所产生的路径并将使用DST_IN模式将路径绘制到前面覆盖的图层上,代码如下所示。使用Path保存用户手指划过的痕迹。当然,如果使用贝塞尔曲线来做优化则会得到更好的显示效果,这里为了简化功能,就不使用贝塞尔曲线了。
@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;
}
//使用DST_IN模式将路径绘制到前面覆盖的图层上
mCanvas.drawPath(mPath, mPaint);
invalidate();
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(600,400);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBgBitmap, 0, 0, null);
canvas.drawBitmap(mFgBitmap, 0, 0, null);
}
效果图如下所示:
在使用PorterDuffXfermode时还有一点需要注意,那就是最好在绘图时,将硬件加速关闭,因为有些模式并不支持硬件加速。
Shader
Shader又被称之为着色器、渲染器,它用来实现一系列的渐变、渲染效果。Android中的Shader包括以下几种。
BitmapShader——位图Shader
LinearGradient——线性Shader
RadialGradient——光束Shader
SweepGradient——梯度Shader
ComposeGradient——混合Shader
- BitmapShader
与其他的Shader所产生的渐变不同,BitmapShader产生的是一个图像,这有点像Photoshop中的图像填充渐变。它的作用就是通过Paint对画布进行指定Bitmap的填充,填充时有以下几种模式可以选择。
CLAMP拉伸——拉伸的是图片最后的那一个像素,不断重复
REPEAT重复——横向、纵向不断重复
MIRROR镜像——横向不断翻转重复,纵向不断翻转重复
这里最常用的就是CLAMP拉伸模式,虽然它会拉伸最后一个像素,但是只要将图像设置为一定的大小,就可以避免这种拉伸。下面将一个矩形的图片变成一张圆形的图片,效果如下:
代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Paint paint = new Paint();
//用一张图片创建一支具有图像填充功能的画笔,并使用这只画笔绘制一个圆形
paint.setShader(bitmapShader);
canvas.drawCircle(500, 250, 200, paint);
}
如果把TileMode改为REPEAT,并将Bitmap改为较小的ic_launcher图标,就可以看到几种模式的区别了,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
BitmapShader bitmapShader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
Paint paint = new Paint();
paint.setShader(bitmapShader);
canvas.drawCircle(500, 250, 200, paint);
}
运行程序,效果图如下:
- LinearGradient
要使用LinearGradient非常简单,只需要指定渐变的起始颜色即可,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
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);
}
效果图如下图,他是一个从(0, 0)到(400, 400)的由蓝色到黄色的渐变效果。
LinearGradient方法参数中的TileMode与在BitmapShader中的含义基本相同,这里将绘制矩形的大小设置为与渐变图像大小相同,所以没有看见REPEAT的效果。如果将图形扩大,REPEAT的效果就出来了,如下图所示:
倒影效果
这些渐变效果通常不会直接使用在程序里。通常情况下,把这种渐变效果作为一个遮罩层来使用,同时结合PorterDuffXfermode。这样处理后,遮罩层就不再是一个生硬的图形,而是一个具有渐变效果的图层。这样处理的效果会更加柔和、更加自然。下面用LinearGradient和PorterDuffXfermode来创建一个具有倒影效果的图片,效果图如下:
代码如下:
public class ReflectView extends View {
private Bitmap mSrcBitmap, mRefBitmap;
private Paint mPaint;
private PorterDuffXfermode mXfermode;
public ReflectView(Context context) {
super(context);
initRes(context);
}
public ReflectView(Context context, AttributeSet attrs) {
super(context, attrs);
initRes(context);
}
public ReflectView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initRes(context);
}
private void initRes(Context context) {
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Matrix matrix = new Matrix();
//实现图片的垂直翻转,避免使用旋转变换的复杂计算
matrix.setScale(1, -1);
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() / 2, 0XDD000000, 0X10000000,
Shader.TileMode.CLAMP));
mXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(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(), mSrcBitmap.getWidth(), mSrcBitmap.getHeight() * 2, mPaint);
mPaint.setXfermode(null);
}
}
PathEffect
PathEffect就是指用用各种笔触效果来绘制路径。Android系统提供了如下中展示的几种绘制PathEffect的方式,从上到下依次是:
没效果
CornerPathEffect——就是将拐角处变得圆滑,具体圆滑的程度,则由参数决定
DiscretePathEffect——使用这个效果之后,线段上就会产生许多杂点
DashPathEffect——这个效果可以用来绘制虚线,用一个数组来设置各个点之间的间隔。此后绘制虚线时就重复这样的间隔进行绘制,另一个参数phase则用来控制绘制时数组的一个偏移量,通常可以通过设置值来实现路径的动态效果。
PathDashPathEffect——这个效果与DashPathEffect类似,只不过它的功能更加强大,可以设置显示点的图形,即方形点的虚线、圆形点的虚线。
ComposePathEffect——通过ComposePathEffect来组合PathEffect,就是将任意两种路径特性组合起来形成一个新的效果。
上图实现代码如下:
public class PathEffectView extends View {
private Paint mPaint;
private Path mPath;
private PathEffect[] mEffects;
public PathEffectView(Context context) {
super(context);
init();
}
public PathEffectView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PathEffectView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.DKGRAY);
//使用随机数来生成一些随机的点形成一条路径
mPath = new Path();
mPath.moveTo(0, 0);
for (int i = 0; i < 30; i++) {
mPath.lineTo(i * 35, (float) (Math.random() * 100));
}
mEffects = new PathEffect[6];
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]);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mEffects.length; i++) {
mPaint.setPathEffect(mEffects[i]);
canvas.drawPath(mPath, mPaint);
canvas.translate(0, 200);
}
}
}