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

前言

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

效果图

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

Android自定义可左右滑动和点击的折线图_第1张图片   

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

自定义折线图的步骤:

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

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



  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
    
    
    
    
    
    
    
    
  

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

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();
  }
  /**
   * 初始化���P
   */
  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()方法里面获取。

 @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()方法进行绘制

 @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()方法,来处理点击和滑动

 @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坐标范围进行判断点击的是那个点

  /**
   * 点击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

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

你可能感兴趣的:(Android自定义可左右滑动和点击的折线图)