zoukankan      html  css  js  c++  java
  • 手把手教你画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。一些优化

        
    分时线:server仅仅须要返回变化的点,不须要所有返回,这些缺失的点直接使用前一分钟补全


    K线:因为k线数据巨多,所以假设在server计算好指标再返回client的话。会使数据量*1.5差点儿相同。所以这些指标还是在本地算好了,仅仅须要算须要显示的。且不须要反复计算



    11。github

    https://github.com/xuzhou4520/AChart1


  • 相关阅读:
    ActiveReports打印的问题
    HTML高级应用
    一些常用的asp.net技巧焦点篇
    数据库操作
    [ASP.NET]在后台引用JavaScript
    点击TextBox复制其中内容
    VB.Net基本语句(推荐)
    ADO 对象
    控件不获得焦点
    32款网站优化工具
  • 原文地址:https://www.cnblogs.com/yjbjingcha/p/7380426.html
Copyright © 2011-2022 走看看