本练习参考 自定义View练习(二)简易折线图控件,折线图支持设置x轴与y轴的取值范围与递增值,效果如下:
首先自定义属性,在res/value目录下新建attrs.xml文件,在此文件中申明自定义的属性,除了颜色、padding等外,自定义 x_max_value 与 x_step用来定义x轴的最大值与递增值,y轴同样,xml文件如下:
然后继承自View自定义折线图,在构造方法中获取设置的值,由于设置的x、y轴最大值可能不是递增值的倍数,定义multiple()方法将值转换为小于设置值的递增值最大数,将chartPadding设置为定义的padding值的1.5倍是为了给接下来的刻度值预留空间。
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineChart, defStyleAttr, 0);
xStep = ta.getInt(R.styleable.LineChart_x_step, DEFAULT_STEP);
yStep = ta.getInt(R.styleable.LineChart_y_step, DEFAULT_STEP);
xMaxValue = multiple(ta.getInt(R.styleable.LineChart_x_max_value, DEFAULT_SCALE_SPACE), xStep);
yMaxValue = multiple(ta.getInt(R.styleable.LineChart_y_max_value, DEFAULT_SCALE_SPACE), yStep);
pointRadius = ta.getDimension(R.styleable.LineChart_point_radius, DensityUtils.dp2px(getContext(), DEFAULT_POINT_RADIUS));
coordinateColor = ta.getColor(R.styleable.LineChart_coordinate_color, Color.BLUE);
pointColor = ta.getColor(R.styleable.LineChart_point_color, Color.RED);
lineColor = ta.getColor(R.styleable.LineChart_line_color, Color.MAGENTA);
float padding = ta.getDimension(R.styleable.LineChart_chart_padding, DensityUtils.dp2px(getContext(), DEFAULT_CHART_PADDING));
chartPadding = padding + padding / 2; //chartPadding为设置的padding的1.5倍,为刻度值预留空间
axisStroke = ta.getDimension(R.styleable.LineChart_axis_stroke, DensityUtils.dp2px(getContext(), DEFAULT_AXIS_STROKE));
lineStroke = ta.getDimension(R.styleable.LineChart_line_stroke, DensityUtils.dp2px(getContext(), DEFAULT_LINE_STROKE));
scaleNumberSize = ta.getDimension(R.styleable.LineChart_scaleNumberSize, DensityUtils.sp2px(getContext(), DEFAULT_SCALE_NUMBER_SIZE));
ta.recycle();
private int multiple(int x, int y) {
int m = x % y;
if (m != 0) x -= m;
return x;
}
接下来复写onMeasure()方法,当使用折线图时如果选择的是wrap_content,MeasureSpec对应的是AT_MOST模式,那么要为折线图设置默认的大小,这里将长宽都设置为200dp(不考虑MeasureSpec.UNSPECIFIED),如下所示:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureDimension(widthMeasureSpec), measureDimension(heightMeasureSpec));
}
private int measureDimension(int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int measureSize = MeasureSpec.getSize(measureSpec);
int result = 0;
if (mode == MeasureSpec.EXACTLY) {
result = measureSize;
} else {
result = DensityUtils.dp2px(this.getContext(), DEFAULT_SIZE);
}
return result;
}
最后复写onDraw()方法绘制整个图
1. 绘制坐标轴
//零点的x,y
float startXPoint = chartPadding;
float startYPoint = getHeight() - chartPadding;
//x轴终点的x,y
float xAxisEndXPoint = getWidth() - chartPadding;
float xAxisEndYPoint = getHeight() - chartPadding;
//y轴终点的x,y
float yAxisEndXPoint = chartPadding;
float yAxisEndYPoint = chartPadding;
//画坐标轴
coordinatePath.reset();
coordinatePath.moveTo(xAxisEndXPoint, xAxisEndYPoint);
coordinatePath.lineTo(startXPoint, startYPoint);
coordinatePath.lineTo(yAxisEndXPoint, yAxisEndYPoint);
canvas.drawPath(coordinatePath, coordinatePaint);
coordinatePath.reset();
//画箭头
coordinatePath.moveTo(xAxisEndXPoint, xAxisEndYPoint - 8);
coordinatePath.lineTo(xAxisEndXPoint + 8, xAxisEndYPoint);
coordinatePath.lineTo(xAxisEndXPoint, xAxisEndYPoint + 8);
coordinatePath.close();
canvas.drawPath(coordinatePath, arrowPaint);
coordinatePath.moveTo(yAxisEndXPoint - 8, yAxisEndYPoint);
coordinatePath.lineTo(yAxisEndXPoint, yAxisEndYPoint - 8);
coordinatePath.lineTo(yAxisEndXPoint + 8, yAxisEndYPoint);
coordinatePath.close();
canvas.drawPath(coordinatePath, arrowPaint);
2. 绘制坐标刻度
//画刻度
//x轴上的刻度数量
xCount = xMaxValue / xStep + 1;
//y轴上的刻度数量
yCount = yMaxValue / yStep + 1;
//每个刻度之间的实际间隔,刚开始没有* 1.0f 算出来的间隔是去掉小数的整数,
//会导致在刻度数量多的情况下每个刻度都向前移了,明显没有用完坐标轴的长度
xScaleSpace = (getWidth() - 2 * chartPadding) * 1.0f / xCount;
yScaleSpace = (getHeight() - 2 * chartPadding) * 1.0f / yCount;
//刻度的起始点
float xScaleXStart = startXPoint;
float xScaleYStart = startYPoint;
//刻度的结束点
float xScaleXEnd = xScaleXStart;
float xScaleYEnd = xScaleYStart - coordinateScaleLength;
//遍历画刻度
for (int i = 0; i < xCount; i++) {
canvas.drawLine(xScaleXStart, xScaleYStart, xScaleXEnd, xScaleYEnd, scalePaint);
String s = String.valueOf(i * xStep);
//获取刻度值的文本长宽,保存在scaleNumberBounds中
scaleNumberPaint.getTextBounds(s, 0, s.length(), scaleNumberBounds);
//画刻度值
canvas.drawText(s, xScaleXStart - scaleNumberBounds.width() / 2,
xScaleYStart + lineStroke + scaleNumberBounds.height() , scaleNumberPaint);
//递增
xScaleXStart += xScaleSpace;
xScaleXEnd += xScaleSpace;
}
//与画x轴刻度值类似
float yScaleXStart = startXPoint;
float yScaleYStart = startYPoint;
float yScaleXEnd = yScaleXStart + coordinateScaleLength;
float yScaleYEnd = yScaleYStart;
for (int i = 0; i < yCount; i++) {
canvas.drawLine(yScaleXStart, yScaleYStart, yScaleXEnd, yScaleYEnd, scalePaint);
if (i != 0) {
String s = String.valueOf(i * yStep);
scaleNumberPaint.getTextBounds(s, 0, s.length(), scaleNumberBounds);
canvas.drawText(s, yScaleXStart - scaleNumberBounds.width() - lineStroke, yScaleYStart + scaleNumberBounds.height() / 2, scaleNumberPaint);
}
yScaleYEnd -= yScaleSpace;
yScaleYStart -= yScaleSpace;
}
3. 绘制折线图
- 定义ChartPoint类
static class ChartPoint {
private int x, y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public ChartPoint(int x, int y) {
this.x = x;
this.y = y;
}
}
- 对外提供setPoints()方法设置坐标点,mChartPoints保存刚设置的坐标点,mLastChartPoints保存上一次的坐标点,默认list的顺序就是节点的顺序。
private ArrayList mChartPoints;
private ArrayList mLastChartPoints;
public void setPoints(ArrayList newList) {
if (newList == null) {
throw new RuntimeException("arrayList is null ");
} else if (newList.size() < 0) {
throw new RuntimeException("arrayList.size() < 0 ");
} else if (newList.size() > xCount) {
throw new RuntimeException("arrayList.size() > xCount ");
}
if (mLastChartPoints == null) {
mChartPoints = newList;
invalidate();
} else {
mChartPoints = newList;
startAnimation(mLastChartPoints, mChartPoints);
}
mLastChartPoints = mChartPoints;
}
- 动画,对于每个节点只演变y轴的值
private void startAnimation(ArrayList fromPoints, final ArrayList toPoints) {
for (int i = 0; i < fromPoints.size(); i++) {
final ChartPoint point = toPoints.get(i);
ValueAnimator valueAnimator = ValueAnimator.ofInt(fromPoints.get(i).getY(), toPoints.get(i).getY());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int val = (int) animation.getAnimatedValue();
point.setY(val);
invalidate();
}
});
valueAnimator.start();
}
}
4.在onDraw()方法中添加绘制折线的代码
if (mChartPoints != null) {
float startX = 0, startY = 0;
for (int i = 0; i < mChartPoints.size(); i++) {
ChartPoint cp = mChartPoints.get(i);
//计算节点的确切位置
float x = startXPoint + cp.getX() / xStep * xScaleSpace;
float y = startYPoint - cp.getY() / yStep * yScaleSpace;
canvas.drawCircle(x, y, pointRadius, mPointPaint);
if (i != 0) {
canvas.drawLine(startX, startY, x, y, mLinePoint);
}
startX = x;
startY = y;
}
}
附代码
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
public class LineChart extends View {
private Paint coordinatePaint;
private static final int DEFAULT_CHART_PADDING = 8;//dp
private static final int DEFAULT_AXIS_STROKE = 2;//dp
private static final int DEFAULT_STEP = 10;
private static final int DEFAULT_SIZE = 200;//dp
private static final int DEFAULT_SCALE_SPACE = 10;
private static final int DEFAULT_POINT_RADIUS = 2;//dp
private static final int DEFAULT_LINE_STROKE = 2;//dp
private static final int DEFAULT_SCALE_NUMBER_SIZE = 4;//sp
private int scaleStroke = 3;
private int coordinateScaleLength = 10;
private float axisStroke;
private float xScaleSpace;//px
private int xStep;
private float yScaleSpace;
private int yStep;
private Paint scalePaint;
private Paint scaleNumberPaint;
private Rect scaleNumberBounds;
private float chartPadding;
private int xCount, yCount;
private Paint mPointPaint;
private Paint mLinePoint;
private Path coordinatePath;
private Paint arrowPaint;
private ArrayList mChartPoints;
private ArrayList mLastChartPoints;
private int pointColor;
private int lineColor;
private int coordinateColor;
private int xMaxValue;
private int yMaxValue;
private float pointRadius;
private float lineStroke;
private float scaleNumberSize;
static class ChartPoint {
private int x, y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public ChartPoint(int x, int y) {
this.x = x;
this.y = y;
}
}
public void setPoints(ArrayList newList) {
if (newList == null) {
throw new RuntimeException("arrayList is null ");
} else if (newList.size() < 0) {
throw new RuntimeException("arrayList.size() < 0 ");
} else if (newList.size() > xCount) {
throw new RuntimeException("arrayList.size() > xCount ");
}
if (mLastChartPoints == null) {
mChartPoints = newList;
invalidate();
} else {
mChartPoints = newList;
startAnimation(mLastChartPoints, mChartPoints);
}
mLastChartPoints = mChartPoints;
}
public LineChart(Context context) {
this(context, null);
}
public LineChart(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LineChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineChart, defStyleAttr, 0);
xStep = ta.getInt(R.styleable.LineChart_x_step, DEFAULT_STEP);
yStep = ta.getInt(R.styleable.LineChart_y_step, DEFAULT_STEP);
xMaxValue = multiple(ta.getInt(R.styleable.LineChart_x_max_value, DEFAULT_SCALE_SPACE), xStep);
yMaxValue = multiple(ta.getInt(R.styleable.LineChart_y_max_value, DEFAULT_SCALE_SPACE), yStep);
pointRadius = ta.getDimension(R.styleable.LineChart_point_radius, DensityUtils.dp2px(getContext(), DEFAULT_POINT_RADIUS));
coordinateColor = ta.getColor(R.styleable.LineChart_coordinate_color, Color.BLUE);
pointColor = ta.getColor(R.styleable.LineChart_point_color, Color.RED);
lineColor = ta.getColor(R.styleable.LineChart_line_color, Color.MAGENTA);
float padding = ta.getDimension(R.styleable.LineChart_chart_padding, DensityUtils.dp2px(getContext(), DEFAULT_CHART_PADDING));
chartPadding = padding + padding / 2; //chartPadding为设置的padding的1.5倍,为刻度值预留空间
axisStroke = ta.getDimension(R.styleable.LineChart_axis_stroke, DensityUtils.dp2px(getContext(), DEFAULT_AXIS_STROKE));
lineStroke = ta.getDimension(R.styleable.LineChart_line_stroke, DensityUtils.dp2px(getContext(), DEFAULT_LINE_STROKE));
scaleNumberSize = ta.getDimension(R.styleable.LineChart_scaleNumberSize, DensityUtils.sp2px(getContext(), DEFAULT_SCALE_NUMBER_SIZE));
ta.recycle();
coordinatePath = new Path();
coordinatePaint = new Paint();
coordinatePaint.setColor(coordinateColor);
coordinatePaint.setStyle(Paint.Style.STROKE);
coordinatePaint.setStrokeWidth(axisStroke);
scalePaint = new Paint();
scalePaint.setStrokeWidth(scaleStroke);
scalePaint.setStyle(Paint.Style.STROKE);
scalePaint.setColor(coordinateColor);
scaleNumberPaint = new Paint();
scaleNumberPaint.setStyle(Paint.Style.FILL);
scaleNumberPaint.setColor(coordinateColor);
scaleNumberPaint.setTextSize(scaleNumberSize);
scaleNumberPaint.setAntiAlias(true);
scaleNumberBounds = new Rect();
mPointPaint = new Paint();
mPointPaint.setStyle(Paint.Style.FILL);
mPointPaint.setColor(pointColor);
mLinePoint = new Paint();
mLinePoint.setStyle(Paint.Style.STROKE);
mLinePoint.setStrokeWidth(lineStroke);
mLinePoint.setColor(lineColor);
mLinePoint.setAntiAlias(true);
arrowPaint = new Paint();
arrowPaint.setColor(coordinateColor);
arrowPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureDimension(widthMeasureSpec), measureDimension(heightMeasureSpec));
}
private int measureDimension(int measureSpec) {
int mode = MeasureSpec.getMode(measureSpec);
int measureSize = MeasureSpec.getSize(measureSpec);
int result = 0;
// MeasureSpec.UNSPECIFIED
if (mode == MeasureSpec.EXACTLY) {
result = measureSize;
} else {
result = DensityUtils.dp2px(this.getContext(), DEFAULT_SIZE);
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
xCount = xMaxValue / xStep + 1;
yCount = yMaxValue / yStep + 1;
xScaleSpace = (getWidth() - 2 * chartPadding) * 1.0f / xCount;
yScaleSpace = (getHeight() - 2 * chartPadding) * 1.0f / yCount;
//零点的x,y
float startXPoint = chartPadding;
float startYPoint = getHeight() - chartPadding;
//x轴终点的x,y
float xAxisEndXPoint = getWidth() - chartPadding;
float xAxisEndYPoint = getHeight() - chartPadding;
//y轴终点的x,y
float yAxisEndXPoint = chartPadding;
float yAxisEndYPoint = chartPadding;
//画坐标轴
coordinatePath.reset();
coordinatePath.moveTo(xAxisEndXPoint, xAxisEndYPoint);
coordinatePath.lineTo(startXPoint, startYPoint);
coordinatePath.lineTo(yAxisEndXPoint, yAxisEndYPoint);
canvas.drawPath(coordinatePath, coordinatePaint);
coordinatePath.reset();
//画箭头
coordinatePath.moveTo(xAxisEndXPoint, xAxisEndYPoint - 8);
coordinatePath.lineTo(xAxisEndXPoint + 8, xAxisEndYPoint);
coordinatePath.lineTo(xAxisEndXPoint, xAxisEndYPoint + 8);
coordinatePath.close();
canvas.drawPath(coordinatePath, arrowPaint);
coordinatePath.moveTo(yAxisEndXPoint - 8, yAxisEndYPoint);
coordinatePath.lineTo(yAxisEndXPoint, yAxisEndYPoint - 8);
coordinatePath.lineTo(yAxisEndXPoint + 8, yAxisEndYPoint);
coordinatePath.close();
canvas.drawPath(coordinatePath, arrowPaint);
// canvas.drawLine(startXPoint, startYPoint, xCoordinateEndXPoint, xCoordinateEndYPoint, mPaint);
// canvas.drawLine(startXPoint, startYPoint, yCoordinateEndXPoint, yCoordinateEndYPoint, mPaint);
//画刻度
float xScaleXStart = startXPoint;
float xScaleYStart = startYPoint;
float xScaleXEnd = xScaleXStart;
float xScaleYEnd = xScaleYStart - coordinateScaleLength;
for (int i = 0; i < xCount; i++) {
canvas.drawLine(xScaleXStart, xScaleYStart, xScaleXEnd, xScaleYEnd, scalePaint);
String s = String.valueOf(i * xStep);
scaleNumberPaint.getTextBounds(s, 0, s.length(), scaleNumberBounds);
canvas.drawText(s, xScaleXStart - scaleNumberBounds.width() / 2,
xScaleYStart + axisStroke + scaleNumberBounds.height(), scaleNumberPaint);
xScaleXStart += xScaleSpace;
xScaleXEnd += xScaleSpace;
}
float yScaleXStart = startXPoint;
float yScaleYStart = startYPoint;
float yScaleXEnd = yScaleXStart + coordinateScaleLength;
float yScaleYEnd = yScaleYStart;
for (int i = 0; i < yCount; i++) {
canvas.drawLine(yScaleXStart, yScaleYStart, yScaleXEnd, yScaleYEnd, scalePaint);
if (i != 0) {
String s = String.valueOf(i * yStep);
scaleNumberPaint.getTextBounds(s, 0, s.length(), scaleNumberBounds);
canvas.drawText(s, yScaleXStart - scaleNumberBounds.width() - axisStroke, yScaleYStart + scaleNumberBounds.height() / 2, scaleNumberPaint);
}
yScaleYEnd -= yScaleSpace;
yScaleYStart -= yScaleSpace;
}
if (mChartPoints != null) {
float startX = 0, startY = 0;
for (int i = 0; i < mChartPoints.size(); i++) {
ChartPoint cp = mChartPoints.get(i);
float x = startXPoint + cp.getX() / xStep * xScaleSpace;
float y = startYPoint - cp.getY() / yStep * yScaleSpace;
canvas.drawCircle(x, y, pointRadius, mPointPaint);
if (i != 0) {
canvas.drawLine(startX, startY, x, y, mLinePoint);
}
startX = x;
startY = y;
}
}
}
public int getxStep() {
return xStep;
}
public void setxStep(int xStep) {
this.xStep = xStep;
}
public int getyStep() {
return yStep;
}
public void setyStep(int yStep) {
this.yStep = yStep;
}
public int getxMaxValue() {
return xMaxValue;
}
public void setxMaxValue(int xMaxValue) {
this.xMaxValue = xMaxValue;
}
public int getyMaxValue() {
return yMaxValue;
}
public void setyMaxValue(int yMaxValue) {
this.yMaxValue = yMaxValue;
}
private void startAnimation(ArrayList fromPoints, final ArrayList toPoints) {
for (int i = 0; i < fromPoints.size(); i++) {
final ChartPoint point = toPoints.get(i);
ValueAnimator valueAnimator = ValueAnimator.ofInt(fromPoints.get(i).getY(), toPoints.get(i).getY());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int val = (int) animation.getAnimatedValue();
point.setY(val);
invalidate();
}
});
valueAnimator.start();
}
}
private int multiple(int x, int y) {
int m = x % y;
if (m != 0) x -= m;
return x;
}
}
public class DensityUtils {
private DensityUtils() {
throw new UnsupportedOperationException("can not be instantiated");
}
public static int dp2px(Context context, float dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics());
}
public static int sp2px(Context context, float spVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, context.getResources().getDisplayMetrics());
}
public static float px2dp(Context context, float pxVal) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, pxVal, context.getResources().getDisplayMetrics());
}
public static float px2sp(Context context, float pxVal) {
return (pxVal / context.getResources().getDisplayMetrics().scaledDensity);
}
}