这篇文章讲述这样绘制直方图控件的思路,其实作任何事的前提条件就是分析它的一些特征,理清思路。先给出绘制的直方图控件图1-1,这是我自定义绘制系列-自绘制控件初篇。因为是自绘控件,这里不会涉及到动画,手势事件处理。所以仍是比较简单的,若是以为直接好像对这种图的实现没有思路的,那你必定要看完。由于看完后你就会了😄😄。 先给出最终效果图1-1算法
在解初中数学题的时候,通常都会先创建出直角坐标系,而后在坐标系上去找点。这里也同样,第一步先绘制坐标系,以及刻度、箭头。接下来就是绘制直方图和数字与文字。canvas
首先咱们须要肯定直角坐标系的宽高,这里的宽高根据实际需求中去定,笔者这里定义宽和高为控件宽高的 2 / 3。代码以下:bash
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// view 的宽度
mWidth = w;
// view 高度.
mHeight = h;
// x 轴坐标的宽度
mXCoordinateWidth = mWidth * 2 / 3;
// y 轴坐标的高度.
mYCoordinateHeight = mHeight * 2 /3;
}
复制代码
为何要在 onSizeChanged 中获取宽高想必你们应该是知道的(当 view 的尺寸通过测量后获得)。知道了宽高,剩下的就是找 x 轴的起点、终点,y 轴的起点、终点坐标。先找 x 轴的起点, 因为咱们须要坐标系是在 view 居中的,那横坐标不就是 view 的宽度减去 x 轴宽度除以 2, 这样左右两边的间隙都相同。纵坐标是 view 的高度减去上边距, 上边距怎么获得呢? 跟前面的求横坐标是一个意思。 即 view 的高度减去 y 轴高度 除以2。 代码以下:ide
view 的宽度减去 x 轴坐标的宽度除以 2
startX = (mWidth - mXCoordinateWidth) / 2;
// view 的高度减去上边距
startY = mHeight - (mHeight - mYCoordinateHeight) / 2;
复制代码
起点获得了,终点就很简单了。只须要改变横坐标的距离,纵坐标不需改变,即起点的 x 坐标加上 x 轴的宽度。代码以下:学习
int endX = startX + mXCoordinateWidth;
// 纵坐标不变。
int endY = startY;
复制代码
OK, 找到了起点和终点,这样已经能够肯定一条线段。动画
// 绘制 x 轴坐标.
startX = (mWidth - mXCoordinateWidth) / 2;
startY = mHeight - (mHeight - mYCoordinateHeight) / 2;
int endX = startX + mXCoordinateWidth;
int endY = startY;
// 绘制 x 轴. 先不要纠结画笔的定义,只想象经过笔画出的而已。
canvas.drawLine(startX, startY, endX, endY, mCoordinatePaint);
复制代码
先不着急绘制箭头,继续绘制 y 轴。首先起点就是 x 轴的起点, 只须要找终点的坐标,终点的横坐标为 startX, 纵坐标为 view 的高度减去 y 轴的高度除以2。spa
// 绘制 y 坐标.
int yCoordinateStartX = startX;
int yCoordinateStartY = startY;
yCoordinateEndX = startX;
yCoordinateEndY = (mHeight - mYCoordinateHeight) / 2;
canvas.drawLine(yCoordinateStartX, yCoordinateStartY, yCoordinateEndX, yCoordinateEndY, mCoordinatePaint);
复制代码
最终的效果以下1-2 3d
在这个问题上想了好一下子,感受要是经过构建直角三角形来求出箭头的坐标很麻烦。其实咱们彻底能够有更讨巧的方式,好比画一条直线,将其旋转指定角度也是能够实现的嘛!首先先从 x 轴开始,由于箭头有必定长度,若是直接绘制在末端点,会遮挡直方图绘制区域,所以我这里在原有 x 轴长度上额外加了 30。 第一步先将画布的起点移到 endX + 30, endY, 再和原来的 x 轴末端点链接起来。rest
// 将原点画布移动
canvas.translate(endX + 30, endY);
// 链接末端点和原点. 这里的 0, 0 为移动后画布的原点
canvas.drawLine(-30, 0, 0, 0, mCoordinatePaint);
复制代码
接下来将画布旋转 150度, 为何是 150 度呢? 是由于我想获得的是和 x 轴呈 30 度夹角, 度数为正表示顺时针旋转。这样就获得了下方的直线,而后将画布旋转逆时针旋转 150 度还原。再接着旋转 210 度,绘制另外一条线段。也是比较好理解的,完整代码以下:code
// x 轴箭头
// 将画布状态保存
int layer = canvas.saveLayer(0, 0, mWidth, mHeight, null);
// 移动画布原点
canvas.translate(endX + 30, endY);
// 链接原点和末端点
canvas.drawLine(-30, 0, 0, 0, mCoordinatePaint);
// 将画布旋转 150 度
canvas.rotate(150);
// 绘制线段
canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);
// 将画布旋转还原
canvas.rotate(-150);
// 接着再旋转 210 度
canvas.rotate(210);
// 绘制线段
canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);
// 还原到指定画布
canvas.restoreToCount(layer);
复制代码
效果图1-3
x 轴的箭头画好了, y 轴其实也是一个原理。只是绘制第二个线段时没有将画布旋转还原,而是继续旋转。也是同样的意思。直接呈上代码:
// y 轴箭头
int layer1 = canvas.saveLayer(0, 0, mWidth, mHeight, null);
canvas.translate(yCoordinateEndX, yCoordinateEndY - 60);
canvas.drawLine(0, 60, 0, 0, mCoordinatePaint);
canvas.rotate(60);
canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);
// 继续旋转60度,即转过的角度为 120 度
canvas.rotate(60);
canvas.drawLine(0, 0, 30, 0, mCoordinatePaint);
canvas.restoreToCount(layer1);
复制代码
箭头的最终效果,如图1-4
绘制刻度分为 x 轴的文字,和 y 轴的数字刻度。由于这里只是举例,至于你想绘制什么无所谓,根据本身的实际需求来更改便可。继续从 x 轴开始,首先咱们要计算出当前的 x 轴宽度能放下几列直方图, 每一列之间都有一个固定间距,间距的个数会比直方图个数多1,这个你能够在白纸上画一下,摆放试试,你或许会忽略最后一列右侧的间距,所以获得相等。
第一步计算一下能摆放多少列,直方图可用空间为: x 轴的宽度 - 间距个数 * 间距,那么可摆放数量为: 直方图可用空间 / 列数。
// 获得直方图列可绘制区域 totoalSpaces 为 间距个数, space = 10, 固定间距
availableSpace = mXCoordinateWidth - totoalSpaces * space;
// 每一列的空间,即宽度.
availableColumnSpace = availableSpace / columns;
复制代码
第二步,找出刻度的起始点坐标,循环日后不断绘制刻度,首先咱们假设当前有 4 列, 那么初始点也就是第一列的中点,我这里是以直方图的中点画刻度。给出一个草图如图1-5:
上图1-5中的 A 点为咱们的起点,这点怎么求呢? 首先 x 轴的起点已知,起点加上间距 space, 以及直方图宽度的一半就能够得出横坐标,纵坐标为 x 轴起点坐标的纵坐标。
// x 轴刻度横坐标
int xScalex = startX + space + availableColumnSpace / 2;
// 纵坐标为 x 轴起点坐标的纵坐标.
int xScaley = startY;
复制代码
找到这一点后,是否是经过循环不断向后移动便可。移动多少距离呢? 从图1-5不难看出, 上一个点的横坐标加上 2 个直方图宽度的一半以及一个间距。完整代码以下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < columns; i++) {
HistogramBean bean = datas.get(i);
int height = bean.height;
int max = bean.max;
String color = bean.color;
String columnName = bean.columnName;
if (max == 0 || max < height) {
throw new IllegalStateException("最大值不能为0");
}
// 绘制 x 轴刻度.
drawXScaleValue(canvas, columnName);
}
}
private void drawXScaleValue(Canvas canvas, String columnName) {
// 绘制 x 轴刻度. xScaleStep 用来记录下一次跳到的点.
int xScalex = 0;
if (xScaleStep > 0) {
xScalex = xScaleStep;
} else {
// 找到第一个点.
xScalex = startX + space + availableColumnSpace / 2;
}
int xScaley = startY;
// 绘制一个 10 像素长度的刻度.
int xScaleEndx = xScalex;
int xScaleEndy = startY + 10;
mPaint.setColor(Color.BLACK);
canvas.drawLine(xScalex, xScaley, xScaleEndx, xScaleEndy, mPaint);
// 记录一个直方图中点的横坐标
xScaleStep = xScalex + 2 * availableColumnSpace / 2 + space;
}
复制代码
如图1-6 所示
接下来是 y 轴的刻度啦!我这里将 y 轴刻度的绘制跟直方图列数来对应的。即多少列就有多少刻度。它的算法是按着 y 轴的高度,根据直方图的最大值进行缩放。说白了就是等分 y 轴,其具体的计算公式为: 直方图的最大值 / 列数 * y 轴高度 / max 获得等分第一个高度。举个列子,若是直方图最大值是 100, 有 4 列, 假定 y 轴高度为 300, 那么 100 / 4 * 300 / 100 = 75。 说明这个刻度的高度为 y 轴高度的 25, 那咱们就求出它的坐标,后续的坐标只要经过循环日后移动便可。它的坐标为 y 轴的终点纵坐标加上 y 轴的高度减去前面公式所得的值。其完整代码以下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < columns; i++) {
HistogramBean bean = datas.get(i);
int height = bean.height;
int max = bean.max;
String color = bean.color;
String columnName = bean.columnName;
if (max == 0 || max < height) {
throw new IllegalStateException("最大值不能为0");
}
// 绘制 x 轴刻度.
drawXScaleValue(canvas, columnName);
// 根据列以及最大值来分配刻度值,能够经过计算更换刻度的密度.
// 这里从 i + 1 开始,由于 i 是从 0 开始循环
int yScaleValue = (i + 1) * max / columns * mYCoordinateHeight / max;
drawYScaleValue(canvas, yScaleValue);
}
}
private void drawYScaleValue(Canvas canvas, int yScaleValue) {
// 绘制 y 轴刻度.
// 根据列的数量进行刻度实现. yScaleValue 这个值是每一个刻度的值,根据前面的公式计算获得
int yScaley = yCoordinateEndY + mYCoordinateHeight - yScaleValue;
int yScalex = yCoordinateEndX;
int yScaleEndx = yCoordinateEndX - 10;
int yScaleEndy = yScaley;
mPaint.setColor(Color.BLACK);
canvas.drawLine(yScalex, yScaley, yScaleEndx, yScaleEndy, mPaint);
}
复制代码
如图1-6所示
其实经过前面的努力,已经将必要点都找到了,咱们只需知道当前直方图在 y 轴高度的值,这个高度为: 直方图的数值 * y 轴的高度 / 直方图的最大值 max获得。 获得这个值能够得出其距离顶部的距离,能够得出该点的纵坐标的值为: y 轴终点纵坐标 + y 轴高度 - 换算后的高度。
// 根据比列缩放获得在 y 轴的高度.
int relalHeight = mYCoordinateHeight * height / max;
// 距离定点的距离.
int top = (yCoordinateEndY + mYCoordinateHeight - relalHeight);
复制代码
直方图无非就是一个矩形,只要就出 left, top,right,bottom 就肯定矩形啦。其中很差求的也就是 top 啦!可是 top 在前面已被求得。那剩下的 left, right, bottom 就十分简单啦!这里的 left 移动规则和前面刻度的移动规则十分类似。若是理解了前面的移动规则,这里就很容易理解。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < columns; i++) {
HistogramBean bean = datas.get(i);
int height = bean.height;
int max = bean.max;
String color = bean.color;
String columnName = bean.columnName;
if (max == 0 || max < height) {
throw new IllegalStateException("最大值不能为0");
}
// 绘制直方图.
drawHistogram(canvas, height, max, color);
// 绘制 x 轴刻度.
drawXScaleValue(canvas, columnName);
// 根据列以及最大值来分配刻度值,能够经过计算更换刻度的密度.
// 这里从 i + 1 开始,由于 i 是从 0 开始循环
int yScaleValue = (i + 1) * max / columns * mYCoordinateHeight / max;
drawYScaleValue(canvas, yScaleValue);
}
}
private void drawHistogram(Canvas canvas, int height, int max, String color) {
// 根据最大值的比列进行缩放.
int relalHeight = mYCoordinateHeight * height / max;
// 将上一次的距离加上固定间距 space
int left = startX + space + lastX;
if (lastX > 0) {
left = lastX + space;
}
int top = (yCoordinateEndY + mYCoordinateHeight - relalHeight);
int right = (left + availableColumnSpace);
int bottom = startY - 3;
// 可经过外界设置直方图的颜色
if (!TextUtils.isEmpty(color)) {
mPaint.setColor(Color.parseColor(color));
}
canvas.drawRect(left, top, right, bottom, mPaint);
// 记录上一次右边的位置
lastX = right;
}
复制代码
如图1-7所示:
终于到尾声了,累死去。绘制数字和文字就很简单啦!由于须要的全部点都以给出,只须要在位置上绘制便可。先从 x 轴开始。只是将文字绘制到刻度的下方并居中显示。直接给出代码。
// 绘制 x 轴刻度值.
Rect columnNameRect = new Rect();
mTextPaint.getTextBounds(columnName, 0, columnName.length(), columnNameRect);
canvas.drawText(columnName,
xScalex - columnNameRect.width() / 2,
xScaleEndy + columnNameRect.height(),
mTextPaint);
复制代码
y 轴的数字刻度也是同样的道理。
// 绘制 y 轴刻度值.
Rect textRect = new Rect();
mTextPaint.getTextBounds(yScaleValueStr, 0, yScaleValueStr.length(), textRect);
canvas.drawText(yScaleValueStr,
startX - textRect.width() - 20,
yScaley + textRect.height() / 2,
mTextPaint);
复制代码
最终的效果图 1-8
若是还须要将 0 刻度绘制,以下代码便可。
// 补画 y 轴 0 刻度
int yScale0x = yCoordinateEndX;
int yScale0y = mYCoordinateHeight + yCoordinateEndY;
int yScaleEnd0x = yCoordinateEndX - 10;
int yScaleEnd0y = yScale0y;
canvas.drawLine(yScale0x, yScale0y, yScaleEnd0x, yScaleEnd0y, mPaint);
// 绘制刻度值.
String yScaleValueStr = "0";
Rect textRect = new Rect();
mTextPaint.getTextBounds(yScaleValueStr, 0, yScaleValueStr.length(), textRect);
canvas.drawText(yScaleValueStr,
startX - textRect.width() - 20,
yScale0y + textRect.height() / 2,
mTextPaint);
复制代码
最后要说的就是,看不表明会,但愿读者能够根据代码理解并实现一遍, 巩固下理解。固然大神就别喷我啦😭,为了更好的适配,确定不能是有多少就展现多少的,应该考虑结合滑动来展现更多,值得说的事,该功能的实现并不是为开源组件而来,只求讲出本身的实现过程。由于考虑的地方不多,不一样的数据类型,以及展现的样式都没加入进来。只看成学习思路,由于会了基础思路。想要加上其余功能就问题不大啦!