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

Posted Android傻大黑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你画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,github

https://github.com/xuzhou4520/AChart1


以上是关于手把手教你画AndroidK线分时图及指标的主要内容,如果未能解决你的问题,请参考以下文章

标准误,标准差,置信区间分不清?派森诺教你画误差线

别再漫无目的分析数据,手把手教你学会,如何体系化搭建数据指标

手把手教你大数据离线综合实战 ETL+Hive+Mysql+Spark

Redis 技术探索「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移 Redis 数据实战指南(scan模式迁移)

Redis 技术探索「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移 Redis 数据实战指南(数据检查对比)

Redis技术探索「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移Redis数据实战指南(离线同步数据)