概述
项目开发过程中使用了MPAndroidChart开源库,在绘制自定义Marker时出现了图像覆盖的问题。类似效果如下图所示:
图中有三个圆形顶点,两条线段和三个矩形标签。而预期效果是顶点始终位于标签的下方。本文针对该问题,阐述具体的解决方法。
问题复现
单独创建一个新项目用于模拟Marker的绘制过程。MPAndroidChart的绘制过程是先画直线,然后再画Marker。我们自定义的Marker包含两部分,即顶点和标签。
- 自定义View,在其
onDraw()
方法中进行折线、Marker的绘制。
//画笔
private Paint mPaint = new Paint();
//起始顶点
private float[] mStart = {200,400};
//中间顶点
private float[] mMiddle = {260,500};
//末尾顶点
private float[] mStop = {300,420};
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//填充背景色
canvas.drawColor(Color.YELLOW);
//绘制折线
drawLines(canvas);
//绘制Marker
drawMarker(canvas,mStart,Color.RED);
drawMarker(canvas,mMiddle,Color.GREEN);
drawMarker(canvas,mStop,Color.BLUE);
}
private void drawLines(Canvas canvas){
mPaint.setColor(Color.GRAY);
mPaint.setStrokeWidth(5);
//绘制线段
canvas.drawLine(mStart[0],mStart[1],mMiddle[0],mMiddle[1],mPaint);
canvas.drawLine(mMiddle[0],mMiddle[1],mStop[0],mStop[1],mPaint);
}
private void drawMarker(Canvas canvas,float[] point,int color) {
mPaint.setColor(color);
//绘制顶点
canvas.drawCircle(point[0], point[1], 10, mPaint);
float x = point[0] - 60;
float y = point[1] - 120;
RectF rectF = new RectF(x, y, x + 120, y + 60);
//绘制标签
canvas.drawRect(rectF, mPaint);
}
- 在布局文件中引入该自定义View,最终显示效果如下图:
可以看到蓝色顶点位于绿色标签的上方,与预期效果不符。
分析及解决
通过调用Canvas
对象的drawXXX()
方法,我们依次绘制了折线和Marker。参考有关博客: 每次Canvas画图时(即调用Draw系列函数),都会产生一个透明图层,然后在这个图层上画图,画完之后覆盖在屏幕上显示。通过这种解释不难理解最后绘制的蓝色顶点位于绿色标签的上方。(有待考证)
使顶点位于标签的下方,涉及到图像的混合模式。Paint
提供有setXfermode(Xfermode xfermode)
方法来处理图像的混合。
其中PorterDuffXfermode
是惟一一个没有过时且沿用至今的子类,它提供了如下混合效果:
其中,Dst为目标图像,表示画布上已有的图像,而Src为源图像,表示当前正要绘制的图像。在Android的PorterDuff.Mode类中列举了他们制定的规则:
android.graphics.PorterDuff.Mode.SRC:只绘制源图像
android.graphics.PorterDuff.Mode.DST:只绘制目标图像
android.graphics.PorterDuff.Mode.DST_OVER:在源图像的顶部绘制目标图像
android.graphics.PorterDuff.Mode.DST_IN:只在源图像和目标图像相交的地方绘制目标图像
android.graphics.PorterDuff.Mode.DST_OUT:只在源图像和目标图像不相交的地方绘制目标图像
……
参见详情
- 通过使用
Paint
对象的setXfermode()
方法,设置圆形顶点始终位于矩形标签的下方。
private void drawMarker(Canvas canvas,float[] point,int color) {
mPaint.setColor(color);
//设置混合模式
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
//绘制顶点
canvas.drawCircle(point[0], point[1], 10, mPaint);
//清除混合模式
mPaint.setXfermode(null);
float x = point[0] - 60;
float y = point[1] - 120;
RectF rectF = new RectF(x, y, x + 120, y + 60);
//绘制标签
canvas.drawRect(rectF, mPaint);
}
运行效果如下图所示:
可以看到顶点全都消失了,这究竟是什么原因?以下对绘制的每一步进行问题排查。
刚开始我们设置了画笔的背景为黄色,然后在其上绘制了两条线段,这个没有问题;
接着开始绘制第一个Marker。我们使用
PorterDuff.Mode.DST_OVER
模式来绘制顶点,然后将顶点(即源图像)与原屏幕上的图像(即目标图像,见上图)进行混合,由于DST_OVER
模式是在源图像的顶部绘制目标图像,因而顶点被黄色背景所替代。而在绘制标签的时候,画笔的混合模式已被清除。
该问题主要是由于两图像在进行混合时存在无用的干扰元素造成的。为此可创建一个新的图层(Layer)专门用于Marker的绘制。Android的Canvas可以使用saveLayerXXX
来创建一些中间层,使用restore/restoreToCount
把本层绘制的图像“绘制”到上层或是Canvas上。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.YELLOW);
//绘制折线
drawLines(canvas);
//新建图层
int saveCount=canvas.saveLayer(getLeft(),getTop(),getRight()
,getBottom(),null,Canvas.ALL_SAVE_FLAG);
//绘制Marker
drawMarker(canvas,mStart,Color.RED);
drawMarker(canvas,mMiddle,Color.GREEN);
drawMarker(canvas,mStop,Color.BLUE);
//还原图层
canvas.restoreToCount(saveCount);
}
运行效果如下图所示:
总结
以上主要涉及到两个问题:
1. 图像的混合模式
Paint对象提供的setXfermode()
方法
2. 图层概念
Canvas提供的saveLayerXXX
方法
参考链接
- http://blog.youkuaiyun.com/harvic880925/article/details/51264653
- http://blog.youkuaiyun.com/iispring/article/details/50472485
- http://www.cnblogs.com/DonkeyTomy/articles/3215137.html
- http://www.cnblogs.com/tianzhijiexian/p/4297172.html
- http://blog.youkuaiyun.com/u011433995/article/details/50475131
- http://blog.youkuaiyun.com/scnuxisan225/article/details/49702979