本練習(xí)參考 自定義View練習(xí)(二)簡(jiǎn)易折線圖控件,折線圖支持設(shè)置x軸與y軸的取值范圍與遞增值,效果如下:
linechart效果圖
首先自定義屬性芍耘,在res/value目錄下新建attrs.xml文件沃琅,在此文件中申明自定義的屬性刀诬,除了顏色荚藻、padding等外屋灌,自定義 x_max_value 與 x_step用來(lái)定義x軸的最大值與遞增值,y軸同樣,xml文件如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LineChart">
<attr name="x_step" format="integer" />
<attr name="y_step" format="integer" />
<attr name="coordinate_color" format="color" />
<attr name="chart_padding" format="dimension" />
<attr name="axis_stroke" format="dimension" />
<attr name="x_max_value" format="integer" />
<attr name="y_max_value" format="integer" />
<attr name="point_color" format="color" />
<attr name="line_color" format="color" />
<attr name="point_radius" format="dimension" />
<attr name="line_stroke" format="dimension"/>
<attr name="scaleNumberSize" format="dimension"/>
</declare-styleable>
</resources>
然后繼承自View自定義折線圖应狱,在構(gòu)造方法中獲取設(shè)置的值共郭,由于設(shè)置的x、y軸最大值可能不是遞增值的倍數(shù)疾呻,定義multiple()方法將值轉(zhuǎn)換為小于設(shè)置值的遞增值最大數(shù)落塑,將chartPadding設(shè)置為定義的padding值的1.5倍是為了給接下來(lái)的刻度值預(yù)留空間。
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為設(shè)置的padding的1.5倍罐韩,為刻度值預(yù)留空間
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;
}
接下來(lái)復(fù)寫onMeasure()方法,當(dāng)使用折線圖時(shí)如果選擇的是wrap_content憾赁,MeasureSpec對(duì)應(yīng)的是AT_MOST模式,那么要為折線圖設(shè)置默認(rèn)的大小散吵,這里將長(zhǎng)寬都設(shè)置為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;
}
最后復(fù)寫onDraw()方法繪制整個(gè)圖
1. 繪制坐標(biāo)軸
//零點(diǎn)的x,y
float startXPoint = chartPadding;
float startYPoint = getHeight() - chartPadding;
//x軸終點(diǎn)的x,y
float xAxisEndXPoint = getWidth() - chartPadding;
float xAxisEndYPoint = getHeight() - chartPadding;
//y軸終點(diǎn)的x,y
float yAxisEndXPoint = chartPadding;
float yAxisEndYPoint = chartPadding;
//畫坐標(biāo)軸
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. 繪制坐標(biāo)刻度
//畫刻度
//x軸上的刻度數(shù)量
xCount = xMaxValue / xStep + 1;
//y軸上的刻度數(shù)量
yCount = yMaxValue / yStep + 1;
//每個(gè)刻度之間的實(shí)際間隔矾睦,剛開始沒(méi)有* 1.0f 算出來(lái)的間隔是去掉小數(shù)的整數(shù)晦款,
//會(huì)導(dǎo)致在刻度數(shù)量多的情況下每個(gè)刻度都向前移了,明顯沒(méi)有用完坐標(biāo)軸的長(zhǎng)度
xScaleSpace = (getWidth() - 2 * chartPadding) * 1.0f / xCount;
yScaleSpace = (getHeight() - 2 * chartPadding) * 1.0f / yCount;
//刻度的起始點(diǎn)
float xScaleXStart = startXPoint;
float xScaleYStart = startYPoint;
//刻度的結(jié)束點(diǎn)
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);
//獲取刻度值的文本長(zhǎng)寬枚冗,保存在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;
}
}
- 對(duì)外提供setPoints()方法設(shè)置坐標(biāo)點(diǎn)缓溅,mChartPoints保存剛設(shè)置的坐標(biāo)點(diǎn),mLastChartPoints保存上一次的坐標(biāo)點(diǎn)赁温,默認(rèn)list的順序就是節(jié)點(diǎn)的順序坛怪。
private ArrayList<ChartPoint> mChartPoints;
private ArrayList<ChartPoint> mLastChartPoints;
public void setPoints(ArrayList<ChartPoint> 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;
}
- 動(dòng)畫,對(duì)于每個(gè)節(jié)點(diǎn)只演變y軸的值
private void startAnimation(ArrayList<ChartPoint> fromPoints, final ArrayList<ChartPoint> 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);
//計(jì)算節(jié)點(diǎn)的確切位置
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<ChartPoint> mChartPoints;
private ArrayList<ChartPoint> 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<ChartPoint> 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為設(shè)置的padding的1.5倍,為刻度值預(yù)留空間
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;
//零點(diǎn)的x股囊,y
float startXPoint = chartPadding;
float startYPoint = getHeight() - chartPadding;
//x軸終點(diǎn)的x,y
float xAxisEndXPoint = getWidth() - chartPadding;
float xAxisEndYPoint = getHeight() - chartPadding;
//y軸終點(diǎn)的x,y
float yAxisEndXPoint = chartPadding;
float yAxisEndYPoint = chartPadding;
//畫坐標(biāo)軸
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<ChartPoint> fromPoints, final ArrayList<ChartPoint> 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);
}
}