自定义柱状图效果实现

2019-04-14  本文已影响0人  刘孙猫咪

项目开发中时不时会碰到柱状图、折线图、饼状图等效果,这些效果肯定是需要自定义控件通过绘制或者摆放来实现,当然了,也有一些很不错的第三方库,比如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);
    }
}

源码

上一篇下一篇

猜你喜欢

热点阅读