欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-02-04 12:55:40
前言        前几天有小盆友让我写一个折线图,可以点击,可以左右滑动。对于折线肯定有很多项目都使用过,所以网上肯定也有很多d...

前言

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

效果图

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

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

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

自定义折线图的步骤:

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

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

<?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的构造方法中获取我们的自定义属性:

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()方法里面获取。

 @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

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