敢说敢作敢为, 无怨无恨无悔。这篇文章主要讲述手把手教你画AndroidK线分时图及指标相关的知识,希望能为你提供帮助。
先废话一下:
来到公司之前,
项目是由外包公司做的,
面试初,
没有接触过分时图k线这块,
觉得好难,
我能搞定不!
但是一段时间之后,
发现之前做的那是一片稀烂,
但是这货是主功能啊,
迟早的自己操刀,
痛下决心,
开搞,
本想用开源控件,
但是想自己实现一下:
接着有了本文
开始用surfaceview,
但是这货在上下滑动的时候会出现黑边,
这个问题我也是纠结了好久,
想想产品肯定会打回,
打回了还丢脸,
算了没多少东西就用view吧,
废话真tm多,
开始吧。
1,
创建项目(
android studio)
2,
对了,
先上个效果图吧,
节省各位的时间:
文章图片
3, 把Activity设置为横屏, 不设置也无所谓, 我觉得横屏的好看点
android:screenOrientation= " landscape"
4, 建俩基类分时图点数据和K线每点的数据, 备注的很清楚了
/**
* 分时所需要的 数据字段
*/
public class CMinute {
//时间
public long time;
//最新价
public double price;
//交易量
public long count;
//均价
public double average ;
//涨跌幅
public double rate ;
//价格
public double money ;
public long getTime() {
return time;
} public String getTimeStr() {
SimpleDateFormat sdf =
new SimpleDateFormat("
HH:mm"
);
try {
return sdf.format(new Date(time * 1000));
} catch (Exception e) {
return "
--:--"
;
}
}
}
public class StickData implements Parcelable {//时间
private long time;
//开盘
private double open;
//收盘
private double close;
//最高
private double high;
//最低
private double low;
//量
private long count;
//昨收
private double last;
//涨跌幅
private double rate;
//价格
private double money;
//计算均线的零时保存的值
private double maValue;
//5段均线
private double sma5;
//10段均线
private double sma10;
//20段均线
private double sma20;
//量5段均线
private double countSma5;
//量10段均线
private double countSma10;
//MACD的三个参数
private double dif;
//线
private double dea;
//线
private double macd;
//柱状
//KDJ的三根线
private double k;
private double d;
private double j;
//计算K时需要
private double rsv;
//K线资金
//超大单净值
private double sp;
//大单净值
private double bg;
//中单净值
private double md;
//小单净值
private double sm;
5,画图的步骤
@
Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//1,初始化需要的数据
initWidthAndHeight();
//2,
画网格
drawGrid(canvas);
//3,
画线(
分时线的价格线、均价线或K线的均线)
drawLines(canvas);
if(lineType !=
TYPE_FENSHI) {
//4,
如果是K线另外画烛形图
drawCandles(canvas);
}
//5,
写上XY轴的文字(
写早了会被覆盖)
drawText(canvas);
//6,
画需要显示的指标
switch (indexType) {
case INDEX_VOL:
drawVOL(canvas);
break;
case INDEX_ZJ:
drawZJ(canvas);
break;
case INDEX_MACD:
drawMACD(canvas);
break;
case INDEX_KDJ:
drawKDJ(canvas);
break;
}
}
6, 画图实现 其实分时线就是画线, 烛形图也是画线, 但是多画个矩形而已, 要是分析成这样的话, 就简单学多了, 那么接下来我来教你画线画矩形。。。。
此处省略10000字, 好了说完了( 其实是不用说了, 就那么俩方法drawLine,drawRect) , 接下来我们重点说说位置的计算:
我们实际拿到的数据, 不可能直接展示到坐标系的, 因为可能很大很小, 先来说说Y轴吧
Y轴
y = height - input * height / (max - min);
y:计算结果
height:view高度
max: 显示的一组数据最大值
min:显示的一组数据中最小值
展示分时线时, 需要在均价和价格取出最大值和最小值
展示K线时, 可以从最高和最低中取出最大最小值
X轴
x = width / drawcount * i;
x:计算结果
width: view宽度
drawcount: 展示的总个数
如上证指数, 上午下午各开盘2小时, 因为分时图是按分钟未单位, 则drawcount就是60*4, K线则需要按照宽度计算出drawcount, 我的代码中, 烛形图和烛形图之后的空白比为10: 2
7, 指标 分时图的资金由于用到了别的接口, demo中就不予展示了, 可以参考K线的资金动向指标( 就几条线, 简单吧)
MACD、KDJ、VOL5、VOL10、VOL20这些指标可以百度一下, 我就不多少了, 计算方法都一样, 我直接贴代码, k线的四个指标, 除了资金, 其他指标直接可以通过K线的高低开收昨收计算出来的,
public class IndexParseUtil {//均线跨度(SMA5,SMA10,SMA20),注意修改该值时,
需要同时增加StickData里面的sma字段、修改本类initSma方法,
否则不会生效
public static final int START_SMA5 =
5;
public static final int START_SMA10 =
10;
public static final int START_SMA20 =
20;
//26:计算MACD时,
26段close均价DIF=
(EMA(CLOSE,12) - EMA(CLOSE,26))
public static final int START_DIF =
26;
//35:
计算MACD时,
35段开始取前九日DIF值 DEA:=
EMA(DIF,9)
public static final int START_DEA =
35;
//12:计算K值
public static final int START_K =
12;
//15:计算DJ
public static final int START_DJ =
15;
//9:计算RSV
public static final int START_REV =
9;
public static final int[] SMA =
{START_SMA5,START_SMA10, START_SMA20};
/**
* 计算MACD
* @
param list
*/
public static void initMACD(List<
StickData>
list) {
if(list =
=
null) return;
//1计算出所有的DIF
for(int i =
0;
i <
list.size();
i+
+
) {
if(i +
START_DIF <
=
list.size()) {
list.get(i +
START_DIF - 1).setDif(getCloseSma(list.subList(i +
START_DIF - 12, i +
START_DIF)) - getCloseSma(list.subList(i +
START_DIF - 26, i +
START_DIF)));
}
}
//2计算出所有的DEA
for(int i =
0;
i <
list.size();
i+
+
) {
if(i +
START_DEA <
=
list.size()) {
list.get(i +
START_DEA - 1).setDea(getDifSma(list.subList(i +
START_DEA - 9, i +
START_DEA)));
//3计算MACD
list.get(i +
START_DEA - 1).setMacd(2d * (list.get(i +
START_DEA - 1).getDif() - list.get(i +
START_DEA - 1).getDea()));
}
}}/**
* 计算KDJ
* @
param list
*/
public static void initKDJ(List<
StickData>
list) {
if(list =
=
null) return;
//1计算出所有的REV
for(int i =
0;
i <
list.size();
i+
+
) {
if(i +
START_REV <
=
list.size()) {
//第9日开始计算RSV
StickData data =
list.get(i +
START_REV - 1);
double[] maxAndMin =
getMaxAndMin(list.subList(i, i +
START_REV));
list.get(i +
START_REV - 1).setRsv((data.getClose() - maxAndMin[1]) / (maxAndMin[0] - maxAndMin[1]) * 100);
}
}
//2计算出所有K
for(int i =
0;
i <
list.size();
i+
+
) {
if(i +
START_K <
=
list.size()) {
list.get(i +
START_K - 1).setK(getRSVSma(list.subList(i +
START_K - 3, i +
START_K)));
}
}
//3计算出所有的DJ
for(int i =
0;
i <
list.size();
i+
+
) {
if(i +
START_DJ <
=
list.size()) {
StickData data =
list.get(i +
START_DJ - 1);
list.get(i +
START_DJ - 1).setD(getKSma(list.subList(i +
START_DJ - 3, i +
START_DJ)));
list.get(i +
START_DJ - 1).setJ(3 * data.getK() - 2 * data.getD());
}
}}
/**
* 把list里面所有数据对应的均线计算出来并且赋值到里面
*
* @
param list k线数据
*/
public static void initSma(List<
StickData>
list) {
if (list =
=
null) return;
for (int i =
0;
i <
list.size();
i+
+
) {
for (int j : SMA) {
if (i +
j <
=
list.size()) {
//第5日开始计算5日均线
if (j =
=
START_SMA5) {
//量的SMA5
list.get(i +
j - 1).setCountSma5(getCountSma(list.subList(i, i +
j)));
//K线的SMA5
list.get(i +
j - 1).setSma5(getCloseSma(list.subList(i, i +
j)));
} else
//第10日开始计算10日均线
if (j =
=
START_SMA10) {
//量的SMA10
list.get(i +
j - 1).setCountSma10(getCountSma(list.subList(i, i +
j)));
//K线的SMA10
list.get(i +
j - 1).setSma10(getCloseSma(list.subList(i, i +
j)));
}else
//第20日开始计算20日均线
if (j =
=
START_SMA20) {
//K线的SMA20
list.get(i +
j - 1).setSma20(getCloseSma(list.subList(i, i +
j)));
}
}
}
}
}/**
* 计算KDJ时,
取9日最高最低值
* @
param datas
* @
return
*/
private static double[] getMaxAndMin(List<
StickData>
datas) {
if(datas =
=
null || datas.size() =
=
0)
return new double[]{0, 0};
double max =
datas.get(0).getHigh();
double min =
datas.get(0).getLow();
for(StickData data : datas) {
max =
max >
data.getHigh() ? max : data.getHigh();
min =
min <
data.getLow() ? min : data.getLow();
}
return new double[]{max, min};
}/**
* K线量计算移动平均值
* @
param datas
* @
return
*/
private static double getCountSma(List<
StickData>
datas) {
if (datas =
=
null) return -1;
double sum =
0;
for (StickData data : datas) {
sum +
=
data.getCount();
}
return NumberUtil.doubleDecimal(sum / datas.size());
}/**
* K线收盘价计算移动平均价
* @
param datas
* @
return
*/
private static double getCloseSma(List<
StickData>
datas) {
if (datas =
=
null) return -1;
double sum =
0;
for (StickData data : datas) {
sum +
=
data.getClose();
}
return NumberUtil.doubleDecimal(sum / datas.size());
}/**
* K线dif的移动平均值
* @
param datas
* @
return
*/
private static double getDifSma(List<
StickData>
datas) {
if (datas =
=
null) return -1;
double sum =
0;
for (StickData data : datas) {
sum +
=
data.getDif();
}
return NumberUtil.doubleDecimal(sum / datas.size());
}/**
* 三日rsv移动平均值,
即K值
* @
param datas
* @
return
*/
private static double getRSVSma(List<
StickData>
datas) {
if (datas =
=
null) return -1;
double sum =
0;
for (StickData data : datas) {
sum +
=
data.getRsv();
}
return NumberUtil.doubleDecimal(sum / datas.size());
}/**
* 三日K移动平均值,
即D值
* @
param datas
* @
return
*/
private static double getKSma(List<
StickData>
datas) {
if (datas =
=
null) return -1;
double sum =
0;
for (StickData data : datas) {
sum +
=
data.getK();
}
return NumberUtil.doubleDecimal(sum / datas.size());
}}
8, 滑动与缩放 这个就简单了, 分时线不支持滑动和缩放, 只有k线需要: 因为k线的数据较多, 默认一屏展示不全, 所以需要直接滑动, 缩放的话, 可能是想看大趋势吧( 我猜的) !
方法就是直接通过手势监听滑动和缩放,
那么: 我拿到600个数据, 展示了500-600, 滑动的时候, 只要吧这100个往前移动就可以了, 如滑到450-550; 缩放的话, 就更简单了, 如果一屏展示100, 那你设置一屏展示80或120就是缩放了, 是不是so easy!
9, 十字线 好了, 图画完了, 需要十字线出来走两步了!
先看看我的布局吧
<
RelativeLayout
android:layout_width=
"
0dp"
android:layout_height=
"
match_parent"
android:layout_weight=
"
686"
>
<
eat.arvin.com.mychart.view.FenshiView
android:id=
"
@
+
id/cff_fenshiview"
android:layout_width=
"
match_parent"
android:layout_height=
"
match_parent"
/>
<
eat.arvin.com.mychart.view.CrossView
android:id=
"
@
+
id/cff_cross"
android:layout_width=
"
match_parent"
android:layout_height=
"
match_parent"
android:visibility=
"
gone"
/>
<
/RelativeLayout>
懂了吧, 这俩货是分开的, 我只要在fenshiView里面捕获单击事件, 然后判断该点是否有数据, 有的话在CrossView画线, 对画两根线, 欧了
@
Override
public boolean onSingleTapUp(final MotionEvent e) {
//延时300毫秒显示,
为双击腾出时间
new Handler().postDelayed(new Runnable() {
@
Override
public void run() {
//单击显示十字线
if(crossView !=
null) {
if (crossView.getVisibility() =
=
View.GONE) {
onCrossMove(e.getX(), e.getY());
}
}
}
}, DOUBLE_TAP_DELAY);
return super.onSingleTapUp(e);
}
crossView
public class CrossView extends View {
/**
* 十字线移动的监听
*/
public interface OnMoveListener {
/**
* 十字线移动(回调到数据存放的位置,
判断是否需要画线后,
再调用本界面画线方法)
*
* @
param x x轴坐标
* @
param y y轴坐标
*/
void onCrossMove(float x, float y);
/**
* 十字线消失的回调
*/
void onDismiss();
}
private CrossBean bean;
//手势控制
private GestureDetector gestureDetector;
private OnMoveListener onMoveListener;
public CrossView(Context context, AttributeSet attrs) {
super(context, attrs);
gestureDetector =
new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@
Override
public boolean onSingleTapUp(MotionEvent e) {
//单击隐藏十字线
setVisibility(GONE);
if (onMoveListener !=
null)
onMoveListener.onDismiss();
return super.onSingleTapUp(e);
}@
Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//滑动时,
通知到接口
if (onMoveListener !=
null) {
onMoveListener.onCrossMove(e2.getX(), e2.getY());
}
return super.onScroll(e1, e2, distanceX, distanceY);
}});
}@
Override
public boolean onTouchEvent(MotionEvent event) {
if (gestureDetector !=
null)
gestureDetector.onTouchEvent(event);
return true;
}@
Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCrossLine(canvas);
}
/**
* //根据x,y画十字线
*
* @
param canvas
*/
private void drawCrossLine(Canvas canvas) {
//当该点没有数据的时候,
不画
if (bean.x <
0 || bean.y <
0) return;
boolean isJunXian =
bean.y2 >
=
0;
Paint p =
new Paint();
p.setAntiAlias(true);
p.setColor(ColorUtil.COLOR_CROSS_LINE);
p.setStrokeWidth(2f);
p.setStyle(Paint.Style.FILL);
//横线
canvas.drawLine(0, bean.y, getWidth(), bean.y, p);
//竖线
canvas.drawLine(bean.x, 0, bean.x, getHeight(), p);
if (isJunXian) {
//均线的时候才画出圆点
//画十字线和均线价格线交汇的圆
canvas.drawCircle(bean.x, bean.y, 10, p);
p.setColor(ColorUtil.COLOR_SMA_LINE);
canvas.drawCircle(bean.x, bean.y2, 10, p);
}
p.setColor(Color.BLACK);
p.setTextSize(32f);
//1, 写价格(竖线靠左时,
价格需要写到右边)
drawPriceTextWithRect(canvas, bean.x, bean.y, bean.price, p);
//2, 写时间
drawTimeTextWithRect(canvas, bean.x, bean.getTime(), p);
//3,
写指标的文字
drawIndexTexts(canvas);
p.reset();
}private void drawIndexTexts(Canvas canvas) {
if(bean.indexText =
=
null || bean.indexColor =
=
null) return;
Paint p =
new Paint();
p.setAntiAlias(true);
p.setTextSize(26f);
float x =
0;
float y =
getHeight() * (ChartConstant.MAIN_SCALE +
ChartConstant.TIME_SCALE) +
25;
for(int i =
0;
i <
bean.indexText.length;
i+
+
) {
p.setColor(bean.indexColor[i]);
canvas.drawText(bean.indexText[i], x, y, p);
x +
=
LineUtil.getTextWidth(p, bean.indexText[i]) +
30;
}}/**
* 写时间,
并且带框
*/
private void drawTimeTextWithRect(Canvas canvas, float x, String time, Paint p) {
p.setTextAlign(Paint.Align.LEFT);
float textWidth =
LineUtil.getTextWidth(p, time) +
20;
float y =
getHeight() * ChartConstant.MAIN_SCALE;
Paint rp =
new Paint();
rp.setColor(Color.WHITE);
rp.setStyle(Paint.Style.FILL);
rp.setStrokeWidth(2f);
//1,先画白底
float startX =
x - textWidth / 2;
float endX =
x +
textWidth / 2;
if(startX <
0) {
startX =
2f;
endX =
startX +
textWidth;
}
if(endX >
getWidth()) {
endX =
getWidth() - 2;
startX =
endX - textWidth;
}
canvas.drawRect(startX, y +
2, endX, y +
30, rp);
rp.setColor(Color.BLACK);
rp.setStyle(Paint.Style.STROKE);
//2,
再画黑框
canvas.drawRect(startX, y +
2, endX, y +
30, rp);
//3,
写文字
canvas.drawText(time, startX +
10, y +
27.5f, p);
}/**
* 写文字,
并且为文字带上背景,
等于在文字后方画上一个Rect
*/
private void drawPriceTextWithRect(Canvas canvas, float x, float y, String text, Paint p) {
float textWidth =
LineUtil.getTextWidth(p, text) +
10;
Paint rp =
new Paint();
rp.setColor(Color.WHITE);
rp.setStyle(Paint.Style.FILL);
rp.setStrokeWidth(2f);
float startY =
y - 15f;
float endY =
y +
15f;
if(startY <
0) {
startY =
0f;
endY =
startY +
30f;
} else if(endY >
getHeight()) {
endY =
getHeight();
startY =
endY - 30f;
}if (x <
100) {
//X轴在左侧,
该框画在右侧
//1,先画白底
canvas.drawRect(getWidth() - textWidth, startY, getWidth(), endY, rp);
rp.setColor(Color.BLACK);
rp.setStyle(Paint.Style.STROKE);
//2,
再画黑框
canvas.drawRect(getWidth() - textWidth, startY, getWidth(), endY, rp);
p.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(text, getWidth() - 5f, endY - 3, p);
} else {
//X轴在右侧,
改框画左侧
canvas.drawRect(0, startY, textWidth, endY, rp);
rp.setColor(Color.BLACK);
rp.setStyle(Paint.Style.STROKE);
canvas.drawRect(0, startY, textWidth, endY, rp);
p.setTextAlign(Paint.Align.LEFT);
canvas.drawText(text, 5f, endY - 3, p);
}
}/**
* 画分时线的十字线
*/
public void drawLine(CrossBean bean) {
this.bean =
bean;
postInvalidate();
}/**
* 设置移动监听
*
* @
param onMoveListener
*/
public void setOnMoveListener(OnMoveListener onMoveListener) {
this.onMoveListener =
onMoveListener;
}}
10, 一些优化
分时线: 服务器只需要返回变化的点, 不需要全部返回, 这些缺失的点直接使用前一分钟补全
K线: 由于k线数据巨多, 所以如果在服务器计算好指标再返回客户端的话, 会使数据量*1.5差不多, 所以这些指标还是在本地算好了, 只需要算需要显示的, 且不需要重复计算
11, githubhttps://github.com/xuzhou4520/AChart1
【手把手教你画AndroidK线分时图及指标】
推荐阅读
- android导入项目出现style错误,menu错误
- android launcher 之踩到的坑
- Android进程保活全攻略(上)
- Android对话框
- android studio 模拟器添加cpu类型
- 简单的android启动跳转页面
- Android 传感器开发详解
- android studio2.0 搭建Robotium环境--eclipse--apk 环境搭建
- Android开发6(Service的使用(简单音乐播放器的实现))