美文网首页
自定义柱状图效果实现

自定义柱状图效果实现

作者: 刘孙猫咪 | 来源:发表于2019-04-14 21:31 被阅读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);
        }
    }
    

    源码

    相关文章

      网友评论

          本文标题:自定义柱状图效果实现

          本文链接:https://www.haomeiwen.com/subject/wmbswqtx.html