目前图标开源项目很多,也很优秀,如MpAndroidChart。这些开源项目都提供了丰富的功能,能够满足大多数的开发需求。然而实际开发中,需求是不会适应框架的,总有些需求是开源项目所不能满足的,那么这时候就需要我们自己搞。同时也告诉自己,我们不是代码的搬运工。
提到折线图,我们日常生活中的坐标系与Android屏幕的坐标系是不同的,至今没明白为什么屏幕坐标系原点要在右上角。首先需要图坐标系与Android坐标系的关系,或者说给定一组图中点的坐标,如何把它转化成Android坐标系内的坐标。
话说得有点绕,上图:
屏幕坐标系左上角是原点,直角坐标系原点在显示区域内。
blankSize你可以理解为任何引起内边距的可能,它可能是直接的padding,也可能是单纯为了显示效果而设置的固定值,在这里是padding和固定值的累加。上几篇博文介绍到view应当支持padding,那么用户如果就不想设置padding,坐标线的显示就会出现问题,所以,这里给他默认一个小小的留白。
那么,在padding和留白的共同作用下,显示区域的边界是什么呢?
_left=paddingLeft+blankLeft;
_right=width-paddingRight-blankRight;
_top=paddingTop+blankTop;
_buttom=width-paddingBottom-blankBottom;
也可以把上下左右的留白都设计成一样的 ,那样也可以。
我们在纸上画折线图啊,柱状图等的时候,总喜欢先画原点,然后x,y两条轴,然后是单位,单位1代表多少。这样做的一个问题就是容易造成纸张大部分的留白,或者单位设的不好,纸不够长了。Android屏幕可没有这么长,所以,为了好看和实用,任意时候,让折线或者曲线在竖直方向上顶天立地。
最大值:maxValue 显示在图表的上边界。
最小值minValue 显示在图表的下边界。
数据数量,valueSize
x轴单位长度xScale:默认图表显示八个点,用图表宽度除以8得到。如果valueSize>defaultNum,则宽度除以valueSize。有人问为什么默认八个点,如果数据一共两个点,那显示出来是多么尴尬的效果,太长了不好看。
y轴单位长度yScale,height/(maxValue-minValue)
(x0,y0)在屏幕上应该绘制的Y坐标:
y=height-blankSizeB-(y0-minValue)*yScale
x的值是随着数组的循环在变化的
x=_left+(i-1)*xScale
这样,直角坐标系中的坐标就被我们转化成了安卓坐标系的坐标。
拿到了坐标,我们就可以遍历数据,一条line一条line的画图了。This is level 1!
这种方式得到的是简单折线图,没有数据怎么办?人家就想看一张空的图表怎么办?难道让用户面对黑屏尴尬吗?N0,不论有没有数据,都要显示图表的边界,而且没数据的时候要用文字展示出来。
理想的文字显示位置是图表的中心。下面是在全屏情况下在中心写Text时的坐标
Paint.FontMetrics fm = paint.getFontMetrics();
文字高度 float textHeight = fm.descent - fm.ascent;
文字长度 = paint.measureText()
横向居中:X = (布局width - 文字长度)/2
纵向居中:Y(baseLine ) = (布局height + 文字高度)/2 - descent
有了无数据时的操作,你的图表更加健壮,然而,它的显示样式不可更改,而且折线图怎么看怎么别扭,太难看。那么上一些二年级水平的内容,如何让你的折线图变平滑的方式:运用贝赛尔曲线。
贝赛尔曲线是像这样:
图片来自百度百科
的一段曲线,关于贝赛尔曲线的详情请查看百度百科,总之很牛叉就对了。运用它就可以使得折线图变成光滑的曲线图,前提是你需要设置控制点。
这里我们用三阶:,(x1,y1)(x2,y2)之间的控制点我选取((x1+x2)/2,y1),((x1+x2)/2,y2)
Android为我们提供了画贝赛尔曲线的方法,Path.cubicTo
然后,就得到了有点优雅的曲线:
最后,很多需求都需要显示一个均值,或者一个标准,我们绘制标准线,并且将标准线上方画上颜色以区分:
这是效果图。
编码中用到的颜色,线宽,文字样式,需要提供xml直接设置的方式,具体操作前几篇讲过,就不再赘述,
接下来附上代码:
public class MySimpleChart extends View {
private DecimalFormat decimalFormat = new DecimalFormat("0.0");
//没数据时候的 显示样式
private int noData_BackColor = Color.WHITE;//backgroundColor
private float noData_TextSize = DensityUtils.dp(getContext(), 15);//提示信息字体大小
private int noData_TextColor = Color.GRAY;//提示信息字体大小
private String noData_Text = "No Data"; //提示信息
private int noData_BorderLineColor = Color.BLUE;//没数据时边界颜色。
//有数据的时候,显示样式 颜色部分
private int hasData_Color_Back = Color.WHITE;//背景色
private int hasData_Color_Point = Color.RED;//如需描点,点的颜色。
private int hasData_Color_Line = Color.RED;//线的颜色。
private int hasData_BorderLineColor = Color.BLACK;//有数据时候边界颜色
//有数据的时候,显示样式 大小部分。
private float hasData_Width_Line = DensityUtils.dp(getContext(), 8);//线宽
//有数据 显示标准线时 的颜色
private int hasData_Color_StanLineUpBack = 0xfffef1ed;//标准线,标准线上方的背景颜色
private int hasData_Color_StanLine = Color.RED;//标准线颜色
private int hasData_Color_StanLineText = Color.RED;//标准线数值文本的颜色
//有数据 显示标准线时 大小
private float hasData_Width_StanLine = DensityUtils.dp(getContext(), 3);//标准线宽
private float hasData_Size_StanLineText = DensityUtils.dp(getContext(), 13);//标准线数值文本的大小
/***********
* 显示区域的边界
**************************/
private int _left;
private int _top;
private int _right;
private int _bottom;
private int _height;
private int _width;
private Paint mPaint;
private Paint mPaint_DrawPoint;
private int blankSize = 8;
private List<Float> values = new ArrayList<>();
private String title = "title";
private boolean needDrawPoint;
private boolean needDrawStanLine;
public MySimpleChart(Context context) {
this(context, null, 0);
}
public MySimpleChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MySimpleChart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint_DrawPoint = new Paint();
//测试时在这里初始化数据,实际使用时候注释掉 initValues();
initSets(context, attrs);
}
private void initSets(Context context, AttributeSet attrs) {
//获取xml中的配置信息
final TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.MyView_ArcScale);
this.noData_BackColor = typedArray.getColor(R.styleable.MySimpleChart_noData_BackColor, noData_BackColor);
this.noData_BorderLineColor = typedArray.getColor(R.styleable.MySimpleChart_noData_BorderLineColor, noData_BorderLineColor);
this.noData_TextColor = typedArray.getColor(R.styleable.MySimpleChart_noData_TextColor, noData_TextColor);
this.noData_Text = typedArray.getString(R.styleable.MySimpleChart_noData_Text);
this.noData_TextSize = typedArray.getDimension(R.styleable.MySimpleChart_noData_TextSize, noData_TextSize);
this.hasData_BorderLineColor = typedArray.getColor(R.styleable.MySimpleChart_hasData_BorderLineColor, hasData_BorderLineColor);
this.hasData_Color_Back = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_Back, hasData_Color_Back);
this.hasData_Color_Line = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_Line, hasData_Color_Line);
this.hasData_Color_Point = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_Point, hasData_Color_Point);
this.hasData_Color_StanLine = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_StanLine, hasData_Color_StanLine);
this.hasData_Color_StanLineUpBack = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_StanLineUpBack, hasData_Color_StanLineUpBack);
this.hasData_Color_StanLineText = typedArray.getColor(R.styleable.MySimpleChart_hasData_Color_StanLineText, hasData_Color_StanLineText);
this.hasData_Size_StanLineText = typedArray.getDimension(R.styleable.MySimpleChart_hasData_Size_StanLineText, hasData_Size_StanLineText);
this.hasData_Width_Line = typedArray.getDimension(R.styleable.MySimpleChart_hasData_Width_Line, hasData_Width_Line);
this.hasData_Width_StanLine = typedArray.getDimension(R.styleable.MySimpleChart_hasData_Width_StanLine, hasData_Width_StanLine);
this.needDrawPoint = typedArray.getBoolean(R.styleable.MySimpleChart_needDrawPoint, needDrawPoint);
this.needDrawStanLine = typedArray.getBoolean(R.styleable.MySimpleChart_needDrawStanLine, needDrawStanLine);
this.stanLineValue = typedArray.getFloat(R.styleable.MySimpleChart_stanLineValue, stanLineValue);
typedArray.recycle();//回收资源
}
int paddingLeft;
int paddingRight;
int paddingTop;
int paddingButtom;
float xScale;// x每一个单位代表多长
float yScale;// y每单位代表多长
int defCount = 8;//默认图标上显示八个节点。
private float stanLineValue;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paddingButtom = getPaddingBottom();
paddingLeft = getPaddingLeft();
paddingRight = getPaddingRight();
paddingTop = getPaddingTop();
_left = paddingLeft + blankSize;
_top = paddingTop + blankSize;
_right = getWidth() - paddingRight - blankSize;
_bottom = getHeight() - paddingButtom - blankSize;
_width = _right - _left;
_height = _bottom - _top;
if (values.isEmpty()) drawNoDataView(mPaint, canvas);
else {
drawDataView(mPaint, canvas);
}
}
private void drawDataView(Paint paint, Canvas canvas) {
//边界线
paint.setStyle(Paint.Style.STROKE);
paint.setColor(hasData_BorderLineColor);
paint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 1));
canvas.drawRect(_left, _top, _right, _bottom, paint);
paint.reset();
initXScale();
initYScale();
if (needDrawStanLine) drawStanLine(paint, canvas, stanLineValue);
//直角坐标系原点
int point_startX = _left;
int point_startY = _bottom;
float startX;
float startY;
float endX = 0;
float endY = 0;
float ctrlX;
Path path = new Path();
for (int i = 1; i < values.size(); i++) {
startX = point_startX + (i - 1) * xScale;
startY = point_startY - (values.get(i - 1) - minValue) * yScale;
endX = point_startX + i * xScale;
endY = point_startY - (values.get(i) - minValue) * yScale;
ctrlX = (startX + endX) / 2;
path.moveTo(startX, startY);
path.cubicTo(ctrlX, startY, ctrlX, endY, endX, endY);
if (needDrawPoint) {
drawPoint(endX, endY, canvas, mPaint_DrawPoint);
}
}
if (needDrawPoint) drawPoint(endX, endY, canvas, mPaint_DrawPoint);
paint.setStrokeWidth(hasData_Width_Line);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(hasData_Color_Line);
canvas.drawPath(path, paint);
}
private void drawPoint(float endX, float endY, Canvas canvas, Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setColor(hasData_Color_Point);
canvas.drawPoint(endX, endY, paint);
}
private void drawStanLine(Paint paint, Canvas canvas, float stanLineValue) {
float yValue = _bottom - (stanLineValue - minValue) * yScale;
//标准线上的部分绘制深色
paint.setStyle(Paint.Style.FILL);
paint.setColor(hasData_Color_StanLineUpBack);
canvas.drawRect(_left, _top, _right, yValue, paint);
//绘制标准线
paint.setStrokeCap(Paint.Cap.BUTT);
paint.setStrokeWidth(hasData_Width_StanLine);
Paint.FontMetrics fm = paint.getFontMetrics();
float baseline = fm.descent;
paint.setStyle(Paint.Style.STROKE);
paint.setColor(hasData_Color_StanLine);
paint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 1));
Path standardLinePath = new Path();
standardLinePath.moveTo(_left, yValue);
standardLinePath.lineTo(_right, yValue);
canvas.drawPath(standardLinePath, paint);
paint.reset();
//绘制右侧边界线和标准值
paint.setPathEffect(null);
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(hasData_Size_StanLineText);
String standard = decimalFormat.format(stanLineValue);
canvas.drawText(standard, _right + paddingRight / 2, yValue + baseline, paint);
paint.setColor(Color.parseColor("#d9d9d9"));
canvas.drawLine(_right, _top, _right, _bottom, paint);
}
float maxValue;
float minValue;
private void initYScale() {
maxValue = values.get(0);
minValue = values.get(0);
for (Float f : values) {
if (f < minValue) minValue = f;
if (f > maxValue) maxValue = f;
}
yScale = _height / (maxValue - minValue);
// L.d("yScale is"+yScale);
}
private void initXScale() {
int size = values.size();
int count = size > defCount ? size : defCount;
xScale = _width / count;
// L.d(size+"----xScale is " +
// ""+xScale);
}
private void drawNoDataView(Paint paint, Canvas canvas) {
//画无数据时显示内容
paint.setStyle(Paint.Style.STROKE);
paint.setColor(noData_BorderLineColor);
paint.setPathEffect(new DashPathEffect(new float[]{5, 5, 5, 5}, 1));
canvas.drawRect(_left, _top, _right, _bottom, paint);
paint.setColor(Color.GRAY);
paint.setStyle(Paint.Style.FILL);
paint.setTextSize(DensityUtils.dp(getContext(), 20));
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText(noData_Text, (_left + _right) / 2, (_top + _bottom) / 2 + fontMetrics.descent, paint);
}
public void setValue(List<Float> values ){
this.values.addAll(values);
invalidate();
}
public void addValue(List<Float> values){
this.values.addAll(values);
invalidate();
}
public void addValue(Float value){
this.values.add(value);
invalidate();
}
public void resetValue(){
this.values.clear();
invalidate();
}
// private void initValues() {
// values.add(4f);
// values.add(2f);
// values.add(3f);
// values.add(4f);
// values.add(5f);
// values.add(6f);
// values.add(7f);
// values.add(8f);
// values.add(9f);
// values.add(4f);
// values.add(5f);
// values.add(6f);
// values.add(7f);
//
// }
}
下一篇准备开发柱状图 ,让其动态显示