效果展示
效果展示.gif
使用方式
// 初始化数据表格相关
with(mTableView) {
// 配置坐标系
setupCoordinator("日", "人", /*这里是横坐标的值*/0f, 5f, 10f, 15f, 20f, 25f, 30f)
// 添加曲线, 确保纵坐标的数值位数相等
addWave(ContextCompat.getColor(this@MainActivity, R.color.colorYellow), false,
0f, 10f, 30f, 54f, 30f, 100f, 10f)
addWave(ContextCompat.getColor(this@MainActivity, R.color.colorGreen), false,
0f, 30f, 20f, 20f, 46f, 25f, 5f)
addWave(ContextCompat.getColor(this@MainActivity, R.color.colorPink), false,
0f, 30f, 20f, 50f, 46f, 30f, 30f)
addWave(Color.parseColor("#8596dee9"), true,
0f, 15f, 10f, 10f, 40f, 20f, 5f)
}
实现思路
- 横坐标是固定的, 纵坐标需要跟随曲线传入的数值去动态的调整
- 绘制坐标轴: 纵横交错的网格
- 根据用户传入坐标数值去绘制坐标轴上的数值
- 给X轴和Y轴添加单位信息
- 根据用户传入的具体的数值绘制曲线(这里不采用Bezier, 不容易精确的控制顶点的位置)
- 绘制填充效果
- 添加属性动画
代码实现
/**
* Created by FrankChoo on 2017/12/29.
* Email: frankchoochina@gmail.com
* Version: 1.0
* Description: 表格自定义View
*/
public class TableView extends View {
private List<WaveConfigData> mWaves;// 数值集合
// 坐标轴的数值
private int mCoordinateYCount = 8;
private float[] mCoordinateXValues;// 外界传入
private float[] mCoordinateYValues;// 动态计算
// 坐标的单位
private String mXUnit;
private String mYUnit;
// 所有曲线中所有数据中的最大值
private float mGlobalMaxValue;// 用于确认是否需要调整坐标系
private Paint mCoordinatorPaint;
private Paint mTextPaint;
private Paint mWrapPaint;
// 坐标轴上描述性文字的空间大小
private int mTopUnitHeight;// 顶部Y轴单位高度
private int mBottomTextHeight;
private int mLeftTextWidth;
// 网格尺寸
private int mGridWidth, mGridHeight;
private float mAnimProgress;
public TableView(Context context) {
this(context, null);
}
public TableView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TableView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
post(new Runnable() {
@Override
public void run() {
showAnimator();
}
});
}
private void init() {
// 初始化数据集合的容器
mWaves = new ArrayList<>();
// 坐标系的单位
mBottomTextHeight = dp2px(40);// X轴底部字体的高度
mLeftTextWidth = mBottomTextHeight;// Y轴左边字体的宽度
mTopUnitHeight = dp2px(30);// 顶部Y轴的单位
// 初始化坐标轴Paint
mCoordinatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mCoordinatorPaint.setColor(Color.LTGRAY);
// 初始化文本Paint
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mTextPaint.setColor(Color.GRAY);
mTextPaint.setTextSize(sp2px(12));
// 初始化曲线Paint
mWrapPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mWrapPaint.setPathEffect(new CornerPathEffect(200f));
}
/**
* 配置坐标轴信息
*
* @param xUnit X 轴的单位
* @param yUnit Y 轴的单位
* @param coordinateXValues X 坐标轴上的数值
*/
public void setupCoordinator(String xUnit, String yUnit, float... coordinateXValues) {
mXUnit = xUnit;
mYUnit = yUnit;
mCoordinateXValues = coordinateXValues;
}
/**
* 添加一条曲线, 确保与横坐标的数值对应
*
* @param color
* @param isCoverRegion
* @param values
*/
public void addWave(int color, boolean isCoverRegion, float... values) {
mWaves.add(new WaveConfigData(color, isCoverRegion, values));
// 根据value的值去计算纵坐标的数值
float maxValue = 0;
for (float value : values) {
maxValue = Math.max(maxValue, value);
}
if (maxValue < mGlobalMaxValue) return;
mGlobalMaxValue = maxValue;
// 保证网格的数值都为 5 的倍数
float gridValue = mGlobalMaxValue / (mCoordinateYCount - 1);
if (gridValue % 5 != 0) {
gridValue += 5 - (gridValue % 5);
}
// 给纵坐标的数值赋值
mCoordinateYValues = new float[mCoordinateYCount];
for (int i = 0; i < mCoordinateYCount; i++) {
mCoordinateYValues[i] = i * gridValue;
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
drawCoordinate(canvas);
drawWrap(canvas);
}
public void showAnimator() {
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimProgress = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
/**
* 绘制坐标系
*/
private void drawCoordinate(Canvas canvas) {
Point start = new Point();
Point stop = new Point();
// 1. 绘制横轴线和纵坐标单位
int xLineCount = mCoordinateYValues.length;
mGridHeight = (getHeight() - getPaddingTop() - getPaddingBottom() - mBottomTextHeight - mTopUnitHeight) / (xLineCount - 1);
for (int i = 0; i < xLineCount; i++) {
start.x = getPaddingLeft() + mLeftTextWidth;
start.y = getHeight() - getPaddingBottom() - mBottomTextHeight - mGridHeight * i;
stop.x = getRight() - getPaddingRight();
stop.y = start.y;
// 绘制横轴线
canvas.drawLine(start.x, start.y, stop.x, stop.y, mCoordinatorPaint);
// 绘制纵坐标单位
if (i == 0) continue;
String drawText = String.valueOf((int) mCoordinateYValues[i]);
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
float offsetY = ((fontMetrics.bottom - fontMetrics.top) / 2 + fontMetrics.bottom) / 2;
float baseLine = start.y + offsetY;
float left = getPaddingLeft() + mLeftTextWidth / 2 - mTextPaint.measureText(drawText) / 2;
canvas.drawText(drawText, left, baseLine, mTextPaint);
// 绘制Y轴单位
if (i == xLineCount - 1) {
drawText = mYUnit;
baseLine = getPaddingTop() + mTopUnitHeight / 2;
canvas.drawText(drawText, left, baseLine, mTextPaint);
}
}
// 2. 绘制纵轴线和横坐标单位
int yLineCount = mCoordinateXValues.length;
mGridWidth = (getWidth() - getPaddingLeft() - getPaddingRight() - mLeftTextWidth) / (yLineCount - 1);
for (int i = 0; i < yLineCount; i++) {
start.x = getPaddingTop() + mLeftTextWidth + mGridWidth * i;
start.y = getPaddingTop() + mTopUnitHeight;
stop.x = start.x;
stop.y = getHeight() - mBottomTextHeight - getPaddingBottom();
// 绘制纵轴线
canvas.drawLine(start.x, start.y, stop.x, stop.y, mCoordinatorPaint);
// 绘制横坐标单位
String drawText = String.valueOf((int) mCoordinateXValues[i]);
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
float offsetY = ((fontMetrics.bottom - fontMetrics.top) / 2 + fontMetrics.bottom) / 2;
float baseLine = getHeight() - getPaddingBottom() - mBottomTextHeight / 2 + offsetY;
float left = start.x - mTextPaint.measureText(drawText) / 2;
// 绘制X轴单位
if (i == 0) {
drawText = mXUnit;
left = getPaddingLeft() + mLeftTextWidth / 2 - mTextPaint.measureText(drawText) / 2;
}
canvas.drawText(drawText, left, baseLine, mTextPaint);
}
}
/**
* 绘制曲线
*/
private void drawWrap(Canvas canvas) {
canvas.clipRect(new RectF(
mLeftTextWidth,
getPaddingTop() + mTopUnitHeight,
(getRight() - getPaddingRight()) * mAnimProgress,
getHeight() - getPaddingBottom() - mBottomTextHeight)
);
float yHeight = mGridHeight * (mCoordinateYCount - 1);
for (WaveConfigData wave : mWaves) {
Path path = new Path();
path.moveTo(0, getHeight());
float maxY = mCoordinateYValues[mCoordinateYCount - 1];// Y轴坐标的最大值
for (int index = 1; index < wave.values.length; index++) {
path.lineTo(
mLeftTextWidth + mGridWidth * index,
getHeight() - getPaddingBottom() - mBottomTextHeight
- yHeight * (wave.values[index] / maxY)
);
}
if (wave.isCoverRegion) {
mWrapPaint.setStyle(Paint.Style.FILL);
path.lineTo(getRight() - getPaddingRight(), getHeight());
path.close();
} else {
mWrapPaint.setStyle(Paint.Style.STROKE);
mWrapPaint.setStrokeWidth(10);
}
mWrapPaint.setColor(wave.color);
canvas.drawPath(path, mWrapPaint);
}
}
private int dp2px(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, getResources().getDisplayMetrics());
}
private int sp2px(float sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp, getResources().getDisplayMetrics());
}
public static class WaveConfigData {
int color;
boolean isCoverRegion;
float values[];
public WaveConfigData(int color, boolean isCoverRegion, float[] values) {
this.color = color;
this.isCoverRegion = isCoverRegion;
this.values = values;
}
}
}
网友评论