老规矩,先上图,整个自定义view分为标题模块,xy轴标注数据模块和数据模块,
其实数据模块是有一个浅灰色的背景,但是动态图里面看不出来,真机上可以看出;
和前面几个图表相比增加了贝塞尔区线,虚线,手势点击显示标注线和当前日数据;
后面还有左右按钮切换上周下周数据,这个不是主要功能,就不加到博客中了,真实项目使用直接将数据准备好,
左右切换的时候切换数据源刷新view即可;
老样子,直接上代码
/**
* Created by zheng on 2018/3/28 0027.
* VFX我的模块 贝塞尔区线数据图
* 前六个绘制方法没写多少注释,都是前面用到过的方法
* 绘制主要数据 和 绘制点击事件的显示数据 注释都写的很详细
*/
public class VFXBesselChart extends View {
//主画笔
private Paint mPaint;
//标题矩形
private Rect titleRect;
//数据区域矩形
private Rect dataRect;
String titleName = "浮动收益";
String[] yStrs = {"$10000.00", "$7500.00", "$5000.00", "$2500.00", "$0.00"};
String[] xStrs = {"3/20", "3/21", "3/22", "3/23", "3/24", "3/25", "3/26"};
Integer[] dataArray = {500, 5000, 1399, 9999, 7499, 999, 10};
//定时器
private CountDownTimer timer;
//手势点击记录周期
private int weekPosition = -1;
/**
* 曲线上数据点
*/
private List<Point> pointList = new ArrayList<>();
// 左上角标题文本颜色 x轴底部文本颜色 x轴刻度颜色
private int textTitleColor, textXColor, scaleXColor;
// y轴左边文本颜色 贝塞尔数据线颜色 手势点击后数据标注线颜色
private int textYColor, lineBesselColor, lineClickColor;
// 虚线颜色 y轴左边文本距离 x轴下边文本距离底部的高度
private int lineImaginaryColor, yScaleDataWidth, xScaleDataHeight;
// x轴刻度线宽度 贝塞尔数据线宽度
private int scaleXWidth, lineBesselWidth;
// 虚线宽度 手势点击后数据标注线宽度 数据区域距离顶部距离
private float lineImaginaryWidth, lineClickWidth, dataMarginTop;
//数据区域距离右边的距离
private int dataMarginRightWidth;
//数据区域内部padding距离
private int dataPaddingLeft;
private int dataPaddingRight;
private int dataPaddingTop;
private int dataPaddingBottom;
public VFXBesselChart(Context context) {
this(context, null);
}
public VFXBesselChart(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VFXBesselChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initXmlAttrs(context, defStyleAttr);
initPaint();
}
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(2);
}
private void initXmlAttrs(Context context, int attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.VFXBesselChart);
if (typedArray == null) return;
textTitleColor = typedArray.getColor(R.styleable.VFXBesselChart_text_title_color, getResources().getColor(R.color.black));
textXColor = typedArray.getColor(R.styleable.VFXBesselChart_text_x_color, getResources().getColor(R.color.black));
scaleXColor = typedArray.getColor(R.styleable.VFXBesselChart_scale_x_color, getResources().getColor(R.color.gray_d5));
textYColor = typedArray.getColor(R.styleable.VFXBesselChart_text_y_color, getResources().getColor(R.color.black));
lineBesselColor = typedArray.getColor(R.styleable.VFXBesselChart_line_bessel_color, getResources().getColor(R.color.gold));
lineClickColor = typedArray.getColor(R.styleable.VFXBesselChart_line_click_color, getResources().getColor(R.color.black_an));
lineImaginaryColor = typedArray.getColor(R.styleable.VFXBesselChart_line_imaginary_color, getResources().getColor(R.color.gray_d5));
scaleXWidth = typedArray.getInteger(R.styleable.VFXBesselChart_scale_x_width, 2);
lineImaginaryWidth = typedArray.getDimension(R.styleable.VFXBesselChart_line_imaginary_width, 2);
lineBesselWidth = typedArray.getInteger(R.styleable.VFXBesselChart_line_bessel_widht, 3);
lineClickWidth = typedArray.getDimension(R.styleable.VFXBesselChart_line_click_widht, 2);
dataMarginTop = typedArray.getDimension(R.styleable.VFXBesselChart_data_margin_top, 120);
yScaleDataWidth = typedArray.getInteger(R.styleable.VFXBesselChart_width_y_scale_data, 150);
xScaleDataHeight = typedArray.getInteger(R.styleable.VFXBesselChart_height_x_scale_data, 60);
dataMarginRightWidth = typedArray.getInteger(R.styleable.VFXBesselChart_width_data_margin_right, 50);
dataPaddingLeft = typedArray.getInteger(R.styleable.VFXBesselChart_padding_data_left, 10);
dataPaddingRight = typedArray.getInteger(R.styleable.VFXBesselChart_padding_data_right, 10);
dataPaddingTop = typedArray.getInteger(R.styleable.VFXBesselChart_padding_data_top, 10);
dataPaddingBottom = typedArray.getInteger(R.styleable.VFXBesselChart_padding_data_bottom, 20);
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制左上角标题 浮动收益
drawTitle(canvas);
//数据区域 (灰色矩形)
drawDataRect(canvas);
//y轴左边数据 ($10000.00)
drawYStrs(canvas);
//x轴下面的数据 (3.20)
drawXStrs(canvas);
//绘制五条虚线
drawImaginary(canvas);
//绘制x轴的刻度
drawXScales(canvas);
//绘制主要数据
drawMainDatas(canvas);
//绘制点击事件的显示数据
drawClickData(canvas);
}
//绘制左上角标题 浮动收益
private void drawTitle(Canvas canvas) {
mPaint.setTextAlign(Paint.Align.LEFT);
mPaint.setColor(textTitleColor);
mPaint.setTextSize(38);
titleRect = new Rect();
mPaint.getTextBounds(titleName, 0, titleName.length(), titleRect);
canvas.drawText(
titleName,
50,
titleRect.height() + 40,
mPaint
);
}
//数据区域 (灰色矩形)
private void drawDataRect(Canvas canvas) {
mPaint.setColor(Color.parseColor("#f8f8f8"));
dataRect = new Rect(
yScaleDataWidth, (int) dataMarginTop,
getWidth() - dataMarginRightWidth, getHeight() - xScaleDataHeight
);
canvas.drawRect(
dataRect,
mPaint
);
}
//y轴左边数据 ($10000.00)
private void drawYStrs(Canvas canvas) {
mPaint.setTextSize(25);
mPaint.setColor(textYColor);
mPaint.setTextAlign(Paint.Align.RIGHT);
for (int i = 0; i < yStrs.length; i++) {
canvas.drawText(
yStrs[i],
yScaleDataWidth - 5,
(dataRect.height() - 30) / 4 * i + 20 + dataMarginTop,
mPaint
);
}
}
//x轴下面的数据 (3.20)
private void drawXStrs(Canvas canvas) {
mPaint.setTextSize(25);
mPaint.setColor(textXColor);
for (int i = 0; i < 7; i++) {
if (i == 0) {
mPaint.setTextAlign(Paint.Align.LEFT);
} else if (i == 6) {
mPaint.setTextAlign(Paint.Align.RIGHT);
} else {
mPaint.setTextAlign(Paint.Align.CENTER);
}
Rect xDataRect = new Rect();
mPaint.getTextBounds(xStrs[i], 0, xStrs[i].length(), xDataRect);
canvas.drawText(
xStrs[i],
yScaleDataWidth + 10 + ((dataRect.width() - 20) / 6) * i,
dataMarginTop + dataRect.height() + (int) (xDataRect.height() * 1.5),
mPaint
);
}
}
//绘制五条虚线
private void drawImaginary(Canvas canvas) {
Paint imaginaryPaint = new Paint();
imaginaryPaint.setStyle(Paint.Style.FILL);
imaginaryPaint.setColor(lineImaginaryColor);
imaginaryPaint.setStrokeWidth(lineImaginaryWidth);
//绘制长度为4的实线后再绘制长度为4的空白区域,0位间隔
DashPathEffect effect = new DashPathEffect(new float[]{4, 4}, 0);
imaginaryPaint.setPathEffect(effect);
imaginaryPaint.setAntiAlias(true);
int allImaginaryHeight = dataRect.height() - dataPaddingTop - dataPaddingBottom;
for (int i = 0; i < 5; i++) {
//两点一线
canvas.drawLine(
yScaleDataWidth + dataPaddingLeft, dataMarginTop + dataPaddingTop + allImaginaryHeight / 4 * i,
getWidth() - dataMarginRightWidth - dataPaddingRight, dataMarginTop + dataPaddingTop + allImaginaryHeight / 4 * i,
imaginaryPaint
);
}
}
//绘制x轴的刻度
private void drawXScales(Canvas canvas) {
Paint xScalesPaint = new Paint();
xScalesPaint.setAntiAlias(true);
xScalesPaint.setStyle(Paint.Style.STROKE);
xScalesPaint.setColor(scaleXColor);
xScalesPaint.setStrokeWidth(scaleXWidth);
int allXScalesWidth = getWidth() - yScaleDataWidth - dataPaddingLeft - dataPaddingRight - dataMarginRightWidth;
for (int i = 0; i < 7; i++) {
canvas.drawLine(
yScaleDataWidth + dataPaddingLeft + allXScalesWidth / 6 * i, getHeight() - xScaleDataHeight,
yScaleDataWidth + dataPaddingLeft + allXScalesWidth / 6 * i, getHeight() - xScaleDataHeight - dataPaddingBottom + 10,
xScalesPaint
);
}
}
//绘制主要数据
private void drawMainDatas(Canvas canvas) {
Paint dataPaint = new Paint();
dataPaint.setAntiAlias(true);
dataPaint.setStyle(Paint.Style.STROKE);
dataPaint.setColor(lineBesselColor);
dataPaint.setStrokeWidth(lineBesselWidth);
Path path = new Path();
path.reset();
//计算整个数据区域的宽高
int allDataHeight = dataRect.height() - dataPaddingTop - dataPaddingBottom;
int allDataWidth = dataRect.width() - dataPaddingLeft - dataPaddingRight;
/**
* 计算数据的XY坐标
*/
for (int i = 0; i < dataArray.length; i++) {
Point point = new Point();
point.set(
yScaleDataWidth + dataPaddingLeft + allDataWidth / 6 * i,
(int) (getHeight() - xScaleDataHeight - dataPaddingBottom - (allDataHeight / 10000.0 * dataArray[i]))
);
pointList.add(point);
}
//数据数据点绘制贝塞尔区线
drawScrollLine(canvas, dataPaint);
}
//绘制点击事件的显示数据
private void drawClickData(Canvas canvas) {
//如果为-1代表没有点击
if (weekPosition == -1) return;
//这里重新重建画笔
Paint weekPaint = new Paint();
weekPaint.setAntiAlias(true);
weekPaint.setStrokeWidth(lineClickWidth);
weekPaint.setTextSize(20);
weekPaint.setColor(lineClickColor);
//绘制竖线(两点一线)
canvas.drawLine(
//y轴标注数据宽度 + 数据模块左padding值 + 六天总宽度 / 6 * 第几天
yScaleDataWidth + dataPaddingLeft + (dataRect.width() - dataPaddingLeft - dataPaddingRight) / 6 * weekPosition,
//数据模块上padding值 + 数据模块距离view顶部距离
dataPaddingTop + dataMarginTop,
yScaleDataWidth + dataPaddingLeft + (dataRect.width() - dataPaddingLeft - dataPaddingRight) / 6 * weekPosition,
//整个view高度 - x轴下方标注数据高度 - 数据模块下padding值
getHeight() - xScaleDataHeight - dataPaddingBottom,
weekPaint
);
//绘制矩形数据
Point weekPoint = pointList.get(weekPosition); //获取数据点
String weekDataStr = "$" + dataArray[weekPosition];
ToastUtils.showToast(getContext(), weekDataStr);
Rect weekRect = new Rect();
weekPaint.getTextBounds(weekDataStr, 0, weekDataStr.length(), weekRect);
weekPaint.setColor(Color.parseColor("#aac69d5e"));
weekPaint.setTextAlign(Paint.Align.CENTER);
//如果是前五天(周一到周五)显示在数据线右边 否则 显示在数据线左边
if (weekPosition < 5) {
/**
* 精细计算了一下宽度,高度同理,宽度根据文本宽度+预留padding值
* 宽度:当前数据点x坐标 + 文本宽度 + 预留padding值(30)
*/
Rect weekBgRect = new Rect(
weekPoint.x + 5, weekPoint.y - 35,
weekPoint.x + weekRect.width() + 30, weekPoint.y - 5
);
canvas.drawRect(weekBgRect, weekPaint);
weekPaint.setColor(Color.WHITE);
canvas.drawText(
weekDataStr,
weekPoint.x + 5 + weekBgRect.width() / 2,
weekPoint.y - 12,
weekPaint
);
} else {
Rect weekBgRect = new Rect(
weekPoint.x - weekRect.width() - 30, weekPoint.y - 35,
weekPoint.x - 5, weekPoint.y - 5
);
canvas.drawRect(weekBgRect, weekPaint);
weekPaint.setColor(Color.WHITE);
canvas.drawText(
weekDataStr,
weekPoint.x - 5 - weekBgRect.width() / 2,
weekPoint.y - 12,
weekPaint
);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
//两天之间的宽度
int weekScale = pointList.get(1).x - pointList.get(0).x;
for (int i = 0; i < pointList.size(); i++) {
//for循环遍历计算手势点击坐标离哪天最近
if (Math.abs(event.getX() - pointList.get(i).x) < weekScale / 2) {
//标注点击
weekPosition = i;
//倒计时4秒隐藏(倒计时之前先取消上次倒计时)
if (timer != null)
timer.cancel();
//开启倒计时
startCountDownTime(4);
invalidate();
break;
}
}
break;
}
return true;
}
//多个数据点绘制贝塞尔区线
private void drawScrollLine(Canvas canvas, Paint dataPaint) {
Point startp = new Point();
Point endp = new Point();
for (int i = 0; i < pointList.size() - 1; i++) {
startp = pointList.get(i);
endp = pointList.get(i + 1);
int wt = (startp.x + endp.x) / 2;
Point p3 = new Point();
Point p4 = new Point();
p3.y = startp.y;
p3.x = wt;
p4.y = endp.y;
p4.x = wt;
Path path = new Path();
path.moveTo(startp.x, startp.y);
path.cubicTo(p3.x, p3.y, p4.x, p4.y, endp.x, endp.y);
canvas.drawPath(path, dataPaint);
}
}
private void startCountDownTime(long time) {
timer = new CountDownTimer(time * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
//每隔countDownInterval秒会回调一次onTick()方法
}
@Override
public void onFinish() {
//倒计时结束
weekPosition = -1;
invalidate();
}
};
timer.start();// 开始计时
//timer.cancel(); // 取消
}
}
设置自定义属性需要在styles.xml中声明:
<!--我的模块贝塞尔区线数据图-->
<declare-styleable name="VFXBesselChart">
<!--左上角标题文本颜色-->
<attr name="text_title_color" format="color" />
<!--x轴底部文本颜色-->
<attr name="text_x_color" format="color" />
<!--x轴刻度颜色-->
<attr name="scale_x_color" format="color" />
<!--y轴左边文本颜色-->
<attr name="text_y_color" format="color" />
<!--贝塞尔数据线颜色-->
<attr name="line_bessel_color" format="color" />
<!--手势点击后数据标注线颜色-->
<attr name="line_click_color" format="color" />
<!--虚线颜色-->
<attr name="line_imaginary_color" format="color" />
<!--x轴刻度线宽度-->
<attr name="scale_x_width" format="integer" />
<!--虚线宽度-->
<attr name="line_imaginary_width" format="dimension" />
<!--贝塞尔数据线宽度-->
<attr name="line_bessel_widht" format="integer" />
<!--手势点击后数据标注线宽度-->
<attr name="line_click_widht" format="dimension" />
<!--数据区域距离顶部距离-->
<attr name="data_margin_top" format="dimension" />
<!--y轴左边文本距离-->
<attr name="width_y_scale_data" format="integer"/>
<!--x轴下边文本距离底部的高度-->
<attr name="height_x_scale_data" format="integer"/>
<!--数据区域距离右边的距离-->
<attr name="width_data_margin_right" format="integer"/>
<!--数据区域内部padding距离-->
<attr name="padding_data_top" format="integer"/>
<attr name="padding_data_bottom" format="integer"/>
<attr name="padding_data_left" format="integer"/>
<attr name="padding_data_right" format="integer"/>
</declare-styleable>
<!--android:layerType="software" 绘制虚线需要设置-->
<com.example.administrator.vfxbesselchart.view.VFXBesselChart
android:layout_width="match_parent"
android:layout_height="240dp"
android:layerType="software"
app:text_title_color="#000000"/>