行情K线图也就是我们常说的蜡烛图,是金融类软件里可以说必不可少的,无论日K, 周K,月K,还是分钟K,准确的来表达个股在一定时间内涨跌走势,K线图有着不可无视的作用,其绘制过程也是彰显一个程序员对自定义控件的熟练程度,尤其是对Canvas的灵活运用,绘线,绘边框,及位置的选取,比例的分配,今天这个Demo,则一步步为你诠释。

按惯例,先看下今天要实现的效果,整个Demo地址为:http://download.csdn.net/detail/ming_147/9732963,也可以关注公众号后(评论区第一条评论扫描即可)回复“行情k线图”,源码就会发送给您,公众号有很多android及其它技术文章,还请大家承蒙关注。

相对来说比较简单的一个小Demo,为什么来说简单呢,一数据是固定的,二,时间是固定的,相比较实际项目中来说,这已经相当的简单了,我们可以简单的分一下步骤模块,然后再按照依次来进行实现,通过上面的图片,我们可以大致分为,边框,横线,纵线,底部时间,左边刻度,柱状图(蜡烛图),十字光标这几个部分,好,分好之后,我们就来一步步实现吧。

由于代码稍多,为显得代码结构清晰,我们可以先写一个父类,用于实现边框,横纵线,及底部时间,左部刻度的绘制,柱状图(蜡烛图)及十字光标我们放在子类中实现。

自定义一个父类继承于View,实现其构造方法,在onMeasure方法里设置View的大小:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(measureWidth(widthMeasureSpec),
            measureHeight(heightMeasureSpec));
}

private int measureWidth(int measureSpec) {
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);//得到模式
    int specSize = MeasureSpec.getSize(measureSpec);//得到尺寸

if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else if (specMode == MeasureSpec.AT_MOST) {
        result = Math.min(result, specSize);
    }
    return result;
}

private int measureHeight(int measureSpec) {
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else if (specMode == MeasureSpec.AT_MOST) {
        result = Math.min(result, specSize);
    }
    return result;
}

这里简单对两个类型做下解释:

MeasureSpec.EXACTLY是精确尺寸,当我们将控件的layout_width或layout_height指定为具体数值时如:andorid:layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。

MeasureSpec.AT_MOST是最大尺寸,当控件的layout_width或layout_height指定为WRAP_CONTENT时,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。

设置完大小之后,我们先在构造方法里初始化一些信息,比如背景色,画笔:

/**
 * 设置背景色及初始化画笔
 */
private void init() {
    setBackgroundColor(Color.parseColor("#222222"));
    mPaint = new Paint();
    mPaint.setStrokeWidth(1);
    mPaint.setStyle(Paint.Style.STROKE);
}

重写onDraw方法,并在方法内绘制相关信息。绘制边框,距离左上各位10,距离右边为View宽度-10,距离底部为View高度-50:

private void drawBorder(Canvas canvas) {
    mPaint.setColor(Color.WHITE);
    Rect r = new Rect();
    r.left = 10;
    r.top = 10;
    r.right = this.getRight() - 10;
    r.bottom = this.getHeight() - 50;
    canvas.drawRect(r, mPaint);
}

绘制横线,因为要留出一段区域做刻度绘制,所以,距离左边要有一段距离,这里我设置的100,所以每条横线的起始位置一定,都是100,因为边框的最右边为View宽度-10,所以横线的终止位置也是一致,起始y的位置和终止y的位置应当一致,按照一定的距离等分开来,这里的lineSize是要分成几份,我定义的是4份,则每份的长度就为:(当前View的高度-距离底部的距离-距离上部的距离)/lineXSize:

private int lineXSize = 4;

private void drawXLine(Canvas canvas) {
    mPaint.setColor(Color.WHITE);
    float height = (this.getHeight() - 10f - 50f) / lineXSize;//平均分为几分
    for (int a = 0; a < lineXSize; a++) {
        float h = height * a + 10f;
        Log.i("BaseLine", h + "===");
        canvas.drawLine(100f, h, this.getRight() - 10f, h, mPaint);
    }
}

绘制纵线,其原理和绘制横线差不多,起始x的位置为距离左边的距离既100,则每份的宽度就是,(当前View的宽度-距离左边的距离-距离右边的距离)/要分为几份,这里我定义的是lineYSize=3:

private int lineYSize = 3;

private void drawYLine(Canvas canvas) {
    mPaint.setColor(Color.WHITE);
    float width = (this.getRight() - 10f - 100f) / lineYSize;
    for (int a = 0; a < lineYSize; a++) {
        float w = width * a + 100f;
        canvas.drawLine(w, 10f, w, this.getHeight() - 50f, mPaint);
    }
}

绘制底部时间,times是自己定义的一个时间数组,其坐标位置和纵线类似,y值是固定不变的,x轴增加的距离和纵线一致:

private int[] times = {5, 6, 7, 8};

private void drawTimes(Canvas canvas) {
    mPaint.setColor(Color.parseColor("#FF00FF"));
    mPaint.setTextSize(24);
    float width = (this.getRight() - 10f - 100f) / lineYSize;
    for (int a = 0; a < lineYSize + 1; a++) {
        float w = width * a + 100f;
        if (a == lineYSize) {
            canvas.drawText(times[a] + "月", w - 30f, this.getHeight() - 25f, mPaint);
        } else {
            canvas.drawText(times[a] + "月", w - 15f, this.getHeight() - 25f, mPaint);
        }
    }
}

绘制Y轴价格刻度,价格刻度的绘制,就和绘制横线有点类似了,price是自己定义的一个刻度数组:

private float[] price = {260f, 240f, 220f};

private void drawYPrice(Canvas canvas) {
    mPaint.setColor(Color.WHITE);
    float height = (this.getHeight() - 10f - 50f) / lineXSize;//平均分为几分
    for (int a = 1; a < lineXSize; a++) {
        float h = height * a + 10f;
        canvas.drawText(price[a - 1] + "", 40f, h, mPaint);
    }
}

经过以上父 类中的绘制,基本的边框,横线,纵线,底部时间,左部价格刻度,就完成了,接下来就是柱状图和十字光标:

自定义一个view集成于父类,实现其构造方法,初始化一些信息,设置画笔为实心的:

private void init() {
    mPaint = new Paint();
    mPaint.setStrokeWidth(1);
    mPaint.setStyle(Paint.Style.FILL);
}

绘制蜡烛图之前,我们需要初始化一些我们需要的数据,这里我定义了一个javaBean,里面我定义了一些数据,开盘,收盘,最高,最低,日期,实现其构造方法和get,set方法。

/**
 * 开盘价
 */
private float open;

/**
 * 最高价
 */
private float high;

/**
 * 最低价
 */
private float low;

/**
 * 收盘价
 */
private float close;

/**
 * 日期
 */
private int date;

javaBean实现之后,我们就可以添加模拟数据了,毕竟不是真实的项目中,所以数据,只能自己去创造了,listData是自己定义存储数据的:

protected List<StockLineBean> listData = new ArrayList<>();

/**
 * 添加数据
 */
private void setLineData() {
    List<StockLineBean> list = new ArrayList<StockLineBean>();
    list.add(new StockLineBean(250, 251, 248, 250, 20170731));
    list.add(new StockLineBean(249, 252, 248, 252, 20170730));
    list.add(new StockLineBean(250, 251, 248, 250, 20170729));
    list.add(new StockLineBean(249, 252, 248, 252, 20170728));
    list.add(new StockLineBean(248, 250, 247, 250, 20170727));
    list.add(new StockLineBean(256, 256, 248, 248, 20170726));
    list.add(new StockLineBean(257, 258, 256, 257, 20170725));
    list.add(new StockLineBean(259, 260, 256, 256, 20170724));
    list.add(new StockLineBean(261, 261, 257, 259, 20170723));
    list.add(new StockLineBean(259, 260, 256, 256, 20170722));
    list.add(new StockLineBean(261, 261, 257, 259, 20170721));
    list.add(new StockLineBean(260, 260, 259, 259, 20170720));
    list.add(new StockLineBean(262, 262, 260, 261, 20170719));
    list.add(new StockLineBean(260, 262, 259, 262, 20170718));
    list.add(new StockLineBean(259, 261, 258, 261, 20170717));
    list.add(new StockLineBean(255, 259, 255, 259, 20170716));
    list.add(new StockLineBean(259, 261, 258, 261, 20170715));
    list.add(new StockLineBean(255, 259, 255, 259, 20170714));
    list.add(new StockLineBean(258, 258, 255, 255, 20170713));
    list.add(new StockLineBean(258, 260, 258, 260, 20170712));
    list.add(new StockLineBean(259, 260, 258, 259, 20170711));
    list.add(new StockLineBean(261, 262, 259, 259, 20170710));
    list.add(new StockLineBean(261, 261, 258, 261, 20170709));
    list.add(new StockLineBean(261, 262, 259, 259, 20170708));
    list.add(new StockLineBean(261, 261, 258, 261, 20170707));
    list.add(new StockLineBean(261, 261, 259, 261, 20170706));
    list.add(new StockLineBean(257, 261, 257, 261, 20170705));
    list.add(new StockLineBean(256, 257, 255, 255, 20170704));
    list.add(new StockLineBean(257, 261, 257, 261, 20170703));
    list.add(new StockLineBean(256, 257, 255, 255, 20170702));
    list.add(new StockLineBean(253, 257, 253, 256, 20170701));
    list.add(new StockLineBean(255, 255, 252, 252, 20170630));
    list.add(new StockLineBean(256, 256, 253, 255, 20170629));
    list.add(new StockLineBean(254, 256, 254, 255, 20170628));
    list.add(new StockLineBean(247, 256, 247, 254, 20170627));
    list.add(new StockLineBean(244, 249, 243, 248, 20170626));
    list.add(new StockLineBean(244, 245, 243, 244, 20170625));
    list.add(new StockLineBean(244, 249, 243, 248, 20170624));
    list.add(new StockLineBean(244, 245, 243, 244, 20170623));
    list.add(new StockLineBean(242, 244, 241, 244, 20170622));
    list.add(new StockLineBean(243, 243, 241, 242, 20170621));
    list.add(new StockLineBean(246, 247, 244, 244, 20170620));
    list.add(new StockLineBean(248, 249, 246, 246, 20170619));
    list.add(new StockLineBean(251, 253, 250, 250, 20170618));
    list.add(new StockLineBean(248, 249, 246, 246, 20170617));
    list.add(new StockLineBean(251, 253, 250, 250, 20170616));
    list.add(new StockLineBean(249, 253, 249, 253, 20170615));
    list.add(new StockLineBean(248, 250, 246, 250, 20170614));
    list.add(new StockLineBean(249, 250, 247, 250, 20170613));
    list.add(new StockLineBean(254, 254, 250, 250, 20170612));
    list.add(new StockLineBean(254, 255, 251, 255, 20170611));
    list.add(new StockLineBean(254, 254, 250, 250, 20170610));
    list.add(new StockLineBean(254, 255, 251, 255, 20170609));
    list.add(new StockLineBean(252, 254, 251, 254, 20170608));
    list.add(new StockLineBean(250, 253, 250, 252, 20170607));
    list.add(new StockLineBean(251, 252, 247, 250, 20170606));
    list.add(new StockLineBean(253, 254, 252, 254, 20170605));
    list.add(new StockLineBean(250, 254, 250, 254, 20170604));
    list.add(new StockLineBean(251, 252, 247, 250, 20170603));
    list.add(new StockLineBean(253, 254, 252, 254, 20170602));
    list.add(new StockLineBean(250, 254, 250, 254, 20170601));
    list.add(new StockLineBean(250, 252, 248, 250, 20170531));
    list.add(new StockLineBean(253, 254, 250, 251, 20170530));
    list.add(new StockLineBean(255, 256, 253, 253, 20170529));
    list.add(new StockLineBean(256, 257, 253, 254, 20170528));
    list.add(new StockLineBean(255, 256, 253, 253, 20170527));
    list.add(new StockLineBean(256, 257, 253, 254, 20170526));
    list.add(new StockLineBean(256, 257, 254, 256, 20170525));
    list.add(new StockLineBean(265, 265, 257, 257, 20170524));
    list.add(new StockLineBean(265, 266, 265, 265, 20170523));
    list.add(new StockLineBean(267, 268, 265, 266, 20170522));
    list.add(new StockLineBean(264, 267, 264, 267, 20170521));
    list.add(new StockLineBean(267, 268, 265, 266, 20170520));
    list.add(new StockLineBean(264, 267, 264, 267, 20170519));
    list.add(new StockLineBean(264, 266, 262, 265, 20170518));
    list.add(new StockLineBean(266, 267, 264, 264, 20170517));
    list.add(new StockLineBean(264, 267, 263, 267, 20170516));
    list.add(new StockLineBean(266, 267, 264, 264, 20170515));
    list.add(new StockLineBean(269, 269, 266, 268, 20170514));
    list.add(new StockLineBean(266, 267, 264, 264, 20170513));
    list.add(new StockLineBean(269, 269, 266, 268, 20170512));
    list.add(new StockLineBean(267, 269, 266, 269, 20170511));
    list.add(new StockLineBean(266, 268, 266, 267, 20170510));
    list.add(new StockLineBean(264, 268, 263, 266, 20170509));
    list.add(new StockLineBean(265, 271, 267, 267, 20170508));
    list.add(new StockLineBean(265, 269, 265, 267, 20170507));
    list.add(new StockLineBean(265, 268, 265, 267, 20170506));
    list.add(new StockLineBean(271, 271, 266, 266, 20170505));
    list.add(new StockLineBean(271, 273, 269, 273, 20170504));
    list.add(new StockLineBean(268, 271, 268, 271, 20170503));
    list.add(new StockLineBean(268, 270, 266, 271, 20170502));
    list.add(new StockLineBean(268, 268, 263, 271, 20170501));
    for (int a = 0; a < list.size(); a++) {
        listData.add(0, list.get(a));
    }

}

有了数据,我们就可以绘制蜡烛图了,如果开盘大于昨收,蜡烛图就为红色,否则就为绿色,因为,纵轴起始位置是从200开始算的,所以我们取得的最大与最小值再计算的时候,要减去其起始位置,和线的宽度;

每个蜡烛图的高就为:(当前View的高度-距离上下的距离)-当前位置最高值*(当前View的高度-距离上下的距离)/要分的份数(这里是200到280共80份);

每个蜡烛图的低就为:(当前View的高度-距离上下的距离)-当前位置最低值 *(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天);

每个蜡烛图的左边就是:(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天)+当前View距离左边的距离*第几个蜡烛图;

每个蜡烛图的右边就是:蜡烛图的左边+(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天);

绘制蜡烛图的中间线,x 轴的起始位置都是:蜡烛图的左边+(当前View的宽度-距离左右的距离)/要分的份数(这里是3个月共92天)/2,y轴的起始位置为:每个蜡烛图的高-10,y轴的终止位置为:每个蜡烛图的低+10;

private void drawCandleSticks(Canvas canvas) {
    float ySize = (this.getHeight() - 60f) / 80f;
    float xSize = (this.getRight() - 110f) / 92;
    for (int a = 0; a < listData.size(); a++) {
        StockLineBean bean = listData.get(a);
        float high = bean.getHigh() - 201.6f;
        float low = bean.getLow() - 201.6f;
        float left = 100f + xSize * a;
        float o = bean.getOpen();
        float y = bean.getClose();
        if (y > o) {
            mPaint.setColor(Color.RED);
        } else {
            mPaint.setColor(Color.GREEN);
        }
        float top = (this.getHeight() - 60f) - high * ySize;
        float bottom = (this.getHeight() - 60f) - low * ySize;

canvas.drawRect(left, top, left + xSize, bottom, mPaint);
        //绘制中间线
        canvas.drawLine(left + xSize / 2, top - 10f, left + xSize / 2, bottom + 10f, mPaint);
    }
}

绘制十字光标,就需要重写onTouchEvent方法:

private float xMove, yMove;

@Override
public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            xMove = event.getX();
            yMove = event.getY();
            super.invalidate();
            break;
    }
    return true;

}

获取好位置之后,我们就可以在onDraw方法里绘制十字光标了,因为 View距离左边和底部有一定的距离,所以在这距离里,我们可以不设置十字光标,十字光标,两条线,一条横线,一条纵线:

横线:起始x轴的位置为当前View距离左边的距离,终止位置就是当前View宽度-10,起始和终止都是手指移动的y值;

纵线:起始x轴的位置就是手指移动的x坐标,起始y值为当前View距离上边的距离,终止y值就是当前View距离底部的距离;

左边变化刻度值:x值为固定的,我这里给出的是75,y坐标是移动的y值+3,其值的计算是:(当前View的高度-距离上下的距离-手指移动的y坐标)/(当前View的高度-距离上下的距离)/要分的份数(这里是200到280共80份)+初始位置刻度。

底部时间变化,x坐标为手指移动的x值-20,y坐标为当前View的高度-35,尽量在底部线的下面,值的计算是:先得到的索引,然后再从listData集合里取得时间。索引的计算方式为:(当前手指移动的x坐标/(当前 View的宽度-左右的距离及几根线的宽度)/总的天数)-(当前 View的宽度-左右的距离及几根线的宽度)/总的天数;

private void drawWithFingerClick(Canvas canvas) {
    float ySize = (this.getHeight() - 60f) / 80f;
    if (xMove < 100f || yMove > this.getBottom() - 50f) {
        mPaint.reset();
    } else {
        canvas.drawLine(100f, yMove, this.getRight() - 10, yMove, mPaint);
        canvas.drawLine(xMove, 10f, xMove, this.getBottom() - 50f, mPaint);
        float xWidth = ((this.getHeight() - 50f) - yMove) / ySize + 200f;
        String xContent = String.format("%.0f", xWidth);

canvas.drawText(xContent, 75f, yMove + 3f, mPaint);

float xSize = (this.getRight() - 125f) / 92;
        float timeSize = (xMove / xSize) - xSize;
        int size = (int) timeSize;
        if (size < listData.size()) {
            canvas.drawText(listData.get(size).getDate() + "", xMove - 20f, this.getHeight() - 35f, mPaint);
        }
    }
}

通过以上的代码我们就可以绘制出文章开头的效果了,具体使用,只需要在用到的Xml里引用就OK了:

<com.ming.abner.stockline.StockLineK
    android:layout_width="match_parent"
    android:layout_height="160dp" />

一步步搞定Android行情K线蜡烛图(带十字光标)相关推荐

  1. python蜡烛图预测_python tushare股票K线蜡烛图绘制

    序言:学着学着就学到股票图形绘制了,尝试了下,入门蛮简单的,后面就不知道了,现在好像mplfinance更换了新版本,老版本不支持了.以下代码能实现单个股票K线蜡烛图图形输出,不过我用的是tushar ...

  2. Android NDK开发之旅(2):一篇文章搞定Android Studio中使用CMake进行NDK/JNI开发

    Android NDK开发之旅(2):一篇文章搞定android Studio中使用CMake进行NDK/JNI开发 (码字不易,转载请声明出处:http://blog.csdn.NET/andrex ...

  3. python绘制k线图的步骤_Python使用PyQtGraph绘制股票行情K线图

    PyQtGraph是Python平台上一种功能强大的2D/3D绘图库,相对于matplotlib库,由于其在内部实现方式上,使用了高速计算的numpy信号处理库以及Qt的GraphicsView框架, ...

  4. Android零基础入门第7节:搞定Android模拟器,开启甜蜜之旅

    原文:Android零基础入门第7节:搞定Android模拟器,开启甜蜜之旅 在前几期中总结分享了Android的前世今生.Android 系统架构和应用组件那些事.带你一起来聊一聊Android开发 ...

  5. android fragment 底部菜单栏,一句话搞定Android底部导航栏,一键绑定Fragment、ViewPager...

    现在大多数App都会用到底部导航栏,比如常见的聊天工具QQ.微信.购物App等等,有了底部导航栏,用户可以随时切换界面,查看不同的内容.它的实现方式也很多,以前大多使用TabHost来实现,但是现在我 ...

  6. 五部搞定Android开发环境部署——费UC噶不过详细的Android开发环境搭建教程

     五步搞定Android开发环境部署--非常详细的Android开发环境搭建教程 引言 在windows安装Android的开发环境不简单也说不上算复杂,本文写给第一次想在自己Windows上建立 ...

  7. 终于搞定android驱动USB摄像头了!

    终于搞定android驱动USB摄像头了! 多亏了stackoverflow看到的一篇帖子,其中有几句关键的话,然后顺藤摸瓜解决了问题. 帖子大意: 讨论的前提是你的USB摄像头是UVC兼容的(如今大 ...

  8. android.os.FileUriExposedException 自动安装APK报错 5分钟搞定Android 7.0+ FileProvider

    android.os.FileUriExposedException 自动安装APK报错  5分钟搞定Android 7.0+ FileProvider 适配步骤: 1. 创建file_paths.x ...

  9. android 打开外置摄像头驱动程序,嵌入式er日常系列!终于搞定android驱动USB摄像头了!...

    原标题:嵌入式er日常系列!终于搞定android驱动USB摄像头了! 感谢网上的大神分享经验,终于解决了让我头疼好久的USB摄像头问题,讨论的前提是你的USB摄像头是UVC兼容的(如今大部分摄像头兼 ...

最新文章

  1. java 操作mysql数据库得到错误码_[数据库/Java]数据库开发过程中产生的MySQL错误代码及其解决方案...
  2. 服务器响应的生成:HTTP响应报头——HttpServletResponse接口的应用
  3. UITableVeiw相关的需求解决
  4. matlab treeview,treeview控件
  5. HDU2106 decimal system
  6. Docker 安装创建
  7. 从 Nginx 到 Pandownload,程序员如何避免面向监狱编程?
  8. android 项目将csv文件写入sqlite数据库 代码,如何将csv文件大容量插入sqlite c#
  9. LeetCode:每日一题(2020.4.15)
  10. 十、Shell脚本编程
  11. CentOS 6.5下安装Docker
  12. Warez 入门指南
  13. 如何前后端分离的架构中使用Shiro框架
  14. 系统集成项目管理工程师中高级一次通过经验之谈
  15. PHP实现物流查询(通过快递网API实现)
  16. DisplayPort-DP接口知识
  17. KGB知识图谱的功能和特色介绍
  18. python读取excel数据使用pyecharts展示
  19. 宽带换了新的账号怎么连接服务器地址,宽带换了路由器设置步骤图解
  20. python 关键字field

热门文章

  1. 多普勒流量计测流系统
  2. GNSS说第(七)讲---自适应动态导航定位(九)---自适应因子模型
  3. 《颠覆者 周鸿伟自传》阅读笔记
  4. java object类
  5. 2019年高考志愿指导书
  6. 一阶逻辑与二阶逻辑的区别【转】
  7. 云数据库UDB的三重境界「下」
  8. 主流的知乎口碑营销方式有哪些
  9. android 绑定微博,Android 跳转至微博用户个人信息页面
  10. rnss和rdss的应用_北斗RNSS/RDSS多模手持终端设计与实现