服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android自定义可左右滑动和点击的折线图

Android自定义可左右滑动和点击的折线图

2022-10-11 15:53呆呆的小木头 Android

这篇文章主要为大家详细介绍了Android自定义可左右滑动和点击的折线图,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

前言

       前几天有小盆友让我写一个折线图,可以点击,可以左右滑动。对于折线肯定有很多项目都使用过,所以网上肯定也有很多demo,像AndroidChart、HelloChart之类的,功能相当丰富,效果也很赞,但是太重了,其他的小demo又不符合要求,当然了,我写的自定义折线图的思想也有来自这些小demo,对他们表示感谢。

效果图

      废话不多说,先上效果图:

Android自定义可左右滑动和点击的折线图

效果是不是很赞,如果上图满足你的需求,那就继续往下看。

自定义折线图的步骤:

1、自定义view所需要的属性

确定所需要的自定义view的属性,然后在res/values目录下,新建一个attrs.xml文件,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<resources>
 <!-- xy坐标轴颜色 -->
 <attr name="xylinecolor" format="color" />
 <!-- xy坐标轴宽度 -->
 <attr name="xylinewidth" format="dimension" />
 <!-- xy坐标轴文字颜色 -->
 <attr name="xytextcolor" format="color" />
 <!-- xy坐标轴文字大小 -->
 <attr name="xytextsize" format="dimension" />
 <!-- 折线图中折线的颜色 -->
 <attr name="linecolor" format="color" />
 <!-- x轴各个坐标点水平间距 -->
 <attr name="interval" format="dimension" />
 <!-- 背景颜色 -->
 <attr name="bgcolor" format="color" />
 <!--是否在ACTION_UP时,根据速度进行自滑动,建议关闭,过于占用GPU-->
 <attr name="isScroll" format="boolean" />
 <declare-styleable name="chartView">
  <attr name="xylinecolor" />
  <attr name="xylinewidth" />
  <attr name="xytextcolor" />
  <attr name="xytextsize" />
  <attr name="linecolor" />
  <attr name="interval" />
  <attr name="bgcolor" />
  <attr name="isScroll" />
 </declare-styleable>
</resources>

2、在自定义view的构造方法中获取我们的自定义属性:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public ChartView(Context context) {
  this(context, null);
 }
 
 public ChartView(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
 }
 
 public ChartView(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  init(context, attrs, defStyleAttr);
  initPaint();
 }
 
 /**
  * 初始化
  *
  * @param context
  * @param attrs
  * @param defStyleAttr
  */
 private void init(Context context, AttributeSet attrs, int defStyleAttr) {
  TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.chartView, defStyleAttr, 0);
  int count = array.getIndexCount();
  for (int i = 0; i < count; i++) {
   int attr = array.getIndex(i);
   switch (attr) {
    case R.styleable.chartView_xylinecolor://xy坐标轴颜色
     xylinecolor = array.getColor(attr, xylinecolor);
     break;
    case R.styleable.chartView_xylinewidth://xy坐标轴宽度
     xylinewidth = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xylinewidth, getResources().getDisplayMetrics()));
     break;
    case R.styleable.chartView_xytextcolor://xy坐标轴文字颜色
     xytextcolor = array.getColor(attr, xytextcolor);
     break;
    case R.styleable.chartView_xytextsize://xy坐标轴文字大小
     xytextsize = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xytextsize, getResources().getDisplayMetrics()));
     break;
    case R.styleable.chartView_linecolor://折线图中折线的颜色
     linecolor = array.getColor(attr, linecolor);
     break;
    case R.styleable.chartView_interval://x轴各个坐标点水平间距
     interval = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, interval, getResources().getDisplayMetrics()));
     break;
    case R.styleable.chartView_bgcolor: //背景颜色
     bgcolor = array.getColor(attr, bgcolor);
     break;
    case R.styleable.chartView_isScroll://是否在ACTION_UP时,根据速度进行自滑动
     isScroll = array.getBoolean(attr, isScroll);
     break;
   }
  }
  array.recycle();
 }
 /**
  * 初始化畫筆
  */
 private void initPaint() {
  xyPaint = new Paint();
  xyPaint.setAntiAlias(true);
  xyPaint.setStrokeWidth(xylinewidth);
  xyPaint.setStrokeCap(Paint.Cap.ROUND);
  xyPaint.setColor(xylinecolor);
  xyTextPaint = new Paint();
  xyTextPaint.setAntiAlias(true);
  xyTextPaint.setTextSize(xytextsize);
  xyTextPaint.setStrokeCap(Paint.Cap.ROUND);
  xyTextPaint.setColor(xytextcolor);
  xyTextPaint.setStyle(Paint.Style.STROKE);
  linePaint = new Paint();
  linePaint.setAntiAlias(true);
  linePaint.setStrokeWidth(xylinewidth);
  linePaint.setStrokeCap(Paint.Cap.ROUND);
  linePaint.setColor(linecolor);
  linePaint.setStyle(Paint.Style.STROKE);
 }

3、获取一写基本点

这些基本点包括:xy轴的原点坐标,第一个点的x轴的初始化坐标值以及其最大值和最小值。这些参数可以在onLayout()方法里面获取。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  if (changed) {
   //这里需要确定几个基本点,只有确定了xy轴原点坐标,第一个点的X坐标值及其最大最小值
   width = getWidth();
   height = getHeight();
   //Y轴文本最大宽度
   float textYWdith = getTextBounds("000", xyTextPaint).width();
   for (int i = 0; i < yValue.size(); i++) {//求取y轴文本最大的宽度
    float temp = getTextBounds(yValue.get(i) + "", xyTextPaint).width();
    if (temp > textYWdith)
     textYWdith = temp;
   }
   int dp2 = dpToPx(2);
   int dp3 = dpToPx(3);
   xOri = (int) (dp2 + textYWdith + dp2 + xylinewidth);//dp2是y轴文本距离左边,以及距离y轴的距离
//   //X轴文本最大高度
   xValueRect = getTextBounds("000", xyTextPaint);
   float textXHeight = xValueRect.height();
   for (int i = 0; i < xValue.size(); i++) {//求取x轴文本最大的高度
    Rect rect = getTextBounds(xValue.get(i) + "", xyTextPaint);
    if (rect.height() > textXHeight)
     textXHeight = rect.height();
    if (rect.width() > xValueRect.width())
     xValueRect = rect;
   }
   yOri = (int) (height - dp2 - textXHeight - dp3 - xylinewidth);//dp3是x轴文本距离底边,dp2是x轴文本距离x轴的距离
   xInit = interval + xOri;
   minXInit = width - (width - xOri) * 0.1f - interval * (xValue.size() - 1);//减去0.1f是因为最后一个X周刻度距离右边的长度为X轴可见长度的10%
   maxXInit = xInit;
  }
  super.onLayout(changed, left, top, right, bottom);
 }

4、利用ondraw()方法进行绘制

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@Override
 protected void onDraw(Canvas canvas) {
//  super.onDraw(canvas);
  canvas.drawColor(bgcolor);
  drawXY(canvas);
  drawBrokenLineAndPoint(canvas);
 }
 
 /**
  * 绘制折线和折线交点处对应的点
  *
  * @param canvas
  */
 private void drawBrokenLineAndPoint(Canvas canvas) {
  if (xValue.size() <= 0)
   return;
  //重新开一个图层
  int layerId = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
  drawBrokenLine(canvas);
  drawBrokenPoint(canvas);
  // 将折线超出x轴坐标的部分截取掉
  linePaint.setStyle(Paint.Style.FILL);
  linePaint.setColor(bgcolor);
  linePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
  RectF rectF = new RectF(0, 0, xOri, height);
  canvas.drawRect(rectF, linePaint);
  linePaint.setXfermode(null);
  //保存图层
  canvas.restoreToCount(layerId);
 }
 /**
  * 绘制折线对应的点
  *
  * @param canvas
  */
 private void drawBrokenPoint(Canvas canvas) {
  float dp2 = dpToPx(2);
  float dp4 = dpToPx(4);
  float dp7 = dpToPx(7);
  //绘制节点对应的原点
  for (int i = 0; i < xValue.size(); i++) {
   float x = xInit + interval * i;
   float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);
   //绘制选中的点
   if (i == selectIndex - 1) {
    linePaint.setStyle(Paint.Style.FILL);
    linePaint.setColor(0xffd0f3f2);
    canvas.drawCircle(x, y, dp7, linePaint);
    linePaint.setColor(0xff81dddb);
    canvas.drawCircle(x, y, dp4, linePaint);
    drawFloatTextBox(canvas, x, y - dp7, value.get(xValue.get(i)));
   }
   //绘制普通的节点
   linePaint.setStyle(Paint.Style.FILL);
   linePaint.setColor(Color.WHITE);
   canvas.drawCircle(x, y, dp2, linePaint);
   linePaint.setStyle(Paint.Style.STROKE);
   linePaint.setColor(linecolor);
   canvas.drawCircle(x, y, dp2, linePaint);
  }
 }
 /**
  * 绘制显示Y值的浮动框
  *
  * @param canvas
  * @param x
  * @param y
  * @param text
  */
 private void drawFloatTextBox(Canvas canvas, float x, float y, int text) {
  int dp6 = dpToPx(6);
  int dp18 = dpToPx(18);
  //p1
  Path path = new Path();
  path.moveTo(x, y);
  //p2
  path.lineTo(x - dp6, y - dp6);
  //p3
  path.lineTo(x - dp18, y - dp6);
  //p4
  path.lineTo(x - dp18, y - dp6 - dp18);
  //p5
  path.lineTo(x + dp18, y - dp6 - dp18);
  //p6
  path.lineTo(x + dp18, y - dp6);
  //p7
  path.lineTo(x + dp6, y - dp6);
  //p1
  path.lineTo(x, y);
  canvas.drawPath(path, linePaint);
  linePaint.setColor(Color.WHITE);
  linePaint.setTextSize(spToPx(14));
  Rect rect = getTextBounds(text + "", linePaint);
  canvas.drawText(text + "", x - rect.width() / 2, y - dp6 - (dp18 - rect.height()) / 2, linePaint);
 }
 /**
  * 绘制折线
  *
  * @param canvas
  */
 private void drawBrokenLine(Canvas canvas) {
  linePaint.setStyle(Paint.Style.STROKE);
  linePaint.setColor(linecolor);
  //绘制折线
  Path path = new Path();
  float x = xInit + interval * 0;
  float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(0)) / yValue.get(yValue.size() - 1);
  path.moveTo(x, y);
  for (int i = 1; i < xValue.size(); i++) {
   x = xInit + interval * i;
   y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);
   path.lineTo(x, y);
  }
  canvas.drawPath(path, linePaint);
 }
 /**
  * 绘制XY坐标
  *
  * @param canvas
  */
 private void drawXY(Canvas canvas) {
  int length = dpToPx(4);//刻度的长度
  //绘制Y坐标
  canvas.drawLine(xOri - xylinewidth / 2, 0, xOri - xylinewidth / 2, yOri, xyPaint);
  //绘制y轴箭头
  xyPaint.setStyle(Paint.Style.STROKE);
  Path path = new Path();
  path.moveTo(xOri - xylinewidth / 2 - dpToPx(5), dpToPx(12));
  path.lineTo(xOri - xylinewidth / 2, xylinewidth / 2);
  path.lineTo(xOri - xylinewidth / 2 + dpToPx(5), dpToPx(12));
  canvas.drawPath(path, xyPaint);
  //绘制y轴刻度
  int yLength = (int) (yOri * (1 - 0.1f) / (yValue.size() - 1));//y轴上面空出10%,计算出y轴刻度间距
  for (int i = 0; i < yValue.size(); i++) {
   //绘制Y轴刻度
   canvas.drawLine(xOri, yOri - yLength * i + xylinewidth / 2, xOri + length, yOri - yLength * i + xylinewidth / 2, xyPaint);
   xyTextPaint.setColor(xytextcolor);
   //绘制Y轴文本
   String text = yValue.get(i) + "";
   Rect rect = getTextBounds(text, xyTextPaint);
   canvas.drawText(text, 0, text.length(), xOri - xylinewidth - dpToPx(2) - rect.width(), yOri - yLength * i + rect.height() / 2, xyTextPaint);
  }
  //绘制X轴坐标
  canvas.drawLine(xOri, yOri + xylinewidth / 2, width, yOri + xylinewidth / 2, xyPaint);
  //绘制x轴箭头
  xyPaint.setStyle(Paint.Style.STROKE);
  path = new Path();
  //整个X轴的长度
  float xLength = xInit + interval * (xValue.size() - 1) + (width - xOri) * 0.1f;
  if (xLength < width)
   xLength = width;
  path.moveTo(xLength - dpToPx(12), yOri + xylinewidth / 2 - dpToPx(5));
  path.lineTo(xLength - xylinewidth / 2, yOri + xylinewidth / 2);
  path.lineTo(xLength - dpToPx(12), yOri + xylinewidth / 2 + dpToPx(5));
  canvas.drawPath(path, xyPaint);
  //绘制x轴刻度
  for (int i = 0; i < xValue.size(); i++) {
   float x = xInit + interval * i;
   if (x >= xOri) {//只绘制从原点开始的区域
    xyTextPaint.setColor(xytextcolor);
    canvas.drawLine(x, yOri, x, yOri - length, xyPaint);
    //绘制X轴文本
    String text = xValue.get(i);
    Rect rect = getTextBounds(text, xyTextPaint);
    if (i == selectIndex - 1) {
     xyTextPaint.setColor(linecolor);
     canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOri + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
     canvas.drawRoundRect(x - xValueRect.width() / 2 - dpToPx(3), yOri + xylinewidth + dpToPx(1), x + xValueRect.width() / 2 + dpToPx(3), yOri + xylinewidth + dpToPx(2) + xValueRect.height() + dpToPx(2), dpToPx(2), dpToPx(2), xyTextPaint);
    } else {
     canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOri + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);
    }
   }
  }
 }

5、点击的处理以及左右

重写ontouchEven()方法,来处理点击和滑动

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public boolean onTouchEvent(MotionEvent event) {
 if (isScrolling)
  return super.onTouchEvent(event);
 this.getParent().requestDisallowInterceptTouchEvent(true);//当该view获得点击事件,就请求父控件不拦截事件
 obtainVelocityTracker(event);
 switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
   startX = event.getX();
   break;
  case MotionEvent.ACTION_MOVE:
   if (interval * xValue.size() > width - xOri) {//当期的宽度不足以呈现全部数据
    float dis = event.getX() - startX;
    startX = event.getX();
    if (xInit + dis < minXInit) {
     xInit = minXInit;
    } else if (xInit + dis > maxXInit) {
     xInit = maxXInit;
    } else {
     xInit = xInit + dis;
    }
    invalidate();
   }
   break;
  case MotionEvent.ACTION_UP:
   clickAction(event);
   scrollAfterActionUp();
   this.getParent().requestDisallowInterceptTouchEvent(false);
   recycleVelocityTracker();
   break;
  case MotionEvent.ACTION_CANCEL:
   this.getParent().requestDisallowInterceptTouchEvent(false);
   recycleVelocityTracker();
   break;
 }
 return true;
}

点击的处理是计算当前点击的X、Y坐标范围进行判断点击的是那个点

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * 点击X轴坐标或者折线节点
 *
 * @param event
 */
private void clickAction(MotionEvent event) {
 int dp8 = dpToPx(8);
 float eventX = event.getX();
 float eventY = event.getY();
 for (int i = 0; i < xValue.size(); i++) {
  //节点
  float x = xInit + interval * i;
  float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);
  if (eventX >= x - dp8 && eventX <= x + dp8 &&
    eventY >= y - dp8 && eventY <= y + dp8 && selectIndex != i + 1) {//每个节点周围8dp都是可点击区域
   selectIndex = i + 1;
   invalidate();
   return;
  }
  //X轴刻度
  String text = xValue.get(i);
  Rect rect = getTextBounds(text, xyTextPaint);
  x = xInit + interval * i;
  y = yOri + xylinewidth + dpToPx(2);
  if (eventX >= x - rect.width() / 2 - dp8 && eventX <= x + rect.width() + dp8 / 2 &&
    eventY >= y - dp8 && eventY <= y + rect.height() + dp8 && selectIndex != i + 1) {
   selectIndex = i + 1;
   invalidate();
   return;
  }
 }
}

处理滑动的原理,就是通过改变第一个点的X坐标,通过改变这个基本点,依次改变后面的X轴的点的坐标。

最后在布局里面应用就可以啦,我就不贴代码啦!

总结:

项目还是有缺点的:

(1)左右滑动时,抬起手指仍然可以快速滑动;代码里面给出了一种解决方案,但是太过于暂用资源,没有特殊要求不建议使用,所以给出一个boolean类型的自定义属性isScroll,true:启动,反之亦然;还有一种解决方案就是外面再加一层横向ScrollView,请读者自行解决,也很简单,只需要稍作修改即可。

(2)点击的时候忘记添加回调,只有添加了回调在可以在activity或者fragment里面获取点击的内容;代码很简单,自行脑补。

项目地址1
项目地址2

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/u014544193/article/details/54313257

延伸 · 阅读

精彩推荐