行情K线图也就是我们常说的蜡烛图,是金融类软件里可以说必不可少的,无论日K, 周K,月K,还是分钟K,准确的来表达个股在一定时间内涨跌走势,K线图有着不可无视的作用,其绘制过程也是彰显一个程序员对自定义控件的熟练程度,尤其是对Canvas的灵活运用,绘线,绘边框,及位置的选取,比例的分配,今天这个Demo,则一步步为你诠释。
按惯例,先看下今天要实现的效果,整个Demo地址为:http://download.csdn.net/detail/ming_147/9732963,也可以关注公众号后(评论区第一条评论扫描即可)回复“行情k线图”,源码就会发送给您,公众号有很多android及其它技术文章,还请大家承蒙关注。
相对来说比较简单的一个小Demo,为什么来说简单呢,一数据是固定的,二,时间是固定的,相比较实际项目中来说,这已经相当的简单了,我们可以简单的分一下步骤模块,然后再按照依次来进行实现,通过上面的图片,我们可以大致分为,边框,横线,纵线,底部时间,左边刻度,柱状图(蜡烛图),十字光标这几个部分,好,分好之后,我们就来一步步实现吧。
由于代码稍多,为显得代码结构清晰,我们可以先写一个父类,用于实现边框,横纵线,及底部时间,左部刻度的绘制,柱状图(蜡烛图)及十字光标我们放在子类中实现。
自定义一个父类继承于View,实现其构造方法,在onMeasure方法里设置View的大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);//得到模式
int specSize = MeasureSpec.getSize(measureSpec);//得到尺寸
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
return result;
}
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
return result;
}
这里简单对两个类型做下解释:
MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如:andorid:layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。
MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
设置完大小之后,我们先在构造方法里初始化一些信息,比如背景色,画笔:
/**
* 设置背景色及初始化画笔
*/
private void init() {
setBackgroundColor(Color.parseColor("#222222"));
mPaint = new Paint();
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.STROKE);
}
重写onDraw方法,并在方法内绘制相关信息。绘制边框,距离左上各位10,距离右边为View宽度-10,距离底部为View高度-50:
private void drawBorder(Canvas canvas) {
mPaint.setColor(Color.WHITE);
Rect r = new Rect();
r.left = 10;
r.top = 10;
r.right = this.getRight() - 10;
r.bottom = this.getHeight() - 50;
canvas.drawRect(r, mPaint);
}
绘制横线,因为要留出一段区域做刻度绘制,所以,距离左边要有一段距离,这里我设置的100,所以每条横线的起始位置一定,都是100,因为边框的最右边为View宽度-10,所以横线的终止位置也是一致,起始y的位置和终止y的位置应当一致,按照一定的距离等分开来,这里的lineSize是要分成几份,我定义的是4份,则每份的长度就为:(当前View的高度-距离底部的距离-距离上部的距离)/lineXSize:
private int lineXSize = 4;
private void drawXLine(Canvas canvas) {
mPaint.setColor(Color.WHITE);
float height = (this.getHeight() - 10f - 50f) / lineXSize;//平均分为几分
for (int a = 0; a
mPaint.setColor(Color.WHITE);
float width = (this.getRight() - 10f - 100f) / lineYSize;
for (int a = 0; a 5, 6, 7, 8};
private void drawTimes(Canvas canvas) {
mPaint.setColor(Color.parseColor("#FF00FF"));
mPaint.setTextSize(24);
float width = (this.getRight() - 10f - 100f) / lineYSize;
for (int a = 0; a
canvas.drawText(times[a] + "月", w - 30f, this.getHeight() - 25f, mPaint);
} else {
canvas.drawText(times[a] + "月", w - 15f, this.getHeight() - 25f, mPaint);
}
}
}
绘制Y轴价格刻度,价格刻度的绘制,就和绘制横线有点类似了,price是自己定义的一个刻度数组:
private float[] price = {260f, 240f, 220f};
private void drawYPrice(Canvas canvas) {
mPaint.setColor(Color.WHITE);
float height = (this.getHeight() - 10f - 50f) / lineXSize;//平均分为几分
for (int a = 1; a
mPaint = new Paint();
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.FILL);
}
绘制蜡烛图之前,我们需要初始化一些我们需要的数据,这里我定义了一个javaBean,里面我定义了一些数据,开盘,收盘,最高,最低,日期,实现其构造方法和get,set方法。
/**
* 开盘价
*/
private float open;
/**
* 最高价
*/
private float high;
/**
* 最低价
*/
private float low;
/**
* 收盘价
*/
private float close;
/**
* 日期
*/
private int date;
javaBean实现之后,我们就可以添加模拟数据了,毕竟不是真实的项目中,所以数据,只能自己去创造了,listData是自己定义存储数据的:
protected List listData = new ArrayList();
/**
* 添加数据
*/
private void setLineData() {
List list = new ArrayList();
list.add(new StockLineBean(250, 251, 248, 250, 20170731));
list.add(new StockLineBean(249, 252, 248, 252, 20170730));
list.add(new StockLineBean(250, 251, 248, 250, 20170729));
list.add(new StockLineBean(249, 252, 248, 252, 20170728));
list.add(new StockLineBean(248, 250, 247, 250, 20170727));
list.add(new StockLineBean(256, 256, 248, 248, 20170726));
list.add(new StockLineBean(257, 258, 256, 257, 20170725));
list.add(new StockLineBean(259, 260, 256, 256, 20170724));
list.add(new StockLineBean(261, 261, 257, 259, 20170723));
list.add(new StockLineBean(259, 260, 256, 256, 20170722));
list.add(new StockLineBean(261, 261, 257, 259, 20170721));
list.add(new StockLineBean(260, 260, 259, 259, 20170720));
list.add(new StockLineBean(262, 262, 260, 261, 20170719));
list.add(new StockLineBean(260, 262, 259, 262, 20170718));
list.add(new StockLineBean(259, 261, 258, 261, 20170717));
list.add(new StockLineBean(255, 259, 255, 259, 20170716));
list.add(new StockLineBean(259, 261, 258, 261, 20170715));
list.add(new StockLineBean(255, 259, 255, 259, 20170714));
list.add(new StockLineBean(258, 258, 255, 255, 20170713));
list.add(new StockLineBean(258, 260, 258, 260, 20170712));
list.add(new StockLineBean(259, 260, 258, 259, 20170711));
list.add(new StockLineBean(261, 262, 259, 259, 20170710));
list.add(new StockLineBean(261, 261, 258, 261, 20170709));
list.add(new StockLineBean(261, 262, 259, 259, 20170708));
list.add(new StockLineBean(261, 261, 258, 261, 20170707));
list.add(new StockLineBean(261, 261, 259, 261, 20170706));
list.add(new StockLineBean(257, 261, 257, 261, 20170705));
list.add(new StockLineBean(256, 257, 255, 255, 20170704));
list.add(new StockLineBean(257, 261, 257, 261, 20170703));
list.add(new StockLineBean(256, 257, 255, 255, 20170702));
list.add(new StockLineBean(253, 257, 253, 256, 20170701));
list.add(new StockLineBean(255, 255, 252, 252, 20170630));
list.add(new StockLineBean(256, 256, 253, 255, 20170629));
list.add(new StockLineBean(254, 256, 254, 255, 20170628));
list.add(new StockLineBean(247, 256, 247, 254, 20170627));
list.add(new StockLineBean(244, 249, 243, 248, 20170626));
list.add(new StockLineBean(244, 245, 243, 244, 20170625));
list.add(new StockLineBean(244, 249, 243, 248, 20170624));
list.add(new StockLineBean(244, 245, 243, 244, 20170623));
list.add(new StockLineBean(242, 244, 241, 244, 20170622));
list.add(new StockLineBean(243, 243, 241, 242, 20170621));
list.add(new StockLineBean(246, 247, 244, 244, 20170620));
list.add(new StockLineBean(248, 249, 246, 246, 20170619));
list.add(new StockLineBean(251, 253, 250, 250, 20170618));
list.add(new StockLineBean(248, 249, 246, 246, 20170617));
list.add(new StockLineBean(251, 253, 250, 250, 20170616));
list.add(new StockLineBean(249, 253, 249, 253, 20170615));
list.add(new StockLineBean(248, 250, 246, 250, 20170614));
list.add(new StockLineBean(249, 250, 247, 250, 20170613));
list.add(new StockLineBean(254, 254, 250, 250, 20170612));
list.add(new StockLineBean(254, 255, 251, 255, 20170611));
list.add(new StockLineBean(254, 254, 250, 250, 20170610));
list.add(new StockLineBean(254, 255, 251, 255, 20170609));
list.add(new StockLineBean(252, 254, 251, 254, 20170608));
list.add(new StockLineBean(250, 253, 250, 252, 20170607));
list.add(new StockLineBean(251, 252, 247, 250, 20170606));
list.add(new StockLineBean(253, 254, 252, 254, 20170605));
list.add(new StockLineBean(250, 254, 250, 254, 20170604));
list.add(new StockLineBean(251, 252, 247, 250, 20170603));
list.add(new StockLineBean(253, 254, 252, 254, 20170602));
list.add(new StockLineBean(250, 254, 250, 254, 20170601));
list.add(new StockLineBean(250, 252, 248, 250, 20170531));
list.add(new StockLineBean(253, 254, 250, 251, 20170530));
list.add(new StockLineBean(255, 256, 253, 253, 20170529));
list.add(new StockLineBean(256, 257, 253, 254, 20170528));
list.add(new StockLineBean(255, 256, 253, 253, 20170527));
list.add(new StockLineBean(256, 257, 253, 254, 20170526));
list.add(new StockLineBean(256, 257, 254, 256, 20170525));
list.add(new StockLineBean(265, 265, 257, 257, 20170524));
list.add(new StockLineBean(265, 266, 265, 265, 20170523));
list.add(new StockLineBean(267, 268, 265, 266, 20170522));
list.add(new StockLineBean(264, 267, 264, 267, 20170521));
list.add(new StockLineBean(267, 268, 265, 266, 20170520));
list.add(new StockLineBean(264, 267, 264, 267, 20170519));
list.add(new StockLineBean(264, 266, 262, 265, 20170518));
list.add(new StockLineBean(266, 267, 264, 264, 20170517));
list.add(new StockLineBean(264, 267, 263, 267, 20170516));
list.add(new StockLineBean(266, 267, 264, 264, 20170515));
list.add(new StockLineBean(269, 269, 266, 268, 20170514));
list.add(new StockLineBean(266, 267, 264, 264, 20170513));
list.add(new StockLineBean(269, 269, 266, 268, 20170512));
list.add(new StockLineBean(267, 269, 266, 269, 20170511));
list.add(new StockLineBean(266, 268, 266, 267, 20170510));
list.add(new StockLineBean(264, 268, 263, 266, 20170509));
list.add(new StockLineBean(265, 271, 267, 267, 20170508));
list.add(new StockLineBean(265, 269, 265, 267, 20170507));
list.add(new StockLineBean(265, 268, 265, 267, 20170506));
list.add(new StockLineBean(271, 271, 266, 266, 20170505));
list.add(new StockLineBean(271, 273, 269, 273, 20170504));
list.add(new StockLineBean(268, 271, 268, 271, 20170503));
list.add(new StockLineBean(268, 270, 266, 271, 20170502));
list.add(new StockLineBean(268, 268, 263, 271, 20170501));
for (int a = 0; a
有了数据,我们就可以绘制蜡烛图了,如果开盘大于昨收,蜡烛图就为红色,否则就为绿色,因为,纵轴起始位置是从200开始算的,所以我们取得的最大与最小值再计算的时候,要减去其起始位置,和线的宽度;
每个蜡烛图的高就为:(当前View的高度-距离上下的距离)-当前位置最高值*(当前View的高度-距离上下的距离)/要分的份数(这里是200到280共80份);
每个蜡烛图的低就为:(当前View的高度-距离上下的距离)-当前位置最低值 *(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天);
每个蜡烛图的左边就是:(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天)+当前View距离左边的距离*第几个蜡烛图;
每个蜡烛图的右边就是:蜡烛图的左边+(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天);
绘制蜡烛图的中间线,x 轴的起始位置都是:蜡烛图的左边+(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天)/2,y轴的起始位置为:每个蜡烛图的高-10,y轴的终止位置为:每个蜡烛图的低+10;
private void drawCandleSticks(Canvas canvas) {
float ySize = (this.getHeight() - 60f) / 80f;
float xSize = (this.getRight() - 110f) / 92;
for (int a = 0; a
mPaint.setColor(Color.RED);
} else {
mPaint.setColor(Color.GREEN);
}
float top = (this.getHeight() - 60f) - high * ySize;
float bottom = (this.getHeight() - 60f) - low * ySize;
canvas.drawRect(left, top, left + xSize, bottom, mPaint);
//绘制中间线
canvas.drawLine(left + xSize / 2, top - 10f, left + xSize / 2, bottom + 10f, mPaint);
}
}
绘制十字光标,就需要重写onTouchEvent方法:
private float xMove, yMove;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
xMove = event.getX();
yMove = event.getY();
super.invalidate();
break;
}
return true;
}
获取好位置之后,我们就可以在onDraw方法里绘制十字光标了,因为 View距离左边和底部有一定的距离,所以在这距离里,我们可以不设置十字光标,十字光标,两条线,一条横线,一条纵线:
横线:起始x轴的位置为当前View距离左边的距离,终止位置就是当前View宽度-10,起始和终止都是手指移动的y值;
纵线:起始x轴的位置就是手指移动的x坐标,起始y值为当前View距离上边的距离,终止y值就是当前View距离底部的距离;
左边变化刻度值:x值为固定的,我这里给出的是75,y坐标是移动的y值+3,其值的计算是:(当前View的高度-距离上下的距离-手指移动的y坐标)/(当前View的高度-距离上下的距离)/要分的份数(这里是200到280共80份)+初始位置刻度。
底部时间变化,x坐标为手指移动的x值-20,y坐标为当前View的高度-35,尽量在底部线的下面,值的计算是:先得到的索引,然后再从listData集合里取得时间。索引的计算方式为:(当前手指移动的x坐标/(当前 View的宽度-左右的距离及几根线的宽度)/总的天数)-(当前 View的宽度-左右的距离及几根线的宽度)/总的天数;
private void drawWithFingerClick(Canvas canvas) {
float ySize = (this.getHeight() - 60f) / 80f;
if (xMove this.getBottom() - 50f) {
mPaint.reset();
} else {
canvas.drawLine(100f, yMove, this.getRight() - 10, yMove, mPaint);
canvas.drawLine(xMove, 10f, xMove, this.getBottom() - 50f, mPaint);
float xWidth = ((this.getHeight() - 50f) - yMove) / ySize + 200f;
String xContent = String.format("%.0f", xWidth);
canvas.drawText(xContent, 75f, yMove + 3f, mPaint);
float xSize = (this.getRight() - 125f) / 92;
float timeSize = (xMove / xSize) - xSize;
int size = (int) timeSize;
if (size |