您的位置:首页 > 移动开发 > Android开发

[置顶] Android实现自定义的折线图MyChartView

2017-04-17 09:21 483 查看
老规矩,先上实现的效果图








github地址

https://github.com/Alan222/MyChartView

这些基本的资源文件写了吧,以免最后忘了加

dimens文件

<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="dp20">20dp</dimen>
<dimen name="dp3">3dp</dimen>
<dimen name="dp6">6dp</dimen>
<dimen name="textSize">10dp</dimen>
</resources>

activity_main

<charview.com.mychartview.MyChartView
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="200dp"
/>


MyChartView 需求分析

需要画的东西

1.x,y轴坐标的线,箭头

2.x,y轴的标题,刻度下的字

3.折线和点,渐变背景色

分析完需求我们先不要急着去实现这些功能,先搭简单的模板;

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
initData();         //画坐标轴之前我们需要先初始化一些数据
drawAxes(canvas);        //  画坐标轴
drawText(canvas);//  画文字
drawLine(canvas);//  画折线
}

private void initData() {

}

private void drawAxes(Canvas canvas) {

}

private void drawLine(Canvas canvas) {

}

private void drawText(Canvas canvas) {

}

好了,画完了








开个玩笑我们先来实现第一步,画线

分析一下画线需要的数据

   1.1.控件的宽高(用来确定x,y轴的宽高)

 
1b5e4
   1.2.画线必须要有画笔

    1.3.坐标起始点

    1.4.x轴两条线之间的间距

   1.5.y轴两条线之间的间距

分析完需求我们用代码去实现, 



@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed) {
//1.1.控件的宽高(用来确定x,y轴的宽高)
mWidth = getWidth();
mHeight = getHeight();
}
}

private void initData() {
//1.2.初始化画笔
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);//去锯齿
mPaint.setColor(mPaintColor);//颜色
mPaint.setTextSize(mTextSize);
//1.3.坐标起始点
xPoint = (int) getContext().getResources().getDimension(R.dimen.dp20);
yTopPoint = (int) getContext().getResources().getDimension(R.dimen.dp20);
yPoint = mHeight - yTopPoint;

//1.4.x轴两条线之间的间距
//1.5.y轴两条线之间的间距
dp3 = (int) getContext().getResources().getDimension(R.dimen.dp3);  //箭头的偏移量
dp6 = (int) getContext().getResources().getDimension(R.dimen.dp6);  //箭头的偏移量
yItemDistance = (yPoint - (yTopPoint + mTextSize)) / 5;//y轴字段坐标的间距 5代表除起始坐标外有5条线,暂时先死,稍后会根据y轴的数据来
xItemDistance = (mWidth - xPoint) / 7; //x轴字段坐标的间距 7代表除起始坐标外有7条线,暂时先死,稍后会根据y轴的数据来
}




有了这些数据我们就可以开始画线了

private void drawAxes(Canvas canvas) {
//画起始坐标轴和箭头
canvas.drawLine(xPoint, yPoint, mWidth - xPoint, yPoint, mPaint);   //x轴起始坐标线
canvas.drawLine(mWidth - xPoint, yPoint, mWidth - xPoint - dp6, yPoint + dp3, mPaint);  //向右箭头
canvas.drawLine(mWidth - xPoint, yPoint, mWidth - xPoint - dp6, yPoint - dp3, mPaint);
canvas.drawLine(xPoint, yPoint, xPoint, yTopPoint, mPaint);   //y起始轴线
canvas.drawLine(xPoint, yTopPoint, xPoint - dp3, yTopPoint + dp6, mPaint);  //向右箭头
canvas.drawLine(xPoint, yTopPoint, xPoint + dp3, yTopPoint + dp6, mPaint);    //画横着的线
int yTextPoint; //y轴字段的坐标
for (int i = 0; i < 5; i++) {
yTextPoint = yTopPoint + mTextSize + yItemDistance * i;
canvas.drawLine(xPoint, yTextPoint, mWidth - xPoint, yTextPoint, mPaint);   //x轴线
}

//画竖着的线
int xTextPoint; //x轴字段的坐标
for (int i = 1; i < 7; i++) {
xTextPoint = xPoint + xItemDistance * i;
canvas.drawLine(xTextPoint, yPoint, xTextPoint, yTopPoint +
mTextSize, mPaint);   //y轴线这里的线比起始Y轴短,留出位置画title
}
}


画完背景线,我们先测试一下

附上成员变量

PS:有值的成员变量暂时先写死,实现需求以后用attr实现

private int mTextSize = (int) getResources().getDimension(R.dimen.textSize);
private int mPaintColor = Color.RED;
private int mWidth;//控件的宽
private int mHeight;//控件的高
private Paint mPaint;
private int xPoint; //原点X轴坐标
private int yPoint; //原点Y轴坐标
private int yTopPoint;//y轴顶点坐标
private int dp3;
private int dp6;
private int yItemDistance;
private int xItemDistance;




ok,接下来开始画x,y轴的字段和标题

我们需要

2.1.x轴的数据

2.2.y轴的数据

2.3.x轴的title

2.4.y轴的title

2.5.数据的宽和高

分析完继续撸代码

private void initData() {
//2.1.x轴的数据
//2.2.y轴的数据    // FIXME: 2017/4/17  暂时先写死
xData = new ArrayList<>();
xData.add("4-11");
xData.add("4-12");
xData.add("4-13");
xData.add("4-14");
xData.add("4-15");
xData.add("4-16");
xData.add("4-17");
yData = new ArrayList<>();
yData.add(0);
yData.add(10);
yData.add(20);
yData.add(30);
yData.add(40);
yData.add(50);
yData.add(60);

mPaint = new Paint();
......
}


//2.3.x轴的title
public void setxTitle(String xTitle) {
this.xTitle = xTitle;
}
//2.4.y轴的title
public void setyTitle(String yTitle) {
this.yTitle = yTitle;
}


//2.5.测量文字的宽,文字的高为textsize
public int measureTextWidth(String text) {
return (int) mPaint.measureText(text);
}


这里写个成员变量吧 不然代码要来亲戚了

private String xTitle = "近七日";
private String yTitle = "增长率/%";
private ArrayList<String> xData = new ArrayList<>();
private ArrayList<Integer> yData = new ArrayList<>();

做完准备工作可以开始画刻度了

private void drawText(Canvas canvas) {
//画x轴的title
int xTitleWidth = measureTextWidth(xTitle);
canvas.drawText(xTitle, mWidth - xPoint - xTitleWidth / 2, yPoint + dp3 + mTextSize, mPaint);    //画y轴的title
canvas.drawText(yTitle, xPoint + dp6, yTopPoint + mTextSize / 2, mPaint);    //画x轴刻度
for (int i = 0; i < xData.size(); i++) {
int dataWidth = measureTextWidth(xData.get(i));
canvas.drawText(xData.get(i), xPoint + xItemDistance * i - dataWidth / 2, yPoint + dp3 + mTextSize, mPaint);
}

//画y轴刻度
for (int i = 0; i < yData.size(); i++) {
int dataWidth = measureTextWidth(yData.get(i) + "");
canvas.drawText(yData.get(i) + "", xPoint - dataWidth - dp3, yPoint - yItemDistance * (i + 1), mPaint);
}
}

这里有了xData和yData我们可以顺手把写死的线的条数



private void initData() {
.......
dp3 = (int) getContext().getResources().getDimension(R.dimen.dp3);  //箭头的偏移量
dp6 = (int) getContext().getResources().getDimension(R.dimen.dp6);  //箭头的偏移量
yItemDistance = (yPoint - (yTopPoint + mTextSize)) / yData.size();
xItemDistance = (mWidth - xPoint - xPoint) / xData.size();
}

private void drawAxes(Canvas canvas) {
......
//画横着的线
int yTextPoint; //y轴字段的坐标
for (int i = 0; i < yData.size(); i++) {
yTextPoint = yTopPoint + mTextSize + yItemDistance * i;
canvas.drawLine(xPoint, yTextPoint, mWidth - xPoint, yTextPoint, mPaint);   //x轴线
}    //画竖着的线
int xTextPoint; //x轴字段的坐标
for (int i = 1; i < xData.size(); i++) {
xTextPoint = xPoint + xItemDistance * i;
canvas.drawLine(xTextPoint, yPoint, xTextPoint, yTopPoint + mTextSize, mPaint);   //y轴线这里的线比起始Y轴短,留出位置画title
}
}






画完背景我们实现第三个需求

 

3.折线和点,渐变背景色

    分析折线和点

    3.1.画折线和折点需要的笔

    3.2.各个点的的数据

    3.3.连接数据的path

分析渐变背景色

    3.4.画背景的笔

    3.5.画背景的路线

private void initData() {
......
//3.1.画折线和折点需要的笔   //折线画笔
mLinePaint = new Paint();
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setAntiAlias(true);//去锯齿    mLinePaint.setColor(mLinePaintColor);//颜色
mLinePaint.setTextSize(mTextSize);    //画折点的笔
mCirclePaint = new Paint();
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true);//去锯齿  mCirclePaint.setColor(mLinePaintColor);//颜色
// 3.4.画背景的笔
mShawerPaint = new Paint();
mShawerPaint.setAntiAlias(true);
mShawerPaint.setStrokeWidth(2f);
//3.3.连接数据的path
mPath = new Path();
//3.5.连接数据的path
mShowerPath = new Path();
//背景的渐变色
shadeColors = new int[]{
Color.argb(80, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
Color.argb(20, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
Color.argb(0, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor))};
}


private void initData() {
//3.2.各个点的数据
// FIXME: 2017/4/17
itemData.add(15);
itemData.add(18);
itemData.add(11);
itemData.add(25);
itemData.add(35);
itemData.add(45);
itemData.add(0);
// FIXME: 2017/4/17
xData.add("4-11");
......

}

准备完毕可以开始画线了

分析折点的坐标

x点的坐标就是x起点的坐标+x线之间的间距

x = xPoint + xItemDistance * i;

y轴的坐标则需要按比例计算

折点的值/y刻度的范围 = y顶点到y坐标的距离/y刻度件的长度

y坐标 = (yPoint - yItemDistance-((integer * 1.0-yMin) / itemLength) * yLength));

y刻度的范围我们需要yMax,yMin;

 

画线的成员变量也就基本确定了

private ArrayList<Integer> itemData = new ArrayList<>();
private Paint mShawerPaint;
private Paint mCirclePaint;
private Paint mLinePaint;
private Path mPath;
private int mLinePaintColor = Color.RED;
// FIXME: 2017/4/17
private Path mShowerPath;
private int[] shadeColors;
private int yMax;
private int yMin;


private void initData() {
......
shadeColors = new int[]{
Color.argb(80, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
Color.argb(20, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)),
Color.argb(0, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor))};
if (yData.size() > 0) {
yMin = yData.get(0);
yMax = yData.get(0);
}
for (int i = 0; i < yData.size(); i++) {
if (yData.get(i) > yMax) {
yMax = yData.get(i);
} else if (yData.get(i) < yMin) {
yMin = yData.get(i);
}
}
}


做完准备工作就开画吧

private void drawLine(Canvas canvas) {
for (int i = 0; i < itemData.size(); i++) {
int integer = itemData.get(i);        //折点x坐标
int itemX = xPoint + xItemDistance * i;        //折点y坐标
int itemLength = yMax - yMin;
int yLength = yPoint - (yTopPoint + mTextSize) - yItemDistance;
int itemY = 0;
if (itemLength != 0) {
itemY = (int) (yPoint - yItemDistance - ((integer * 1.0 - yMin) / itemLength) * yLength);
}

//画折点
canvas.drawCircle((xPoint + xItemDistance * i), itemY, 3, mCirclePaint);//画小圆点
// 连接折线的路径,阴影的路径
if (i == 0) {
mPath.moveTo(itemX, itemY);
mShowerPath.moveTo(itemX, itemY);
} else {
mPath.lineTo(itemX, itemY);
mShowerPath.lineTo(itemX, itemY);
if (i == itemData.size() - 1) {
mShowerPath.lineTo(itemX, yPoint - yItemDistance);
mShowerPath.lineTo(xPoint, yPoint - yItemDistance);
mShowerPath.close();
}
}
}
}




写到这里我们基本的ui图已经都画出来了,但是我们前面的代码就标注了,还有些写死的数据需要改,而且我们的是动态刻度图,自然y轴的刻度也不会让亲们自己去写

我们这里先改简单的attr数据,这里自定义的attr数据有textSize,mPaintColor,mLinePaintColor,如果有其他需求,可以自行添加。

在values下创建attrs文件夹,自定义名字和属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyChartView">
<attr name="paintColor" format="color"/><!--坐标轴和刻度的颜色-->
<attr name="textSize" format="dimension"/><!--字体的大小-->
<attr name="chartLineColor" format="color"/><!--折线和阴影的颜色-->
</declare-styleable>
</resources>


定义完后,我们在view中读取

public MyChartView(Context context) {
this(context, null);
}

public MyChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public MyChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
float textSize = getContext().getResources().getDimension(R.dimen.textSize);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyChartView);
mPaintColor = typedArray.getColor(R.styleable.MyChartView_paintColor,Color.RED);
mLineColor = typedArray.getColor(R.styleable.MyChartView_chartLineColor, Color.RED);
mTextSize = (int) typedArray.getDimension(R.styleable.MyChartView_textSize, textSize);
typedArray.recycle();
}

我们再来分析一下y轴的刻度;

y轴的刻度如果偏差加大,那刻度的差值也要随之改变,而且可能出现负数,显然y轴刻度不能写死,让用户自己设置也会让控件使用变得麻烦so我们这里在设置itemData的时候也动态修改yData的值。

通过分析这里我把最低刻度和最高刻度都设为10的整数倍,中间值则有折点值来决定

public void setItemData(@NonNull ArrayList<Integer> itemData) {
this.itemData = itemData;
if (itemData.size() == 0) {
return;
}

itemMin = itemData.get(0);  //折点的最小值
itemMax = itemData.get(0);  //折点的最大值
for (int i = 0; i < itemData.size(); i++) {
if (itemData.get(i) > itemMax) {
itemMax = itemData.get(i);
} else if (itemData.get(i) < itemMin) {
itemMin = itemData.get(i);
}
}
//设置y轴刻度的最小值为比itemMin小的 最大的10的整数倍
int yMin = itemMin / 10 * 10;
if (yMin < 0) {
yMin = yMin - 10;
}
//设置y轴刻度的最大值为比itemMax大的 最小的10的整数倍
int yMax = (itemMax / 10) * 10 + 10;
if (itemMax % 10 == 0) {
yMax = itemMax / 10 * 10;
}
yData.clear();        //最大值,最小值都是10的倍数,那么我们的中间刻度也好取了,
// 为了刻度美观,我们这里如果yMax-yMin≤50,刻度件的差值就≤10,如果>50,就取10的整数倍
for (int i = 0; i <= 5; i++) {
if ((yMax - yMin) / 5 <= 10) {
yData.add(yMin + (yMax - yMin) / 5 * i);
} else {
yData.add(yMin + ((yMax - yMin) / 50 + 1) * 10 * i);
}
}

//动态设置x轴的data
Date date = new Date();
long time = date.getTime();
SimpleDateFormat dateFormat = new SimpleDateFormat("M-d");
xData.clear();
for (long i = itemData.size(); i > 0; i--) {
long xDataTime = time - 24 * 60 * 60 * 1000 * i;
xData.add(dateFormat.format(xDataTime));
}
}

写到这里自定义的ChartView就实现的差不多了,我们可以删掉上面写死的数据,如果需要自己设置xData和yData添加如下代码

 

//注意在代码中调用这两个方法的话都要在调用setItemData()后面调用,否则无效。

public void setyData(ArrayList<String> xData) {
this.yData = yData;
}

public void setxData(ArrayList<String> xData) {
this.xData = xData;
if (xData.size() != itemData.size()) {
try {
throw new Exception("XData Count Unmatched Exception");
} catch (Exception e) {
e.printStackTrace();
}
}
}


自定义的代码就写到这里了。

---------------------------------华丽的分割线-------------------------------------

这里说两句题外话

一开始拿到这种需求图我是懵逼的,对于阴影背景的效果实现,我原本想的画渐变的shape当背景色,然后想circleImageView那样擦掉折线上面的颜色使其透明

刚好写需求的当天看郭神的微信公众号刚好出了一篇自定义的View之颜色渐变折线图。









这简直是为我量身打造的有木有
,老夫敲代码数十载,拿起键盘就是一顿ctrl+c,ctrl+v,瞬间阴影的效果就有了。

郭神千秋万代,一桶浆糊。

郭神链接

完事后看了关于shader的博客,感觉还是挺强大的,有兴趣的小伙伴可以去看看

Shader链接
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: