我们要实现的角标View的效果如上图所示。
怎么去做呢?当然需要自定义View了,角标的背景(三角形、梯形)可以先通过Path连接每一个边,然后绘制Path到画布上,最后再把文字绘制到画布上,接下来一步步看。
我们的角标View在画布上占据一个正方形区域(图1虚线区域),初始状态如下图:
可以看到,初始状态下画布的坐标系原点在View的左上角,但是这样不方便进行数据计算,所以我们先将坐标系原点移动到画布正中心:
对应代码如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//角标View最小边长的一半
mHalfWidth = Math.min(w, h) / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//将原点移动到画布中心
canvas.translate(mHalfWidth, mHalfWidth);
}
将原点移动到画布中心后,就可以愉快的绘制角标的背景区域了,我们以角标在右上角的情况为例:
只需要连接A、B、C、D四个点就可以了,剩下的事情就是计算四个点的坐标了,还是跟简单的嘛,只需要知道AB的长度,即角标的边长sideLength,还有正方形的边长即可,这两个值可以自行配置,关键代码如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//将原点移动到画布中心
canvas.translate(mHalfWidth, mHalfWidth);
//绘制角标背景
mPath.moveTo(-mHalfWidth, -mHalfWidth); //将Path的起点移动到A
mPath.lineTo(sideLength - mHalfWidth, -mHalfWidth); //连接AB
mPath.lineTo(mHalfWidth, mHalfWidth - sideLength); //连接BC
mPath.lineTo(mHalfWidth, mHalfWidth); //连接CD
mPath.close(); //闭合Path,即连接DA
canvas.drawPath(mPath, mPaint);
}
这样就完成了角标背景的绘制,即图3的红线区域,其实是一个红色的实心区域,这里只是为了方便解释。
接下来我们看文字的绘制,直接把文字绘制成倾斜的难度略大,所以我们将画布顺时针旋转45度(为什么是45度应该很明显吧),旋转后如下图所示:
这样就相当于在 x轴 水平方向防线绘制文字了。默认情况下,我们将文字绘制在角标背景(图中的梯形)的中心,如果AB的长度大于正方形View的边长,文字默认绘制在AB的长度等于正方形View的边长一半时对应的梯形中心(仅仅是为了美观默认操作,你可以更改它)。看一下文字绘制的代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
.......
if (sideLength > mHalfWidth) {
sideLength = mHalfWidth;
}
//文字绘制的坐标
int x = (int) -mTextPaint.measureText(text) / 2;
int y = (int) (-Math.sqrt(2) / 2.0 * sideLength -(mTextPaint.ascent() + mTextPaint.descent())) / 2;
//绘制文字
canvas.drawText(text, x, y, mTextPaint);
}
简单的说明一下,x坐标保证文字水平居中,y标准保证文字垂直居中,其中sideLength代表AB边长,默认情况下当AB的长度大于正方形View的边长一半时,更改sideLength的值为正方形View的边长一半,但不会影响角标背景的显示,因为背景是先绘制的。到这里角标在右上角的情况就分析完了。
如果角标在右下角显示呢?其实和角标在右上角的情况类似,还是按照上边先画质角标背景再绘制文字的流程分析。通过观察发现将图3绕坐标原点顺时针旋转90度,就可以实现角标在右下角显示的效果。如下图:
类似的,如果旋转180度就是角标在左下角的显示效果、旋转270度就是角标在左上角的显示效果,图就不贴了,大家可以结合图3体会下。我们在自定义属性中用枚举值(position)0、1、2、3代表右上、右下、左下、左上的情况,则对应的画布旋转代如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//将原点移动到画布中心
canvas.translate(mHalfWidth, mHalfWidth);
//根据角标位置旋转画布
canvas.rotate(position * 90);
//绘制角标背景
......
}
这样就完全解决了角标背景绘制的问题,但是文字绘制呢?问题来了,如果角标在右下角,按照之前的方式将画布顺时针旋转45度再绘制,得到的是如下的效果:
文字反了,看起来好别扭,角标在左下角显示时也存在同样的问题。解决方案也不难,以图6为基础,忽略显示异常的文字,将画布向y轴负方向平移梯形的高度,再在x、y方向翻转画布,对应的代码如下:
//如果角标在右下、左下则进行画布平移、翻转,解决绘制的文字显示问题
if (position == 1 || position == 2) {
canvas.translate(0, (float) (-Math.sqrt(2) / 2.0 * sideLength));
canvas.scale(-1, -1);
}
然后再绘制文字,对应的图如下:
这样显示效果就正常了。
最后贴一下onDraw()方法的代码:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//将原点移动到画布中心
canvas.translate(mHalfWidth, mHalfWidth);
//根据角标位置旋转画布
canvas.rotate(position * 90);
if (sideLength > mHalfWidth * 2) {
sideLength = mHalfWidth * 2;
}
//绘制角标背景
mPath.moveTo(-mHalfWidth, -mHalfWidth);
mPath.lineTo(sideLength - mHalfWidth, -mHalfWidth);
mPath.lineTo(mHalfWidth, mHalfWidth - sideLength);
mPath.lineTo(mHalfWidth, mHalfWidth);
mPath.close();
canvas.drawPath(mPath, mPaint);
//绘制文字前画布旋转45度
canvas.rotate(45);
//角标实际高度
int h1 = (int) (Math.sqrt(2) / 2.0 * sideLength);
int h2 = (int) -(mTextPaint.ascent() + mTextPaint.descent());
//文字绘制坐标
int x = (int) -mTextPaint.measureText(text) / 2;
int y;
if (marginLeanSide >= 0) { //使用clv:margin_lean_side属性时
if (position == 1 || position == 2) {
if (h1 - (marginLeanSide - mTextPaint.ascent()) < (h1 - h2) / 2) {
y = -(h1 - h2) / 2;
} else {
y = (int) -(h1 - (marginLeanSide - mTextPaint.ascent()));
}
} else {
if (marginLeanSide < mTextPaint.descent()) {
marginLeanSide = (int) mTextPaint.descent();
}
if (marginLeanSide > (h1 - h2) / 2) {
marginLeanSide = (h1 - h2) / 2;
}
y = -marginLeanSide;
}
} else { //默认情况下
if (sideLength > mHalfWidth) {
sideLength = mHalfWidth;
}
y = (int) (-Math.sqrt(2) / 2.0 * sideLength + h2) / 2;
}
//如果角标在右下、左下则进行画布平移、翻转,已解决绘制的文字显示问题
if (position == 1 || position == 2) {
canvas.translate(0, (float) (-Math.sqrt(2) / 2.0 * sideLength));
canvas.scale(-1, -1);
}
//绘制文字
canvas.drawText(text, x, y, mTextPaint);
}
其中if (marginLeanSide >= 0) { }
的这部分代码在这里说明一下,前边我们提到过默认情况下,我们将文字绘制在角标背景(图中的梯形)的中心,如果AB的长度大于正方形View的边长,文字默认绘制在AB的长度等于正方形View的边长一半时对应的梯形中心,如果我们设置了clv:margin_lean_side
属性,则不采用默认操作,而是根据clv:margin_lean_side
属性值计算文字到角标斜边(CD边)的距离,但是最大距离为文字在角标中心时到斜边的距离。
整个实现过程还是比较简单的,关键是要正确的理解画布平移、旋转操作,记住一点,画布的平移、旋转并不影响之前的绘制效果,只对之后的操作有影响。这里我们只分析了核心的部分,其它就是自定义属性、View的测量,有兴趣的可看源码。