手把手教你画AndroidK线分时图及指标

敢说敢作敢为, 无怨无恨无悔。这篇文章主要讲述手把手教你画AndroidK线分时图及指标相关的知识,希望能为你提供帮助。

先废话一下: 来到公司之前, 项目是由外包公司做的, 面试初, 没有接触过分时图k线这块, 觉得好难, 我能搞定不! 但是一段时间之后, 发现之前做的那是一片稀烂, 但是这货是主功能啊, 迟早的自己操刀, 痛下决心, 开搞, 本想用开源控件, 但是想自己实现一下: 接着有了本文
开始用surfaceview, 但是这货在上下滑动的时候会出现黑边, 这个问题我也是纠结了好久, 想想产品肯定会打回, 打回了还丢脸, 算了没多少东西就用view吧, 废话真tm多, 开始吧。
1, 创建项目( android studio) 2, 对了, 先上个效果图吧, 节省各位的时间:

手把手教你画AndroidK线分时图及指标

文章图片
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线分时图及指标】

    推荐阅读