自定义之----折线图

总结:


1,初始化画笔和一些自定义属性

2, 通过layout确定圆点的位置
               xy轴的位置

3,在onDraw()里
 通过canvas.drawLine()绘制xy轴线
 通过canvas.drawPath()绘制折线
 通过canvas.drawCircle()绘制圆点

4触摸事件
处理x轴坐标点击事件
处理圆点点击事件
处理冲突,拦截父类点击
处理速度追踪 velocityTracker = VelocityTracker.obtain();

前言

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

效果图

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

                                                            

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

自定义折线图的步骤:

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

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

 
  1. <?xml version="1.0" encoding="utf-8"?>

  2. <resources>

  3. <!-- xy坐标轴颜色 -->

  4. <attr name="xylinecolor" format="color" />

  5. <!-- xy坐标轴宽度 -->

  6. <attr name="xylinewidth" format="dimension" />

  7. <!-- xy坐标轴文字颜色 -->

  8. <attr name="xytextcolor" format="color" />

  9. <!-- xy坐标轴文字大小 -->

  10. <attr name="xytextsize" format="dimension" />

  11. <!-- 折线图中折线的颜色 -->

  12. <attr name="linecolor" format="color" />

  13. <!-- x轴各个坐标点水平间距 -->

  14. <attr name="interval" format="dimension" />

  15. <!-- 背景颜色 -->

  16. <attr name="bgcolor" format="color" />

  17. <!--是否在ACTION_UP时,根据速度进行自滑动,建议关闭,过于占用GPU-->

  18. <attr name="isScroll" format="boolean" />

  19. <declare-styleable name="chartView">

  20. <attr name="xylinecolor" />

  21. <attr name="xylinewidth" />

  22. <attr name="xytextcolor" />

  23. <attr name="xytextsize" />

  24. <attr name="linecolor" />

  25. <attr name="interval" />

  26. <attr name="bgcolor" />

  27. <attr name="isScroll" />

  28. </declare-styleable>

  29. </resources>

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

 
  1. public ChartView(Context context) {

  2. this(context, null);

  3. }

  4.  
  5. public ChartView(Context context, AttributeSet attrs) {

  6. this(context, attrs, 0);

  7. }

  8.  
  9. public ChartView(Context context, AttributeSet attrs, int defStyleAttr) {

  10. super(context, attrs, defStyleAttr);

  11. init(context, attrs, defStyleAttr);

  12. initPaint();

  13. }

  14.  
  15. /**

  16. * 初始化

  17. *

  18. * @param context

  19. * @param attrs

  20. * @param defStyleAttr

  21. */

  22. private void init(Context context, AttributeSet attrs, int defStyleAttr) {

  23. TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.chartView, defStyleAttr, 0);

  24. int count = array.getIndexCount();

  25. for (int i = 0; i < count; i++) {

  26. int attr = array.getIndex(i);

  27. switch (attr) {

  28. case R.styleable.chartView_xylinecolor://xy坐标轴颜色

  29. xylinecolor = array.getColor(attr, xylinecolor);

  30. break;

  31. case R.styleable.chartView_xylinewidth://xy坐标轴宽度

  32. xylinewidth = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xylinewidth, getResources().getDisplayMetrics()));

  33. break;

  34. case R.styleable.chartView_xytextcolor://xy坐标轴文字颜色

  35. xytextcolor = array.getColor(attr, xytextcolor);

  36. break;

  37. case R.styleable.chartView_xytextsize://xy坐标轴文字大小

  38. xytextsize = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, xytextsize, getResources().getDisplayMetrics()));

  39. break;

  40. case R.styleable.chartView_linecolor://折线图中折线的颜色

  41. linecolor = array.getColor(attr, linecolor);

  42. break;

  43. case R.styleable.chartView_interval://x轴各个坐标点水平间距

  44. interval = (int) array.getDimension(attr, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, interval, getResources().getDisplayMetrics()));

  45. break;

  46. case R.styleable.chartView_bgcolor: //背景颜色

  47. bgcolor = array.getColor(attr, bgcolor);

  48. break;

  49. case R.styleable.chartView_isScroll://是否在ACTION_UP时,根据速度进行自滑动

  50. isScroll = array.getBoolean(attr, isScroll);

  51. break;

  52. }

  53. }

  54. array.recycle();

  55.  
  56. }

  57.  
  58. /**

  59. * 初始化畫筆

  60. */

  61. private void initPaint() {

  62. xyPaint = new Paint();

  63. xyPaint.setAntiAlias(true);

  64. xyPaint.setStrokeWidth(xylinewidth);

  65. xyPaint.setStrokeCap(Paint.Cap.ROUND);

  66. xyPaint.setColor(xylinecolor);

  67.  
  68. xyTextPaint = new Paint();

  69. xyTextPaint.setAntiAlias(true);

  70. xyTextPaint.setTextSize(xytextsize);

  71. xyTextPaint.setStrokeCap(Paint.Cap.ROUND);

  72. xyTextPaint.setColor(xytextcolor);

  73. xyTextPaint.setStyle(Paint.Style.STROKE);

  74.  
  75. linePaint = new Paint();

  76. linePaint.setAntiAlias(true);

  77. linePaint.setStrokeWidth(xylinewidth);

  78. linePaint.setStrokeCap(Paint.Cap.ROUND);

  79. linePaint.setColor(linecolor);

  80. linePaint.setStyle(Paint.Style.STROKE);

  81. }

3、获取一写基本点

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

 
  1. @Override

  2. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

  3. if (changed) {

  4. //这里需要确定几个基本点,只有确定了xy轴原点坐标,第一个点的X坐标值及其最大最小值

  5. width = getWidth();

  6. height = getHeight();

  7. //Y轴文本最大宽度

  8. float textYWdith = getTextBounds("000", xyTextPaint).width();

  9. for (int i = 0; i < yValue.size(); i++) {//求取y轴文本最大的宽度

  10. float temp = getTextBounds(yValue.get(i) + "", xyTextPaint).width();

  11. if (temp > textYWdith)

  12. textYWdith = temp;

  13. }

  14. int dp2 = dpToPx(2);

  15. int dp3 = dpToPx(3);

  16. xOri = (int) (dp2 + textYWdith + dp2 + xylinewidth);//dp2是y轴文本距离左边,以及距离y轴的距离

  17. // //X轴文本最大高度

  18. xValueRect = getTextBounds("000", xyTextPaint);

  19. float textXHeight = xValueRect.height();

  20. for (int i = 0; i < xValue.size(); i++) {//求取x轴文本最大的高度

  21. Rect rect = getTextBounds(xValue.get(i) + "", xyTextPaint);

  22. if (rect.height() > textXHeight)

  23. textXHeight = rect.height();

  24. if (rect.width() > xValueRect.width())

  25. xValueRect = rect;

  26. }

  27. yOri = (int) (height - dp2 - textXHeight - dp3 - xylinewidth);//dp3是x轴文本距离底边,dp2是x轴文本距离x轴的距离

  28. xInit = interval + xOri;

  29. minXInit = width - (width - xOri) * 0.1f - interval * (xValue.size() - 1);//减去0.1f是因为最后一个X周刻度距离右边的长度为X轴可见长度的10%

  30. maxXInit = xInit;

  31. }

  32. super.onLayout(changed, left, top, right, bottom);

  33. }

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

 
  1. @Override

  2. protected void onDraw(Canvas canvas) {

  3. // super.onDraw(canvas);

  4. canvas.drawColor(bgcolor);

  5. drawXY(canvas);

  6. drawBrokenLineAndPoint(canvas);

  7. }

  8.  
  9. /**

  10. * 绘制折线和折线交点处对应的点

  11. *

  12. * @param canvas

  13. */

  14. private void drawBrokenLineAndPoint(Canvas canvas) {

  15. if (xValue.size() <= 0)

  16. return;

  17. //重新开一个图层

  18. int layerId = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);

  19. drawBrokenLine(canvas);

  20. drawBrokenPoint(canvas);

  21.  
  22. // 将折线超出x轴坐标的部分截取掉

  23. linePaint.setStyle(Paint.Style.FILL);

  24. linePaint.setColor(bgcolor);

  25. linePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

  26. RectF rectF = new RectF(0, 0, xOri, height);

  27. canvas.drawRect(rectF, linePaint);

  28. linePaint.setXfermode(null);

  29. //保存图层

  30. canvas.restoreToCount(layerId);

  31. }

  32.  
  33. /**

  34. * 绘制折线对应的点

  35. *

  36. * @param canvas

  37. */

  38. private void drawBrokenPoint(Canvas canvas) {

  39. float dp2 = dpToPx(2);

  40. float dp4 = dpToPx(4);

  41. float dp7 = dpToPx(7);

  42. //绘制节点对应的原点

  43. for (int i = 0; i < xValue.size(); i++) {

  44. float x = xInit + interval * i;

  45. float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);

  46. //绘制选中的点

  47. if (i == selectIndex - 1) {

  48. linePaint.setStyle(Paint.Style.FILL);

  49. linePaint.setColor(0xffd0f3f2);

  50. canvas.drawCircle(x, y, dp7, linePaint);

  51. linePaint.setColor(0xff81dddb);

  52. canvas.drawCircle(x, y, dp4, linePaint);

  53. drawFloatTextBox(canvas, x, y - dp7, value.get(xValue.get(i)));

  54. }

  55. //绘制普通的节点

  56. linePaint.setStyle(Paint.Style.FILL);

  57. linePaint.setColor(Color.WHITE);

  58. canvas.drawCircle(x, y, dp2, linePaint);

  59. linePaint.setStyle(Paint.Style.STROKE);

  60. linePaint.setColor(linecolor);

  61. canvas.drawCircle(x, y, dp2, linePaint);

  62.  
  63. }

  64. }

  65.  
  66. /**

  67. * 绘制显示Y值的浮动框

  68. *

  69. * @param canvas

  70. * @param x

  71. * @param y

  72. * @param text

  73. */

  74. private void drawFloatTextBox(Canvas canvas, float x, float y, int text) {

  75. int dp6 = dpToPx(6);

  76. int dp18 = dpToPx(18);

  77. //p1

  78. Path path = new Path();

  79. path.moveTo(x, y);

  80. //p2

  81. path.lineTo(x - dp6, y - dp6);

  82. //p3

  83. path.lineTo(x - dp18, y - dp6);

  84. //p4

  85. path.lineTo(x - dp18, y - dp6 - dp18);

  86. //p5

  87. path.lineTo(x + dp18, y - dp6 - dp18);

  88. //p6

  89. path.lineTo(x + dp18, y - dp6);

  90. //p7

  91. path.lineTo(x + dp6, y - dp6);

  92. //p1

  93. path.lineTo(x, y);

  94. canvas.drawPath(path, linePaint);

  95. linePaint.setColor(Color.WHITE);

  96. linePaint.setTextSize(spToPx(14));

  97. Rect rect = getTextBounds(text + "", linePaint);

  98. canvas.drawText(text + "", x - rect.width() / 2, y - dp6 - (dp18 - rect.height()) / 2, linePaint);

  99. }

  100.  
  101. /**

  102. * 绘制折线

  103. *

  104. * @param canvas

  105. */

  106. private void drawBrokenLine(Canvas canvas) {

  107. linePaint.setStyle(Paint.Style.STROKE);

  108. linePaint.setColor(linecolor);

  109. //绘制折线

  110. Path path = new Path();

  111. float x = xInit + interval * 0;

  112. float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(0)) / yValue.get(yValue.size() - 1);

  113. path.moveTo(x, y);

  114. for (int i = 1; i < xValue.size(); i++) {

  115. x = xInit + interval * i;

  116. y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);

  117. path.lineTo(x, y);

  118. }

  119. canvas.drawPath(path, linePaint);

  120. }

  121.  
  122. /**

  123. * 绘制XY坐标

  124. *

  125. * @param canvas

  126. */

  127. private void drawXY(Canvas canvas) {

  128. int length = dpToPx(4);//刻度的长度

  129. //绘制Y坐标

  130. canvas.drawLine(xOri - xylinewidth / 2, 0, xOri - xylinewidth / 2, yOri, xyPaint);

  131. //绘制y轴箭头

  132. xyPaint.setStyle(Paint.Style.STROKE);

  133. Path path = new Path();

  134. path.moveTo(xOri - xylinewidth / 2 - dpToPx(5), dpToPx(12));

  135. path.lineTo(xOri - xylinewidth / 2, xylinewidth / 2);

  136. path.lineTo(xOri - xylinewidth / 2 + dpToPx(5), dpToPx(12));

  137. canvas.drawPath(path, xyPaint);

  138. //绘制y轴刻度

  139. int yLength = (int) (yOri * (1 - 0.1f) / (yValue.size() - 1));//y轴上面空出10%,计算出y轴刻度间距

  140. for (int i = 0; i < yValue.size(); i++) {

  141. //绘制Y轴刻度

  142. canvas.drawLine(xOri, yOri - yLength * i + xylinewidth / 2, xOri + length, yOri - yLength * i + xylinewidth / 2, xyPaint);

  143. xyTextPaint.setColor(xytextcolor);

  144. //绘制Y轴文本

  145. String text = yValue.get(i) + "";

  146. Rect rect = getTextBounds(text, xyTextPaint);

  147. canvas.drawText(text, 0, text.length(), xOri - xylinewidth - dpToPx(2) - rect.width(), yOri - yLength * i + rect.height() / 2, xyTextPaint);

  148. }

  149. //绘制X轴坐标

  150. canvas.drawLine(xOri, yOri + xylinewidth / 2, width, yOri + xylinewidth / 2, xyPaint);

  151. //绘制x轴箭头

  152. xyPaint.setStyle(Paint.Style.STROKE);

  153. path = new Path();

  154. //整个X轴的长度

  155. float xLength = xInit + interval * (xValue.size() - 1) + (width - xOri) * 0.1f;

  156. if (xLength < width)

  157. xLength = width;

  158. path.moveTo(xLength - dpToPx(12), yOri + xylinewidth / 2 - dpToPx(5));

  159. path.lineTo(xLength - xylinewidth / 2, yOri + xylinewidth / 2);

  160. path.lineTo(xLength - dpToPx(12), yOri + xylinewidth / 2 + dpToPx(5));

  161. canvas.drawPath(path, xyPaint);

  162. //绘制x轴刻度

  163. for (int i = 0; i < xValue.size(); i++) {

  164. float x = xInit + interval * i;

  165. if (x >= xOri) {//只绘制从原点开始的区域

  166. xyTextPaint.setColor(xytextcolor);

  167. canvas.drawLine(x, yOri, x, yOri - length, xyPaint);

  168. //绘制X轴文本

  169. String text = xValue.get(i);

  170. Rect rect = getTextBounds(text, xyTextPaint);

  171. if (i == selectIndex - 1) {

  172. xyTextPaint.setColor(linecolor);

  173. canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOri + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);

  174. 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);

  175. } else {

  176. canvas.drawText(text, 0, text.length(), x - rect.width() / 2, yOri + xylinewidth + dpToPx(2) + rect.height(), xyTextPaint);

  177. }

  178. }

  179. }

  180. }

5、点击的处理以及左右

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

 
  1. @Override

  2. public boolean onTouchEvent(MotionEvent event) {

  3. if (isScrolling)

  4. return super.onTouchEvent(event);

  5. this.getParent().requestDisallowInterceptTouchEvent(true);//当该view获得点击事件,就请求父控件不拦截事件

  6. obtainVelocityTracker(event);

  7. switch (event.getAction()) {

  8. case MotionEvent.ACTION_DOWN:

  9. startX = event.getX();

  10. break;

  11. case MotionEvent.ACTION_MOVE:

  12. if (interval * xValue.size() > width - xOri) {//当期的宽度不足以呈现全部数据

  13. float dis = event.getX() - startX;

  14. startX = event.getX();

  15. if (xInit + dis < minXInit) {

  16. xInit = minXInit;

  17. } else if (xInit + dis > maxXInit) {

  18. xInit = maxXInit;

  19. } else {

  20. xInit = xInit + dis;

  21. }

  22. invalidate();

  23. }

  24. break;

  25. case MotionEvent.ACTION_UP:

  26. clickAction(event);

  27. scrollAfterActionUp();

  28. this.getParent().requestDisallowInterceptTouchEvent(false);

  29. recycleVelocityTracker();

  30. break;

  31. case MotionEvent.ACTION_CANCEL:

  32. this.getParent().requestDisallowInterceptTouchEvent(false);

  33. recycleVelocityTracker();

  34. break;

  35. }

  36. return true;

  37. }


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

 
  1. /**

  2. * 点击X轴坐标或者折线节点

  3. *

  4. * @param event

  5. */

  6. private void clickAction(MotionEvent event) {

  7. int dp8 = dpToPx(8);

  8. float eventX = event.getX();

  9. float eventY = event.getY();

  10. for (int i = 0; i < xValue.size(); i++) {

  11. //节点

  12. float x = xInit + interval * i;

  13. float y = yOri - yOri * (1 - 0.1f) * value.get(xValue.get(i)) / yValue.get(yValue.size() - 1);

  14. if (eventX >= x - dp8 && eventX <= x + dp8 &&

  15. eventY >= y - dp8 && eventY <= y + dp8 && selectIndex != i + 1) {//每个节点周围8dp都是可点击区域

  16. selectIndex = i + 1;

  17. invalidate();

  18. return;

  19. }

  20. //X轴刻度

  21. String text = xValue.get(i);

  22. Rect rect = getTextBounds(text, xyTextPaint);

  23. x = xInit + interval * i;

  24. y = yOri + xylinewidth + dpToPx(2);

  25. if (eventX >= x - rect.width() / 2 - dp8 && eventX <= x + rect.width() + dp8 / 2 &&

  26. eventY >= y - dp8 && eventY <= y + rect.height() + dp8 && selectIndex != i + 1) {

  27. selectIndex = i + 1;

  28. invalidate();

  29. return;

  30. }

  31. }

  32. }


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

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

总结:

    项目还是有缺点的:

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

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

转载自:Android自定义折线图,可左右滑动,可点击

项目地址

猜你喜欢

转载自blog.csdn.net/qq_38859786/article/details/82459480