自己動(dòng)手繪制一個(gè)折線圖控件ChartView

折線圖在很多App中都有用到,GitHub上面有一些功能全面的折線圖框架,比如hellecharts-android,achartengine。但是很多時(shí)候設(shè)計(jì)師給定的樣式,通過這些框架不一定能完全達(dá)到效果。所以只有琢磨著通過自定義View來自己繪制一個(gè)折線圖:

最終效果圖

根據(jù)展示效果,坐標(biāo)軸、刻度、刻度值、數(shù)據(jù)點(diǎn)線、標(biāo)題全部都通過自繪來實(shí)現(xiàn)。

初始化繪制相關(guān)參數(shù)

在構(gòu)造函數(shù)中初始化畫筆及數(shù)據(jù)刻度值等參數(shù):

private void init() {
    this.setBackgroundColor(Color.WHITE);
    // x軸刻度值
    if (xLabel == null) {
        xLabel = new String[]{"12-11", "12-12", "12-13", "12-14", "12-15", "12-16", "12-17"};
    }
    // 數(shù)據(jù)點(diǎn)
    if (data == null) {
        data = new String[]{"2.98", "2.99", "2.99", "2.98", "2.92", "2.94", "2.95"};
    }
    // 標(biāo)題
    if (title == null) {
        title = "七日年化收益率(%)";
    }

    // 根據(jù)設(shè)置的數(shù)據(jù)值生成Y坐標(biāo)刻度值
    yLabel = createYLabel();
    // 數(shù)據(jù)點(diǎn)及其連線的顏色集合
    mDataLineColors = new String[]{"#fbbc14", "#fbaa0c", "#fbaa0c", "#fb8505", "#ff6b02", "#ff5400", "#ff5400"};
    // 新建畫筆
    mDataLinePaint = new Paint();       // 數(shù)據(jù)(點(diǎn)和連線)畫筆
    mScaleLinePaint = new Paint();      // 坐標(biāo)(刻度線條)值畫筆
    mScaleValuePaint = new Paint();      // 圖表(刻度值)畫筆
    mBackColorPaint = new Paint();       // 背景(色塊)畫筆
    // 畫筆抗鋸齒
    mDataLinePaint.setAntiAlias(true);
    mScaleLinePaint.setAntiAlias(true);
    mScaleValuePaint.setAntiAlias(true);
    mBackColorPaint.setAntiAlias(true);
}

x軸的刻度值、數(shù)據(jù)點(diǎn)、y中刻度值設(shè)置初始值根據(jù)初始值先進(jìn)行繪制。后續(xù)重新設(shè)置數(shù)據(jù)后再重繪。
在根據(jù)給定數(shù)據(jù)點(diǎn)生成y的坐標(biāo)刻度值時(shí),需要考慮兩點(diǎn):
1.數(shù)據(jù)點(diǎn)及其連線需要繪制在坐標(biāo)區(qū)域的中間位置,并且數(shù)據(jù)點(diǎn)的臨界值(最大或最小值)不能超過y坐標(biāo)刻度的臨界值;
2.y刻度值必須均分,并且根據(jù)不同數(shù)據(jù)值展示合適的y刻度值。
所以createYLabel()方法實(shí)現(xiàn)了根據(jù)給定的數(shù)據(jù)點(diǎn)的值來計(jì)算出對應(yīng)y刻度值的算法。

/**
 * 根據(jù)數(shù)據(jù)值data生成合適的Y坐標(biāo)刻度值
 *
 * @return y軸坐標(biāo)刻度值數(shù)組
 */
private String[] createYLabel() {
    float[] dataFloats = new float[7];
    for (int i = 0; i < data.length; i++) {
        dataFloats[i] = Float.parseFloat(data[i]);
    }
    // 將數(shù)據(jù)值從小到大排序
    Arrays.sort(dataFloats);
    // 中間值
    float middle = format3Bit((dataFloats[0] + dataFloats[6]) / 2f);
    // y軸刻度,+0.01f為了避免所有數(shù)據(jù)點(diǎn)都相等時(shí)計(jì)算出的y刻度為0.
    float scale = format3Bit((dataFloats[6] - dataFloats[0]) / 4 + 0.01f);
    String[] yText = new String[5];
    yText[0] = (middle - 2 * scale) + "";
    yText[1] = (middle - scale) + "";
    yText[2] = middle + "";
    yText[3] = (middle + scale) + "";
    yText[4] = (middle + 2 * scale) + "";
    for (int i = 0; i < yText.length; i++) {
        yText[i] = format3Bit(yText[i]);
    }
    return yText;
}

將數(shù)據(jù)值排序后計(jì)算出中間值middle、y軸刻度值scale。format3Bit(float number)將計(jì)算結(jié)果進(jìn)行格式化,保證刻度值的小數(shù)位數(shù)一致。

/**
 * 格式化數(shù)字 ###.000
 *
 * @return ###.000
 */
private float format3Bit(float number) {
    DecimalFormat decimalFormat = new DecimalFormat("###.000");
    String target = decimalFormat.format(number);
    if (target.startsWith(".")) {
        target = "0" + target;
    }
    return Float.parseFloat(target);
}

/**
 * 格式化數(shù)據(jù) ###.000
 *
 * @param numberStr 格式化后的字符串形式
 * @return ###.000
 */
private String format3Bit(String numberStr) {
    if (TextUtils.isEmpty(numberStr)) {
        return "0.000";
    }
    float numberFloat = Float.parseFloat(numberStr);
    DecimalFormat decimalFormat = new DecimalFormat("###.000");
    String target = decimalFormat.format(numberFloat);
    if (target.startsWith(".")) {
        target = "0" + target;
    }
    return target;
}

onMeasure中初始化繪制尺寸及畫筆屬性等參數(shù):

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    initParams();
}

private void initParams() {
    int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

    yScale = height / 7.5f;         // y軸刻度
    xScale = width / 7.5f;          // x軸刻度
    startPointX = xScale / 2;       // 開始繪圖的x坐標(biāo)
    startPointY = yScale / 2;       // 開始UI圖的y坐標(biāo)
    xLength = 6.5f * xScale;        // x軸長度
    yLength = 5.5f * yScale;        // y軸長度
    xTextPlaceHeight = yScale / 2;       // x軸刻度文字的占位高度
    yTextPlaceWidth = xScale / 2;        // y軸刻度文字的占位寬度
    titleHeight = yScale;

    chartLineStrokeWidth = xScale / 50;     // 圖表線條的線寬
    coordTextSize = xScale / 5;             // 坐標(biāo)刻度文字的大小
    dataLineStrodeWidth = xScale / 15;      // 數(shù)據(jù)線條的線寬

    // 設(shè)置畫筆相關(guān)屬性
    mBackColorPaint.setColor(0x11DEDE68);
    mScaleLinePaint.setStrokeWidth(chartLineStrokeWidth);
    mScaleLinePaint.setColor(0xFFDEDCD8);
    mScaleValuePaint.setColor(0xFF999999);
    mScaleValuePaint.setTextSize(coordTextSize);
    mDataLinePaint.setStrokeWidth(dataLineStrodeWidth);
    mDataLinePaint.setStrokeCap(Paint.Cap.ROUND);
    mDataLinePaint.setTextSize(1.5f * coordTextSize);
}

onMeasure中就不去判斷測量模式了,布局中直接使用match_parent或具體的dp值。

繪制

onDraw方法中進(jìn)行繪制:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawBackColor(canvas);              // 繪制背景色塊
    drawYAxisAndXScaleValue(canvas);    // 繪制y軸和x刻度值
    drawXAxisAndYScaleValue(canvas);    // 繪制x軸和y刻度值
    drawDataLines(canvas);              // 繪制數(shù)據(jù)連線
    drawDataPoints(canvas);             // 繪制數(shù)據(jù)點(diǎn)
    drawTitle(canvas);                  // 繪制標(biāo)題
}

繪制方法的具體實(shí)現(xiàn):

/**
 * 繪制背景色塊
 * @param canvas
 */
private void drawBackColor(Canvas canvas) {
    for (int i = 0; i < 7; i++) {
        if (i == 2 || i == 4 || i == 6) {
            canvas.drawRect(startPointX + (i - 1) * xScale,
                    startPointY,
                    startPointX + i * xScale,
                    yLength + startPointY,
                    mBackColorPaint);
        }
    }
}

private void drawYAxisAndXScaleValue(Canvas canvas) {
    for (int i = 0; i < 7; i++) {
        canvas.drawLine(startPointX + i * xScale,
                startPointY,
                startPointX + i * xScale,
                startPointY + yLength,
                mScaleLinePaint);
        mScaleValuePaint.getTextBounds(xLabel[i], 0, xLabel[i].length(), bounds);
        if (i == 0) {
            canvas.drawText(xLabel[i],
                    startPointX,
                    startPointY + yLength + bounds.height() + yScale / 15,
                    mScaleValuePaint);
        } else {
            canvas.drawText(xLabel[i],
                    startPointX + i * xScale - bounds.width() / 2,
                    startPointY + yLength + bounds.height() + yScale / 15,
                    mScaleValuePaint);
        }
    }
}

/**
 * 繪制x軸和y刻度值
 * @param canvas
 */
private void drawXAxisAndYScaleValue(Canvas canvas) {
    for (int i = 0; i < 6; i++) {
        if (i < 5) {
            mScaleValuePaint.getTextBounds(yLabel[4 - i], 0, yLabel[4 - i].length(), bounds);
            canvas.drawText(yLabel[4 - i],
                    startPointX + xScale / 15,
                    startPointY + yScale * (i + 0.5f) + bounds.height() / 2,
                    mScaleValuePaint);
            canvas.drawLine(startPointX + bounds.width() + 2 * xScale / 15,
                    startPointY + (i + 0.5f) * yScale,
                    startPointX + xLength,
                    startPointY + (i + 0.5f) * yScale,
                    mScaleLinePaint);
        } else {
            canvas.drawLine(startPointX,
                    startPointY + (i + 0.5f) * yScale,
                    startPointX + xLength,
                    startPointY + (i + 0.5f) * yScale,
                    mScaleLinePaint);
        }
    }
}

/**
 * 繪制數(shù)據(jù)線條
 * @param canvas
 */
private void drawDataLines(Canvas canvas) {
    getDataRoords();
    for (int i = 0; i < 6; i++) {
        mDataLinePaint.setColor(Color.parseColor(mDataLineColors[i]));
        canvas.drawLine(mDataCoords[i][0], mDataCoords[i][1], mDataCoords[i + 1][0], mDataCoords[i + 1][1], mDataLinePaint);
    }
}

/**
 * 繪制數(shù)據(jù)點(diǎn)
 * @param canvas
 */
private void drawDataPoints(Canvas canvas) {
    // 點(diǎn)擊后,繪制數(shù)據(jù)點(diǎn)
    if (isClick && clickIndex > -1) {
        mDataLinePaint.setColor(Color.parseColor(mDataLineColors[clickIndex]));
        canvas.drawCircle(mDataCoords[clickIndex][0], mDataCoords[clickIndex][1], xScale / 10, mDataLinePaint);
        mDataLinePaint.setColor(Color.WHITE);
        canvas.drawCircle(mDataCoords[clickIndex][0], mDataCoords[clickIndex][1], xScale / 20, mDataLinePaint);
        mDataLinePaint.setColor(Color.parseColor(mDataLineColors[clickIndex]));
    }
}

/**
 * 繪制標(biāo)題
 * @param canvas
 */
private void drawTitle(Canvas canvas) {
    // 繪制標(biāo)題文本和線條
    mDataLinePaint.getTextBounds(title, 0, title.length(), bounds);
    canvas.drawText(title, (getWidth() - bounds.width()) / 2, startPointY + yLength + yScale, mDataLinePaint);
    canvas.drawLine((getWidth() - bounds.width()) / 2 - xScale / 15,
            startPointY + yLength + yScale - bounds.height() / 2 + coordTextSize / 4,
            (getWidth() - bounds.width()) / 2 - xScale / 2,
            startPointY + yLength + yScale - bounds.height() / 2 + coordTextSize / 4,
            mDataLinePaint);
}

/**
 * 獲取數(shù)據(jù)值的坐標(biāo)點(diǎn)
 *
 * @return 數(shù)據(jù)點(diǎn)的坐標(biāo)
 */
private void getDataRoords() {
    float originalPointX = startPointX;
    float originalPointY = startPointY + yLength - yScale;
    for (int i = 0; i < data.length; i++) {
        mDataCoords[i][0] = originalPointX + i * xScale;
        float dataY = Float.parseFloat(data[i]);
        float oriY = Float.parseFloat(yLabel[0]);
        mDataCoords[i][1] = originalPointY - (yScale * (dataY - oriY) / (Float.parseFloat(yLabel[1]) - Float.parseFloat(yLabel[0])));
    }
}

getDataRoords()是為了根據(jù)數(shù)據(jù)點(diǎn)的值及坐標(biāo)刻度的的比例關(guān)系計(jì)算出數(shù)據(jù)點(diǎn)的坐標(biāo),數(shù)據(jù)點(diǎn)(小圓圈)是點(diǎn)擊后重繪才顯示。
點(diǎn)擊數(shù)據(jù)點(diǎn)后,詳細(xì)的數(shù)據(jù)信息采用PopupWindow來展示。點(diǎn)擊事件就直接在onTouchEnvent中來處理:

@Override
public boolean onTouchEvent(MotionEvent event) {
    float touchX = event.getX();
    float touchY = event.getY();
    for (int i = 0; i < 7; i++) {
        float dataX = mDataCoords[i][0];
        float dataY = mDataCoords[i][1];
        // 控制觸摸/點(diǎn)擊的范圍,在有效范圍內(nèi)才觸發(fā)
        if (Math.abs(touchX - dataX) < xScale / 2 && Math.abs(touchY - dataY) < yScale / 2) {
            isClick = true;
            clickIndex = i;
            invalidate();     // 重繪展示數(shù)據(jù)點(diǎn)小圓圈
            showDetails(i);   // 通過PopupWindow展示詳細(xì)數(shù)據(jù)信息
            return true;
        } else {
            hideDetails();
        }
        clickIndex = -1;
        invalidate();
    }
    return super.onTouchEvent(event);
}```
遍歷數(shù)據(jù)點(diǎn),根據(jù)觸摸的位置判斷是否在數(shù)據(jù)點(diǎn)的有效范圍內(nèi),在有效范圍內(nèi)則通過`showDetails(i)`彈出彈窗,展示詳細(xì)的百分比信息。

private void showDetails(int index) {
if (mPopWin != null) mPopWin.dismiss();
TextView tv = new TextView(getContext());
tv.setTextColor(Color.WHITE);
tv.setBackgroundResource(R.drawable.shape_pop_bg);
GradientDrawable myGrad = (GradientDrawable) tv.getBackground();
myGrad.setColor(Color.parseColor(mDataLineColors[index]));
tv.setPadding(20, 0, 20, 0);
tv.setGravity(Gravity.CENTER);
tv.setText(data[index] + "%");
mPopWin = new PopupWindow(tv, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mPopWin.setBackgroundDrawable(new ColorDrawable(0));
mPopWin.setFocusable(false);
// 根據(jù)坐標(biāo)點(diǎn)的位置計(jì)算彈窗的展示位置
int xoff = (int) (mDataCoords[index][0] - 0.5f * xScale);
int yoff = -(int) (getHeight() - mDataCoords[index][1] + 0.75f * yScale);
mPopWin.showAsDropDown(this, xoff, yoff);
mPopWin.update();
}

private void hideDetails() {
if (mPopWin != null) mPopWin.dismiss();
}

需要注意的是:根據(jù)需要展示的數(shù)據(jù)點(diǎn)的`index`以及數(shù)據(jù)點(diǎn)的坐標(biāo),計(jì)算出展示彈窗的位置`xoff `和`xoff `,彈窗的展示位置是從控件的最左下角為原點(diǎn)算的偏移量。
#####配置添加數(shù)據(jù)的方法,供外部調(diào)用

/**

  • 設(shè)置x軸刻度值
  • @param xLabel x刻度值
    */
    public void setxLabel(String[] xLabel) {
    this.xLabel = xLabel;
    }

/**

  • 設(shè)置數(shù)據(jù)
  • @param data 數(shù)據(jù)值
    */
    public void setData(String[] data) {
    this.data = data;
    }

/**

  • 設(shè)置標(biāo)題
  • @param title 標(biāo)題
    */
    public void setTitle(String title) {
    this.title = title;
    }

/**

  • 重新設(shè)置x軸刻度、數(shù)據(jù)、標(biāo)題后必須刷新重繪
    */
    public void fresh() {
    init();
    postInvalidate();
    }
當(dāng)然,也可以添加自定義屬性,將數(shù)據(jù)、坐標(biāo)刻度值在布局文件中來配置,這里就不添加了。
#####使用

private void setData() {
String title = "7日年化收益率(%)";
String[] xLabel1 = {"12-11", "12-12", "12-13", "12-14", "12-15", "12-16", "12-17"};
String[] xLabel2 = {"2-13", "2-14", "2-15", "2-16", "2-17", "2-18", "2-19"};
String[] data1 = {"2.92", "2.99", "3.20", "2.98", "2.92", "2.94", "2.90"};
String[] data2 = {"2.50", "2.50", "2.50", "2.50", "2.50", "2.50", "2.50"};
mChartView1.setTitle(title);
mChartView1.setxLabel(xLabel1);
mChartView1.setData(data1);
mChartView1.fresh();
mChartView2.setTitle(title);
mChartView2.setxLabel(xLabel2);
mChartView2.setData(data2);
mChartView2.fresh();
}

![最終效果圖1](http://upload-images.jianshu.io/upload_images/1801191-c593c7a590797c62.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![最終效果圖2](http://upload-images.jianshu.io/upload_images/1801191-7ae480b871c84795.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


***
源碼:https://github.com/xiaoyanger0825/ChartView
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容