您好,欢迎来到源码搜藏网!分享精神,快乐你我!
[加入VIP] 设为首页 | 收藏本站 | 网站地图 | Sitemap | TAG标签
  • 首 页
  • 在线工具
  • jquery手册
  • 当前位置:首页 > 安卓源码 > 技术博客 >

    Android开发一步步教你画股票图,K线图,高仿雪球股票

    时间:2019-01-11 08:29 来源:互联网 作者:源码搜藏 浏览:收藏 挑错 推荐 打印

    讲K线图之前,先来一个引言. 作为Android开发者需要立即提升的三项技能: 分别是: UI,网络,线程,而UI又分:布局,绘制,以及触摸事件的反馈 .其实UI确实只有这么几个最主要的东西,但是很多人却没能搞明白. UI的这三方面,说容易也容易,说难也难.有同学当时也问到:

    Android开发一步步教你画股票图,K线图,高仿雪球股票

    Android开发一步步教你画股票图,K线图,高仿雪球股票

    讲K线图之前,先来一个引言.

    作为Android开发者需要立即提升的三项技能:
    分别是:UI,网络,线程,而UI又分:布局,绘制,以及触摸事件的反馈.其实UI确实只有这么几个最主要的东西,但是很多人却没能搞明白.

    UI的这三方面,说容易也容易,说难也难.有同学当时也问到:怎么样才能算是掌握了这三个方面呢?
    凯哥当时的回答是:给出一个不算是为难的界面,能布局出来,绘制好,并掌握相应的触摸反馈,就算是基本上掌握UI了.

    实际上UI容易也是在这里,初步上手,对大部分比较认真的同学来说都是可以做到的,但是深入了解的,却比较少.例如,触摸Touch事件是如何分发的,
    它的原理是什么,这就需要大家更加深入的学习了.

    前面说到的UI的三个方面,其实在股票图里面都有比较好的体现,下面就这三个方法,讲解一下实现股票图的思路

    股票图基本知识

    了解股票图如何绘制,首先应该了解股票图的业务逻辑是怎样的,这篇文章是仿雪球股票写的,建议大家下载雪球股票软件体验一下.在写这个股票图之前,我对股票是一无所知(原谅我穷买不起股票),
    所以花了一点时间了解了一下股票图的基本信息,如果知道股票图是如何解读的,可以跳过这节.

    股票图的种类特别多,不同的种类的股票图也不一样,例如股票有港股,美股,上证,深圳,创业板等等.然后上证又有:分时,日K,月K等等.
    复杂程度完全可以直接绕晕人,没错,我就是看不懂所以不敢买.

    股票图的种类之多,本文也没有一一编写,这里主要是仿照了雪球股票之上证指数的:分时图,以及日K图.
    也就是股票的两大图种:分时图,以及蜡烛图.

    分时图

    Android开发一步步教你画股票图,K线图,高仿雪球股票

    分时图有股票当天的涨跌情况,以及一些最高点,最低点,比分比,
    长按分时图,可以定位当时手指按下的时间所对应的股票点是多少点,并且可以左右滑动

    股票的开盘时间是早上09:30-11:30,下午是13:00-15:00.

    蜡烛图

    Android开发一步步教你画股票图,K线图,高仿雪球股票

    蜡烛图和分时图类似,先除去那三条折线.分时图是把涨跌情况用折线表示,而蜡烛图是用一个矩形加一条竖线表示,和一根蜡烛一样,所以形象的称它为蜡烛图,
    其中竖线的最高点代表当日最高涨到了多少点,最低表示最低跌到了多少点.
    矩形的顶端,表示当日开盘是多少点,底端,表示收盘是多少点.
    颜色红,代表收盘后,相对于昨天,涨了,颜色绿,则表示跌了.
    三条折线分别代表了MA线,MA是“移动平均线”的简称,后面的数字:5、10、20.....是时间周期。MA5即5天收盘股票的平均点,其他的类推.博主这个例子没有实现MA线,作为大家的补充练手
    日K图每个月一个间隔.

    下面就分时图,蜡烛图,分别讲解其布局,绘制,触摸反馈

    分时图

    布局

    布局无论是xml引用layout编写,亦或是java直接new出来,或者是使用canvas直接绘制,最重要的不是应该使用
    RelativeLayout还是LinearLayout,而是应该剖析它的层次与结构.

    层次

    根据上面的基本介绍,分时图的可以分为以下几个层次:

    • 第1层:横线,竖线,以及底部时间(底部时间没有其他的元素,可以处于任意一层)

    • 第2层:折线,以及阴影部分

    • 第3层:文字,包括最高点,最低点,百分比

    结构

    分时图的结构相对简单,在基本介绍上已经说明其基本信息.

    股票的开盘时间是早上09:30-11:30,下午是13:00-15:00,所以其分上午,下午两部分.
    中间的虚线是昨天收盘的股票点,以此为基准线,计算折线图的位置.

    绘制

    布局分析好之后,就开始绘制这些基本信息.普通View的绘制,是写好xml或者java代码,然后交给每个view自己绘制,这里我们自己控制其绘制.

    绘制的步骤,其实就是布局中所说的层次,绘制的规则,则是布局中的结构.换句话说,这个结构,规则,就是数学中的公式,步骤就是我们解题的思路.

    详细绘制步骤

    开始.

    自定义一个View,覆写其四个构造方法(注意最好四个构造方法都覆写,这样就可以通过多种途径新建这个View),覆写onDraw()方法,画图的时候就是在这个方法进行绘制的.

    public class KLineView extends View {
            public KLineView(Context context) {
                super(context);
                init();
            }
    
            public KLineView(Context context, AttributeSet attrs) {
                super(context, attrs);
                init();
            }
    
            public KLineView(Context context, AttributeSet attrs, int defStyleAttr) {
                super(context, attrs, defStyleAttr);
                init();
            }
    
            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            public KLineView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
                super(context, attrs, defStyleAttr, defStyleRes);
                init();
            }
    
            @Override
            protected void onDraw(Canvas canvas) {
                super.onDraw(canvas);
            }
    }

    一般还需要初始化一些信息.为了让自己能看到每一步的绘制效果,编写一个添加测试数据方法,初始化的时候执行该方法即可.

    /**
     * canvas paint
     */
    private Paint mPaint;
    
    private void init() {
        mPaint = new Paint();
        createTestData();
    }
    
    
    /**
     * create the test data
     */
    private void createTestData() {
        baseData = 3120.50f;
        try {
            times = new ArrayList<>();
            prices = new ArrayList<>();
            @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat
                    ("yyyy-MM-dd HH:mm:ss");
            Date date = dateFormat.parse("2017-01-01 09:30:00");
            for (int i = 0; i < 240; i++) {
                if (i == 120) {
                    date = dateFormat.parse("2017-01-01 13:00:00");
                }
                date.setTime(date.getTime() + 60 * 1000);
                times.add(formatTime(dateFormat.format(date)));
    
                float tmp;
                if (i == 0) tmp = (float) (baseData + 5 - Math.random() * 10);
                else tmp = (float) (prices.get(i - 1) + 5 - Math.random() * 10);
                tmp = formatPrice(tmp);
                if (tmp > maxPrice) {
                    maxPrice = tmp;
                }
                if (tmp < minPrice) {
                    minPrice = tmp;
                }
                prices.add(tmp);
            }
    //            for (String str : times) {
    //                Log.e("time", str);
    //            }
    //            for (Float item : prices) {
    //                Log.e("time", item + "");
    //            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    
    }

    绘制线.

    使用MarkMan量取,分时图在720*1280分辨率下,高度是是410,则我们可以把其高度分成410份.
    它一共有5条横线,从上到下,每条线距离顶部的距离依次为:10,30,190,360,380.其中第3条为虚线.还有一条竖线,水平居中.

    依次画出每一条线.

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewHeight = getHeight();
        int viewWidth = getWidth();
        float item = viewHeight / 410f;
    
        /**
         * draw lines
         */
        drawLines(canvas, viewWidth, item);
    }
    
    /**
     * draw lines
     * <p>from top to bottom, it have 5 horizontal lines,
     * <br> 1 vertical line in the horizontal center.
     * </p>
     *
     * @param canvas    canvas
     * @param viewWidth the view's width
     * @param item      the view's height divided into 410
     */
    private void drawLines(Canvas canvas, int viewWidth, float item) {
        mPaint.setColor(Color.parseColor("#AAAAAA"));
        mPaint.setStrokeWidth(0f);
        canvas.drawLine(0, item * 10, viewWidth, item * 10, mPaint);
        canvas.drawLine(0, item * 30, viewWidth, item * 30, mPaint);
        drawDashEffect(canvas, 0, item * 190, viewWidth, item * 190);
        canvas.drawLine(0, item * 360, viewWidth, item * 360, mPaint);
        canvas.drawLine(0, item * 380, viewWidth, item * 380, mPaint);
        canvas.drawLine(viewWidth / 2.0f, item * 10, viewWidth / 2.0f, item * 380, mPaint);
    }
    
    /**
     * draw a doted line
     *
     * @param canvas canvas
     * @param x      startX
     * @param y      startY
     * @param endX   endX
     * @param endY   endY
     */
    private void drawDashEffect(Canvas canvas, float x, float y, float endX, float endY) {
        PathEffect effects = new DashPathEffect(new float[]{8, 8, 8, 8}, 1);
        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
        p.setColor(Color.parseColor("#AAAAAA"));
        p.setPathEffect(effects);
        p.setStyle(Paint.Style.STROKE);
        Path path = new Path();
        path.moveTo(x, y);
        path.lineTo(endX, endY);
        canvas.drawPath(path, p);
    }

    绘制时间.

    时间的最简单,三个时间是固定的,位置也是固定的.

    需要注意的是,绘制文字的x,y坐标,x=文字的左边,y=文字的baseline,文字的baseline默认等于-mPaint.getFontMetrics().top

    想了解更多关于文字绘制的细节,请移步到这篇文章StyleTextView

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewHeight = getHeight();
        int viewWidth = getWidth();
        float item = viewHeight / 410f;
    
        /**
         * draw time
         */
        drawTimes(canvas, viewWidth, item);
    }
    
    /**
     * draw times
     * <br><br>
     * draw text method:
     * <p>params: 1:content, 2:x, 3: the baseline</p>
     * <br><b>Note:the baseline == -mPaint.getFontMetrics().top in default</b>
     * <br><br><b>More information, please
     * <a >click this</a></b>
     *
     * @param canvas    canvas
     * @param viewWidth view's width
     * @param item      the view's height divided into 410
     */
    private void drawTimes(Canvas canvas, int viewWidth, float item) {
        mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f,
                getResources().getDisplayMetrics()));
        mPaint.setColor(Color.parseColor("#999999"));
        float textWidth = mPaint.measureText("09:30");
        canvas.drawText("09:30", item * 10, -mPaint.getFontMetrics().top + item * 380, mPaint);
        canvas.drawText("11:30", viewWidth / 2.0f - textWidth / 2.0f, -mPaint.getFontMetrics()
                .top + item * 380, mPaint);
        canvas.drawText("15:00", viewWidth - textWidth - item * 10, -mPaint.getFontMetrics().top
                + item * 380, mPaint);
    }

    绘制折线,以及折线的阴影面积.

    转到canvas上来说,其实就是绘制路径,在前面绘制横线的时候,绘制虚线其实就是绘制路径.
    注意绘制阴影的时候,要把画笔设置为实心的,这样才会有阴影的效果,同时路径path要多连接几个点,包括右下角,左下角,表明折线下方,第五条横线上方,就是阴影部分.

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewHeight = getHeight();
        int viewWidth = getWidth();
        float item = viewHeight / 410f;
    
        /**
         * draw broken line and shadow graph
         */
        drawBrokenLine(canvas, viewWidth, item, "#504F76DB", Paint.Style.FILL);
        drawBrokenLine(canvas, viewWidth, item, "#4F76DB", Paint.Style.STROKE);
    }
    
    /**
     * draw broken line
     *
     * @param canvas    canvas
     * @param viewWidth view's width
     * @param item      the view's height divided into 410
     * @param color     paint color
     * @param style     paint style,FILL: draw shadow, STROKE:draw line
     */
    private void drawBrokenLine(Canvas canvas, int viewWidth, float item, String color, Paint
            .Style style) {
        Path path = new Path();
        Paint paint = new Paint();
        float xItem = viewWidth / 2.0f / 120f;
    
        // get biggest  difference value, it will be calculated proportion
        float yCount = maxPrice - baseData > baseData - minPrice ? maxPrice - baseData : baseData
                - minPrice;
        //get one item height
        float yItem = 330 * item / yCount / 2.0f;
    
        //set path start point,item * 195 is baseData's y point.
        path.moveTo(0, item * 195);
        //set other points
        for (int i = 0; i < times.size(); i++) {
            path.lineTo(xItem * (i + 1), item * 195 + yItem * (baseData - prices.get(i)));
        }
        //if draw shadow, we should add 3 points to draw a complete graphics.
        //if draw lines, we should let lines bold.
        if (Paint.Style.FILL == style) {
            path.lineTo(viewWidth, item * 380);
            path.lineTo(0, item * 380);
            path.lineTo(0, item * 195);
            path.close();
        } else {
            paint.setStrokeWidth(2f);
        }
        paint.setColor(Color.parseColor(color));
        paint.setAntiAlias(true);
        paint.setStyle(style);
        canvas.drawPath(path, paint);
    }

    绘制最高点,最低点,以及百分比.

    有了绘制时间的经验,我们知道x,y分别代表的是文字的左下角,baseline,直接绘制即可.

    绘制最低点的时候需要注意,最低点距离第四条横线的距离,应该与第二条线距离最高点的距离一致.放大雪球股票的图,发现其K线图,以及后面要绘制的蜡烛图,这
    两个距离都不相等,虽然无伤大雅.但是如果我们能做到,那就更好不过.

    凯哥live中说到,设计或者产品出来一个交互,一个需求,你做不到,没什么关系,因为别人也做不到.但是假设别人做不到,但是你做到了,那么很明显,你就强于别人

    在前面绘制文字的时候提到过这篇文章StyleTextView,发布到郭霖的公众号后,有部分同学说,为什么这么麻烦搞这么多,感觉不需要这么复杂.
    实际上如果只是单纯做一个需求,确实不需要多复杂的代码,直接绘制是最简单的,但是绘制也涉及到留白的问题,在一个要求不是特别精确的View,一两个像素的差距,确实可有可无,甚至有同学直接根据
    实际运行出来的效果图,调整空白大小.

    但是你为什么调整空白大小,为什么要这么调,调了以后其他的机型适配吗?如果在一个很大的View上,字体大小很大,此时能保证也能满足正常视觉吗?

    故有时候追求一些细节,对自己的代码,以及技术,都是一种负责任的态度.

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewHeight = getHeight();
        int viewWidth = getWidth();
        float item = viewHeight / 410f;
    
        /**
         * draw max, min price and percent
         */
        drawPriceAndPercent(canvas, viewWidth, item);
    }
    
    /**
     * draw price and percent
     * <br><br>
     * draw text method:
     * <p>params: 1:content, 2:x, 3: the baseline</p>
     * <br><b>Note:the baseline == -mPaint.getFontMetrics().top in default</b>
     * <br><br><b>More information, please
     * <a >click this</a></b>
     *
     * @param canvas    canvas
     * @param viewWidth view's width
     * @param item      the view's height divided into 410
     */
    private void drawPriceAndPercent(Canvas canvas, int viewWidth, float item) {
        // get biggest  difference value, it will be calculated proportion
        float yCount = maxPrice - baseData > baseData - minPrice ? maxPrice - baseData : baseData
                - minPrice;
        mPaint.setStrokeWidth(2f);
    
    
        mPaint.setColor(Color.RED);
        //draw max price
        canvas.drawText(yCount + baseData + "", item * 10, -mPaint.getFontMetrics().top + item *
                30, mPaint);
        String percentStr = formatPrice(yCount * 100 / baseData) + "%";
        float textWidth = mPaint.measureText(percentStr);
        //draw max percent
        canvas.drawText(percentStr, viewWidth - textWidth - item * 10, -mPaint.getFontMetrics()
                .top + item * 30, mPaint);
    
    
        mPaint.setColor(Color.parseColor("#008000"));
        //draw min price
        canvas.drawText(baseData - yCount + "", item * 10, item * 360 - (mPaint.getFontMetrics()
                .descent - mPaint.getFontMetrics().ascent - mPaint.getTextSize() + mPaint
                .getFontMetrics().ascent - mPaint.getFontMetrics().top), mPaint);
        percentStr = "-" + percentStr;
        textWidth = mPaint.measureText(percentStr);
        //draw min percent
        canvas.drawText(percentStr, viewWidth - textWidth - item * 10, item * 360 - (mPaint
                .getFontMetrics().descent - mPaint.getFontMetrics().ascent -
                mPaint.getTextSize() + mPaint.getFontMetrics().ascent - mPaint.getFontMetrics()
                .top), mPaint);
    }

    至此,绘制基本已经结束了,直接运行,就能看到一个基本K线图,但是还差K线图的交互,也就是长按K线图的交互,这其实就是一个触摸反馈的过程

    Android开发一步步教你画股票图,K线图,高仿雪球股票

    触摸

    网上有很多的触摸文章教程,这里就不展开篇幅讲解了,这里直接使用手势识别类:GestureDetector

    但是实际使用发现,假设手指长按了,就不能再接收到
    手指的移动事件,看GestureDetector发现,如果它判断是长按就直接break了,同时发现它也没有发送手指离开屏幕的事件,这都不是我想要的,所以我就把它源码直接复制出来了,删掉了一些用不到的事件,并
    添加了手指离开事件.第二个手指按下,离开事件.

    boolean onDown2(MotionEvent e);
    boolean onUp2(MotionEvent e);
    boolean onUp(MotionEvent e);

    添加手势触摸监听,首先在init初始化GestureDetector,并在onTouch中拦截触摸事件

    //初始化
     private void init() {
            detector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {
    
                @Override
                public void onLongPress(MotionEvent e) {
                    showTouchLine(e.getRawX());
                    Log.e("onLongPress", getActionName(e));
                }
    
    
                @Override
                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float
                        distanceY) {
                    Log.e("onScroll", getActionName(e2) + "  Y: " + distanceY + "  e2: " + e2.getRawY
                            ());
                    if (e2.getAction() == MotionEvent.ACTION_MOVE &amp;&amp; longPressFlag) {
                        showTouchLine(e2.getRawX());
                    }
                    return true;
                }
    
    
                @Override
                public boolean onDown2(MotionEvent e) {
                    return false;
                }
    
                @Override
                public boolean onUp2(MotionEvent e) {
                    return false;
                }
    
    
                @Override
                public boolean onUp(MotionEvent e) {
                    Log.e("onUp", getActionName(e));
                    hideTouchLine();
                    return true;
                }
    
                @Override
                public boolean onDown(MotionEvent e) {
    //                Log.e("onDown", getActionName(e));
                    return true;
                }
    
                @Override
                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
                        velocityY) {
    //                Log.e("onFling", getActionName(e2));
                    return true;
                }
    
                @Override
                public boolean onSingleTapUp(MotionEvent e) {
                    Log.e("onSingleTapUp", getActionName(e));
                    return true;
                }
    
                @Override
                public void onShowPress(MotionEvent e) {
    //                Log.e("onShowPress", getActionName(e));
                }
            });
    }
    
    
    //拦截触摸事件
    @Override
       public boolean onTouchEvent(MotionEvent event) {
           return detector.onTouchEvent(event);
       }
    
    
    
    /**
     * hide touch line
     */
    private void hideTouchLine() {
        touchIndex = -1;
        longPressFlag = false;
        if (touchMoveListener != null) {
            touchMoveListener.change("", "", "", "");
        }
        postInvalidate();
    }
    
    /**
     * show touch line
     */
    private void showTouchLine(float touchX) {
        longPressFlag = true;
        //根据触摸的坐标,计算当前被触摸的indext
        float itemX = (float) getWidth() / prices.size();
        for (int i = 1; i <= prices.size(); i++) {
            if (itemX * i >= touchX) {
                touchIndex = i - 1;
                break;
            }
        }
        //绘制触摸线
        postInvalidate();
        //交给外部的触摸回调监听
        if (touchMoveListener != null &amp;&amp; touchIndex >= 0) {
            touchMoveListener.change(times.get(touchIndex), prices.get(touchIndex) + "",
                    formatPrice((prices.get(touchIndex) - baseData) / baseData * 100) + "%",
                    "4613.93万");
        }
    }
    
    
    //onDraw处理触摸事件
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewHeight = getHeight();
        int viewWidth = getWidth();
        float item = viewHeight / 410f;
    
        /**
         * draw touch lines and point
         */
        drawTouchLines(canvas, viewWidth, item);
    }
    
    /**
     * draw touch lines and point
     *
     * @param canvas    canvas
     * @param viewWidth view's width
     * @param item      the view's height divided into 410
     */
    private void drawTouchLines(Canvas canvas, int viewWidth, float item) {
        if (longPressFlag) {
            // get biggest  difference value, it will be calculated proportion
            float yCount = maxPrice - baseData > baseData - minPrice ? maxPrice - baseData :
                    baseData - minPrice;
            float xItem = viewWidth / 2.0f / 120f;
            float yItem = 330 * item / yCount / 2.0f;
            float x = xItem * (touchIndex + 1);
            float y = item * 195 + yItem * (baseData - prices.get(touchIndex));
    
            //draw the lines
            mPaint.setColor(Color.parseColor("#999999"));
            canvas.drawLine(0, y, viewWidth, y, mPaint);
            canvas.drawLine(x, item * 10, x, item * 380, mPaint);
    
            //draw the point
            mPaint.setColor(Color.parseColor("#FFC125"));
            mPaint.setStrokeWidth(10f);
            canvas.drawPoint(x, y, mPaint);
        }
    }

    分时图总结

    至此,分时图的布局,绘制,触摸反馈都已经完整,如果再加上设置数据的方法,就可以作为一个基本的分时图使用了.
    详细代码请点击:(/app/src/main/java/com/siyehua/klinegraph/KLineView.java" target="_blank">KLineView

    蜡烛图

    布局

    层次

    • 第1层:横竖刻度线

    • 第2层:股票点,时间

    • 第3层:蜡烛,以及MA线(MA其实就是绘制折线,这个Demo中没有绘制)

    结构

    蜡烛图的结构相对复杂,首先是,数据是从右往左的呈现的,最右边是最新的数据,越往左时间越久.

    其次,蜡烛图没有昨天收盘的时候的股票点,也就是它没有基准线,它的涨跌情况都是与前一天对比.所有它的刻度范围是不固定的,需要根据
    当前呈现的数据,动态计算它的最高点和最低点

    绘制

    开始

    创建类,并初始化,并构建测试数据方便调试

    public class CandleView extends View {
        public CandleView(Context context) {
            super(context);
            init();
        }
    
        public CandleView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public CandleView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        public CandleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            init();
        }
    }
    
    
    private void init() {
    
        candles = new ArrayList<>();
        mPaint = new Paint();
        createTestData();
    }
    
    /**
     * create test data
     */
    private void createTestData() {
        //create 4 months data
        Date date = new Date();
        @SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat
                ("yyyy-MM-dd HH:mm:ss");
        Float todayStart = 3150.10f;
        for (int i = 0; i < 1200; i++) {
            Candle candle = new Candle();
            date.setTime(date.getTime() - 24L * 60L * 60L * 1000L);
              candle.time = formatTime(dateFormat.format(date));
            candle.time = dateFormat.format(date);
            if (i == 0) candle.start = todayStart;
            else
                candle.start = formatPrice((float) (candles.get(i - 1).end + 100 - Math.random()
                        * 200));
            candle.end = formatPrice((float) (candle.start + candle.start * 0.05 - Math.random()
                    * candle.start * 0.1));
            float tmp = formatPrice((float) (candle.start * 0.05 - Math.random() * candle.start *
                    0.1));
            candle.max = formatPrice(candle.start + (tmp < 0 ? 0 : tmp));
            tmp = formatPrice((float) (candle.start * 0.05 - Math.random() * candle.start * 0.1));
            candle.min = formatPrice(candle.start + (tmp > 0 ? 0 : tmp));
            candles.add(candle);
        }
        for (int i = 0; i < candles.size(); i++) {
            float total = 0f;
            if (i < candles.size() - 5) {
                for (int j = i; j < i + 5; j++) {
                    total += candles.get(j).end;
                }
                candles.get(i).ma5 = total / 5;
            } else {
                candles.get(i).ma5 = candles.get(i).end;
            }
            total = 0f;
            if (i < candles.size() - 10) {
                for (int j = i; j < i + 10; j++) {
                    total += candles.get(j).end;
                }
                candles.get(i).ma10 = total / 10;
            } else {
                candles.get(i).ma10 = candles.get(i).end;
            }
            total = 0f;
            if (i < candles.size() - 20) {
                for (int j = i; j < i + 20; j++) {
                    total += candles.get(j).end;
                }
                candles.get(i).ma20 = total / 20;
            } else {
                candles.get(i).ma20 = candles.get(i).end;
            }
        }
    
    //        for (Candle candle : candles) {
    //            Log.e("item", candle.toString());
    //        }
    }

    绘制横竖线,并绘制刻度,时间

    蜡烛图的高度与分时图一致,总体高度410,第1条线距离顶部是10,View可绘制高度是370.

    在绘制刻度之前,要先计算出当前展示的数据的最小值,最大值,以及刻度的比例,才能根据比例来绘制刻度.

    /**
     * calculate min and max y,the scale y.
     */
    private void getYData() {
        //计算最大值与最小值
        maxPrice = 0;
        minPrice = Float.MAX_VALUE;
        for (int i = startIndex; i < startIndex + count; i++) {
            if (candles.get(i).start > maxPrice) maxPrice = candles.get(i).start;
            if (candles.get(i).start < minPrice) minPrice = candles.get(i).start;
            if (candles.get(i).end > maxPrice) maxPrice = candles.get(i).end;
            if (candles.get(i).end < minPrice) minPrice = candles.get(i).end;
            if (candles.get(i).max > maxPrice) maxPrice = candles.get(i).max;
            if (candles.get(i).max < minPrice) minPrice = candles.get(i).max;
            if (candles.get(i).min > maxPrice) maxPrice = candles.get(i).min;
            if (candles.get(i).min < minPrice) minPrice = candles.get(i).min;
        }
        //根据最大值最小值的,来计算刻度的最高点,最低点
        yScale = 1;
        int diff = (int) (maxPrice - minPrice);
        if (diff / 100000 >= 1) {
            yScale = 100000;
            minY = (int) minPrice / 100000 * 100000;
            maxY = ((int) maxPrice / 100000 + 1) * 100000;
        } else if (diff / 10000 >= 1) {
            yScale = 10000;
            minY = (int) minPrice / 10000 * 10000;
            maxY = ((int) maxPrice / 10000 + 1) * 10000;
        } else if (diff / 1000 >= 1) {
            yScale = 1000;
            minY = (int) minPrice / 1000 * 1000;
            maxY = ((int) maxPrice / 1000 + 1) * 1000;
        } else if (diff / 100 >= 1) {
            yScale = 100;
            minY = (int) minPrice / 100 * 100;
            maxY = ((int) maxPrice / 100 + 1) * 100;
        } else if (diff / 10 >= 1) {
            yScale = 10;
            minY = (int) minPrice / 10 * 10;
            maxY = ((int) maxPrice / 10 + 1) * 10;
        }
          Log.e("siyehua", maxPrice + "  " + minPrice + "  " + maxY + "  " + minY + "  " +
                  yScale + "  " + "  ");
    }

    计算好Y轴刻度后,则开始绘制

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewHeight = getHeight();
        int viewWidth = getWidth();
        float itemW = (float) viewWidth / count;
        float itemH = viewHeight / 410f;
        drawLinesAndText(canvas, viewWidth, viewHeight, itemW, itemH);
    }
    
    /**
     * draw lines and text
     *
     * @param canvas     canvas
     * @param viewWidth  the view's width
     * @param viewHeight the view's height
     * @param itemW      the view's wight divided into count
     * @param itemH      the view's height divided into 410
     */
    private void drawLinesAndText(Canvas canvas, int viewWidth, int viewHeight, float itemW,
                                  float itemH) {
        mPaint.setColor(Color.parseColor("#AAAAAA"));
        mPaint.setStrokeWidth(0f);
        mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f,
                getResources().getDisplayMetrics()));
        /**
         * draw x lines and price text
         */
        getYData();
        int lineCount = (maxY - minY) / yScale;
        if (lineCount > 5) {//假设线条超过5条,则把刻度大小翻倍
            yScale *= 2;
            lineCount = (maxY - minY) / yScale;
        }
        //draw first line
        canvas.drawLine(0, itemH * 10, viewWidth, itemH * 10, mPaint);
        float percent = 370 / (float) lineCount;
        for (int i = 1; i < lineCount; i++) {
            //draw prices
            String content = minY + (lineCount - i) * yScale + "";
            canvas.drawText(content, itemH * 10, itemH * (10 + percent * i) - mPaint
                    .getFontMetrics().bottom, mPaint);
            //draw middle lines
            canvas.drawLine(0, itemH * (10 + percent * i), viewWidth, itemH * (10 + percent * i),
                    mPaint);
        }
        //draw last line
        canvas.drawLine(0, itemH * 380, viewWidth, itemH * 380, mPaint);
        /**
         * draw y lines and time
         */
        String tmpMonth = candles.get(startIndex).time.substring(5, 7);
        for (int i = startIndex + 1; i < startIndex + count; i++) {
            //假设明天的时间与今天不一样,说明一个月的间隔到了,则绘制一条竖线
            if (!tmpMonth.equals(candles.get(i + 1).time.substring(5, 7))) {
                tmpMonth = candles.get(i + 1).time.substring(5, 7);
                String timeStr = candles.get(i).time.substring(0, 7);
                //注意数据是从右到左呈现的,所有要从右边开始计算坐标
                float tmp = itemW * (count + startIndex - i) - itemW / 2;
                //draw times
                float timeWidth = mPaint.measureText(timeStr);
                canvas.drawText(timeStr, tmp - timeWidth / 2, itemH * 380 + -mPaint
                        .getFontMetrics().top, mPaint);
                //draw liens
                canvas.drawLine(tmp, itemH * 10, tmp, itemH * 380, mPaint);
            }
        }
    }

    绘制蜡烛

    绘制好线与文字,就可以绘制蜡烛了.蜡烛图在View的视觉上占了很大的份量,但是绘制的时候,实际上很简单,当刻度计算好后,只需要绘制一条线,以及一个实心的矩形即可.

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int viewHeight = getHeight();
        int viewWidth = getWidth();
        float itemW = (float) viewWidth / count;
        float itemH = viewHeight / 410f;
        drawCandles(canvas, viewWidth, viewHeight, itemW, itemH);
    }
    
    
    /**
     * draw candles
     *
     * @param canvas     canvas
     * @param viewWidth  the view's width
     * @param viewHeight the view's height
     * @param itemW      the view's wight divided into count
     * @param itemH      the view's height divided into 410
     */
    private void drawCandles(Canvas canvas, int viewWidth, int viewHeight, float itemW, float
            itemH) {
        mPaint.setStrokeWidth(2f);
        mPaint.setStyle(Paint.Style.FILL);
        String tmpMonth = candles.get(startIndex).time.substring(5, 7);
    
        //绘制每一个蜡烛
        for (int i = startIndex; i < startIndex + count; i++) {
            //set paint color
            if (candles.get(i).end > candles.get(i + 1).end) {
                mPaint.setColor(Color.RED);
            } else mPaint.setColor(Color.GREEN);
    
    
            float left, top, right, bottom;
    
            //绘制蜡烛灯芯,注意从右往左开始计算
            //draw line
            float tmp = itemW * (count + startIndex - i) - itemW / 2;
            left = tmp;
            top = ((maxY - candles.get(i).max) / (maxY - minY) * 370 + 10) * itemH;
            right = tmp;
            bottom = ((maxY - candles.get(i).min) / (maxY - minY) * 370 + 10) * itemH;
            if (top > bottom) {
                float a = top;
                top = bottom;
                bottom = a;
            }
            canvas.drawLine(left, top, right, bottom, mPaint);
    //          Log.e("siyehua", tmp + ", " + (((maxY - candles.get(i).start) / (maxY - minY) * 370 +
    //                  10) * itemH) + ", " + tmp + ", " + (((maxY - candles.get(i).end) / (maxY -
    //                  minY) * 370 + 10) * itemH));
    
            //绘制蜡烛,注意从右往左开始计算
            //draw candles
            left = itemW * (count - 1 + startIndex - i) + 2f;
            top = ((maxY - candles.get(i).start) / (maxY - minY) * 370 + 10) * itemH;
            right = itemW * (count + startIndex - i) - 2f;
            bottom = ((maxY - candles.get(i).end) / (maxY - minY) * 370 + 10) * itemH;
            if (top > bottom) {
                float a = top;
                top = bottom;
                bottom = a;
            }
            canvas.drawRect(left, top, right, bottom, mPaint);
        }
    
    }

    绘制其他元素

    雪球股票的蜡烛图,还有三条折线,以及左上角的文字提示.这些在分时图已经讲解了如何绘制了,只需要依样画葫芦即可.

    因为蜡烛图的复杂不在于它的绘制,而在于它的触摸反馈,所以这里把更多的精力花在触摸反馈的处理上.

    触摸

    初始化触摸类

    private void init() {
        detector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown2(MotionEvent e) {
            //第2跟手指按下
                Log.e("onDown2", e.getX(1) + "");
                flag1 = flag2 = false;
                type = 2;
                down2X = e.getX(1);
                return true;
            }
    
            @Override
            public boolean onUp2(MotionEvent e) {
            //第2根手指抬起
                flag2 = true;
                if (flag1) type = 1;
                return true;
            }
    
            @Override
            public boolean onUp(MotionEvent e) {
            //第1根手指抬起
                if (type == 2) {
                    flag1 = true;
                    if (flag2) type = 1;
                } else hideTouchLine();
                return true;
            }
    
            @Override
            public boolean onDown(MotionEvent e) {
            //第1根手指按下
                Log.e("onDown", e.getRawX() + "");
    
                type = 1;
                downIndex = startIndex;
                downX = e.getX(0);
                return true;
            }
    
    
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float
                    distanceY) {
                //16ms刷新一次,因为人的眼睛能反应过来的最快速度是16ms,而过快的刷新会造成内存上升,以及View闪烁
                //16ms refresh one time,because man resolution 16ms only.
                if (refreshFlag) {
                    refreshFlag = false;
                    handler.sendEmptyMessageDelayed(10086, 15);
                    if (type == 1) {
                    //单根手指刷新
                        showTouchLine(e2.getRawX());
                    } else {//多根手指刷新
                        if (e2.getPointerCount() >= 2) {
                            float moveDistance = Math.abs(e2.getX(0) - e2.getX(1)) - Math.abs
                                    (downX - down2X);
                            Log.e("onScroll", Math.abs(e2.getX(0) - e2.getX(1)) + "   " + Math
                                    .abs(downX - down2X) + "   " + moveDistance + "    " + getWidth()
                                    / 30);
                            scaleCandle(moveDistance);
                        }
                    }
                }
                return true;
            }
    
            @Override
            public void onLongPress(MotionEvent e) {
                if (type == 1) {
                    longPressFlag = true;
                    showTouchLine(e.getRawX());
                }
            }
    
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }
    
            @Override
            public void onShowPress(MotionEvent e) {
    
            }
    
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
                    velocityY) {
                return true;
            }
        });
    
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return detector.onTouchEvent(event);
    }

    单根手指触摸

        /**
         * show touch line
         */
        private void showTouchLine(float touchX) {
            float itemX = (float) getWidth() / count;
    
            if (longPressFlag) {//长按,与分时图一个效果,先获取触摸的index,再刷新界面
                for (int i = 1; i <= count; i++) {
                    if (itemX * i >= touchX) {
                        touchIndex = i + 1;
                        break;
                    }
                }
                if (touchMoveListener != null &amp;&amp; touchIndex >= 0) {
                    touchMoveListener.change(candles.get(count + startIndex - touchIndex).time
                            .substring(0, 10), candles.get(count + startIndex - touchIndex).end + "",
                            formatPrice((candles.get(count + startIndex - touchIndex).end - candles
                                    .get(count + startIndex - touchIndex + 1).end) / candles.get
                                    (count + startIndex - touchIndex + 1).end * 100) + "%", "4613" +
                                    ".93万");
    
                }
            } else {//左右滑动,左右滑动,View不需要滑动,只需要修改startIndex的值,即达到滑动的效果
                int number = (int) ((touchX - downX) / itemX);
    //            Log.e("number", number + "");
                startIndex = downIndex + number;
                if (startIndex < 0) startIndex = 0;
                if (startIndex > candles.size() - count - 1) startIndex = candles.size() - count - 1;
            }
            postInvalidate();
        }
    
        /**
         * draw lines and text
         *
         * @param canvas     canvas
         * @param viewWidth  the view's width
         * @param viewHeight the view's height
         * @param itemW      the view's wight divided into count
         * @param itemH      the view's height divided into 410
         */
        private void drawTouchLines(Canvas canvas, int viewWidth, int viewHeight, float itemW, float
                itemH) {
            if (longPressFlag) {//长按
                float x = itemW * touchIndex - itemW / 2;
                float y;
                float a = ((maxY - candles.get(count + startIndex - touchIndex).start) / (maxY -
                        minY) * 370 + 10) * itemH;
                float b = ((maxY - candles.get(count + startIndex - touchIndex).end) / (maxY - minY)
                        * 370 + 10) * itemH;
                if (candles.get(count + startIndex - touchIndex).end < candles.get(count + startIndex
                        - touchIndex + 1).end) {
                    y = a > b ? a : b;
                } else y = a < b ? a : b;
    
    
                //draw the lines
                mPaint.setColor(Color.parseColor("#999999"));
                canvas.drawLine(0, y, viewWidth, y, mPaint);
                canvas.drawLine(x, itemH * 10, x, itemH * 380, mPaint);
    
                //draw the point
    //            mPaint.setColor(Color.parseColor("#FFC125"));
    //            mPaint.setStrokeWidth(10f);
    //            canvas.drawPoint(x, y, mPaint);
            }
        }

    多根手指缩放

    屏幕默认分为60根蜡烛,缩放后,只需要修改默认的蜡烛数目即可.

        private void scaleCandle(float moveDistance) {
            if (moveDistance > getWidth() / 30) {
    
                if (count == 20) count = 10;
                else if (count == 10) return;
                else count -= 20;
            } else if (moveDistance < -getWidth() / 30) {
                if (count == 240) return;
                else count += 20;
            }
            postInvalidate();
        }

    蜡烛图总结

    蜡烛图的需求,功能基本上已经实现,除了三条折线没有绘制.只需要再添加设置数据方法,该类便可直接使用.

    总结

    本文从布局,绘制,触摸,三个方面讲解了分时图,蜡烛图的一步步实现过程.其实股票图的业务上还有很多是没有讲解到的,例如大家应该都有注意到截图中
    下面有柱状图,其实这个应该也是图的一部分,还有一些高亮显示等问题.

    但其实基本的原理都是不变的,一个View的三个方面都有详细的讲解到,涉及到更多业务逻辑,无非也就是在这个基础上,绘制自己业务想要的效果,万变不离其宗,只要掌握好方法,再复杂的图也信手捏来.

    之所以要画这两个图,是因为之前有人问博主有没有别人写好的能直接用的股票图,搜了好久没发现有比较成熟的Android股票图绘制,有的都必须在项目的基础上改造,而且BUG也比较多

    最后在CSDN看了一个教程一步一步教你写股票走势图,
    这个教程的绘制是依赖另外一库的基础上改造的,而且也不是很成熟.作者花了大量的篇幅,主要问题并不是教大家如何绘制,而是教大家如何处理绘制股票图中遇到的一些问题,
    例如数组越界了等一些实质性的问题,最后还把这个系列商用了.如果大家在股票图的应用中遇到了一下问题,也可以参考这个系列的教程

    就目前来讲,Android暂时还没有一个较为成熟的股票图项目可以依赖,而使用半成熟的项目,本身就会遇到许多bug,与其花精力去修改别人的bug,不如自己编写一个.

    博主本人公司并没有涉及到股票图的绘制,个人对股票也不是很熟悉,以上所有的讲解都是个人理解,难免会有错误,欢迎大家留言交流指正.
    项目下载:https://github.com/siyehua/KLineGraph

    Android开发一步步教你画股票图,K线图,高仿雪球股票转载http://www.codesocang.com/appboke/39203.html
    标签:网站源码