需求
接手的公司的一个项目,有一个需求是折线图可以通过上下滑动改变数值。原先的大佬自己从头写的,也能实现功能。大佬后来也把思路和代码分享出来了。大家有兴趣的可以看一下。
手把手教你写一个可以上下滑动点改变值的安卓折线图
因为后来需求有些变动,原来的代码改动起来略显吃力,于是自己又以hellocharts为基础重新封装了一个自定义VIew,以此来实现功能。使用起来也更简洁一些。
需求明确:
- 多条折线图切换滑动
- 手动上下滑动改变数值
实现效果图
分析
现在最好用的图表库大概就是hellochart了。但是hellochart并没有提供滑动改变数值的接口以及相关内容。因此我们在hellochart的基础上进行功能扩展,通过自定义控件来实现如图效果。
首先需要确定用户点击的是哪一条线,哪一个点。然后对对应的数据进行操作。重新进行赋值改变图表显示。
实现
尺寸计算
首先计算控件宽高,因为要滑动,不宜进行1:1的计算,滑动难度高。因此乘以0.75,以方便响应用户手势操作。
int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
hLength = width * 0.75f; // x轴长度
vLength = height * 0.75f; // y轴长度
触摸 - 按下响应事件
lineChartData = vccharts.this.getLineChartData();
for (int i = 0; i < lineChartData.getLines().size(); i++) {
Line line = lineChartData.getLines().get(i);
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
for (int j = 0; j < line.getValues().size(); j++) {
PointValue pointValue = line.getValues().get(j);
final float rawValueX = vccharts.this.getChartComputator().computeRawX(pointValue.getX());
final float rawValueY = vccharts.this.getChartComputator().computeRawY(pointValue.getY());
if (isInArea(rawValueX, rawValueY, touchX, touchY, pointRadius + touchToleranceMargin)) {
LogUtils.e(" xuanzhong xxx " + pointValue.getX() + " yyy " + pointValue.getY() + " ");
vccharts.this.lineIndex = i;
vccharts.this.pointIndex = j;
isMoveChange = true;
for (Line line1 : lineChartData.getLines()) {
line1.setStrokeWidth(2);
}
line.setStrokeWidth(4);
selectLineNumber = i;
}
}
}
触摸 - 移动响应事件
移动的时候需要判断位移以及差值,重新计算比例,确定新数值,进而更新数据,刷新View显示。
float y_max = 10;
float y_min = 0;
// 如果没有选中任何点
if (pointIndex == -1 || lineIndex == -1) {
return super.onTouchEvent(event);
}
lineChartData = vccharts.this.getLineChartData();
float y = lineChartData.getLines().get(lineIndex).getValues().get(pointIndex).getY();
float x = lineChartData.getLines().get(lineIndex).getValues().get(pointIndex).getX();
float y_new = y_max - coordinateConversionY(touchY);
数值保护,防止超过数据边界
if (y_new < y_pre) {
y_new = y_pre;
}
if (y_new > y_after) {
y_new = y_after;
}
if (y_new < y_min) {
y_new = y_min;
}
if (y_new > y_max) {
y_new = y_max;
}
重新设置数据,通知数据变化
lineChartData.getLines().get(lineIndex).getValues().get(pointIndex).set(x, y_new);
// 通知数据变化
postInvalidate();
触摸 - 抬起事件
抬起的时候,取消选择的线条的状态,并且计算最终数据,进行显示
if (vccharts.this.pointIndex == -1 || vccharts.this.lineIndex == -1) {
isMoveChange = false;
return true;
}
// 通知数据变化
List<PointValue> values = lineChartData.getLines().get(lineIndex).getValues();
float[] changeData = new float[values.size()];
for (int i = 0; i < values.size(); i++) {
changeData[i] = values.get(i).getY();
}
try {
if (vcListener != null) {
vcListener.DataChange(lineIndex, new ZuniData(changeData));
}
} catch (Exception e) {
e.printStackTrace();
}
postInvalidate();
isMoveChange = false;
vccharts.this.pointIndex = -1;
vccharts.this.lineIndex = -1;
return true;
判断是否点击对应点
private boolean isInArea(float x, float y, float touchX, float touchY, float radius) {
float diffX = touchX - x;
float diffY = touchY - y;
return Math.pow(diffX, 2) + Math.pow(diffY, 2) <= 2 * Math.pow(radius, 2);
}
完整代码:
public class vccharts extends LineChartView {
private static final int DEFAULT_TOUCH_TOLERANCE_MARGIN_DP = 4;
private final ChartTouchHandler0 touchHandler0;
boolean isMoveChange = false; // 是否需要根据移动改变
boolean isBetterPreAfter = true; // 是否需要比较前后
int pointIndex;
int lineIndex;
private LineChartData lineChartData;
private float vLength;// 竖线长度
private float hLength;// 横线长度
private float density;
private int touchToleranceMargin;
private int moveNumber = 0;
private int selectLineNumber = -1; // 现在选中的线条
private vcDataChangeListener vcListener;
public vccharts(Context context, AttributeSet attrs) {
super(context, attrs);
try {
density = context.getResources().getDisplayMetrics().density;
touchToleranceMargin = ChartUtils.dp2px(density, DEFAULT_TOUCH_TOLERANCE_MARGIN_DP);
LogUtils.e(" touchToleranceMargin " + touchToleranceMargin);
} catch (Exception e) {
e.printStackTrace();
}
touchHandler0 = new ChartTouchHandler0(context, this);
vccharts.this.post(new Runnable() {
@Override
public void run() {
//在此处调用view.getMeasuredWidth()和getMeasuredHeight()可得到正确值
int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
hLength = width * 0.75f; // x轴长度
vLength = height * 0.75f; // y轴长度
LogUtils.e(" vLength " + vLength + " h " + hLength);
}
});
}
public void setVcListener(vcDataChangeListener vcListener) {
this.vcListener = vcListener;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float touchY = event.getY();
float touchX = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
LogUtils.e(" 按下了 ");
moveNumber = 0;
lineChartData = vccharts.this.getLineChartData();
for (int i = 0; i < lineChartData.getLines().size(); i++) {
Line line = lineChartData.getLines().get(i);
int pointRadius = ChartUtils.dp2px(density, line.getPointRadius());
for (int j = 0; j < line.getValues().size(); j++) {
PointValue pointValue = line.getValues().get(j);
final float rawValueX = vccharts.this.getChartComputator().computeRawX(pointValue.getX());
final float rawValueY = vccharts.this.getChartComputator().computeRawY(pointValue.getY());
if (isInArea(rawValueX, rawValueY, touchX, touchY, pointRadius + touchToleranceMargin)) {
LogUtils.e(" xuanzhong xxx " + pointValue.getX() + " yyy " + pointValue.getY() + " ");
vccharts.this.lineIndex = i;
vccharts.this.pointIndex = j;
isMoveChange = true;
for (Line line1 : lineChartData.getLines()) {
line1.setStrokeWidth(2);
}
line.setStrokeWidth(4);
selectLineNumber = i;
}
}
}
return true;
case MotionEvent.ACTION_MOVE:
if (isMoveChange) {
float y_max = 10;
float y_min = 0;
// 如果没有选中任何点
if (pointIndex == -1 || lineIndex == -1) {
return super.onTouchEvent(event);
}
lineChartData = vccharts.this.getLineChartData();
float y = lineChartData.getLines().get(lineIndex).getValues().get(pointIndex).getY();
float x = lineChartData.getLines().get(lineIndex).getValues().get(pointIndex).getX();
float y_new = y_max - coordinateConversionY(touchY);
if (isBetterPreAfter) {
float y_pre = y_min;
float y_after = y_max;
if (pointIndex < lineChartData.getLines().get(lineIndex).getValues().size() - 1) {
y_after = lineChartData.getLines().get(lineIndex).getValues().get(pointIndex + 1).getY();
}
if (pointIndex > 0) {
y_pre = lineChartData.getLines().get(lineIndex).getValues().get(pointIndex - 1).getY();
}
if (y_new < y_pre) {
y_new = y_pre;
}
if (y_new > y_after) {
y_new = y_after;
}
}
if (y_new < y_min) {
y_new = y_min;
}
if (y_new > y_max) {
y_new = y_max;
}
lineChartData.getLines().get(lineIndex).getValues().get(pointIndex).set(x, y_new);
// 通知数据变化
/*List<PointValue> values = lineChartData.getLines().get(lineIndex).getValues();
float[] changeData = new float[values.size()];
for (int i = 0; i < values.size(); i++) {
changeData[i] = values.get(i).getY();
}*/
postInvalidate();
/*if (vcListener != null) {
moveNumber++;
if (moveNumber % 3 == 0) {
vcListener.DataChange(lineIndex, new ZuniData(changeData));
}
}*/
}
return super.onTouchEvent(event);
case MotionEvent.ACTION_UP:
LogUtils.e(" move " + moveNumber);
if (isMoveChange) {
if (vccharts.this.pointIndex == -1 || vccharts.this.lineIndex == -1) {
isMoveChange = false;
return true;
}
// 通知数据变化
List<PointValue> values = lineChartData.getLines().get(lineIndex).getValues();
float[] changeData = new float[values.size()];
for (int i = 0; i < values.size(); i++) {
changeData[i] = values.get(i).getY();
}
try {
if (vcListener != null) {
vcListener.DataChange(lineIndex, new ZuniData(changeData));
}
} catch (Exception e) {
e.printStackTrace();
}
postInvalidate();
isMoveChange = false;
vccharts.this.pointIndex = -1;
vccharts.this.lineIndex = -1;
return true;
}
return true;
case MotionEvent.ACTION_CANCEL:
if (isMoveChange) {
isMoveChange = false;
vccharts.this.pointIndex = -1;
vccharts.this.lineIndex = -1;
return true;
}
return true;
}
return super.onTouchEvent(event);
}
// 把坐标从 y 轴的位置转换成实际的值
private float coordinateConversionY(float y) {
LogUtils.i("坐标从 y 轴的位置转换成实际的值" + (10 - 0) / vLength * y);
return (10 - 0) / vLength * y;
}
// 把坐标从 x 轴的位置转换成实际的值
private float coordinateConversionX(float x) {
LogUtils.i("坐标从 x 轴的位置转换成实际的值" + (11 - 0) / hLength * x);
return (11 - 0) / hLength * x;
}
private boolean isInArea(float x, float y, float touchX, float touchY, float radius) {
float diffX = touchX - x;
float diffY = touchY - y;
return Math.pow(diffX, 2) + Math.pow(diffY, 2) <= 2 * Math.pow(radius, 2);
}
public int getSelectLineNumber() {
return selectLineNumber;
}
public void setSelectLineNumber(int selectLineNumber) {
this.selectLineNumber = selectLineNumber;
}
public boolean isBetterPreAfter() {
return isBetterPreAfter;
}
public void setBetterPreAfter(boolean betterPreAfter) {
isBetterPreAfter = betterPreAfter;
}
public interface vcDataChangeListener {
void DataChange(int line_index, ZuniData ZuniData);
}
}