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


按惯例,先看下今天要实现的效果,整个Demo地址为,


android k线图 开发 安卓k线图的实现_AndroidK线图怎么画

相对来说比较简单的一个小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" />