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