自定义柱状图效果实现

项目开发中时不时会碰到柱状图、折线图、饼状图等效果,这些效果肯定是需要自定义控件通过绘制或者摆放来实现,当然了,也有一些很不错的第三方库,比如MPAndroidhellocharts等,里面就实现了柱状图、折线图、饼状图等各种效果,甚至还有k线图效果;这里自定义柱状图的目的是为了熟悉Android的自定义view、Canvas绘制等知识,提升自己的Android开发水平等;先来看下大致实现的一个效果:

微信截图_20190414185816.png

通过看效果,大致需要绘制实现下面这些东西:

1、标题的绘制
2、横轴、纵轴的绘制
3、横轴/纵轴刻度 箭头 文字的绘制,纵轴还有测度的绘制
4、柱状图的绘制

而对于自定义view来说,首先继承自view,初始化参数和自定义属性,测量,绘制...大致就是一个这样的流程;老规矩还是先看初始这一步;

public class HistogramView extends View {
    //图表标题
    private String graphTitle = "";
    //标题字体的大小
    private int graphTitleSize = 18;
    //标题的字体颜色
    private int graphTitleColor = Color.RED;
    //x轴名称
    private String xAxisName = "";
    //y轴名称
    private String yAxisName = "";
    //坐标轴字体颜色
    private int axisTextSize = 12;
    //坐标轴字体颜色
    private int axisTextColor = Color.BLACK;
    //x y坐标线条的颜色
    private int axisLineColor = Color.BLACK;
    //x,y坐标线的宽度
    private int axisLineWidth = 2;
    private Paint mPaint;
    private int screenWith, screenHeight;
    //视图的宽度
    private int width;
    //视图的高度
    private int height;
    //起点x坐标值
    private int originalX;
    //起点y坐标值
    private int originalY;
    //y轴等份划分
    private int axisDivideSizeY;

    //标题距离x轴的距离
    private int titleMarginXaxis = 60;
    //x y轴刻度的高度
    private int xAxisScaleHeight = 5;
    //刻度的最大值
    private Integer maxValue;
    //y轴空留部分高度
    private int yMarign = 30;

    //柱状图数据
    private List<Integer> columnList;
    //柱状图颜色
    private List<Integer> columnColors;

    public HistogramView(Context context) {
        this(context, null);
    }

    public HistogramView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HistogramView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取屏幕的宽高
        WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics metrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(metrics);
        screenWith = metrics.widthPixels;
        screenHeight = metrics.heightPixels;
        initAttrs(context, attrs);
        initPaint();
    }

    /**
     * //获取自定义属性
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.HistogramView);
        graphTitle = array.getString(R.styleable.HistogramView_graphTitle);
        xAxisName = array.getString(R.styleable.HistogramView_xAxisName);
        yAxisName = array.getString(R.styleable.HistogramView_yAxisName);
        axisTextSize = array.getDimensionPixelSize(R.styleable.HistogramView_axisTextSize, sp2px(axisTextSize));
        axisTextColor = array.getColor(R.styleable.HistogramView_axisTextColor, axisTextColor);
        axisLineColor = array.getColor(R.styleable.HistogramView_axisLineColor, axisLineColor);
        graphTitleSize = array.getDimensionPixelSize(R.styleable.HistogramView_graphTitleSize, sp2px(graphTitleSize));
        graphTitleColor = array.getColor(R.styleable.HistogramView_graphTitleColor, graphTitleColor);
        axisLineWidth = (int) array.getDimension(R.styleable.HistogramView_axisLineWidth, dip2px(axisLineWidth));
        array.recycle();
    }

    /**
     * 初始化paint
     */
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);

    }
    private int sp2px(int sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
    }

    private int dip2px(int dip) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
    }
}

就是一些常量、成员变量的定义和赋值,初始化自定义属性和画笔,接下来还是测量,那就看看onMeasure方法;

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int w = MeasureSpec.getSize(widthMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST) {
            w = screenWith;
        }
        int h = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {
            h = screenHeight;
        }
        setMeasuredDimension(w, h);
        if (width == 0 || height == 0) {
            //x轴的起点位置
            originalX = dip2px(30);
            //视图的宽度  空间的宽度减去左边和右边的位置
            width = getMeasuredWidth() - originalX * 2;
            //y轴的起点位置 空间高度的2/3
            originalY = getMeasuredHeight() * 2 / 3;
            //图表显示的高度为空间高度的一半
            height = getMeasuredHeight() / 2;
        }
    }

onMeasure方法有时候会多次调用,所以当试图的width和height赋值后,就没有必要再去计算一些值了,对于Android屏幕来说,它的原点x、y和生活的坐标轴x,y有点一样,左上角顶点是它的原点x,y;x轴往右边走还是一样的增大,y轴往下走就不一样了,往上走是增大,往上走是减小的;


微信截图_20190414203936.png

A点是屏幕的原点,B点是自定义柱状图的原始点,就需要对B点原始点进行定义,再根据B点原始点来计算柱状图显示的宽度和高度;originalX也就是B的x点,往右移动了30,originalY也就是B的y为屏幕高度的2/3,这个可以根据自己的需要进行设定,原点知道了,就可以计算试图宽度width了,就是getMeasuredWidth() - originalX * 2就可以了,测量ok了,剩下就只有绘制了,先易后难,先绘制柱状图的标题;

/**
     * 绘制标题
     *
     * @param canvas
     */
    private void drawTitle(Canvas canvas) {
        if (!TextUtils.isEmpty(graphTitle)) {
            //绘制标题
            mPaint.setTextSize(graphTitleSize);
            mPaint.setColor(graphTitleColor);
            //设置文字粗体
            mPaint.setFakeBoldText(true);
            //获取文字的宽度
            float measureText = mPaint.measureText(graphTitle);
            canvas.drawText(
                    graphTitle,
                    getWidth() / 2 - measureText / 2,
                    originalY + dip2px(titleMarginXaxis),
                    mPaint
            );
        }
    }

标题有才会进行绘制,一开始也就是paint的设置,绘制文字调用drawText就可以进行绘制了,不过要先确定文字的x,y的起始位置;


微信截图_20190414205042.png

y的话就在originalY的基础上往下移动一定距离就可以,看效果,标题是屏幕居中显示,那就用屏幕宽度/2-文字宽度/2就可以得到x的位置了;

x轴和y轴的绘制放一起进行绘制,x轴变动的x的终点,y轴变动的也只是y轴的终点;

/**
     * 绘制x轴
     *
     * @param canvas
     */
    protected void drawXAxis(Canvas canvas) {
        mPaint.setColor(axisLineColor);
        mPaint.setStrokeWidth(axisLineWidth);
        canvas.drawLine(originalX, originalY, originalX + width, originalY, mPaint);
    }
/**
     * 绘制y轴
     *
     * @param canvas
     */
    protected void drawYAxis(Canvas canvas) {
        mPaint.setColor(axisLineColor);
        mPaint.setStrokeWidth(axisLineWidth);
        canvas.drawLine(originalX, originalY, originalX, originalY - height, mPaint);
    }

接下来是x刻度值,y轴刻度和刻度值的绘制;

/**
     * 绘制x轴刻度值
     *
     * @param canvas
     */
    protected void drawXAxisScaleValue(Canvas canvas) {
        int xTxtMargin = dip2px(15);
        mPaint.setColor(axisTextColor);
        mPaint.setTextSize(axisTextSize);
        mPaint.setFakeBoldText(true);
        float cellWidth = width / (columnList.size() + 2);
        for (int i = 0; i < columnList.size() + 1; i++) {
            if (i == 0) {
                continue;
            }
            String txt = i + "";
            //测量文字的宽度
            float txtWidth = mPaint.measureText(txt);
            canvas.drawText(txt, cellWidth * i + originalX + (cellWidth / 2 - txtWidth / 2),
                    originalY + xTxtMargin,
                    mPaint);
        }
    }

首先要计算每一份显示的宽度,第一和最后一个位置要多空置各一个宽度,就要在柱状图数据集合size上+2;就是width / (columnList.size() + 2),然后调用drawText进行绘制;

/**
     * 绘制y轴刻度
     *
     * @param canvas
     */
    protected void drawYAxisScale(Canvas canvas) {
        mPaint.setColor(axisLineColor);
        float cellHeight = (height - dip2px(yMarign)) / axisDivideSizeY;
        for (int i = 0; i < axisDivideSizeY; i++) {
            canvas.drawLine(originalX,
                    originalY - cellHeight * (i + 1),
                    originalX + 10,
                    originalY - cellHeight * (i + 1),
                    mPaint);
        }
    }

y轴刻度的高度是根据调用是传入的axisDivideSizeY来计算的,要看y上面显示多少分,计算出每份的高度cellHeight后,调用drawLine进行绘制;

/**
     * 绘制y轴刻度值
     *
     * @param canvas
     */
    protected void drawYAxisScaleValue(Canvas canvas) {
        try {
            mPaint.setColor(axisTextColor);
            mPaint.setTextSize(axisTextSize);
            int cellHeight = (height - dip2px(yMarign)) / axisDivideSizeY;
            float cellValue = maxValue / (axisDivideSizeY + 0f);
            //这里只处理的大于1时的绘制  小于等于1的绘制没有处理
            int ceil = (int) Math.ceil(cellValue);
//            DecimalFormat df2 = new DecimalFormat("###.00");
//            String format = df2.format(ceil);
//            float result = Float.parseFloat(format);
            for (int i = 0; i < axisDivideSizeY + 1; i++) {
                if (i == 0) {
                    continue;
                }
                String s = ceil * i + "";
                float v = mPaint.measureText(s);
                canvas.drawText(s,
                        originalX - v - 10,
                        originalY - cellHeight * i + 10,
                        mPaint);
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
    }

每份的高度和刻度一样也是通过axisDivideSizeY来计算出cellHeight,每份显示的value也就是刻度值,通过柱状图数据集合中的最大值/axisDivideSizeY y轴显示的份数,最大值的话是用过调用setColumnInfo方法设置参数时获取的;

/**
     * 调用该方法进行图表的设置
     * @param columnList 柱状图的数据
     * @param columnColors  颜色
     * @param axisDivideSizeY y轴显示的等份数
     */
    public void setColumnInfo(List<Integer> columnList, List<Integer> columnColors, int axisDivideSizeY) {
        this.columnList = columnList;
        this.columnColors = columnColors;
        this.axisDivideSizeY = axisDivideSizeY;
        //获取刻度的最大值
        maxValue = Collections.max(columnList);
        Log.e("TAG", "maxValue-->" + maxValue);
        invalidate();
    }

计算出每份的刻度值,遍历循环就可以计算出对应的刻度值,调用drawText就可以进行绘制了;x、y轴,标题,x、y轴的刻度和刻度值都绘制好了,就剩下x、y的箭头,柱状图了;

/**
     * 绘制x轴箭头
     *
     * @param canvas
     */
    private void drawXAxisArrow(Canvas canvas) {
        mPaint.setColor(axisTextColor);
        Path xPath = new Path();
        xPath.moveTo(originalX + width + 30, originalY);
        xPath.lineTo(originalX + width, originalY + 10);
        xPath.lineTo(originalX + width, originalY - 10);
        xPath.close();
        canvas.drawPath(xPath, mPaint);
        //绘制x轴名称
        if (!TextUtils.isEmpty(xAxisName)) {
            canvas.drawText(xAxisName, originalX + width, originalY + 50, mPaint);
        }
    }
/**
     * 绘制y轴箭头
     *
     * @param canvas
     */
    private void drawYAxisArrow(Canvas canvas) {
        mPaint.setColor(axisTextColor);
        Path yPath = new Path();
        yPath.moveTo(originalX, originalY - height - 30);
        yPath.lineTo(originalX - 10, originalY - height);
        yPath.lineTo(originalX + 10, originalY - height);
        yPath.close();
        canvas.drawPath(yPath, mPaint);
        //绘制y轴名称
        if (!TextUtils.isEmpty(yAxisName)) {
            canvas.drawText(yAxisName, originalX - 50, originalY - height - 35, mPaint);
        }
    }

x、y轴的箭头、文字绘制差不多,不过要绘制三角形箭头,canvas并没有提供绘制三角形的api,需要利用path路径来绘制,最后看看柱状图的绘制;

/**
     * 绘制柱状图
     *
     * @param canvas
     */
    protected void drawColumn(Canvas canvas) {
        if (columnList != null && columnColors != null) {
            float cellWidth = width / (columnList.size() + 2);
            //根据最大值和高度计算比例
            float scale = (height - dip2px(yMarign)) / maxValue;
            for (int i = 0; i < columnList.size(); i++) {
                mPaint.setColor(columnColors.get(i));
                float leftTopY = originalY - columnList.get(i) * scale;
                canvas.drawRect(originalX + cellWidth * (i + 1),
                        leftTopY,
                        originalX + cellWidth * (i + 2),
                        originalY - axisLineWidth / 2,
                        mPaint);
            }
        }
    }

x轴每份的宽度和x轴刻度值的计算一样的,根据柱状图显示的高度/maxValue,计算出每份的高度,调用drawRect绘制矩形,绘制时需要注意矩形矩形的起始x、y点,终点x、y点,x轴的话,其实上一个的终点就是下一个的x起始点,因为第一个是空置的,所以x的起始点就是originalX + cellWidth * (i + 1) x原点+对应index位置的每份宽度;y轴的话,终点是一致的,都是原点-x轴宽度/2(originalY - axisLineWidth / 2),起始点就是y轴原点-index对应的value*scale;这样就确定了每个矩形的起始x、y点,终点x、y点绘制出来就ok了;使用的话通过setColumnInfo传入对应的参数就可以了。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.lsm.histogramview.HistogramView
        android:id="@+id/histogram_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:graphTitle="柱状图效果"
        app:xAxisName="天"
        app:yAxisName="营业额"/>

</RelativeLayout>
public class MainActivity extends AppCompatActivity {
    private HistogramView histogramView;
    private List<Integer> values;
    private List<Integer> colors;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        histogramView = findViewById(R.id.histogram_view);
        values = new ArrayList<>();
        colors = new ArrayList<>();
        values.add(16);
        values.add(25);
        values.add(44);
        values.add(11);
        values.add(22);
        values.add(17);
        values.add(35);

        colors.add(Color.BLUE);
        colors.add(Color.BLACK);
        colors.add(Color.GREEN);
        colors.add(Color.GRAY);
        colors.add(Color.RED);
        colors.add(Color.YELLOW);
        colors.add(Color.LTGRAY);

        histogramView.setColumnInfo(values, colors, 7);
    }
}

源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,270评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,489评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,630评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,906评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,928评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,718评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,442评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,345评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,802评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,984评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,117评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,810评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,462评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,011评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,139评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,377评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,060评论 2 355

推荐阅读更多精彩内容