大家好,又一次和大家见面了!最近在写关于股票应用开发过程中的新的体会,希望对大家有所帮助,让大家不要向我一样,少走一些弯路。还用就是最近在更新这个系列的博客,怕到后面忙起来或者不想写了就落下了!
今天主要说的是图表的绘制,当然你需要继承View,也就是现在一搜一大把的自定义View。我的整体思路是这样的:
首先需要定义图表的基类,在基类内需要定义或者绘制一些共性的东西,如图表的大小,背景色,字体,坐标轴等等信息;
然后就是继承图表的基类进行图形的绘制;
下面先说一下图表实体类的基类:
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
一、基类的定义:Chart.java
基类解决的是共性问题,首先我们需要初始化一些图表的设置,确定图表的大小,颜色,字体等;其次就是在onDraw()方法内进行相关的绘制工作,如图表的边框,网格线等;最后就是定义图表点击的回调函数;
下面是代码示例:
package com.zxm.chart;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.PathEffect;
import android.graphics.Rect;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.zxm.mykchart.R;
import com.zxm.util.LogUtil;
/**
* Created by ZhangXinmin on 2016/8/22.
* Base Class for Chart!
*/
public class Chart extends View{
/**constractor*/
public Chart(Context context) {
super(context);
init();
}
public Chart(Context context, AttributeSet attrs) {
super(context, attrs);
init();
LogUtil.i("tag","Chart..构造器2参数"+getTime());
}
public Chart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
LogUtil.i("tag","Chart..构造器3参数");
}
/**
* 默认值
*/
/** 默认背景色 */
public static final int DEFAULT_BACKGROUD = R.color.marker_bg;
/** 默认XY轴字体大小 **/
public static final int DEFAULT_AXIS_LABLE_SIZE = 22;
/** 默认XY坐标轴颜色 */
private static final int DEFAULT_AXIS_COLOR = Color.rgb(224,225,227);
/** 默认经纬线颜色 */
private static final int DEFAULT_LONGI_LAITUDE_COLOR = Color.rgb(224,225,227);
/** 默认上表纬线数 */
public static final int DEFAULT_UPER_LATITUDE_NUM = 3;
/** 默认下表纬线数 */
private static final int DEFAULT_LOWER_LATITUDE_NUM = 1;
/** 默认经线数 */
public static final int DEFAULT_LOGITUDE_NUM = 3;
/** 默认边框的颜色 */
public static final int DEFAULT_BORDER_COLOR = Color.rgb(224,225,227);
/** 默认虚线效果 */
private static final PathEffect DEFAULT_DASH_EFFECT = new DashPathEffect(new float[] { 3, 3, 3,
3 }, 1);
/** 下表的顶部 */
public static float LOWER_CHART_TOP;
/** 上表的底部 */
public static float UPER_CHART_BOTTOM;
/**
* 属性
*/
/** 背景色 */
private int mBackGround;
/** 坐标轴XY颜色 */
private int mAxisColor;
/** 经纬线颜色 */
private int mLongiLatitudeColor;
/** 虚线效果 */
private PathEffect mDashEffect;
/** 边线色 */
private int mBorderColor;
/** 上表高度 */
private float mUperChartHeight;
/** 是否显示下表Tabs */
private boolean showLowerChartTabs;
/** 是否显示顶部Titles */
private boolean showTopTitles;
/** 顶部Titles高度 */
private float topTitleHeight;
/** 下表TabTitles */
private String[] mLowerChartTabTitles;
/** 下表Tab宽度 */
private float mTabWidth;
/** 下表Tab高度 */
private float mTabHight;
/** 下表TabIndex */
private int mTabIndex;
/** 下表高度 */
private float mLowerChartHeight;
private float latitudeSpacing;
private OnTabClickListener mOnTabClickListener;
public void setOnTabClickListener(OnTabClickListener onTabClickListener) {
mOnTabClickListener = onTabClickListener;
}
/**
* 定义图表点击的监听接口
*/
public interface OnTabClickListener {
void onTabClick(int indext);
}
/**初始化设置表格属性*/
private void init(){
mBackGround = DEFAULT_BACKGROUD;
mAxisColor = DEFAULT_AXIS_COLOR;
mLongiLatitudeColor = DEFAULT_LONGI_LAITUDE_COLOR;
mDashEffect = DEFAULT_DASH_EFFECT;
mBorderColor = DEFAULT_BORDER_COLOR;
showLowerChartTabs = true;
showTopTitles = true;
topTitleHeight = 0;
mTabIndex = 0;
mOnTabClickListener = null;
mTabWidth = 0;
mTabHight = 0;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setBackgroundResource(mBackGround);//设置背景色
int viewHeight = getHeight();
int viewWidth = getWidth();
mLowerChartHeight = viewHeight -2 - LOWER_CHART_TOP;//下表的高度
//是否显示下表的tab
if (showLowerChartTabs){//下表TAB的高度
mTabHight = viewHeight / 24.0f;
}
//是否显示顶部title
if (false){//顶部title的高度
topTitleHeight = DEFAULT_AXIS_LABLE_SIZE + 2;
}else {
topTitleHeight = 0;
}
latitudeSpacing = (viewHeight - 4 -DEFAULT_AXIS_LABLE_SIZE - topTitleHeight - mTabHight) /
(DEFAULT_UPER_LATITUDE_NUM + DEFAULT_LOWER_LATITUDE_NUM +2);
//上表高度
mUperChartHeight = latitudeSpacing * (DEFAULT_UPER_LATITUDE_NUM + 1);
//下表顶部
LOWER_CHART_TOP = viewHeight - 1 - latitudeSpacing * (DEFAULT_LOWER_LATITUDE_NUM+1);
//上表的底部
UPER_CHART_BOTTOM = 1 + topTitleHeight + latitudeSpacing * (DEFAULT_UPER_LATITUDE_NUM + 1);
// 绘制边框
drawBorders(canvas, viewHeight, viewWidth);
// 绘制经线
// drawLongitudes(canvas, viewHeight, longitudeSpacing);
// 绘制纬线
drawLatitudes(canvas, viewHeight, viewWidth, latitudeSpacing);
// 绘制X线及LowerChartTitles
drawRegions(canvas, viewHeight, viewWidth);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Rect rect = new Rect();
getGlobalVisibleRect(rect);
float x = event.getRawX();
float y = event.getRawY();
if (y <= LOWER_CHART_TOP + rect.top +2 &&
y >= UPER_CHART_BOTTOM + DEFAULT_AXIS_LABLE_SIZE + rect.top){
if (mTabWidth <= 0){
return true;
}
int indext = (int) (x / mTabWidth);
if (mTabIndex != indext){
mTabIndex = indext;
mOnTabClickListener.onTabClick(mTabIndex);
return true;
}
}
return false;
}
/**
* 绘制边框
* @param canvas
* @param viewHeight
* @param viewWidth
*/
private void drawBorders(Canvas canvas, int viewHeight, int viewWidth){
Paint paint = new Paint();
paint.setColor(Color.rgb(224,225,227));//设置画笔颜色
paint.setStrokeWidth(1);
canvas.drawLine(1, 1, viewWidth - 1, 1, paint);
canvas.drawLine(1,topTitleHeight,1,viewHeight - 1,paint);//左边框
canvas.drawLine(viewWidth - 1,viewHeight - 1,viewWidth - 1,topTitleHeight,paint);//右边框
canvas.drawLine(viewWidth - 1,viewHeight - 1,1, viewHeight - 1, paint);//下边框
}
/**
* 绘制网格线;经线
* @param canvas
* @param viewHeight
* @param longitudeSpacing
*/
private void drawLongitudes(Canvas canvas, int viewHeight, float longitudeSpacing){
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setPathEffect(mDashEffect);//设置绘制虚线
for (int i = 1; i <= DEFAULT_LOGITUDE_NUM;i++){
canvas.drawLine(1 + longitudeSpacing * i,topTitleHeight + 2,
1 + longitudeSpacing * i,UPER_CHART_BOTTOM,paint);
canvas.drawLine(1 + longitudeSpacing * i,LOWER_CHART_TOP,
1 + longitudeSpacing * i,viewHeight - 1,paint);
}
}
/**
* 绘制网格线;纬线
* @param canvas
* @param viewHeight
* @param viewWidth
* @param latitudeSpacing
*/
private void drawLatitudes(Canvas canvas, int viewHeight, int viewWidth, float latitudeSpacing){
Paint paint = new Paint();
paint.setColor(mLongiLatitudeColor);
paint.setPathEffect(mDashEffect);
for (int i = 1;i <= DEFAULT_UPER_LATITUDE_NUM;i++){
canvas.drawLine(1,topTitleHeight + 1 + latitudeSpacing * i,viewWidth - 1,
topTitleHeight + 1 + latitudeSpacing * i,paint);
for (int j = 1;j <= DEFAULT_LOWER_LATITUDE_NUM;j++){
canvas.drawLine(1,viewHeight - 1 - latitudeSpacing,viewWidth - 1,
viewHeight - 1 - latitudeSpacing,paint);
}
}
}
private void drawRegions(Canvas canvas, int viewHeight, int viewWidth){
Paint paint = new Paint();
paint.setColor(mAxisColor);
paint.setAlpha(150);
if (showTopTitles){//是否显示顶部title
canvas.drawLine(1,1 + DEFAULT_AXIS_LABLE_SIZE + 2,viewWidth - 1,
1 + DEFAULT_AXIS_LABLE_SIZE + 2,paint);
}
canvas.drawLine(1,UPER_CHART_BOTTOM,viewWidth - 1,UPER_CHART_BOTTOM,paint);
canvas.drawLine(1,LOWER_CHART_TOP,viewWidth - 1,LOWER_CHART_TOP,paint);
if (showLowerChartTabs){//是否显示下表Tab
canvas.drawLine(1, UPER_CHART_BOTTOM + DEFAULT_AXIS_LABLE_SIZE + 2, viewWidth - 1,
UPER_CHART_BOTTOM + DEFAULT_AXIS_LABLE_SIZE + 2, paint);
if (mLowerChartTabTitles == null || mLowerChartTabTitles.length <= 0){
return;
}
mTabWidth = (viewWidth - 2) / 10.0f * 10.0f / mLowerChartTabTitles.length;//下表Tab的宽度
if (mTabWidth < DEFAULT_AXIS_LABLE_SIZE * 2.5f + 2){
mTabWidth = DEFAULT_AXIS_LABLE_SIZE * 2.5f + 2;
}
Paint textPaint = new TextPaint();
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(DEFAULT_AXIS_LABLE_SIZE);
/*Paint bgPaint = new Paint();
bgPaint.setColor(Color.MAGENTA);
for (int i = 0; i < mLowerChartTabTitles.length && mTabWidth * (i + 1) <= viewWidth - 2;i++){
if (i == mTabIndex){
canvas.drawRect(mTabWidth * i + 1,LOWER_CHART_TOP,mTabWidth * (i + 1) + 1,
UPER_CHART_BOTTOM + DEFAULT_AXIS_LABLE_SIZE + 2,bgPaint);
}
canvas.drawLine(mTabWidth * i + 1, LOWER_CHART_TOP, mTabWidth * i + 1,
UPER_CHART_BOTTOM + DEFAULT_AXIS_LABLE_SIZE + 2, paint);
canvas.drawText(mLowerChartTabTitles[i], mTabWidth * i + mTabWidth / 2.0f
- mLowerChartTabTitles[i].length() / 3.0f * DEFAULT_AXIS_LABLE_SIZE,
LOWER_CHART_TOP - mTabHight / 2.0f + DEFAULT_AXIS_LABLE_SIZE / 2.0f,
textPaint);
}*/
}
}
public int getBackGround() {
return mBackGround;
}
public void setBackGround(int BackGround) {
this.mBackGround = BackGround;
}
public int getAxisColor() {
return mAxisColor;
}
public void setAxisColor(int AxisColor) {
this.mAxisColor = AxisColor;
}
public int getLongiLatitudeColor() {
return mLongiLatitudeColor;
}
public void setLongiLatitudeColor(int LongiLatitudeColor) {
this.mLongiLatitudeColor = LongiLatitudeColor;
}
public PathEffect getDashEffect() {
return mDashEffect;
}
public void setDashEffect(PathEffect DashEffect) {
this.mDashEffect = DashEffect;
}
public int getBorderColor() {
return mBorderColor;
}
public void setBorderColor(int BorderColor) {
this.mBorderColor = BorderColor;
}
public float getUperChartHeight() {
return mUperChartHeight;
}
public void setUperChartHeight(float UperChartHeight) {
this.mUperChartHeight = UperChartHeight;
}
public boolean isShowLowerChartTabs() {
return showLowerChartTabs;
}
public void setShowLowerChartTabs(boolean showLowerChartTabs) {
this.showLowerChartTabs = showLowerChartTabs;
}
public float getLowerChartHeight() {
return mLowerChartHeight;
}
public void setLowerChartHeight(float LowerChartHeight) {
this.mLowerChartHeight = LowerChartHeight;
}
public String[] getLowerChartTabTitles() {
return mLowerChartTabTitles;
}
public void setLowerChartTabTitles(String[] LowerChartTabTitles) {
this.mLowerChartTabTitles = LowerChartTabTitles;
}
public float getLatitudeSpacing() {
return latitudeSpacing;
}
public void setLatitudeSpacing(float latitudeSpacing) {
this.latitudeSpacing = latitudeSpacing;
}
public void setShowTopTitles(boolean showTopTitles) {
this.showTopTitles = showTopTitles;
}
public float getTopTitleHeight() {
return topTitleHeight;
}
public float getmTabHight() {
return mTabHight;
}
protected String getTime() {
return ":" + System.currentTimeMillis();
}
}
定义CandleChart.java继承Chart.java并实现Chart内部定义的接口:
重写onDraw(Canvas canvas)方法,在方法内部,对K线图,成交量图,MACD图,KDJ图,绘制坐标轴以及刻度值,绘制十字线以及信息详情框;
下面说一下技术难点:
A. 屏幕的手势设置:
思路参考:如下思维导图
关于手势实现思路:
首先是拖动十字线的实现,在实现的过程中是通过调用postInvalidate()去刷新View;
其次是拖动图表的实现,在实现过程中首先是隐藏十字线和信息显示框,然后判断移动距离是否满足最小移动距离,在这里是通过定义一个变量表示图表显示的起始位置来实现图表的移动的;
最后是实现图表的缩放,实现的思路是首先计算手指触屏的初始位置,计算两手指之间的距离,当手指移动之后计算此时手指之间的距离,如果当前手指之间的距离大于初始时刻手指之间的距离,就对图表进行放大,反之缩小;还有需要注意的是在计算两手指之间的距离的时候,也就是在设置触摸模式之前需要判断当前屏幕有几个触摸点,否则会报异常(pointer index out of range);
B.图形的绘制:
a>无论是在绘制蜡烛图还是在绘制其他图形的时候首先需要将数据转化成屏幕上对应的像素,也就是转换的比率rate = getLowerChartHeight() / (max - 0),然后需要注意绘制每条数据的坐标位置,尤其是绘制上下两个关联的图表时,一定要注意坐标的一致性;
b>绘制MACD图表:
为什么要单独说一下绘制MACD的图表呢?因为该图表存在y = 0 这条轴线,当然如果你不希望绘制的图形有固定的X轴,则可以不考虑这个问题;
但是若没有固定的y = 0这条轴线,那么可能会出现当你的图表进行缩放的时候会在画布中移动,也就是X轴不固定;
若存在这条固定的轴线,会出现该轴线上下两侧的缩放比例不一致的问题;
------------------------------------------------------------------------------------------------------------------
就说这么多吧,下面是代码示例:
绘制蜡烛图代码:
/**
* 绘制蜡烛图,也就是K线图
*/
private void drawUpperRegion() {
// LogUtil.i("tag", "CandleChart..drawUpperRegion" + getTime());
//绘制蜡烛图
Paint redPaint = new Paint();
redPaint.setColor(Color.RED);
Paint greenPaint = new Paint();
greenPaint.setColor(Color.GREEN);
int width = getWidth();//屏幕的宽度
//设置Candle的宽度
mCandleWidth = (width - 4) / 10.0f * 10.0f / mShowDataNum;
/**计算蜡烛图每一个矩形的高度比例*/
double rate = (getUperChartHeight() - 60) / (mMaxPrice - mMinPrice);
for (int i = 0; i < mShowDataNum && mDataStartIndext + i < mList.size(); i++) {
StockInfo info = mList.get(mDataStartIndext + i);
/**计算每一天股票的价格所对应的像素*/
float open = (float) ((mMaxPrice - info.getOpen()) * rate + DEFAULT_AXIS_LABLE_SIZE + 4);
float close = (float) ((mMaxPrice - info.getClose()) * rate + DEFAULT_AXIS_LABLE_SIZE + 4);
float high = (float) ((mMaxPrice - info.getHigh()) * rate + DEFAULT_AXIS_LABLE_SIZE + 4);
float low = (float) ((mMaxPrice - info.getLow()) * rate + DEFAULT_AXIS_LABLE_SIZE + 4);
float right = (float) (3 + mCandleWidth * (i + 1));
float left = (float) (3 + mCandleWidth * i);
//计算绘制每个蜡烛的开始X坐标
float startX = (float) (3 + mCandleWidth * i + (mCandleWidth - 1) / 2);
/**设置蜡烛图的颜色*/
if (open < close) {
canvas.drawRect(left, open, right, close, greenPaint);
// LogUtil.i("tag","开盘价小于收盘价:left"+left+";close:"+close+";right:"+right+";open:"+open);
canvas.drawLine(startX, high, startX, low, greenPaint);
} else if (open == close) {
canvas.drawLine(left, open, right, open, redPaint);
// LogUtil.i("tag","开盘价=收盘价:left"+left+";open:"+open+";right:"+right+";open:"+open);
canvas.drawLine(startX, high, startX, low, redPaint);
} else {
canvas.drawRect(left, close, right, open, redPaint);
// LogUtil.i("tag","开盘价大于收盘价:left"+left+";open:"+open+";right:"+right+";close:"+close);
canvas.drawLine(startX, high, startX, low, redPaint);
}
}
}
绘制MACD图代码片段:
/**
* 绘制MACD指标图形
*/
private void drawMACDChart() {
float middle = LOWER_CHART_TOP + getLowerChartHeight() / 2;//获取下部绘图区域的中间位置
mCandleWidth = (getWidth() - 4) / 10.0f * 10.0f / mShowDataNum;
Paint linePaint = new Paint();//绘制曲线
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(2);
Paint textPaint = new Paint();
textPaint.setColor(DEFAULT_AXIS_X_LABLE_COLOR);
textPaint.setTextSize(DEFAULT_AXIS_LABLE_SIZE);
/**初始化集合数据*/
MACD = mMacdData.getMACD();//集合不要忘记添加数据!
DEA = mMacdData.getDEA();
DIF = mMacdData.getDIF();
if (!MACD.isEmpty() && !DEA.isEmpty() && !DIF.isEmpty()) {
double low = DEA.get(mDataStartIndext);//刻度最小值
double high = low;//刻度最大值
double upRate = 0.0;//0刻度线之上绘图比例
double downRate = 0.0;//0刻度线之下绘图比例
double lineRate = 0.0;//
for (int i = mDataStartIndext; i < mDataStartIndext + mShowDataNum && i < MACD.size(); i++) {
low = low < MACD.get(i) ? low : MACD.get(i);
low = low < DEA.get(i) ? low : DEA.get(i);
low = low < DIF.get(i) ? low : DIF.get(i);
high = high > MACD.get(i) ? high : MACD.get(i);
high = high > DEA.get(i) ? high : DEA.get(i);
high = high > DIF.get(i) ? high : DIF.get(i);
}
upRate = (getLowerChartHeight() / 2 - 4) / (high - 0);//计算上部比例
downRate = (getLowerChartHeight() / 2 - 4) / Math.abs(low - 0);//计算下部比例
lineRate = getLowerChartHeight() / (high - low);
Paint redP = new Paint();
redP.setColor(Color.RED);
Paint greenP = new Paint();
greenP.setColor(Color.GREEN);
//两条曲线
float dea = 0.0f;
float dif = 0.0f;
//绘制MACD图形
for (int i = mDataStartIndext; i < mDataStartIndext + mShowDataNum
&& i < MACD.size(); i++) {
//绘制矩形
//计算绘制每个蜡烛的开始X坐标
float value = MACD.get(i);
//计算绘制每个蜡烛的开始X坐标
float startX = (float) (3 + mCandleWidth * (i - mDataStartIndext) + (mCandleWidth - 1) / 2);
if (MACD.get(i) >= 0) {//MACD值大于0绘制在y = 0上方,红色;
canvas.drawLine(startX, (float) ((high - value) * upRate) + LOWER_CHART_TOP, startX, middle, redP);
} else {
canvas.drawLine(startX, middle, startX, (float) (middle + Math.abs(value) * downRate), greenP);
}
if (i != mDataStartIndext) {
linePaint.setColor(Color.YELLOW);
canvas.drawLine(3 + (float) mCandleWidth * (i + 1 - mDataStartIndext) + (float) mCandleWidth / 2,
(float) ((high - DEA.get(i)) * lineRate) + LOWER_CHART_TOP,
3 + (float) mCandleWidth * (i - mDataStartIndext) + (float) mCandleWidth / 2,
dea,
linePaint);
linePaint.setColor(Color.RED);
canvas.drawLine(3 + (float) mCandleWidth * (i + 1 - mDataStartIndext) + (float) mCandleWidth / 2,
(float) ((high - DIF.get(i)) * lineRate) + LOWER_CHART_TOP,
3 + (float) mCandleWidth * (i - mDataStartIndext)
+ (float) mCandleWidth / 2, dif, linePaint);
}
dea = (float) ((high - DEA.get(i)) * lineRate) + LOWER_CHART_TOP;
dif = (float) ((high - DIF.get(i)) * lineRate) + LOWER_CHART_TOP;
}
canvas.drawText(ValueFormater.format(high) + "", 2,
LOWER_CHART_TOP + DEFAULT_AXIS_LABLE_SIZE - 2, textPaint);
canvas.drawText(ValueFormater.format((high + low) / 2) + "", 2,
LOWER_CHART_TOP + getLowerChartHeight() / 2 + DEFAULT_AXIS_LABLE_SIZE, textPaint);
canvas.drawText(ValueFormater.format(low) + "", 2,
LOWER_CHART_TOP + getLowerChartHeight(),
textPaint);
}
}
其他的指标绘制方法类似,就不在贴代码了。