自定义view:快速实现柱状图的绘制

来源:互联网 发布:java web前端开发工具 编辑:程序博客网 时间:2024/05/05 22:24

自定义view:快速实现柱状图的绘制

标签(空格分隔): 安卓


1、前言

本文讲解如何通过canvas快速实现柱状图表的绘制,先看下最终效果图:

网上开源的图表绘制框架还是很多的,功能也非常强大,比如hellocharts,MPAndroidCharts等。

但是作为一个有追求的程序员,过多地使用第三方的东西总是觉得缺少点安全感。主要原因是现成的轮子固然好,但是仅仅作为使用者的我们对其理解不深,导致不易于扩展修改。尤其是涉及自定义view这种效果显示的,保不准哪天产品和设计就把效果给改了,然后我们只能苦逼的研究源码,这样太被动了。而自己写的东西就不一样了,全在自己掌控之下,任你需求设计随便改,我自横刀对你笑。

2、分析

整个柱状图的绘制可以分为三个步骤:

  1. 坐标轴绘制
  2. 绘制标尺
  3. 刻度值绘制
  4. 柱状绘制

3、代码实现

3.1 坐标轴的绘制

坐标轴的绘制使用的是cavans的drawLine方法:

drawLine(float startX, float startY, float stopX, float stopY,Paint paint)

其中传入的参数分别是起始坐标、终点坐标、画笔。俩点妥妥的确认一条直线,所以我们知道俩个绘制坐标点就行。

x轴和y轴都是从原点(坐标轴的原点,而得view的,请勿混淆了)开始画,为了确认这个点,我们全局定义了一个变量mPaddingLabLine表示坐标轴到view边界(Y轴就是到左边,x就是相对于底部)的距离,这个距离我们是用来绘制坐标轴刻度以及标题。涉及到位置计算时,为了兼容性我们去除view的内边距。这个原点的x、y坐标计算方法代码如下:

    float startX = getPaddingLeft() + mPaddingLabLine;    float startY = getHeight() - getPaddingBottom() - mPaddingLabLine;

x轴终点坐标很好理解,y方向不变与起始点相等,x方向宽度减去内边距即可:

    float stopX = getWidth() - getPaddingRight();    float stopY = startY;

y轴终点坐标与x轴的类似,只是此时x点坐标没变,y点变成了内边距,起始点计算出来了后,线条的绘制就很简单了:

   canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint); //x轴   canvas.drawLine(startX, startY, startX, getPaddingTop(), mTabLinePaint); //y轴

3.2 标尺绘制

标尺这部分也是绘制直线,在图表上绘制mLabelCount条直线作为标尺线.

    float height = startY;    float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount;    for (int i = 0; i < mLabelCount; i++)    {        canvas.save();        canvas.translate(0, -ceilHeight * i);        canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint);        canvas.restore();    }

我们计算根据标尺数目计算出每根标尺线之间的距离ceilHeight,每次绘制标尺线时,只需要在Y方向平移画布就行。可以理解为绘制一与x轴重叠的直线,然后往上平移,这样逻辑上更简洁。

3.3 刻度值绘制

刻度值包括俩个,一个是坐标轴名称,比如Y轴的销售额,x轴的年份等,另一个是每一个刻度的具体值。

坐标轴名称绘制

x轴相对简单一点,直接调用cavans的drawText方法即可。这里主要分析下y轴的画法,因为y轴名称文字是竖着显示,所以绘画的时候需要把画布逆时针旋转90度,画布旋转注意中心点的位置为文字绘制的位置,但是需要稍微右移一点,防止文字左偏导致部分内容不可见:

    canvas.save();    canvas.rotate(-90, getPaddingLeft(), mHeight / 3 - textHeight);    canvas.drawText(axisTitle, getPaddingLeft(), mHeight / 3, mTabLinePaint);    canvas.restore();
坐标轴刻度值绘制

绘制刻度值的关键代码:

canvas.drawText(axisName[i], pointFs[i].x, pointFs[i].y, mTabLinePaint);

其中axisName表示需要绘制的坐标刻度值,pointFs表示每个刻度值文字绘制时的坐标点数组,俩个坐标轴的计算方法不同,先看x轴:

    int textHeight = getTextHeight();    int cellWidth = (mWidth - mPaddingLabLine - mColumnMarginRight) / this.mColumnName.length;    final int y = mHeight - mPaddingLabLine + textHeight;    for (int i = 0; i < pointFs.length; i++)    {        float textWidth = mTabLinePaint.measureText(axisName[i]);        PointF pointF = new PointF();        pointF.x = mPaddingLabLine + mColumnMarginRight + i * cellWidth + textWidth / 2;        pointF.y = y;        pointFs[i] = pointF;    }

根据x轴刻度值数目把x轴平分为宽度为cellWidth的若干等分,mColumnMarginRight的含义为,第一根柱子从原点向像右偏移的距离,防止第一根柱子紧挨Y轴。为了保证每个刻度值都在柱子的正中间,每次计算该刻度的x位置时,加上该刻度值宽度的一半。所有X轴刻度值得y坐标都是一样的,x轴加上文字高度即可textHeight

接下来是Y轴刻度值绘制:

        float height = getHeight() - getPaddingBottom() - mPaddingLabLine;        float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount;        for (int i = 0; i < pointFs.length; i++)        {            PointF pointF = new PointF();            float textWidth = mTabLinePaint.measureText(axisName[i]);            pointF.x = mPaddingLabLine - textWidth - mAxisTextDistance / 3;            pointF.y = height - i * ceilHeight;            pointFs[i] = pointF;        }    }

Y轴刻度值得计算与X轴类似,有个小细节注意一下就好了,可能刻度值文字个数不一样,按drawText方法又是从左开始画的(刻度值左对齐),这样的显示效果非常丑,要让刻度值右对齐的话就必须测量文字的宽度,然后Y轴减去这个宽度就可以,这样就紧靠Y轴,所以再减去mAxisTextDistance / 3就可以实现Y轴刻度值右对齐并且左偏移Y轴一定距离。

getTextHeight()测量字体的绘制高度:

//获取字符串的高度private int getTextHeight(){    Paint.FontMetricsInt fontMetrics = mTabLinePaint.getFontMetricsInt();    return fontMetrics.bottom - fontMetrics.top;}

3.4 绘制柱型

先上代码

    //每一条数据占有的宽度    int cellWidth = (int) (mWidth - mPaddingLabLine - mColumnMarginRight) / mColumnData.length;    cellWidth = cellWidth > mMaxSubcolumnWidth ? mMaxSubcolumnWidth : cellWidth; //限制柱状图的最大宽度,防止列数过少时柱形图过宽    //矩形区有效绘制高度    float height = getHeight() - getPaddingBottom() - mPaddingLabLine;    float proportion = height / (maxValue - minValue); //高度数值比例尺    mTabLinePaint.setStyle(Paint.Style.FILL);    for (int j = 0; j < mColumnData.length; j++)    {        int startPos = mPaddingLabLine + mColumnMarginRight + cellWidth * j;        float top =height - proportion * (mColumnData[j] - minValue);        RectF rectF = new RectF(startPos + cellWidth / 5, top, startPos + cellWidth * 4 / 5, height);        canvas.drawRect(rectF, mTabLinePaint);    }

4、完整代码

public class ColumnView extends View{    private int mWidth;    private int mHeight;    private final static int X_LAB = 0; //表示x轴绘制    private final static int Y_LAB = 1; //y轴的绘制    private Paint mTabLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);    private int color_label_line = Color.parseColor("#DDDDDD"); //线条颜色    private int mLineStroke = 1;//线条宽度    private int mLabelCount = 10; //标尺线条数目    /**     * 图标所能显示的数值范围,     * 最大值比能显示的极值大20%,防止如果出现较大数值时,矩形过高而过于贴近顶部     */    private float maxValue = 100 * 1.2f;    private float minValue = 20;    private float[] mColumnData;    //模拟数据    private String mAxisXTitle = "销售项"; //x轴含义    private String mAxisYTitle = "销售额"; //y轴含义    private String[] mColumnName; //每个矩形数据项名称,即x轴刻度值    private String[] mAxisYName;  //y轴刻度值    private int mAxisTitleSize = DensityUtils.dp2px(getContext(), 12); //坐标轴标题大小    private int mAxisNameTextSize = DensityUtils.dp2px(getContext(), 10); //坐标轴刻度值文字大小    private int mAxisTextDistance = DensityUtils.dp2px(getContext(), 6); // 坐标轴标题文字与刻度值之间的距离    private int mColumnMarginRight = DensityUtils.dp2px(getContext(), 3); // 矩形离Y轴的距离    private int mMaxSubcolumnWidth = DensityUtils.dp2px(getContext(), 40);    //坐标轴到view边界的留出的空间,用于绘制文字    private int mPaddingLabLine = mAxisTitleSize + mAxisNameTextSize + 2 * mAxisTextDistance;    public ColumnView(Context context)    {        this(context, null);    }    public ColumnView(Context context, @Nullable AttributeSet attrs)    {        this(context, attrs, 0);    }    public ColumnView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)    {        super(context, attrs, defStyleAttr);        init();    }    private void init()    {        mTabLinePaint.setColor(color_label_line);        mTabLinePaint.setStrokeWidth(mLineStroke);        initData();    }    private void initData()    {        //生成数据        mColumnData = new float[8];        for (int i = 0; i < mColumnData.length; i++)        {            mColumnData[i] = new Random().nextInt(80) + 20;        }        //Y轴刻度        mColumnName = new String[8];        for (int i = 0; i < mColumnData.length; i++)        {            mColumnName[i] = "数据" + String.valueOf(i + 1);        }        //y轴刻度        mAxisYName = new String[mLabelCount];        int v = (int) ((maxValue - minValue) / mLabelCount * 100);        float value = v / 100f; //转成精确小数点后俩位的小数        for (int i = 0; i < mAxisYName.length; i++)        {            String axisName = value * i + minValue + "";            mAxisYName[i] = axisName;        }    }    @Override    public void layout(int l, int t, int r, int b)    {        super.layout(l, t, r, b);        int width = getWidth();        int height = getHeight();        //兼容性处理,获取view的可绘制区域的宽高        mWidth = width - getPaddingLeft() - getPaddingRight();        mHeight = height - getPaddingBottom() - getPaddingTop();    }    @Override    protected void onDraw(Canvas canvas)    {        super.onDraw(canvas);        canvas.drawColor(Color.WHITE);        drawLabLine(canvas);        drawAxis(canvas);        drawSubcolumn(canvas);    }    //画柱状矩形    private void drawSubcolumn(Canvas canvas)    {        mTabLinePaint.setColor(Color.parseColor("#4476AB"));        //每一条数据占有的宽度        int cellWidth = (int) (mWidth - mPaddingLabLine - mColumnMarginRight) / mColumnData.length;        cellWidth = cellWidth > mMaxSubcolumnWidth ? mMaxSubcolumnWidth : cellWidth; //限制柱状图的最大宽度        //矩形区有效绘制高度        float height = getHeight() - getPaddingBottom() - mPaddingLabLine;        float proportion = height / (maxValue - minValue); //高度数值比例尺        mTabLinePaint.setStyle(Paint.Style.FILL);        for (int j = 0; j < mColumnData.length; j++)        {            int startPos = mPaddingLabLine + mColumnMarginRight + cellWidth * j;            float top =height - proportion * (mColumnData[j] - minValue);            RectF rectF = new RectF(startPos + cellWidth / 5, top, startPos + cellWidth * 4 / 5, height);            canvas.drawRect(rectF, mTabLinePaint);        }    }    //画刻度值    private void drawAxis(Canvas canvas)    {        drawAxisTitle(canvas, mAxisYTitle, Y_LAB);        drawAxisTitle(canvas, mAxisXTitle, X_LAB);        drawAxisName(canvas, Y_LAB, mAxisYName);        drawAxisName(canvas, X_LAB, mColumnName);    }    // 画刻度值    private void drawAxisName(Canvas canvas, int labelType, String[] axisName)    {        mTabLinePaint.setTextSize(mAxisNameTextSize);        //计算绘制坐标        PointF[] pointFs = calculateAxisNamePosition(labelType, axisName);        for (int i = 0; i < axisName.length; i++)        {            canvas.drawText(axisName[i], pointFs[i].x, pointFs[i].y, mTabLinePaint);        }    }    @NonNull    private PointF[] calculateAxisNamePosition(int labelType, String[] axisName)    {        PointF[] pointFs = new PointF[axisName.length];        if (labelType == X_LAB)  //x轴        {            int textHeight = getTextHeight();            int cellWidth = (mWidth - mPaddingLabLine - mColumnMarginRight) / this.mColumnName.length;            final int y = mHeight - mPaddingLabLine + textHeight;            for (int i = 0; i < pointFs.length; i++)            {                float textWidth = mTabLinePaint.measureText(axisName[i]);                PointF pointF = new PointF();                pointF.x = mPaddingLabLine + mColumnMarginRight + i * cellWidth + textWidth / 2;                pointF.y = y;                pointFs[i] = pointF;            }        } else        {            float height = getHeight() - getPaddingBottom() - mPaddingLabLine;            float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount;            for (int i = 0; i < pointFs.length; i++)            {                PointF pointF = new PointF();                float textWidth = mTabLinePaint.measureText(axisName[i]);                pointF.x = mPaddingLabLine - textWidth - mAxisTextDistance / 3;                pointF.y = height - i * ceilHeight;                pointFs[i] = pointF;            }        }        return pointFs;    }    //绘制坐标轴名称    private void drawAxisTitle(Canvas canvas, String axisTitle, int labelType)    {        mTabLinePaint.setTextSize(mAxisTitleSize);        int textHeight = getTextHeight();        if (labelType == X_LAB)        {            //x轴            canvas.drawText(mAxisXTitle, mWidth / 2, mHeight - textHeight + mAxisTextDistance, mTabLinePaint);        } else        {            //y轴            canvas.save();            //注意中心点的位置,需要稍微右移一点,防止文字太过于左偏            canvas.rotate(-90, getPaddingLeft(), mHeight / 3 - textHeight);            canvas.drawText(axisTitle, getPaddingLeft(), mHeight / 3, mTabLinePaint);            canvas.restore();        }    }    //获取字符串的高度    private int getTextHeight()    {        Paint.FontMetricsInt fontMetrics = mTabLinePaint.getFontMetricsInt();        return fontMetrics.bottom - fontMetrics.top;    }    //画坐标轴    private void drawLabLine(Canvas canvas)    {        mTabLinePaint.setColor(color_label_line);        //x轴起始点(也是Y轴的起点),终点        float startX = getPaddingLeft() + mPaddingLabLine;        float startY = getHeight() - getPaddingBottom() - mPaddingLabLine;        float stopX = getWidth() - getPaddingRight();        float stopY = startY;        canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint); //x轴        canvas.drawLine(startX, startY, startX, getPaddingTop(), mTabLinePaint); //y轴        //画标尺        float height = startY;        float ceilHeight = (mHeight - mAxisTitleSize) / mLabelCount;        for (int i = 0; i < mLabelCount; i++)        {            canvas.save();            canvas.translate(0, -ceilHeight * i);            canvas.drawLine(startX, startY, stopX, stopY, mTabLinePaint);            canvas.restore();        }    }}

5、总结

在xml文件中的使用与普通view用法相同:

    <widget.qike.com.columnview.widget.ColumnView        android:id="@+id/cv_test"        android:layout_width="match_parent"        android:layout_height="match_parent"/>

demo下载地址

1 0
原创粉丝点击