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<BarEntry> mBarEntries;
    private IValueFormatterCallback mCallback;
    //横轴显示的时间格式:3种
    // "HH:mm","MM-dd","yyyy-MM"
    public String mDisplayTimeFormat = Constant.TIME_SHARING_YY_MM;

    public BarAXisValueFormatter(List<BarEntry> 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的童鞋才能知道我写的啥吧-。-, 后面有空了我将源码整理开源出来吧。)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341

推荐阅读更多精彩内容