折れ線グラフコントロールを自分で作成するChartView

13232 ワード

折れ線グラフは多くのAppで役立ち、GitHubにはhellecharts-android、achartengineなどの機能的な折れ線グラフのフレームワークがあります.しかし、多くの場合、デザイナーが与えたスタイルは、これらのフレームワークによって完全に効果が得られるとは限らない.だから、ビューをカスタマイズして自分で折れ線図を描くことを考えています.
最終効果図
展示効果によって、座標軸、目盛り、目盛り値、データ点線、タイトルはすべて自画で実現されます.
ドラフト関連パラメータの初期化
コンストラクション関数でブラシやデータの目盛り値などのパラメータを初期化します.
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"};
    }
    //  
    if (data == null) {
        data = new String[]{"2.98", "2.99", "2.99", "2.98", "2.92", "2.94", "2.95"};
    }
    //  
    if (title == null) {
        title = " (%)";
    }

    //  Y 
    yLabel = createYLabel();
    //  
    mDataLineColors = new String[]{"#fbbc14", "#fbaa0c", "#fbaa0c", "#fb8505", "#ff6b02", "#ff5400", "#ff5400"};
    //  
    mDataLinePaint = new Paint();       //  ( ) 
    mScaleLinePaint = new Paint();      //  ( ) 
    mScaleValuePaint = new Paint();      //  ( ) 
    mBackColorPaint = new Paint();       //  ( ) 
    //  
    mDataLinePaint.setAntiAlias(true);
    mScaleLinePaint.setAntiAlias(true);
    mScaleValuePaint.setAntiAlias(true);
    mBackColorPaint.setAntiAlias(true);
}

x軸の目盛値、データ点、yにおける目盛値設定初期値は、初期値に基づいて先に描画される.後でデータを再設定してから再描画します.所与のデータ点からyの座標スケーリング値を生成する際には、2点:1を考慮する必要がある.データ点とその連線は座標領域の中間位置に描画する必要があり、データ点の臨界値(最大または最小値)はy座標目盛りの臨界値を超えてはならない.2.y目盛り値は均等に分け、異なるデータ値に基づいて適切なy目盛り値を示す必要がある.従って、createYLabel()法は、所与のデータ点の値に基づいて対応するyスケール値を算出するアルゴリズムを実現する.
/**
 *  data Y 
 *
 * @return y 
 */
private String[] createYLabel() {
    float[] dataFloats = new float[7];
    for (int i = 0; i < data.length; i++) {
        dataFloats[i] = Float.parseFloat(data[i]);
    }
    //  
    Arrays.sort(dataFloats);
    //  
    float middle = format3Bit((dataFloats[0] + dataFloats[6]) / 2f);
    // y ,+0.01f 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;
}

データ値を並べ替えて、中間値middle、y軸目盛り値scaleを算出する.format3Bit(float number)は計算結果をフォーマットし、目盛り値の小数位数が一致することを保証する.
/**
 *   ###.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);
}

/**
 *   ###.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でペイントサイズやブラシアトリビュートなどのパラメータを初期化します.
@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 
    startPointY = yScale / 2;       //  UI y 
    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;             //  
    dataLineStrodeWidth = xScale / 15;      //  

    //  
    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メソッドで描画します.
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawBackColor(canvas);              //  
    drawYAxisAndXScaleValue(canvas);    //  y x 
    drawXAxisAndYScaleValue(canvas);    //  x y 
    drawDataLines(canvas);              //  
    drawDataPoints(canvas);             //  
    drawTitle(canvas);                  //  
}

描画方法の具体的な実装:
/**
 *  
 * @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);
        }
    }
}

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

/**
 *  
 * @param canvas
 */
private void drawDataPoints(Canvas canvas) {
    //  , 
    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]));
    }
}

/**
 *  
 * @param canvas
 */
private void drawTitle(Canvas canvas) {
    //  
    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);
}

/**
 *  
 *
 * @return  
 */
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()は、データ点の値および座標目盛りの比例関係からデータ点の座標を算出するためであり、データ点(小円)はクリックして再描画して表示される.データポイントをクリックすると、詳細なデータ情報がPopupWindowで表示されます.クリックイベントは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];
        //  / , 
        if (Math.abs(touchX - dataX) < xScale / 2 && Math.abs(touchY - dataY) < yScale / 2) {
            isClick = true;
            clickIndex = i;
            invalidate();     //  
            showDetails(i);   //  PopupWindow 
            return true;
        } else {
            hideDetails();
        }
        clickIndex = -1;
        invalidate();
    }
    return super.onTouchEvent(event);
}```
 , , `showDetails(i)` , 。

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);//座標点の位置からポップアップ窓の展示位置int xoff=(int)(mDataCoords[index][0]-0.5 f*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(); }
 : `index` , `xoff ` `xoff `, 。
##### , 

/**
  • x軸目盛り値
  • を設定.
  • @param xLabel x目盛り*/public void setxLabel(String[]xLabel){this.xLabel=xLabel;}

  • /**
  • 設定データ
  • @param dataデータ値*/public void setData(String[]data){this.data=data;}

  • /**
  • 設定タイトル
  • @param titleタイトル*/public void settitle(String title){this.title=title;}

  • /**
  • x軸目盛り、データ、タイトルを再設定した後、再描画*/public void fresh(){init();postInvalidate();
  •  , , 、 , 。
    ##### 
    

    private void setData(){String title="7日年化収益率(%)";String[]xLabel 1={"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