自定義柱狀圖效果實(shí)現(xiàn)

項(xiàng)目開發(fā)中時(shí)不時(shí)會碰到柱狀圖、折線圖、餅狀圖等效果,這些效果肯定是需要自定義控件通過繪制或者擺放來實(shí)現(xiàn),當(dāng)然了,也有一些很不錯(cuò)的第三方庫,比如MPAndroidhellocharts等,里面就實(shí)現(xiàn)了柱狀圖、折線圖、餅狀圖等各種效果,甚至還有k線圖效果;這里自定義柱狀圖的目的是為了熟悉Android的自定義view、Canvas繪制等知識,提升自己的Android開發(fā)水平等;先來看下大致實(shí)現(xiàn)的一個(gè)效果:

微信截圖_20190414185816.png

通過看效果,大致需要繪制實(shí)現(xiàn)下面這些東西:

1、標(biāo)題的繪制
2、橫軸、縱軸的繪制
3、橫軸/縱軸刻度 箭頭 文字的繪制,縱軸還有測度的繪制
4、柱狀圖的繪制

而對于自定義view來說,首先繼承自view,初始化參數(shù)和自定義屬性,測量,繪制...大致就是一個(gè)這樣的流程;老規(guī)矩還是先看初始這一步;

public class HistogramView extends View {
    //圖表標(biāo)題
    private String graphTitle = "";
    //標(biāo)題字體的大小
    private int graphTitleSize = 18;
    //標(biāo)題的字體顏色
    private int graphTitleColor = Color.RED;
    //x軸名稱
    private String xAxisName = "";
    //y軸名稱
    private String yAxisName = "";
    //坐標(biāo)軸字體顏色
    private int axisTextSize = 12;
    //坐標(biāo)軸字體顏色
    private int axisTextColor = Color.BLACK;
    //x y坐標(biāo)線條的顏色
    private int axisLineColor = Color.BLACK;
    //x,y坐標(biāo)線的寬度
    private int axisLineWidth = 2;
    private Paint mPaint;
    private int screenWith, screenHeight;
    //視圖的寬度
    private int width;
    //視圖的高度
    private int height;
    //起點(diǎn)x坐標(biāo)值
    private int originalX;
    //起點(diǎn)y坐標(biāo)值
    private int originalY;
    //y軸等份劃分
    private int axisDivideSizeY;

    //標(biāo)題距離x軸的距離
    private int titleMarginXaxis = 60;
    //x y軸刻度的高度
    private int xAxisScaleHeight = 5;
    //刻度的最大值
    private Integer maxValue;
    //y軸空留部分高度
    private int yMarign = 30;

    //柱狀圖數(shù)據(jù)
    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軸的起點(diǎn)位置
            originalX = dip2px(30);
            //視圖的寬度  空間的寬度減去左邊和右邊的位置
            width = getMeasuredWidth() - originalX * 2;
            //y軸的起點(diǎn)位置 空間高度的2/3
            originalY = getMeasuredHeight() * 2 / 3;
            //圖表顯示的高度為空間高度的一半
            height = getMeasuredHeight() / 2;
        }
    }

onMeasure方法有時(shí)候會多次調(diào)用,所以當(dāng)試圖的width和height賦值后,就沒有必要再去計(jì)算一些值了,對于Android屏幕來說,它的原點(diǎn)x、y和生活的坐標(biāo)軸x,y有點(diǎn)一樣,左上角頂點(diǎn)是它的原點(diǎn)x,y;x軸往右邊走還是一樣的增大,y軸往下走就不一樣了,往上走是增大,往上走是減小的;


微信截圖_20190414203936.png

A點(diǎn)是屏幕的原點(diǎn),B點(diǎn)是自定義柱狀圖的原始點(diǎn),就需要對B點(diǎn)原始點(diǎn)進(jìn)行定義,再根據(jù)B點(diǎn)原始點(diǎn)來計(jì)算柱狀圖顯示的寬度和高度;originalX也就是B的x點(diǎn),往右移動了30,originalY也就是B的y為屏幕高度的2/3,這個(gè)可以根據(jù)自己的需要進(jìn)行設(shè)定,原點(diǎn)知道了,就可以計(jì)算試圖寬度width了,就是getMeasuredWidth() - originalX * 2就可以了,測量ok了,剩下就只有繪制了,先易后難,先繪制柱狀圖的標(biāo)題;

/**
     * 繪制標(biāo)題
     *
     * @param canvas
     */
    private void drawTitle(Canvas canvas) {
        if (!TextUtils.isEmpty(graphTitle)) {
            //繪制標(biāo)題
            mPaint.setTextSize(graphTitleSize);
            mPaint.setColor(graphTitleColor);
            //設(shè)置文字粗體
            mPaint.setFakeBoldText(true);
            //獲取文字的寬度
            float measureText = mPaint.measureText(graphTitle);
            canvas.drawText(
                    graphTitle,
                    getWidth() / 2 - measureText / 2,
                    originalY + dip2px(titleMarginXaxis),
                    mPaint
            );
        }
    }

標(biāo)題有才會進(jìn)行繪制,一開始也就是paint的設(shè)置,繪制文字調(diào)用drawText就可以進(jìn)行繪制了,不過要先確定文字的x,y的起始位置;


微信截圖_20190414205042.png

y的話就在originalY的基礎(chǔ)上往下移動一定距離就可以,看效果,標(biāo)題是屏幕居中顯示,那就用屏幕寬度/2-文字寬度/2就可以得到x的位置了;

x軸和y軸的繪制放一起進(jìn)行繪制,x軸變動的x的終點(diǎn),y軸變動的也只是y軸的終點(diǎn);

/**
     * 繪制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);
        }
    }

首先要計(jì)算每一份顯示的寬度,第一和最后一個(gè)位置要多空置各一個(gè)寬度,就要在柱狀圖數(shù)據(jù)集合size上+2;就是width / (columnList.size() + 2),然后調(diào)用drawText進(jìn)行繪制;

/**
     * 繪制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軸刻度的高度是根據(jù)調(diào)用是傳入的axisDivideSizeY來計(jì)算的,要看y上面顯示多少分,計(jì)算出每份的高度cellHeight后,調(diào)用drawLine進(jìn)行繪制;

/**
     * 繪制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時(shí)的繪制  小于等于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來計(jì)算出cellHeight,每份顯示的value也就是刻度值,通過柱狀圖數(shù)據(jù)集合中的最大值/axisDivideSizeY y軸顯示的份數(shù),最大值的話是用過調(diào)用setColumnInfo方法設(shè)置參數(shù)時(shí)獲取的;

/**
     * 調(diào)用該方法進(jìn)行圖表的設(shè)置
     * @param columnList 柱狀圖的數(shù)據(jù)
     * @param columnColors  顏色
     * @param axisDivideSizeY y軸顯示的等份數(shù)
     */
    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();
    }

計(jì)算出每份的刻度值,遍歷循環(huán)就可以計(jì)算出對應(yīng)的刻度值,調(diào)用drawText就可以進(jìn)行繪制了;x、y軸,標(biāo)題,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);
            //根據(jù)最大值和高度計(jì)算比例
            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軸刻度值的計(jì)算一樣的,根據(jù)柱狀圖顯示的高度/maxValue,計(jì)算出每份的高度,調(diào)用drawRect繪制矩形,繪制時(shí)需要注意矩形矩形的起始x、y點(diǎn),終點(diǎn)x、y點(diǎn),x軸的話,其實(shí)上一個(gè)的終點(diǎn)就是下一個(gè)的x起始點(diǎn),因?yàn)榈谝粋€(gè)是空置的,所以x的起始點(diǎn)就是originalX + cellWidth * (i + 1) x原點(diǎn)+對應(yīng)index位置的每份寬度;y軸的話,終點(diǎn)是一致的,都是原點(diǎn)-x軸寬度/2(originalY - axisLineWidth / 2),起始點(diǎn)就是y軸原點(diǎn)-index對應(yīng)的value*scale;這樣就確定了每個(gè)矩形的起始x、y點(diǎn),終點(diǎn)x、y點(diǎn)繪制出來就ok了;使用的話通過setColumnInfo傳入對應(yīng)的參數(shù)就可以了。

<?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="營業(yè)額"/>

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

源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容