渐变风格的折线图
参考了好几个大牛的博客之后,还是上手自己写。因为现在开源的图表框架有很多是作者没有心情再维护了的,而且有的框架也太大,使得后期代码非常臃肿,在公司的计划里,这个折线图只是作为一个很小的模块,用来展示信息就可以,完全不需要框架其他的功能,所以我根据自己的需求自绘了一个view。
项目需求
项目要求封装出来的控件主要提供一个setParams方法就可以,其中三个形参分别是x轴的起始字符串、终止字符串和点的Y坐标的数组。也就是说,我不用纠结X坐标的取值,x代表的是数组的个数,只需要写出起始字符串和终止字符串就可以(其实也就是日期)。然后y轴规定是写出五个数字,以数组的最大值和最小值的差值算一个比例,然后进行平均分配。
因此,项目需求可以简化到:
- 画出五条水平线,分别对应Y坐标的五个值
- x坐标的起始位置和终止位置的字符串(由于实验的关系,我感觉用drawText方法比较顺手,应该也可以用TextView这个控件做,不过我没试过,只是提供给自己以后一种思路)
- 把所有的点找出来,画线(有很多框架是把点和线分开做,而我这边的需求是不用显示点,为了达到最精简的程度,我把这个省略了)
- 填充线与X轴区域的渐变颜色
具体代码
public class LineChartView extends View {
private static final int DEFAULT_PADDING = 10;
private final float DP = getResources().getDisplayMetrics().density;
private final int mDipPadding;
private final int mFillColor;
private final int mAxisColor;
private final float mStrokeWidth;
private final int mStrokeSpacing;
private boolean mUseDips;
private final int mBackgroundColor;
private Paint mPaint = new Paint();
private Bitmap mFullImage;
private Canvas mCanvas;
//提出来的变量
private float mTextSizeOfAxis = 15;//坐标轴字体大小
private float mXAxisUsedWidth = 400;//x轴使用宽度
private String[] mXAxisContents = {"无", "无"};//x轴的坐标内容
private float mYAxisPoints[];//数据y轴的坐标
private float mMinYAxis;//y轴起点,根据数组最大值和最小值的差值换算
private float mMaxYAxis;//y轴重点,根据数组最大值和最小值的差值换算
private String mXAxisColor;//横线的颜色,默认是#E6E6E6
private String mTextColor;//字体的颜色,默认是#999999
private String mLineStartColor;//渐变折线的起始颜色(淡),默认是#FFCC02
private String mLineEndColor;//渐变折线的终止颜色(浓),默认是#FF4E00
private String mPathStartColor;//填充色块的起始颜色(淡),默认是#11FFFFFF
private String mPathEndColor;//填充色块的终止颜色(浓),默认是#AAFF8247
private boolean bNeedClean = false;
//使用此函数设置x轴起始坐标和终结坐标,利率(浮点型)数组。
public void setParams(String xminDesc, String xmaxDesc, float values[]) {
bNeedClean = true;
postInvalidate();
setmXAxisContents(new String[]{xminDesc, xmaxDesc});
setmYAxisPoints(values);
this.mYAxisPoints = values;
float mMinValues = values[0];
float mMaxValues = values[0];
for (int i = 1; i < values.length; i++) {
if (mMinValues > values[i]) {
mMinValues = values[i];
}
if (values[i] > mMaxValues) {
mMaxValues = values[i];
}
}
float difference = mMaxValues - mMinValues;
mMinYAxis = mMinValues - 0.5f * difference;
mMaxYAxis = mMaxValues + 0.05f * difference;
setmXAxisColor("#efeff4");
setmTextColor("#9a9a9a");
setmLineStartColor("#FFCC02");
setmLineEndColor("#FF4E00");
setmPathStartColor("#11FFFFFF");
setmPathEndColor("#aaFFdaca");
postInvalidate();
}
public LineChartView(Context context) {
this(context, null);
}
public LineChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LineChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDipPadding = getPixelForDip(DEFAULT_PADDING);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.DIYForCharts, 0, 0);
mFillColor = a.getColor(R.styleable.DIYForCharts_lineStrokeColor, Color.BLACK);
mAxisColor = a.getColor(R.styleable.DIYForCharts_lineAxisColor, Color.LTGRAY);
mBackgroundColor = a.getColor(R.styleable.DIYForCharts_lineBackground, Color.TRANSPARENT);
mStrokeWidth = a.getDimension(R.styleable.DIYForCharts_lineStrokeWidth, 2);
mStrokeSpacing = a.getDimensionPixelSize(R.styleable.DIYForCharts_lineStrokeSpacing, 10);
mUseDips = a.getBoolean(R.styleable.DIYForCharts_lineUseDip, false);
a.recycle();
}
public float[] getmYAxisPoints() {
return mYAxisPoints;
}
public void setmXAxisContents(String[] mXAxisContents) {
this.mXAxisContents = mXAxisContents;
}
public void setmYAxisPoints(float[] mYAxisPoints) {
this.mYAxisPoints = mYAxisPoints;
}
public void setmXAxisColor(String mXAxisColor) {
this.mXAxisColor = mXAxisColor;
}
public void setmTextColor(String mTextColor) {
this.mTextColor = mTextColor;
}
public void setmLineStartColor(String mLineStartColor) {
this.mLineStartColor = mLineStartColor;
}
public void setmLineEndColor(String mLineEndColor) {
this.mLineEndColor = mLineEndColor;
}
public void setmPathStartColor(String mPathStartColor) {
this.mPathStartColor = mPathStartColor;
}
public void setmPathEndColor(String mPathEndColor) {
this.mPathEndColor = mPathEndColor;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
//设置宽高
setMeasuredDimension(width, height);
}
//根据xml的设定获取宽度
private int measureWidth(int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//wrap_content
if (specMode == MeasureSpec.AT_MOST) {
specSize = Utils.px2dip(getContext(), 280);
}
//fill_parent或者精确值
else if (specMode == MeasureSpec.EXACTLY) {
if (specSize < 280) {
specSize = Utils.px2dip(getContext(), 280);
}
}
this.mXAxisUsedWidth = specSize * 9 / 10;
return specSize;
}
//根据xml的设定获取高度
private int measureHeight(int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//wrap_content
if (specMode == MeasureSpec.AT_MOST) {
specSize = Utils.px2dip(getContext(), 150);
}
//fill_parent或者精确值
else if (specMode == MeasureSpec.EXACTLY) {
if (specSize < 150) {
specSize = Utils.px2dip(getContext(), 150);
}
}
this.mTextSizeOfAxis = Utils.px2dip(getContext(), specSize / 60);
return specSize;
}
//重置画笔和抗锯齿
private void resetPaintWithAntiAlias(Paint paint, boolean antiAlias) {
paint.reset();
paint.setAntiAlias(antiAlias);
}
public void onDraw(Canvas canvas) {
if (null == mFullImage) {
mFullImage = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mFullImage);
}
if (bNeedClean) {
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
bNeedClean = false;
}
mCanvas.drawColor(mBackgroundColor);
//没有数据时
if (mYAxisPoints == null || mYAxisPoints.length == 0) {
resetPaintWithAntiAlias(mPaint, true);
mPaint.setColor(Color.parseColor("#000000"));
mPaint.setStrokeWidth(DP);
mPaint.setTextAlign(Paint.Align.LEFT);
// mCanvas.drawText(“暂无数据,请稍后重试”, getWidth() / 3, getHeight() / 8, mPaint);
return;
}
// mCanvas.drawText(“”, getWidth() / 3, getHeight() / 8, mPaint);
//再画5条一样的线
resetPaintWithAntiAlias(mPaint, true);
mPaint.setColor(Color.parseColor(mXAxisColor));
mPaint.setStrokeWidth(DP);
for (int i = 1; i <= 5; i++) {
mCanvas.drawLine(getWidth() - mXAxisUsedWidth, getHeight() / 8 + getHeight() * 3 / 16 * (i - 1), getWidth(), getHeight() / 8 + getHeight() * 3 / 16 * (i - 1), mPaint);
}
//写y轴的5个数字
resetPaintWithAntiAlias(mPaint, true);
mPaint.setColor(Color.parseColor(mTextColor));
mPaint.setTextSize(mTextSizeOfAxis);
mPaint.setStrokeWidth(DP);
mPaint.setTextAlign(Paint.Align.RIGHT);
float f = mMaxYAxis - mMinYAxis;
mCanvas.drawText(Utils.omit4Float(mMaxYAxis) + " ", getWidth() - mXAxisUsedWidth, getHeight() / 8 + mTextSizeOfAxis / 3, mPaint);
mCanvas.drawText(Utils.omit4Float(mMaxYAxis - f / 4f) + " ", getWidth() - mXAxisUsedWidth, getHeight() / 8 + getHeight() * 3 / 16 + mTextSizeOfAxis / 3, mPaint);
mCanvas.drawText(Utils.omit4Float(mMaxYAxis - f / 2f) + " ", getWidth() - mXAxisUsedWidth, getHeight() / 8 + getHeight() * 3 / 16 * 2 + mTextSizeOfAxis / 3, mPaint);
mCanvas.drawText(Utils.omit4Float(mMaxYAxis - 3f * f / 4f) + " ", getWidth() - mXAxisUsedWidth, getHeight() / 8 + getHeight() * 3 / 16 * 3 + mTextSizeOfAxis / 3, mPaint);
mCanvas.drawText(Utils.omit4Float(mMaxYAxis - f) + " ", getWidth() - mXAxisUsedWidth, getHeight() / 8 + getHeight() * 3 / 16 * 4 + mTextSizeOfAxis / 3, mPaint);
//x轴数字
resetPaintWithAntiAlias(mPaint, true);
mPaint.setColor(Color.parseColor(mTextColor));
mPaint.setTextSize(mTextSizeOfAxis);
mPaint.setStrokeWidth(DP);
mPaint.setTextAlign(Paint.Align.LEFT);
mCanvas.drawText(mXAxisContents[0], getWidth() - mXAxisUsedWidth, getHeight() * 7 / 8 + mTextSizeOfAxis, mPaint);
resetPaintWithAntiAlias(mPaint, true);
mPaint.setColor(Color.parseColor(mTextColor));
mPaint.setTextSize(mTextSizeOfAxis);
mPaint.setStrokeWidth(DP);
mPaint.setTextAlign(Paint.Align.RIGHT);
mCanvas.drawText(mXAxisContents[1], getWidth(), getHeight() * 7 / 8 + mTextSizeOfAxis, mPaint);
int colors[] = new int[]{Color.parseColor(mLineEndColor), Color.parseColor(mLineStartColor)};
float positions[] = new float[]{0f, 0.5f};
LinearGradient linearGradient = new LinearGradient(0, 0, 0, getHeight(), colors, positions, Shader.TileMode.CLAMP);
mPaint.setShader(linearGradient);
mPaint.setStrokeWidth(2 * DP);
Path path = new Path();
float lastXPoint = 0;
float lastYPoint = 0;
float currentXPoint = 0;
float currentYPoint = 0;
path.moveTo(getWidth() - mXAxisUsedWidth, getHeight() * 7 / 8);
for (int i = 0; i < mYAxisPoints.length; i++) {
float xPercent = (float) i / (mYAxisPoints.length - 1);
float yPercent = (mYAxisPoints[i] - mMinYAxis) / (mMaxYAxis - mMinYAxis);
if (i == 0) {
lastXPoint = xPercent * mXAxisUsedWidth + getWidth() - mXAxisUsedWidth;
lastYPoint = getHeight() * 7 / 8 - yPercent * getHeight() * 3 / 4;
path.lineTo(lastXPoint, lastYPoint);
} else {
currentXPoint = xPercent * mXAxisUsedWidth + getWidth() - mXAxisUsedWidth;
currentYPoint = getHeight() * 7 / 8 - yPercent * getHeight() * 3 / 4;
mCanvas.drawLine(lastXPoint, lastYPoint, currentXPoint, currentYPoint, mPaint);
path.lineTo(currentXPoint, currentYPoint);
lastXPoint = currentXPoint;
lastYPoint = currentYPoint;
}
if (i == mYAxisPoints.length - 1) {
path.lineTo(lastXPoint, getHeight() * 7 / 8);
//Log.v("test", "close");
path.close();
resetPaintWithAntiAlias(mPaint, true);
LinearGradient linearGradient1 = new LinearGradient(0, 0, 0, getHeight(), Color.parseColor(mPathEndColor), Color.parseColor(mPathStartColor), Shader.TileMode.CLAMP);
mPaint.setShader(linearGradient1);
mCanvas.drawPath(path, mPaint);
}
}
canvas.drawBitmap(mFullImage, 0, 0, null);
}
private int getPixelForDip(int dipValue) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dipValue,
getResources().getDisplayMetrics());
}
}
代码解读
其实代码部分已经没什么好说的了,我的注释和命名基本上大家对自定义view有了解的话都能看出我这是很简单的一个自绘图形,自定义view牵扯到最大的两个方法就是onMeasure和onDraw,我在onMeasure的自我测量里把不合比例的宽高写死的原因是,我的字体大小是根据给定的高度来的,如果比例特别不协调会造成最后出来的图形特别难看,字也有可能显示不完全,这么写的另外一个原因就是为我同事使用控件服务的,因为是定制款的所以不需要考虑特别多的适配问题。
折线渐变的实现思路起始就是设置两个颜色,然后控制它们的相对位置,区域填色也类似,反正起点到终点是代表颜色渐变的方向,而不是颜色渐变的线。
Utils工具类里面只有两个方法,这里就不贴了,事实上我是一个安卓新人,所以没有太多markdown的使用经验,好像代码块显示也有一点问题,不过不影响观看就不管了。
写在后面
写这些在博客上,一是给自己提个醒,把一些工作中遇到的难题解决思路记录下来,二是看看自己有多勤奋可以保持更新,三是希望在成为大牛的路上开始奔跑,四是好多大神的博客都是互相转来转去,对解决问题帮助不大,就好像我一开始被一个大牛的博客推荐用HelloCharts这个框架,用过的人自然会知道有多坑,反正不改源码是不可能正常使用的。MPAndroidCharts没用过,不做评价。