MPAndroidChart绘制K线图(二)

MPAndroidChart绘制K线图(一)高亮线自定义
MPAndroidChart绘制K线图(二)动态时间格式+高亮时底部滑动时间刻度
MPAndroidChart绘制K线图(三)长按高亮,双击事件,缩放中心点变换,图表联动,跨表缩放失效

更新GitHub地址
自定义股线图StockChart

二、时间刻度

1.动态的时间格式

需求是这样的:底部的时间刻度会自动的根据时间跨度变化显示格式,比如60分钟的K线图,当双指缩放到可见范围较小时(小于2天)就显示HH:mm; 缩放至可见范围大于2天小于1年时,显示月日MM-dd; 缩放至大于1年的跨度时就显示年月yyyy-MM。而后端返回的时间有2种"yyyy-MM-dd HH:mm", "yyyy-MM-dd"(竟然不是时间戳。。。)

初步分析:x轴刻度是由Chart的横轴XAxis通过ValueFormatter来确定,

// ValueFormatter.java
  public String getFormattedValue(float value) {
        return String.valueOf(value);
    }

getFormattedValue方法返回值即是要显示的内容,默认直接返回x轴的刻度value值,也就是数据源Entry中设置x值,需要重写该方法。同时在绘制刻度label前就需要确认页面内显示的范围,用以确认时间格式。于是需要在x轴渲染器XAxisRenderer渲染方法renderAxisLabels(Canvas c)中处理。

由于ValueFormatter中是拿不到源数据和chart对象的,无法获取图表显示范围和时间字符串,需要从其他地方传过来,维护变量mDisplayTimeFormat来确定到底使用哪种格式来格式化时间,每次更新数据时重新new实例设置给XAxis即可,needUpdateValueRange表示图表范围变更了,这时候需要更新时间格式化的格式。代码如下

// BarAXisValueFormatter类
public class BarAXisValueFormatter extends ValueFormatter {
    private List mBarEntries;
    private IValueFormatterCallback mCallback;
    //横轴显示的时间格式:3种
    // "HH:mm","MM-dd","yyyy-MM"
    public String mDisplayTimeFormat = Constant.TIME_SHARING_YY_MM;

    public BarAXisValueFormatter(List entries, IValueFormatterCallback callback) {
        mBarEntries = entries;
        mCallback = callback;
    }

    @Override
    public String getFormattedValue(float value) {
        int index = (int) value;
        if (index >= mBarEntries.size()) {
            return "";
        }
        BarEntry barEntry = mBarEntries.get(index);
        Date time = ((KLineData) barEntry.getData()).getDate();
        // 拿到时间, 格式化为指定的字符串
        return new SimpleDateFormat(mDisplayTimeFormat).format(time);
    }

    public void needUpdateValueRange() {
        if (mCallback != null) {
            int highestVisibleX = (int) mCallback.getHighestVisibleX();
            int lowestVisibleX = (int) mCallback.getLowestVisibleX();
            // 根据可见范围确认当前时间格式
            mDisplayTimeFormat = getRangeTimeFormat(highestVisibleX, lowestVisibleX);
        }
    }

    // 根据可见范围计算对应的时间格式
    private String getRangeTimeFormat(int highestVisibleX, int lowestVisibleX) {
        if (lowestVisibleX < 0) {
            lowestVisibleX = 0;
        }
        if (highestVisibleX >= mBarEntries.size()) {
            highestVisibleX = mBarEntries.size() - 1;
        }

        Date dateMin = ((KLineData) mBarEntries.get(lowestVisibleX).getData()).getDate();
        Date dateMax = ((KLineData) mBarEntries.get(highestVisibleX).getData()).getDate();
        long diffTime = dateMax.getTime() - dateMin.getTime();
        String displayTimeFormat;
        if (diffTime < Constant.MILLI_SECOND_2_DAY) {
            displayTimeFormat = Constant.TIME_SHARING_HH_MM;
        } else if (diffTime < Constant.MILLI_SECOND_1_YEAR) {
            displayTimeFormat = Constant.TIME_SHARING_MM_DD;
        } else {
            displayTimeFormat = Constant.TIME_SHARING_YY_MM;
        }
        return displayTimeFormat;
    }

    public interface IValueFormatterCallback {
        float getHighestVisibleX();

        float getLowestVisibleX();
    }
}
//每次数据准备好,或者更新之后需要重新给x轴设置时间格式器(也可以和getHighestVisibleX方法一样用回调的方式,不用每次都重新创建实例了)
XAxis barXAxis = mBarChart.getXAxis();
barXAxis.setValueFormatter(new BarAXisValueFormatter(barEntries, this));
// 更新数据的类实现了回调接口, 用chart的api来获取可见最大最小值
   @Override
    public float getHighestVisibleX() {
        return mBarChart.getHighestVisibleX();
    }

    @Override
    public float getLowestVisibleX() {
        return mBarChart.getLowestVisibleX();
    }

BarAXisValueFormatter类中needUpdateValueRange是在哪里调用呢,当然是每次渲染坐标轴之前了,简单写个XAxisRenderer的子类,重写renderAxisLabels方法,在super.renderAxisLabels(c)之前拿到valueFormatter让其更新范围即可,chart初始化时实例化BarXAxisRenderer设置给chart。

public class BarXAxisRenderer extends XAxisRenderer {
    public BarXAxisRenderer(ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans) {
        super(viewPortHandler, xAxis, trans);
    }

    @Override
    public void renderAxisLabels(Canvas c) {
        ValueFormatter valueFormatter = mXAxis.getValueFormatter();
        if (valueFormatter instanceof BarAXisValueFormatter) {
            ((BarAXisValueFormatter) valueFormatter).needUpdateValueRange();
        }
        super.renderAxisLabels(c);
    }
}
 BarXAxisRenderer xAxisRenderer = new BarXAxisRenderer(mViewPortHandler, mXAxis, mLeftAxisTransformer);
 setXAxisRenderer(xAxisRenderer);
2.高亮时底部滑动时间刻度

需求如下图,高亮时底部会显示一个时间来覆盖时间刻度,会随着高亮线变化显示在不同的位置。


image.png

这里能想到的有2个思路,一个就是绘制高亮线的drawHighlighted()方法中,同时在底部绘制日期文本,但是一看前面一节分析就知道,drawHighlighted方法是在被剪裁过的区域内执行的,Canvas不包含底部刻度区域了,放弃。 其二就是绘制底部刻度时,获取高亮值来计算绘制,既能拿到轴线刻度的paint,位置,又有高亮值,再合适不过了。还是在BarXAxisRenderer中处理, 这个类变成了这样子:

public class BarXAxisRenderer extends XAxisRenderer {
    private Paint mMarkLabelPaint;
    private IXAxisRendererCallback mCallback;
    private MPPointF mPointF;

    public BarXAxisRenderer(ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans) {
        super(viewPortHandler, xAxis, trans);
    }

    public BarXAxisRenderer(ViewPortHandler viewPortHandler, XAxis xAxis, Transformer trans, IXAxisRendererCallback callback) {
        super(viewPortHandler, xAxis, trans);
        mCallback = callback;
    }

    public void drawMarkLabels(Canvas c) {
        if (mCallback != null) {
            Highlight highlighted = mCallback.getHighlightDef();
            // callback其实就是chart, chart中提供了api :getHighlighted() ,getHeight(),只需要我们实现getDateForHighlight拿到标签显示文本即可
            if (highlighted == null) {
                return;
            }
            String text = mCallback.getDateForHighlight(highlighted);
            if (TextUtils.isEmpty(text)) {
                return;
            }
            float drawX = highlighted.getDrawX();
            float labelY = mViewPortHandler.contentBottom();
            Paint markPaint = getMarkLabelPaint();
            markPaint.setColor(ResourceUtils.getThemeColorReverse());
            float width = markPaint.measureText(text);
            Paint paint = new Paint();
            paint.setColor(ResourceUtils.getThemeColor());
            paint.setStyle(Paint.Style.FILL);
            c.drawRect(drawX - width / 2, labelY + 1, drawX + width / 2, mCallback.getHeight(), paint);
            // Utils.drawXAxisValue是MPAndroidChart的API,而pointF是定值调用不到,在这里也copy了一份
            MPPointF pointF = getMPPointF();
            Utils.drawXAxisValue(c, text, drawX, labelY + mAxis.getYOffset(), markPaint, pointF, mXAxis.getLabelRotationAngle());
        }
    }

    private MPPointF getMPPointF() {
        if (mPointF == null) {
            mPointF = MPPointF.getInstance(0, 0);
            mPointF.x = 0.5f;
            mPointF.y = 0.0f;
        }
        return mPointF;
    }

    private Paint getMarkLabelPaint() {
        if (mMarkLabelPaint == null) {
            mMarkLabelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mMarkLabelPaint.setTextSize(mAxisLabelPaint.getTextSize());
            mMarkLabelPaint.setTextAlign(Paint.Align.CENTER);
        }
        return mMarkLabelPaint;
    }

    @Override
    public void renderAxisLabels(Canvas c) {
        ValueFormatter valueFormatter = mXAxis.getValueFormatter();
        if (valueFormatter instanceof BarAXisValueFormatter) {
            ((BarAXisValueFormatter) valueFormatter).needUpdateValueRange();
        }
        super.renderAxisLabels(c);
        // 绘制高亮标签
        drawMarkLabels(c);
    }

    public interface IXAxisRendererCallback {
        // 这里只是使用简单的图表的话直接使用chart的getHighlighted()是可以的,但是后续需求原因,自己又维护了一个highlightDef。
        Highlight getHighlightDef();

        int getHeight();

        String getDateForHighlight(Highlight highlight);
    }
}

接下来是这个标签文本怎么获取, 刚刚定义了标签格式器BarAXisValueFormatter,当然是从它那里取了。

  @Override
    public String getDateForHighlight(Highlight highlight) {
        // Highlight中包含高亮点的xy位置信息
        if (mhighlighted != null) {
            IAxisValueFormatter valueFormatter = mXAxis.getValueFormatter();
            if (valueFormatter instanceof BarAXisValueFormatter) {
      // 通过Highlight索引可以计算出对应点的数据对象,(mHighlightData是我在后续的同步高亮时单独维护的对象,目的是一样的,从data中拿到时间信息)
                return ((BarAXisValueFormatter) valueFormatter).formatLabelTime((int) mhighlighted.getX(), mHighlightData);
            }
        }
        return "";
    }
    // 格式器中需要显示什么格式的文本就对应返回即可
    public String formatLabelTime(int index, @NonNull KLineData kline) {
        String formatTime = "";
        Date date = kline.getDate();
        if (!TextUtils.isEmpty(kline.getTime())) {
            switch (mDisplayTimeFormat) {
                case Constant.TIME_SHARING_YY_MM:
                    formatTime = FormatUtils.changedDateFormat(date, Constant.SOURCE_TIME_STRING[1]);
                    break;
                case Constant.TIME_SHARING_HH_MM:
                    formatTime = FormatUtils.changedDateFormat(date, Constant.TIME_LABEL_MARK_TIME);
                    break;
                case Constant.TIME_SHARING_MM_DD:
                    int formatDateType = FormatUtils.getFormatDateType(kline.getTime());
                    formatTime = FormatUtils.changedDateFormat(date, formatDateType == 1 ? Constant.SOURCE_TIME_STRING[1] : Constant.TIME_LABEL_MARK_TIME);
                    break;
            }
        }
        // 计算不出来直接使用x刻度值。。
        if (TextUtils.isEmpty(formatTime)) {
            formatTime = getFormattedValue(index);
        }
        return formatTime;
    }

(写得比较简单,估计使用过MPAndroidChart的童鞋才能知道我写的啥吧-。-, 后面有空了我将源码整理开源出来吧。)

你可能感兴趣的:(MPAndroidChart绘制K线图(二))