安卓自定义view-绘制直方图

安卓自定义view-绘制直方图

Android小彩虹2021-07-07 23:44:1760A+A-

这篇文章讲述这样绘制直方图控件的思路,其实做任何事的前提条件就是分析它的一些特征,理清思路。先给出绘制的直方图控件图1-1,这是我自定义绘制系列-自绘制控件初篇。由于是自绘控件,这里不会涉及到动画,手势事件处理。因此还是比较简单的,如果觉得直接好像对这种图的实现没有思路的,那你一定要看完。因为看完后你就会了。 先给出最终效果图1-1

1-1

步骤分析

在解初中数学题的时候,一般都会先建立出直角坐标系,然后在坐标系上去找点。这里也一样,第一步先绘制坐标系,以及刻度、箭头。接下来就是绘制直方图和数字与文字。

坐标系的绘制

首先我们需要确定直角坐标系的宽高,这里的宽高根据实际需求中去定,笔者这里定义宽和高为控件宽高的 2 / 3。代码如下:

 @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。 代码如下:

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。

        // 绘制 y 坐标.
        int yCoordinateStartX = startX;
        int yCoordinateStartY = startY;
                
        yCoordinateEndX = startX;       
        yCoordinateEndY = (mHeight - mYCoordinateHeight) / 2;
        canvas.drawLine(yCoordinateStartX, yCoordinateStartY, yCoordinateEndX, yCoordinateEndY, mCoordinatePaint);

最终的效果如下1-2

1-2

绘制坐标系箭头

在这个问题上想了好一会儿,感觉要是通过构建直角三角形来求出箭头的坐标很麻烦。其实我们完全可以有更讨巧的方式,比如画一条直线,将其旋转指定角度也是可以实现的嘛!首先先从 x 轴开始,因为箭头有一定长度,如果直接绘制在末端点,会遮挡直方图绘制区域,因此我这里在原有 x 轴长度上额外加了 30。 第一步先将画布的起点移到 endX + 30, endY, 再和原来的 x 轴末端点连接起来。

 // 将原点画布移动
 canvas.translate(endX + 30, endY);
// 连接末端点和原点. 这里的 0, 0 为移动后画布的原点
 canvas.drawLine(-30, 0, 0, 0, mCoordinatePaint);

接下来将画布旋转 150度, 为什么是 150 度呢? 是因为我想得到的是和 x 轴呈 30 度夹角, 度数为正表示顺时针旋转。这样就得到了下方的直线,然后将画布旋转逆时针旋转 150 度还原。再接着旋转 210 度,绘制另一条线段。也是比较好理解的,完整代码如下:

        // 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

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

1-4

绘制坐标系刻度

绘制刻度分为 x 轴的文字,和 y 轴的数字刻度。因为这里只是举例,至于你想绘制什么无所谓,根据自己的实际需求来更改即可。继续从 x 轴开始,首先我们要计算出当前的 x 轴宽度能放下几列直方图, 每一列之间都有一个固定间距,间距的个数会比直方图个数多1,这个你可以在白纸上画一下,摆放试试,你或许会忽略最后一列右侧的间距,因此得到相等。

第一步计算一下能摆放多少列,直方图可用空间为: x 轴的宽度 - 间距个数 * 间距,那么可摆放数量为: 直方图可用空间 / 列数。

  // 得到直方图列可绘制区域 totoalSpaces 为 间距个数, space = 10, 固定间距
 availableSpace = mXCoordinateWidth - totoalSpaces * space;

// 每一列的空间,即宽度.
availableColumnSpace = availableSpace / columns;

第二步,找出刻度的起始点坐标,循环往后不断绘制刻度,首先我们假设当前有 4 列, 那么初始点也就是第一列的中点,我这里是以直方图的中点画刻度。给出一个草图如图1-5:

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 所示

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所示

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所示:

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

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);

最后要说的就是,看不代表会,希望读者可以根据代码理解并实现一遍, 巩固下理解。当然大神就别喷我啦,为了更好的适配,肯定不能是有多少就展示多少的,应该考虑结合滑动来展示更多,值得说的事,该功能的实现并非为开源组件而来,只求讲出自己的实现过程。因为考虑的地方很少,不同的数据类型,以及展示的样式都没加入进来。只当作学习思路,因为会了基础思路。想要加上其他功能就问题不大啦!

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们