当前位置:首页 > 安卓源码 > 技术博客 >

android-柱状图、折线图、x轴、y轴绘制以及实例代码

时间:2019-06-05 16:37 来源:互联网 作者:源码搜藏 浏览: 收藏 挑错 推荐 打印

第一幅图是几个实例,折线图、柱状图,同时还有x轴、y轴的实现。 下面的两幅图分别有横轴、纵轴、和折线图或者柱状图。 作者的思路非常明确,每幅图分为三个部分,x轴部分,y轴部分,和中间的图形部分。

android-柱状图、折线图、x轴、y轴绘制以及实例代码

android-柱状图、折线图、x轴、y轴绘制以及实例代码

android-柱状图、折线图、x轴、y轴绘制以及实例代码

 
第一幅图是几个实例,折线图、柱状图,同时还有x轴、y轴的实现。
 
下面的两幅图分别有横轴、纵轴、和折线图或者柱状图。
 
作者的思路非常明确,每幅图分为三个部分,x轴部分,y轴部分,和中间的图形部分。每一部分都是一个自定义的view!! 

android-柱状图、折线图、x轴、y轴绘制以及实例代码

 
三个部分如图所示,每个部分可有可无,可以组装。 
其中横轴、纵轴都是数字的列表的显示,只不过是横向和纵向的区别,因此可以有一个基类。中间的柱状图部分和折线图也可以有一个基类,整个代码的结构如下图: 
 android-柱状图、折线图、x轴、y轴绘制以及实例代码
看类名称就可以看出类之间的关系。在此不多说。
 
下面是实现思路:
 
横轴、纵轴实现思路 
因为是坐标图,所以横轴、纵轴数据应该从服务器或者本地获取到的,应该提前知道要显示的数据,此时,横轴纵轴要显示的数据的个数已知,最大致最小值也可以知道,由此,可以得到两个数据之间显示的间距,有了间距,就可以一个个的显示数据啦。 
公式表示就是:
x = gap * (i - 1) + gap - (textWidth / 2);
 
x 表示横轴坐标 
gap 最大值最小值差值除以数据个数,就是间距 
textWidth 表示数字宽度 
i 循环变量 循环绘制数据
 
有了关键的x轴坐标,那么y轴坐标呢?这不就简单了吗? 
对于横向显示的坐标来说y轴是固定值啊。y轴设置为view的高度的一半值就可以啦!
 
想想! 
再想! 
是不是? 
就是这么easy!!
 
上面的公式表示的是横轴的显示,对于纵轴显示的呢?那就是比葫芦画瓢!!!不说啦!!!
 
折线图实现思路
折线图是模拟真实数据的形式展现出来的,把真实数据按一定的比例放在坐标轴中进行显示的。首先,折线图中显示的也是一个个的数据,然后使用path类把一个个的数据连接起来,连成线就可以啦。剩下的问题就是如何把真实数据换算成坐标中的x、y值?要显示的数据我们已经提前知道啦。不然,我么你是画不出来图形的。数据的最大值最小值也已知。最大值最小值的差值与纵轴的坐标关系可以得到数值显示的y值坐标。 
公式表示就是:
 
    y = height -(value-min)*(max-min)/height
 
其中y代表数据显示的y轴坐标。 
height代表view的高度值 
value代表数据值 
min代表数据的最小值 
max代表数据的最大值 
看懂此公式至关重要。
 
那么x坐标如何搞呢?那就是数据的index啦。有了自定义view的宽度值,要显示的数据的个数,那么横轴方向上的数据间距是不是有了? 
想想上面的横轴纵轴的思路,是不是有啦!!比葫芦画瓢!!easy!
 
柱状图实现思路
也是比葫芦画瓢啊!柱状图是矩形!需要left、top、right、bottom四个值才能确定矩形的大小。首先我们知道数据的个数、数据的最大值最小值,由此得到矩形的宽度,矩形之间应该还有间距差、宽度还要减去这个间距差值!
 
下面,知道最大值最小值和自定义view的高度值,由此可以得到每个高度所对应的数值,纵轴上的数据计算和上面的一样啊!
 
y = height -(value-min)*(max-min)/height
 
这样,代码表示如下:
 
    RectF rectF = new RectF();
      rectF.left = (i * barWidth) + barMargin;
      rectF.top = height - (sliceHeight * (valuesTransition[i] - minY));
      //如果top值等于view高度值,显示默认的柱形最小值,不至于有点都不显示。
      rectF.top = rectF.top == height ? rectF.top - DEFAULT_BAR_MIN_CORRECTION : rectF.top;
      rectF.right = (i * barWidth) + barWidth - barMargin;
      rectF.bottom = height;
 
barWidth 就是矩形的宽度 
barMargin 就是矩形间距 
sliceHeight 就是高度片值 该值就是最大值最小值除以自定义view的高度得到。 
minY 是数据最小值 
sliceHeight 是自定义view的高度 
valuesTransition[i]就是要显示的数据
 
对照上面的代码想想,再想想,是不是!
 
有了这些就Ok啦!
 
再说一点,细心的朋友应该发现了上面的动画了吧,每点击一次,都会有动画,这个牛逼!怎么搞得?到现在也没有想明白,怎么搞得? 
看了代码之后,觉得作者真是牛!!
 
思路如下,听我慢慢道来: 
首先我们会得到要显示的数据,有了数据我们可以得到数据的个数,数据的最大值最小值。我们拷贝一份和原有数据相同长度的数据,每个数据都是最小值。
 
 private void initValuesTarget(float[] values) {
    this.valuesTransition = values.clone();
    for (int i = 0; i < valuesTransition.length; i++) {
      valuesTransition[i] = minY;
    }
  }
 
代码中的这个方法就是这样的作用! 
有了这一组数据之后,通过这个方法:
 
 //计算动画的显示值 一步步接近实际值
  void calculateNextAnimStep() {
    animFinished = true;
    for (int i = 0; i < valuesTransition.length; i++) {
      float diff = values[i] - minY;
      float step = (diff * ANIM_DELAY_MILLIS) / animDuration;
 
      if (valuesTransition[i] + step >= values[i]) {
        valuesTransition[i] = values[i];
      } else {
        valuesTransition[i] = valuesTransition[i] + step;
        animFinished = false;
      }
    }
 
    if (animFinished && animListener != null) {
      animListener.onAnimFinish();
    }
  }
 
其中
 
 float diff = values[i] - minY;
      float step = (diff * ANIM_DELAY_MILLIS) / animDuration;
 
这两句代码最为关键! 
diff表示当前显示的值和最下值的差值 
ANIM_DELAY_MILLIS表示动画的延时时间 默认30毫秒,当然该值可以改 
animDuration 动画的持续时间 默认500毫秒
 
由此可以得到,在动画持续时间内,每一次动画累加的值!
 
这样一点点累加,不断重绘,就形成了动画!!!!
 
那么动画是如何开启的呢?
 
/**
   * 绘画柱状图的核心方法
   * @param canvas
   */
  public void draw(Canvas canvas) {
    super.draw(canvas);
    .........
    //通知动画绘制
    if (anim && !animFinished) {
      handlerAnim.postDelayed(doNextAnimStep, ANIM_DELAY_MILLIS);
    }
 
在ondraw方法中的末尾,会使用hander的postDelayed方法,延时30毫秒进行重绘。 
第一个参数是
 
final Runnable doNextAnimStep = new Runnable() {
    @Override public void run() {
      invalidate();
    }
  };
 
看到了吧,invalidate()方法,进行重绘。
 
OK!核心内容全部解释完毕!! 
下面就是代码啦!
 
动画监听器实现代码
 
看代码,不多说:
 
public interface CharterAnimListener {
  void onAnimFinish();
}
 
简单吧,就是个接口,当动画完成之后,调用此接口实现动画完成之后的操作。具体怎么使用,请看下面的代码。这里有个印象就成。
 
 ChartLabels 横轴纵轴实现代码
 
横轴纵轴共分为两部分,CharterXLabels CharterYLabels类。他们有一个共同的基类CharterLabelsBase类。 
首先看CharterLabelsBase基类的代码:
 
public class CharterLabelsBase extends View {
  /**
   * 垂直方向默认三种 上中下
   */
  public static final int VERTICAL_GRAVITY_TOP = 0;
  public static final int VERTICAL_GRAVITY_CENTER = 1;
  public static final int VERTICAL_GRAVITY_BOTTOM = 2;
  /**
   * 水平方向默认三种:左中右
   */
  public static final int HORIZONTAL_GRAVITY_LEFT = 0;
  public static final int HORIZONTAL_GRAVITY_CENTER = 1;
  public static final int HORIZONTAL_GRAVITY_RIGHT = 2;
  //垂直方向默认居下
  private static final int DEFAULT_VERTICAL_GRAVITY = VERTICAL_GRAVITY_BOTTOM;
  //水平方向默认居左
  private static final int DEFAULT_HORIZONTAL_GRAVITY = HORIZONTAL_GRAVITY_LEFT;
  private static final boolean DEFAULT_STICKY_EDGES = false;
 
  Paint paintLabel;//标签的画笔
  boolean[] visibilityPattern;//标签的显示模式
  int verticalGravity;//纵轴标签显示位置
  int horizontalGravity;//横轴标签的显示位置
  String[] values;//标签数值
  boolean stickyEdges;//是否跨边显示
  private int paintLabelColor;//标签的颜色
  private float paintLabelSize;//标签的大小
 
  protected CharterLabelsBase(Context context) {
    this(context, null);
  }
 
  protected CharterLabelsBase(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  protected CharterLabelsBase(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs);
  }
 
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  protected CharterLabelsBase(Context context, AttributeSet attrs, int defStyleAttr,
      int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init(context, attrs);
  }
 
  private void init(Context context, AttributeSet attrs) {
    /**
     * isInEditMode()是view类的方法,默认返回false
     */
    if (isInEditMode()) {
      return;
    }
 
    final TypedArray typedArray = context.obtainStyledAttributes(attrs,
            R.styleable.Charter);
    stickyEdges = typedArray.getBoolean(
            R.styleable.Charter_c_stickyEdges, DEFAULT_STICKY_EDGES);
    //垂直方向 默认居中
    verticalGravity =
        typedArray.getInt(R.styleable.Charter_c_verticalGravity,
                DEFAULT_VERTICAL_GRAVITY);
    //水平方向,默认居左
    horizontalGravity =
        typedArray.getInt(R.styleable.Charter_c_horizontalGravity,
                DEFAULT_HORIZONTAL_GRAVITY);
    //标签的颜色
    paintLabelColor = typedArray.getColor(R.styleable.Charter_c_labelColor,
        getResources().getColor(R.color.default_labelColor));
    //标签大小,默认10sp
    paintLabelSize = typedArray.getDimension(R.styleable.Charter_c_labelSize,
        getResources().getDimension(R.dimen.default_labelSize));
    typedArray.recycle();//回收
    //标签画笔
    paintLabel = new Paint();
    paintLabel.setAntiAlias(true);
    paintLabel.setColor(paintLabelColor);
    paintLabel.setTextSize(paintLabelSize);
    /**
     *  标签可见性模式 默认显示、显示、显示。。。。。
     *  当然也可以设置模式。
     */
    visibilityPattern = new boolean[] { true };
  }
 
  public boolean isStickyEdges() {
    return stickyEdges;
  }
 
  public void setStickyEdges(boolean stickyEdges) {
    this.stickyEdges = stickyEdges;
    invalidate();
  }
 
  public Paint getPaintLabel() {
    return paintLabel;
  }
 
  public void setPaintLabel(Paint paintLabel) {
    this.paintLabel = paintLabel;
    invalidate();
  }
 
  public boolean[] getVisibilityPattern() {
    return visibilityPattern;
  }
 
  public void setVisibilityPattern(boolean[] visibilityPattern) {
    this.visibilityPattern = visibilityPattern;
    invalidate();
  }
 
  public int getVerticalGravity() {
    return verticalGravity;
  }
  //使用注解 限制设置的值
  public void setVerticalGravity(@VerticalGravity int verticalGravity) {
    this.verticalGravity = verticalGravity;
    invalidate();
  }
 
  public int getHorizontalGravity() {
    return horizontalGravity;
  }
 
  public void setHorizontalGravity(@HorizontalGravity int horizontalGravity) {
    this.horizontalGravity = horizontalGravity;
    invalidate();
  }
 
  public int getLabelColor() {
    return paintLabelColor;
  }
 
  public void setLabelColor(@ColorInt int labelColor) {
    paintLabel.setColor(labelColor);
    paintLabelColor = labelColor;
    invalidate();
  }
 
  public float getLabelSize() {
    return paintLabelSize;
  }
 
  public void setLabelSize(float labelSize) {
    paintLabel.setTextSize(labelSize);
    paintLabelSize = labelSize;
    invalidate();
  }
 
  public void setLabelTypeface(Typeface typeface) {
    paintLabel.setTypeface(typeface);
    invalidate();
  }
 
  public String[] getValues() {
    return values;
  }
 
  public void setValues(float[] values) {
    setValues(floatArrayToStringArray(values));
  }
 
  public void setValues(String[] values) {
    if (values == null || values.length == 0) {
      return;
    }
 
    this.values = values;
    invalidate();
  }
 
  public void setValues(float[] values, boolean summarize) {
    if (summarize) {
      values = summarize(values);
    }
    //将值转化成字符串
    setValues(floatArrayToStringArray(values));
  }
 
  private String[] floatArrayToStringArray(float[] values) {
    if (values == null) {
      return new String[] {};
    }
 
    String[] stringArray = new String[values.length];
    for (int i = 0; i < stringArray.length; i++) {
      stringArray[i] = String.valueOf((int) values[i]);
    }
    return stringArray;
  }
 
  /**
   * 将值进行汇总
   * 汇总之后的值共有五个。最后显示的值也就五个值。
   * @param values
   * @return
   */
  private float[] summarize(float[] values) {
    if (values == null) {
      return new float[] {};
    }
 
    float max = values[0];
    float min = values[0];
    for (float value : values) {
      if (value > max) {
        max = value;
      }
      if (value < min) {
        min = value;
      }
    }
    float diff = max - min;
 
    return new float[] { min, diff / 5, diff / 2, (diff / 5) * 4, max };
  }
 
  /**
   * 定义注解
   */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({ VERTICAL_GRAVITY_TOP, VERTICAL_GRAVITY_CENTER,
          VERTICAL_GRAVITY_BOTTOM })
  public @interface VerticalGravity {
  }
 
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({ HORIZONTAL_GRAVITY_LEFT, HORIZONTAL_GRAVITY_CENTER,
          HORIZONTAL_GRAVITY_RIGHT })
  public @interface HorizontalGravity {
  }
}
 
基类大部分代码一看就懂。其中让我最佩服的就是注解!!! 
卧槽,没发现还有这样的巨大的用处!佩服的五体投地!
 
/**
   * 定义注解
   */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({ VERTICAL_GRAVITY_TOP, VERTICAL_GRAVITY_CENTER,
          VERTICAL_GRAVITY_BOTTOM })
  public @interface VerticalGravity {
  }
 
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({ HORIZONTAL_GRAVITY_LEFT, HORIZONTAL_GRAVITY_CENTER,
          HORIZONTAL_GRAVITY_RIGHT })
  public @interface HorizontalGravity {
  }
 
代码的最后使用public @interface来定义注解!并限定了值的范围。 
其中@Retention代表注解的存在范围。
 
public enum RetentionPolicy {
    SOURCE,
    CLASS,
    RUNTIME
}
 
有这三种取值。源码、二进制文件、运行时。关于注解详细的信息,就不多说啦。大家不明白的恶补一番。
 
下面接着说代码。上面的基类是横轴纵轴的基类,定义了一些通用的方法,大家看看方法就会明白,并且主要的地方我都给出了注释。通用的方法和变量都设置了set 和get的方法,用于在代码中进行控制。
 
Paint paintLabel;//标签的画笔
  boolean[] visibilityPattern;//标签的显示模式
  int verticalGravity;//纵轴标签显示位置
  int horizontalGravity;//横轴标签的显示位置
  String[] values;//标签数值
  boolean stickyEdges;//是否跨边显示
  private int paintLabelColor;//标签的颜色
  private float paintLabelSize;//标签的大小
 
这几个是基类中定义的变量,大家稍微记住一下,下面具体的横轴纵轴的代码要用到这些变量。 
值的说明的是,boolean[] visibilityPattern;//标签的显示模式 
这是是定义标签的如何显示的。 
例如:visibilityPattern=boolean[]{true};则全部的标签都会显示出来。 
visibilityPattern=boolean[]{true,false};则标签隔一个显示一个 
visibilityPattern=boolean[]{true,false,false};则标签隔两个显示一个 
大家看下面的 横轴纵轴的实现onDraw方法时会明白这个地方的设置。
 
还有一个是boolean stickyEdges;//是否跨边显示 
这个值意味着标签是否全部占满整个view的空间,不留边距。具体意义请看下面的代码。
 
下面就是横轴和纵轴的实现代码。 
先看横轴x轴的代码:
 
public class CharterXLabels extends CharterLabelsBase {
  public CharterXLabels(Context context) {
    this(context, null);
  }
 
  public CharterXLabels(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  public CharterXLabels(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }
 
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public CharterXLabels(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }
 
  @Override public void draw(Canvas canvas) {
    super.draw(canvas);
 
    if (values == null || values.length == 0) {
      return;
    }
 
    final int valuesLength = values.length;
 
    final float height = getMeasuredHeight();
    final float width = getMeasuredWidth();
    //计算标签间距
    final float gap = stickyEdges ? width / (valuesLength - 1) : width / valuesLength;
 
    int visibilityPatternPos = -1;
 
    for (int i = 0; i < valuesLength; i++) {
      if (visibilityPatternPos + 1 >= visibilityPattern.length) {
        visibilityPatternPos = 0;
      } else {
        visibilityPatternPos++;
      }
 
      if (visibilityPattern[visibilityPatternPos]) {
        Rect textBounds = new Rect();
        /**
         * Return in bounds (allocated by the caller) the smallest rectangle that
         * encloses all of the characters, with an implied origin at (0,0).
         * getTextBounds方法返回包裹字符串的最小的矩形Rect
         */
        paintLabel.getTextBounds(values[i], 0, values[i].length(), textBounds);
        int textHeight = 2 * textBounds.bottom - textBounds.top;
        float textWidth = textBounds.right;
 
        float x;
        float y;
 
        switch (verticalGravity) {
          case VERTICAL_GRAVITY_TOP:
            y = 0;
            break;
 
          case VERTICAL_GRAVITY_BOTTOM:
            y = height - textHeight/2;
            break;
          case VERTICAL_GRAVITY_CENTER:
            y = (height - textHeight) / 2;
            break;
 
          default:
            // VERTICAL_GRAVITY_CENTER
            y = (height - textHeight) / 2;
            break;
        }
 
        if (stickyEdges) {
          if (i == 0) {
            x = 0;
          } else if (i == valuesLength - 1) {
            x = width - textWidth;
          } else {
            x = gap * (i - 1) + gap - (textWidth / 2);
          }
          canvas.drawText(values[i], x, y, paintLabel);
        } else {
          x = gap * i + (gap / 2) - (textWidth / 2);
          canvas.drawText(values[i], x, y, paintLabel);
        }
      }
    }
  }
}
 
代码量不多,除了三个构造器,就是一个onDraw方法啦。核心也就是这个方法! 
看懂这个类的代码,需要知道基类中各个变量的意思是什么,在基类中每个变量我均给出了意义的注释。主要的代码就是onDraw方法的for循环部分。具体思路请看上面的实现思路的说明部分。 
下面是纵轴的实现代码:
 
public class CharterYLabels extends CharterLabelsBase {
  public CharterYLabels(Context context) {
    this(context, null);
  }
 
  public CharterYLabels(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  public CharterYLabels(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }
 
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public CharterYLabels(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }
 
  @Override public void draw(Canvas canvas) {
    super.draw(canvas);
 
    if (values == null || values.length == 0) {
      return;
    }
 
    final int valuesLength = values.length;
 
    final float height = getMeasuredHeight();
    final float width = getMeasuredWidth();
    //计算两个标签间的距离
    final float gap = height / (valuesLength - 1);
 
    int visibilityPatternPos = -1;
 
    for (int i = 0; i < valuesLength; i++) {
      //可见性模式
      if (visibilityPatternPos + 1 >= visibilityPattern.length) {
        visibilityPatternPos = 0;
      } else {
        visibilityPatternPos++;
      }
 
      if (visibilityPattern[visibilityPatternPos]) {
        Rect textBounds = new Rect();
        //返回包裹标签的最小矩形rect
        paintLabel.getTextBounds(values[i], 0, values[i].length(), textBounds);
        int textHeight = 2 * textBounds.bottom - textBounds.top;
        float textWidth = textBounds.right;
 
        float x;
        float y;
 
        switch (horizontalGravity) {
          default:
            // HORIZONTAL_GRAVITY_LEFT
            x = 0;//默认居左
            break;
 
          case HORIZONTAL_GRAVITY_CENTER:
            x = (width - textWidth) / 2;
            break;
 
          case HORIZONTAL_GRAVITY_RIGHT:
            x = width - textWidth;
            break;
        }
 
        if (i == 0) {
          y = height;
        } else if (i == valuesLength - 1) {
          y = textHeight;
        } else {
          y = gap * i + (textHeight / 2);
        }
        canvas.drawText(values[i], x, y, paintLabel);
      }
    }
  }
}
 
同样的代码,三个构造器一个onDraw方法,onDraw方法是实现的核心。
 
细心的朋友你会发现,这两个标签的代码都没有使用上面开始说明的动画接口?是的。因为我们现在说明的是X轴 Y轴的标签,标签不应该有什么动画显示。动画的显示是在柱状图或者折线图中进行的。
 
ChartLine 折线图实现代码
 
CharterLine类实现折线图的定义,CharterBar实现柱状图的定义,CharterBase是两者的基类。 
首先看CharterBase基类的代码:
 
class CharterBase extends View {
//自定义的动画接口
  private CharterAnimListener animListener;
 
  protected CharterBase(Context context) {
    this(context, null);
  }
 
  protected CharterBase(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  protected CharterBase(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }
 
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  protected CharterBase(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init();
  }
  ...............
 }
 
首先是构造器,调用了init()方法。看init()方法的代码:
 
private void init() {
    //isInEditMode()返回值fasle
    if (isInEditMode()) {
      return;
    }
 
    animFinished = false;
    handlerAnim = new Handler();
  }
  //线程中调用绘画
  final Runnable doNextAnimStep = new Runnable() {
    @Override public void run() {
      invalidate();
    }
  };
 
其中//isInEditMode()返回值fasle 这个方式View类的代码,默认返回false值。代表自定义view是否编辑模式。 
另一个就是handler变量,handler变量就是发送消息进行重绘的,结合Runnable doNextAnimStep线程变量,调用invalidate()方法显示view的不断重绘。
 
class CharterBase extends View {
  static final int ANIM_DELAY_MILLIS = 30;//动画延时时间设置
  static final boolean DEFAULT_ANIM = true;//是否是默认动画
  static final long DEFAULT_ANIM_DURATION = 500;//默认动画持续时间
  //默认自动显示 这个属性是否在自己中进行绘画 请看子类调用setWillNotDraw方法
  static final boolean DEFAULT_AUTOSHOW = true;
  //线程中调用绘画
  final Runnable doNextAnimStep = new Runnable() {
    @Override public void run() {
      invalidate();
    }
  };
 
  float minY;
  float maxY;
 
  float[] values;
  float[] valuesTransition;
 
  boolean anim;
  long animDuration;
  boolean animFinished;
  Handler handlerAnim;
  //自定义的动画接口
  private CharterAnimListener animListener;
 
  protected CharterBase(Context context) {
    this(context, null);
  }
 
  protected CharterBase(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  protected CharterBase(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }
 
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  protected CharterBase(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init();
  }
 
  private void init() {
    //isInEditMode()返回值fasle
    if (isInEditMode()) {
      return;
    }
 
    animFinished = false;
    handlerAnim = new Handler();
  }
 
  public void show() {
    setWillNotDraw(false);
    invalidate();
  }
 
  public float[] getValues() {
    return values;
  }
 
  public void setValues(float[] values) {
    if (values == null || values.length == 0) {
      return;
    }
 
    this.values = values;
    //获取值中最大值最小值
    getMaxMinValues(values);
    initValuesTarget(values);
 
    animFinished = false;
    invalidate();
  }
  //重置数据
  public void resetValues() {
    if (values == null || values.length == 0) {
      return;
    }
 
    for (int i = 0; i < values.length; i++) {
      values[i] = minY;
    }
 
    setValues(values);
  }
 
  private void getMaxMinValues(float[] values) {
    if (values != null && values.length > 0) {
      maxY = values[0];
      minY = values[0];
      for (float y : values) {
        if (y > maxY) {
          maxY = y;
        }
        if (y < minY) {
          minY = y;
        }
      }
    }
  }
 
  private void initValuesTarget(float[] values) {
    this.valuesTransition = values.clone();
    for (int i = 0; i < valuesTransition.length; i++) {
      valuesTransition[i] = minY;
    }
  }
 
  public float getMaxY() {
    return maxY;
  }
 
  public void setMaxY(float maxY) {
    if (values == null) {
      throw new IllegalStateException("You must call setValues() first");
    }
    this.maxY = maxY;
    invalidate();
  }
 
  public float getMinY() {
    return minY;
  }
 
  public void setMinY(float minY) {
    if (values == null) {
      throw new IllegalStateException("You must call setValues() first");
    }
    this.minY = minY;
    invalidate();
  }
  //计算动画的显示值 一步步接近实际值
  void calculateNextAnimStep() {
    animFinished = true;
    for (int i = 0; i < valuesTransition.length; i++) {
      float diff = values[i] - minY;
      float step = (diff * ANIM_DELAY_MILLIS) / animDuration;
 
      if (valuesTransition[i] + step >= values[i]) {
        valuesTransition[i] = values[i];
      } else {
        valuesTransition[i] = valuesTransition[i] + step;
        animFinished = false;
      }
    }
 
    if (animFinished && animListener != null) {
      animListener.onAnimFinish();
    }
  }
  //重播动画
  public void replayAnim() {
    if (values == null || values.length == 0) {
      return;
    }
 
    initValuesTarget(values);
    animFinished = false;
    invalidate();
  }
 
  public boolean isAnim() {
    return anim;
  }
 
  public void setAnim(boolean anim) {
    this.anim = anim;
    replayAnim();
  }
 
  public long getAnimDuration() {
    return animDuration;
  }
 
  public void setAnimDuration(long animDuration) {
    this.animDuration = animDuration;
    replayAnim();
  }
 
  public void setAnimListener(CharterAnimListener animListener) {
    this.animListener = animListener;
  }
}
 
完整代码如上,其中包括很多的set get方法,值的说明的就是
 
//计算动画的显示值 一步步接近实际值
  void calculateNextAnimStep() {
    animFinished = true;
    for (int i = 0; i < valuesTransition.length; i++) {
      float diff = values[i] - minY;
      float step = (diff * ANIM_DELAY_MILLIS) / animDuration;
 
      if (valuesTransition[i] + step >= values[i]) {
        valuesTransition[i] = values[i];
      } else {
        valuesTransition[i] = valuesTransition[i] + step;
        animFinished = false;
      }
    }
 
    if (animFinished && animListener != null) {
      animListener.onAnimFinish();
    }
  }
 
该方法是实现动画的核心方法!要想了解动画的过程,请务必看懂此方法的实现过程。思路也简单,上面说过啦,就是每一次重绘,不断增加一个step值,不断靠近目标值,当已经达到目标值,该值不在增加,保持目标值,当没有达到目标值,就继续增加,止到靠近目标值,只要有一个没有达到目标值,动画就没有结束。止到所有的值达到目标值以后,动画结束,调用动画接口的animListener.onAnimFinish()方法进行处理。
 
基类说明完毕,下面就是折线图的实现代码:
 
public class CharterLine extends CharterBase {
  //指示点的类型。0是圆形  1是方形
  public static final int INDICATOR_TYPE_CIRCLE = 0;
  public static final int INDICATOR_TYPE_SQUARE = 1;
  //指示点的样式 0实心圆圈 1空心圆圈
  public static final int INDICATOR_STYLE_FILL = 0;
  public static final int INDICATOR_STYLE_STROKE = 1;
  //默认指示点的类型 圆形
  private static final int DEFAULT_INDICATOR_TYPE = INDICATOR_TYPE_CIRCLE;
  //默认指示点的样式 空心圆圈
  private static final int DEFAULT_INDICATOR_STYLE = INDICATOR_STYLE_STROKE;
  //默认指示点可见
  private static final boolean DEFAULT_INDICATOR_VISIBLE = true;
  //线的平滑度
  private static final float DEFAULT_SMOOTHNESS = 0.2f;
  //默认全宽 no!
  private static final boolean DEFAULT_FULL_WIDTH = false;
  public boolean fullWidth;
  private Paint paintLine;//画线的笔
  private Paint paintFill;//填充
  private Paint paintIndicator;//指示点
  private Path path;//路径
  private int lineColor;//线颜色
  private int chartFillColor;//填充颜色
  private int defaultBackgroundColor;//默认背景色
  private int chartBackgroundColor;//背景色
  private float strokeSize;//线宽
  private float smoothness;//线的平滑度 from = 0.0, to = 0.5
  private float indicatorSize;//指示点大小
  private boolean indicatorVisible;//指示点是否可见
  private int indicatorType;//类型
  private int indicatorColor;//颜色
  private int indicatorStyle;//样式
  private float indicatorStrokeSize;//指示点线宽
 
  public CharterLine(Context context) {
    this(context, null, 0);
  }
 
  public CharterLine(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  public CharterLine(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init(context, attrs);
  }
 
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public CharterLine(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init(context, attrs);
  }
 
  private void init(final Context context, final AttributeSet attrs) {
    final TypedArray typedArray = context.obtainStyledAttributes(attrs,
            R.styleable.Charter);
    //是否全部宽度
    fullWidth = typedArray.getBoolean(R.styleable.Charter_c_fullWidth,
            DEFAULT_FULL_WIDTH);
    //线颜色
    lineColor = typedArray.getColor(R.styleable.Charter_c_lineColor,
        getResources().getColor(R.color.default_lineColor));
    //填充颜色
    chartFillColor = typedArray.getColor(R.styleable.Charter_c_chartFillColor,
        getResources().getColor(R.color.default_chartFillColor));
    //指示点是否可见 默认可见
    indicatorVisible =
        typedArray.getBoolean(R.styleable.Charter_c_indicatorVisible,
                DEFAULT_INDICATOR_VISIBLE);
    //指示点类型  默认圆形
    indicatorType = typedArray.getInt(R.styleable.Charter_c_indicatorType,
            DEFAULT_INDICATOR_TYPE);
    //指示点大小  默认6dp
    indicatorSize = typedArray.getDimension(R.styleable.Charter_c_indicatorSize,
        getResources().getDimension(R.dimen.default_indicatorSize));
    //指示点的线宽 默认1dp的宽度
    indicatorStrokeSize = typedArray.getDimension(R.styleable
                    .Charter_c_indicatorStrokeSize,getResources().getDimension(R.dimen.default_indicatorStrokeSize));
    //指示点的颜色
    indicatorColor = typedArray.getColor(R.styleable.Charter_c_indicatorColor,
        getResources().getColor(R.color.default_indicatorColor));
    //指示点的样式 默认圆圈
    indicatorStyle =
        typedArray.getInt(R.styleable.Charter_c_indicatorStyle,
                DEFAULT_INDICATOR_STYLE);
    //线宽 指的是折线的线宽 默认2dp
    strokeSize = typedArray.getDimension(R.styleable.Charter_c_strokeSize,
        getResources().getDimension(R.dimen.default_strokeSize));
    //线的平滑度
    smoothness = typedArray.getFloat(R.styleable.Charter_c_smoothness,
            DEFAULT_SMOOTHNESS);
    //默认动画与否  默认显示动画
    anim = typedArray.getBoolean(R.styleable.Charter_c_anim, DEFAULT_ANIM);
    //动画持续时间
    animDuration =
        typedArray.getInt(R.styleable.Charter_c_animDuration,
                (int) DEFAULT_ANIM_DURATION);
    //是否在自己中进行绘画  默认true
    setWillNotDraw(!typedArray.getBoolean(R.styleable.Charter_c_autoShow,
            DEFAULT_AUTOSHOW));
    typedArray.recycle();//回收
 
    /**
     * 下面是三个画笔
     * 一个是折线的画笔
     * 一个是填充的画笔
     * 一个是指示点的画笔
     */
    paintLine = new Paint();
    paintLine.setAntiAlias(true);
    paintLine.setStrokeWidth(strokeSize);
    paintLine.setColor(lineColor);
    paintLine.setStyle(Paint.Style.STROKE);
 
    paintFill = new Paint();
    paintFill.setAntiAlias(true);
    paintFill.setColor(chartFillColor);
    paintFill.setStyle(Paint.Style.FILL);
 
    paintIndicator = new Paint();
    paintIndicator.setAntiAlias(true);
    paintIndicator.setStrokeWidth(indicatorStrokeSize);
    //默认的背景色
    defaultBackgroundColor = getResources().getColor(
            R.color.default_chartBackgroundColor);
    chartBackgroundColor = defaultBackgroundColor;
    //折线path
    path = new Path();
  }
 
  public Paint getPaintLine() {
    return paintLine;
  }
 
  public void setPaintLine(Paint paintLine) {
    this.paintLine = paintLine;
    invalidate();
  }
 
  public Paint getPaintFill() {
    return paintFill;
  }
 
  public void setPaintFill(Paint paintFill) {
    this.paintFill = paintFill;
    invalidate();
  }
 
  public Paint getPaintIndicator() {
    return paintIndicator;
  }
 
  public void setPaintIndicator(Paint paintIndicator) {
    this.paintIndicator = paintIndicator;
    invalidate();
  }
 
  public float getIndicatorStrokeSize() {
    return indicatorStrokeSize;
  }
 
  public void setIndicatorStrokeSize(float indicatorStrokeSize) {
    paintIndicator.setStrokeWidth(indicatorStrokeSize);
    this.indicatorStrokeSize = indicatorStrokeSize;
    invalidate();
  }
 
  /**
   * 设置指示点的类型
   * 类型支持两种类型
   * 圆形 方形
   * @return
   */
  public int getIndicatorStyle() {
    return indicatorStyle;
  }
 
  public void setIndicatorStyle(@IndicatorStyle int indicatorStyle) {
    this.indicatorStyle = indicatorStyle;
    invalidate();
  }
 
  public int getIndicatorColor() {
    return indicatorColor;
  }
 
  public void setIndicatorColor(@ColorInt int indicatorColor) {
    paintIndicator.setColor(indicatorColor);
    this.indicatorColor = indicatorColor;
    invalidate();
  }
 
  /**
   * 设置或者获取指示点的样式
   * 样式支持两种:
   * 空心圆圈 实心圆圈
   * @return
   */
  public int getIndicatorType() {
    return indicatorType;
  }
 
  /**
   * 这里作者使用了自定义的annotation
   * 请看本类最后的用法!!!
   * 牛逼啊!
   * @param indicatorType
   */
  public void setIndicatorType(@IndicatorType int indicatorType) {
    this.indicatorType = indicatorType;
    invalidate();
  }
 
  public int getLineColor() {
    return lineColor;
  }
 
  /**
   * 这里的set方法使用的是注解!!!
   * @param color
   */
  public void setLineColor(@ColorInt int color) {
    paintLine.setColor(lineColor);
    lineColor = color;
    invalidate();
  }
 
  public float getIndicatorSize() {
    return indicatorSize;
  }
 
  public void setIndicatorSize(float indicatorSize) {
    this.indicatorSize = indicatorSize;
    invalidate();
  }
 
  public float getStrokeSize() {
    return strokeSize;
  }
 
  public void setStrokeSize(float strokeSize) {
    paintLine.setStrokeWidth(strokeSize);
    this.strokeSize = strokeSize;
    invalidate();
  }
 
  public int getChartFillColor() {
    return chartFillColor;
  }
 
  /**
   * 这里的set方法使用的是注解!!!
   * @param chartFillColor
   */
  public void setChartFillColor(@ColorInt int chartFillColor) {
    paintFill.setColor(chartFillColor);
    this.chartFillColor = chartFillColor;
    invalidate();
  }
 
  public boolean isIndicatorVisible() {
    return indicatorVisible;
  }
 
  public void setIndicatorVisible(boolean indicatorVisible) {
    this.indicatorVisible = indicatorVisible;
    invalidate();
  }
 
  /**
   * 设置或者获取线的平滑度
   * 值从0.0 到0.5之间
   * @return
   */
  public float getSmoothness() {
    return smoothness;
  }
 
  /**
   * 注解!!
   * @param smoothness
   */
  public void setSmoothness(@FloatRange(from = 0.0, to = 0.5) float smoothness) {
    this.smoothness = smoothness;
    invalidate();
  }
 
  public boolean isFullWidth() {
    return fullWidth;
  }
 
  public void setFullWidth(boolean fullWidth) {
    this.fullWidth = fullWidth;
    invalidate();
  }
 
  /**
   * 绘图的核心方法
   * @param canvas
   */
  public void draw(Canvas canvas) {
    super.draw(canvas);
    //如果值为空,直接返回
    if (values == null || values.length == 0) {
      return;
    }
    /**
     * 如果设置显示动画,这一步步获取动画的值。
     * 否则,直接拷贝值,进行绘画
     */
    if (anim) {
      calculateNextAnimStep();
    } else {
      valuesTransition = values.clone();
    }
 
    float fullWidthCorrectionX;
 
    final int valuesLength = valuesTransition.length;
    //边距 也就是线宽和指示点宽度
    final float border = strokeSize + indicatorSize;
    //得到实际所用的高度值
    final float height = getMeasuredHeight() - border;
    //得到x的修正值
    fullWidthCorrectionX = fullWidth ? 0 : border;
    //得到实际所占用的宽度
    final float width = getMeasuredWidth() - fullWidthCorrectionX;
    //根据值的个数,计算x的间距
    final float dX = valuesLength > 1 ? valuesLength - 1 : 2;
    //根据最大值,最小值 计算y间距
    final float dY = maxY - minY > 0 ? maxY - minY : 2;
 
    path.reset();
 
    // calculate point coordinates
    /**
     * 计算坐标点集合
     * minY代表数据集中的最小值
     */
    List<PointF> points = new ArrayList<>(valuesLength);
    fullWidthCorrectionX = fullWidth ? 0 : (border / 2);
    for (int i = 0; i < valuesLength; i++) {
      float x = fullWidthCorrectionX + i * width / dX;
      float pointBorder = !indicatorVisible && valuesTransition[i]
              == minY ? border : border / 2;
      /**
       * y的计算有点麻烦
       * 主要是因为y的坐标原点在上方。
       * 高度值减去实际值得到绘画的值。
       * 实际的值越大,y值越小,绘画的高度就越高!!
       */
      float y = pointBorder + height
              - (valuesTransition[i] - minY) * height / dY;
      points.add(new PointF(x, y));
    }
 
    float lX = 0;
    float lY = 0;
    //路径移动到首个坐标点
    path.moveTo(points.get(0).x, points.get(0).y);
    for (int i = 1; i < valuesLength; i++) {
      PointF p = points.get(i);
      float pointSmoothness = valuesTransition[i] == minY ? 0 : smoothness;
 
      PointF firstPointF = points.get(i - 1);
      float x1 = firstPointF.x + lX;
      float y1 = firstPointF.y + lY;
 
      PointF secondPointF = points.get(i + 1 < valuesLength ? i + 1 : i);
      lX = (secondPointF.x - firstPointF.x) / 2 * pointSmoothness;
      lY = (secondPointF.y - firstPointF.y) / 2 * pointSmoothness;
      float x2 = p.x - lX;
      float y2 = p.y - lY;
      if (y1 == p.y) {
        y2 = y1;
      }
      /**
      * Add a cubic bezier from the last point, approaching control points
      * (x1,y1) and (x2,y2), and ending at (x3,y3).
      */
      path.cubicTo(x1, y1, x2, y2, p.x, p.y);
    }
    canvas.drawPath(path, paintLine);
 
    // fill area 填充区域
    if (valuesLength > 0) {
      fullWidthCorrectionX = !fullWidth ? 0 : (border / 2);
      path.lineTo(points.get(valuesLength - 1).x + fullWidthCorrectionX,
              height + border);
      path.lineTo(points.get(0).x - fullWidthCorrectionX,
              height + border);
      path.close();
      canvas.drawPath(path, paintFill);
    }
 
    // draw indicator
    if (indicatorVisible) {
      for (int i = 0; i < points.size(); i++) {
        RectF rectF = new RectF();
        float x = points.get(i).x;
        float y = points.get(i).y;
 
        paintIndicator.setColor(lineColor);
        paintIndicator.setStyle(Paint.Style.FILL_AND_STROKE);
        if (indicatorType == INDICATOR_TYPE_CIRCLE) {
          canvas.drawCircle(x, y, indicatorSize / 2, paintIndicator);
        } else {
          rectF.left = x - (indicatorSize / 2);
          rectF.top = y - (indicatorSize / 2);
          rectF.right = x + (indicatorSize / 2);
          rectF.bottom = y + (indicatorSize / 2);
          canvas.drawRect(rectF.left, rectF.top, rectF.right,
                  rectF.bottom, paintIndicator);
        }
 
        if (indicatorStyle == INDICATOR_STYLE_STROKE) {
          paintIndicator.setColor(chartBackgroundColor);
          paintIndicator.setStyle(Paint.Style.FILL);
 
          if (indicatorType == INDICATOR_TYPE_CIRCLE) {
            canvas.drawCircle(x, y, (indicatorSize - indicatorStrokeSize) / 2,
                    paintIndicator);
          } else {
            rectF.left = x - (indicatorSize / 2) + indicatorStrokeSize;
            rectF.top = y - (indicatorSize / 2) + indicatorStrokeSize;
            rectF.right = x + (indicatorSize / 2) - indicatorStrokeSize;
            rectF.bottom = y + (indicatorSize / 2) - indicatorStrokeSize;
            canvas.drawRect(rectF.left, rectF.top, rectF.right, rectF.bottom,
                    paintIndicator);
          }
        }
      }
    }
 
    if (anim && !animFinished) {
      handlerAnim.postDelayed(doNextAnimStep, ANIM_DELAY_MILLIS);
    }
  }
 
  /**
   * 设置背景色
   * @param color
   */
  @Override public void setBackgroundColor(@ColorInt int color) {
    super.setBackgroundColor(color);
    chartBackgroundColor = color;
  }
 
  @Override public void setBackground(Drawable background) {
    super.setBackground(background);
    chartBackgroundColor = defaultBackgroundColor;
    Drawable drawable = getBackground();
    if (drawable instanceof ColorDrawable) {
      chartBackgroundColor = ((ColorDrawable) drawable).getColor();
    }
  }
 
  /**
   * 定义自己的annotation
   * Retention的意思是保留 指示的是保留的级别
   * 这里设置的是保留在源码中。
   * 有三种保留级别:SOURCE  RUNTIME CLASS
   */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({ INDICATOR_STYLE_FILL, INDICATOR_STYLE_STROKE })
  public @interface IndicatorType {
  }
 
  /**
   * 注解!!!
   */
  @Retention(RetentionPolicy.SOURCE)
  @IntDef({ INDICATOR_TYPE_CIRCLE, INDICATOR_TYPE_SQUARE })
  public @interface IndicatorStyle {
  }
}
 
其中也包含了不少的set get方法,这些方法不多说,一看就明白。其中,最核心的也就是onDraw方法,在onDraw方法中不仅仅绘制了各个点,还绘制了折线图、折线图围绕的区域 以及整个view的背景。 
1 各个点的绘制
 
final int valuesLength = valuesTransition.length;
    //边距 也就是线宽和指示点宽度
    final float border = strokeSize + indicatorSize;
    //得到实际所用的高度值
    final float height = getMeasuredHeight() - border;
    //得到x的修正值
    fullWidthCorrectionX = fullWidth ? 0 : border;
    //得到实际所占用的宽度
    final float width = getMeasuredWidth() - fullWidthCorrectionX;
    //根据值的个数,计算x的间距
    final float dX = valuesLength > 1 ? valuesLength - 1 : 2;
    //根据最大值,最小值 计算y间距
    final float dY = maxY - minY > 0 ? maxY - minY : 2;
 
    path.reset();
 
    // calculate point coordinates
    /**
     * 计算坐标点集合
     * minY代表数据集中的最小值
     */
    List<PointF> points = new ArrayList<>(valuesLength);
    fullWidthCorrectionX = fullWidth ? 0 : (border / 2);
for (int i = 0; i < valuesLength; i++) {
      float x = fullWidthCorrectionX + i * width / dX;
      float pointBorder = !indicatorVisible && valuesTransition[i]
              == minY ? border : border / 2;
      /**
       * y的计算有点麻烦
       * 主要是因为y的坐标原点在上方。
       * 高度值减去实际值得到绘画的值。
       * 实际的值越大,y值越小,绘画的高度就越高!!
       */
      float y = pointBorder + height
              - (valuesTransition[i] - minY) * height / dY;
      points.add(new PointF(x, y));
    }
 
onDraw方法中的第一个for循环完成了各个点的绘制。默认点样式为空心圆圈。代码中x y值就代表各个点的坐标。
 
float lX = 0;
    float lY = 0;
    //路径移动到首个坐标点
    path.moveTo(points.get(0).x, points.get(0).y);
    for (int i = 1; i < valuesLength; i++) {
      PointF p = points.get(i);
      float pointSmoothness = valuesTransition[i] == minY ? 0 : smoothness;
 
      PointF firstPointF = points.get(i - 1);
      float x1 = firstPointF.x + lX;
      float y1 = firstPointF.y + lY;
 
      PointF secondPointF = points.get(i + 1 < valuesLength ? i + 1 : i);
      lX = (secondPointF.x - firstPointF.x) / 2 * pointSmoothness;
      lY = (secondPointF.y - firstPointF.y) / 2 * pointSmoothness;
      float x2 = p.x - lX;
      float y2 = p.y - lY;
      if (y1 == p.y) {
        y2 = y1;
      }
      /**
      * Add a cubic bezier from the last point, approaching control points
      * (x1,y1) and (x2,y2), and ending at (x3,y3).
      */
      path.cubicTo(x1, y1, x2, y2, p.x, p.y);
    }
    canvas.drawPath(path, paintLine);
 
这是第二个for循环,绘制折线图。利用路径path完成。cubicTo方法完成贝瑟尔曲线绘制,有三个点完成,中间的x2 y2作为控制点,这里代码中x2 y2取的是x1 y1点和p.x p.y点的中点加上一个浮动值完成的。作者在这里的处理非常完美!!
 
// fill area 填充区域
    if (valuesLength > 0) {
      fullWidthCorrectionX = !fullWidth ? 0 : (border / 2);
      path.lineTo(points.get(valuesLength - 1).x + fullWidthCorrectionX,
              height + border);
      path.lineTo(points.get(0).x - fullWidthCorrectionX,
              height + border);
      path.close();
      canvas.drawPath(path, paintFill);
    }
 
这个if判断完成了折线图所围绕的区域的绘制。利用的就是path,上面我们绘制折线的过程中,已经完成了折线的绘制,然后if语句中 
 android-柱状图、折线图、x轴、y轴绘制以及实例代码
两个path.lineto 和一个close方法完成了路径的闭合,完成了区域的绘制。像图中所示的样子。
 
if (indicatorVisible) {
      for (int i = 0; i < points.size(); i++) {
        RectF rectF = new RectF();
        float x = points.get(i).x;
        float y = points.get(i).y;
 
        paintIndicator.setColor(lineColor);
        paintIndicator.setStyle(Paint.Style.FILL_AND_STROKE);
        if (indicatorType == INDICATOR_TYPE_CIRCLE) {
          canvas.drawCircle(x, y, indicatorSize / 2, paintIndicator);
        } else {
          rectF.left = x - (indicatorSize / 2);
          rectF.top = y - (indicatorSize / 2);
          rectF.right = x + (indicatorSize / 2);
          rectF.bottom = y + (indicatorSize / 2);
          canvas.drawRect(rectF.left, rectF.top, rectF.right,
                  rectF.bottom, paintIndicator);
        }
 
        if (indicatorStyle == INDICATOR_STYLE_STROKE) {
          paintIndicator.setColor(chartBackgroundColor);
          paintIndicator.setStyle(Paint.Style.FILL);
 
          if (indicatorType == INDICATOR_TYPE_CIRCLE) {
            canvas.drawCircle(x, y, (indicatorSize - indicatorStrokeSize) / 2,
                    paintIndicator);
          } else {
            rectF.left = x - (indicatorSize / 2) + indicatorStrokeSize;
            rectF.top = y - (indicatorSize / 2) + indicatorStrokeSize;
            rectF.right = x + (indicatorSize / 2) - indicatorStrokeSize;
            rectF.bottom = y + (indicatorSize / 2) - indicatorStrokeSize;
            canvas.drawRect(rectF.left, rectF.top, rectF.right, rectF.bottom,
                    paintIndicator);
          }
        }
      }
    }
 
这个是最后一步,完成坐标点的绘制,默认样式是空心圆圈,否则,就是矩形的点进行绘制。 
 android-柱状图、折线图、x轴、y轴绘制以及实例代码
这个图所示的就是矩形点的绘制。 
关于空心圆圈的绘制,就不多说了,主要思路上面已有,过程主要是计算坐标点X Y值,有了X Y坐标点的值,设置圆圈的半径,就可以绘画圆圈啦。 
矩形的绘制也是如此,矩形就是计算left right top bottom 的值,有了这几个值就可以绘制矩形了。计算的方法就是X Y的坐标点分别加减一个微小的值,即可。代码:
 
rectF.left = x - (indicatorSize / 2) + indicatorStrokeSize;
            rectF.top = y - (indicatorSize / 2) + indicatorStrokeSize;
            rectF.right = x + (indicatorSize / 2) - indicatorStrokeSize;
            rectF.bottom = y + (indicatorSize / 2) - indicatorStrokeSize;
            canvas.drawRect(rectF.left, rectF.top, rectF.right, rectF.bottom,
                    paintIndicator);
 
indicatorSize 代表的就是小矩形的边距大小值。 
indicatorStrokeSize代表的是绘制矩形边距的线的宽度值。 
OK,说明完毕。
 
CharBar 柱状图实现代码
 
柱状图的设计和折线图的绘画过程类似,先看代码:
 
public class CharterBar extends CharterBase {
  private static final boolean DEFAULT_PAINT_BAR_BACKGROUND = true;
  private static final float DEFAULT_BAR_MIN_CORRECTION = 2f;
  //柱状图背景色是否有
  private boolean paintBarBackground;
  //柱状图背景色
  private int barBackgroundColor;
  //柱状图间距
  private float barMargin;
  //柱状图画笔
  private Paint paintBar;
  private int[] colors;
  private int[] colorsBackground;
 
  public CharterBar(Context context) {
    this(context, null);
  }
 
  public CharterBar(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  public CharterBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs);
  }
 
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public CharterBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init(context, attrs);
  }
 
  private void init(final Context context, final AttributeSet attrs) {
    final TypedArray typedArray = context
            .obtainStyledAttributes(attrs, R.styleable.Charter);
    //是否有柱状图背景色
    paintBarBackground = typedArray.getBoolean(
            R.styleable.Charter_c_paintBarBackground,
        DEFAULT_PAINT_BAR_BACKGROUND);
    //柱状图颜色
    int barColor = typedArray.getColor(R.styleable.Charter_c_barColor,
        getResources().getColor(R.color.default_barColor));
    //柱状图的背景颜色
    int barBackgroundColor = typedArray.getColor(
            R.styleable.Charter_c_barBackgroundColor,
        getResources().getColor(R.color.default_barBackgroundColor));
    //柱状图间距
    barMargin = typedArray.getDimension(
            R.styleable.Charter_c_barMargin,
        getResources().getDimension(R.dimen.default_barMargin));
    //是否显示动画
    anim = typedArray.getBoolean(R.styleable.Charter_c_anim, DEFAULT_ANIM);
    //动画持续时间 默认500毫秒
    animDuration =typedArray.getInt(R.styleable.Charter_c_animDuration,
                (int) DEFAULT_ANIM_DURATION);
    //是否绘画 该方法在View类中
    setWillNotDraw(!typedArray.getBoolean(R.styleable.Charter_c_autoShow,
            DEFAULT_AUTOSHOW));
    typedArray.recycle();//回收
    //柱状图画笔
    paintBar = new Paint();
    paintBar.setAntiAlias(true);
    //柱状图颜色
    colors = new int[] { barColor };
    //柱状图背景颜色
    colorsBackground = new int[] { barBackgroundColor };
    /**
     * 柱状图颜色是值柱的颜色
     * 而其背景色是柱的背景色!
     */
  }
 
  public Paint getPaintBar() {
    return paintBar;
  }
 
  public void setPaintBar(Paint paintBar) {
    this.paintBar = paintBar;
    invalidate();
  }
 
  public int[] getColors() {
    return colors;
  }
 
  public void setColors(@ColorInt int[] colors) {
    if (colors == null || colors.length == 0) {
      return;
    }
 
    this.colors = colors;
    invalidate();
  }
 
  public int[] getColorsBackground() {
    return colorsBackground;
  }
 
  public void setColorsBackground(@ColorInt int[] colorsBackground) {
    if (colorsBackground == null || colorsBackground.length == 0) {
      return;
    }
 
    this.colorsBackground = colorsBackground;
    invalidate();
  }
 
  public float getBarMargin() {
    return barMargin;
  }
 
  public void setBarMargin(float barMargin) {
    this.barMargin = barMargin;
    invalidate();
  }
 
  public boolean isPaintBarBackground() {
    return paintBarBackground;
  }
 
  public void setPaintBarBackground(boolean paintBarBackground) {
    this.paintBarBackground = paintBarBackground;
    invalidate();
  }
 
  public int getBarBackgroundColor() {
    return barBackgroundColor;
  }
 
  public void setBarBackgroundColor(@ColorInt int barBackgroundColor) {
    this.barBackgroundColor = barBackgroundColor;
    invalidate();
  }
 
  /**
   * 绘画柱状图的核心方法
   * @param canvas
   */
  public void draw(Canvas canvas) {
    super.draw(canvas);
 
    if (values == null || values.length == 0) {
      return;
    }
 
    if (anim) {
      calculateNextAnimStep();
    } else {
      valuesTransition = values.clone();
    }
 
    final int valuesLength = valuesTransition.length;
 
    final float height = getMeasuredHeight();
    final float width = getMeasuredWidth();
    //计算每条柱子的宽度
    final float barWidth = width / valuesLength;
    //最大值和最小值的差值
    final float diff = maxY - minY;
    //高度片值
    final float sliceHeight = height / diff;
 
    int colorsPos = 0;
    int colorsBackgroundPos = -1;
 
    for (int i = 0; i < valuesLength; i++) {
      RectF rectF = new RectF();
      rectF.left = (i * barWidth) + barMargin;
      rectF.top = height - (sliceHeight * (valuesTransition[i] - minY));
      //如果top值等于view高度值,显示默认的柱形最小值,不至于有点都不显示。
      rectF.top = rectF.top == height ? rectF.top - DEFAULT_BAR_MIN_CORRECTION : rectF.top;
      rectF.right = (i * barWidth) + barWidth - barMargin;
      rectF.bottom = height;
 
      // paint background
      //向间绘画背景色 背景色可以有多个,向间绘画背景色。
      if (paintBarBackground) {
        if (colorsBackgroundPos + 1 >= colorsBackground.length) {
          colorsBackgroundPos = 0;
        } else {
          colorsBackgroundPos++;
        }
        paintBar.setColor(colorsBackground[colorsBackgroundPos]);
        //绘画柱形背景色 这里完成背景色的柱形绘制
        canvas.drawRect(rectF.left, 0, rectF.right, rectF.bottom, paintBar);
      }
 
      // paint bar
      if (colorsPos + 1 >= colors.length) {
        colorsPos = 0;
      } else {
        colorsPos++;
      }
      paintBar.setColor(colors[colorsPos]);
      //绘画柱形 这里完成柱形绘制
      canvas.drawRect(rectF.left, rectF.top, rectF.right, rectF.bottom, paintBar);
    }
    //通知动画绘制
    if (anim && !animFinished) {
      handlerAnim.postDelayed(doNextAnimStep, ANIM_DELAY_MILLIS);
    }
  }
}
 
CharterBar类继承了基类CharterBase,包含了不少的set get方法。 
核心方法在于ondraw方法的绘制。 
核心过程包括两点:柱状图背景的绘画和柱状图的绘画。
 
 for (int i = 0; i < valuesLength; i++) {
      RectF rectF = new RectF();
      rectF.left = (i * barWidth) + barMargin;
      rectF.top = height - (sliceHeight * (valuesTransition[i] - minY));
      //如果top值等于view高度值,显示默认的柱形最小值,不至于有点都不显示。
      rectF.top = rectF.top == height ? rectF.top - DEFAULT_BAR_MIN_CORRECTION : rectF.top;
      rectF.right = (i * barWidth) + barWidth - barMargin;
      rectF.bottom = height;
 
      // paint background
      //向间绘画背景色 背景色可以有多个,向间绘画背景色。
      if (paintBarBackground) {
        if (colorsBackgroundPos + 1 >= colorsBackground.length) {
          colorsBackgroundPos = 0;
        } else {
          colorsBackgroundPos++;
        }
        paintBar.setColor(colorsBackground[colorsBackgroundPos]);
        //绘画柱形背景色 这里完成背景色的柱形绘制
        canvas.drawRect(rectF.left, 0, rectF.right, rectF.bottom, paintBar);
      }
 
      // paint bar
      if (colorsPos + 1 >= colors.length) {
        colorsPos = 0;
      } else {
        colorsPos++;
      }
      paintBar.setColor(colors[colorsPos]);
      //绘画柱形 这里完成柱形绘制
      canvas.drawRect(rectF.left, rectF.top, rectF.right, rectF.bottom, paintBar);
    }
 
这个for循环完成了上面的两点绘画。
 
...............
 //绘画柱形背景色 这里完成背景色的柱形绘制
        canvas.drawRect(rectF.left, 0, rectF.right, rectF.bottom, paintBar);
............................
//绘画柱形 这里完成柱形绘制
      canvas.drawRect(rectF.left, rectF.top, rectF.right, rectF.bottom, paintBar);
 
这两个drawRect分别完成了柱状图背景的绘画和柱状图的绘画。并且这两者的绘画的left top right bottom的值还有关系。 
left right bottom的值是相同的,只有top值不同。 
下面问题就是left top right bottom矩形的四边值如何计算:
 
    final int valuesLength = valuesTransition.length;
 
    final float height = getMeasuredHeight();
    final float width = getMeasuredWidth();
    //计算每条柱子的宽度
    final float barWidth = width / valuesLength;
    //最大值和最小值的差值
    final float diff = maxY - minY;
    //高度片值
    final float sliceHeight = height / diff;
RectF rectF = new RectF();
      rectF.left = (i * barWidth) + barMargin;
      rectF.top = height - (sliceHeight * (valuesTransition[i] - minY));
      //如果top值等于view高度值,显示默认的柱形最小值,不至于有点都不显示。
      rectF.top = rectF.top == height ? rectF.top - DEFAULT_BAR_MIN_CORRECTION : rectF.top;
      rectF.right = (i * barWidth) + barWidth - barMargin;
      rectF.bottom = height;
 
其中各个变量的值: 
barWidth代表每个柱状图宽度 
barMargin代表柱状图的间距 
sliceHeight 代表高度分值 也就是view的每个高度值所代表的真实数据的单位值
 
如果看不懂计算过程,请细细思量,该过程是绘制柱状图的核心所在!
 
总算是自定义部分说明完毕了。下面就是怎么用的问题啦!
 
看xml布局文件:
 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.hrules.charter.demo.XYLineActivity">
    <LinearLayout
        android:id="@+id/linear"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <com.hrules.charter.CharterYLabels
            android:id="@+id/ylable"
            android:layout_width="20dp"
            android:layout_height="300dp"
            />
        <com.hrules.charter.CharterLine
            android:id="@+id/charter_line"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            />
    </LinearLayout>
    <com.hrules.charter.CharterXLabels
        android:id="@+id/xlable"
        android:layout_below="@id/linear"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:layout_marginLeft="20dp"
        />
</RelativeLayout>
 
请仔细看布局文件,相对布局中包含两个布局,水平布局和CharterXLabels两个,水平布局中又有两个CharterYLabels和CharterLine,看效果图: 
 android-柱状图、折线图、x轴、y轴绘制以及实例代码
Y轴和折线图对应水平布局部分,X轴代表下面的CharterXLabels。
 
再看activity类的代码:
 
public class XYLineActivity extends AppCompatActivity {
 
    private CharterYLabels mYlableCharterYLabels;
    private CharterLine mLineCharterLine;
    private LinearLayout mLinearLinearLayout;
    private CharterXLabels mXlableCharterXLabels;
    private float[] valueX;
    private float[] valueY;
    private float[] valueLine;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_xyline);
        mYlableCharterYLabels = (CharterYLabels) findViewById(R.id.ylable);
        mLineCharterLine = (CharterLine) findViewById(R.id.charter_line);
        mLinearLinearLayout = (LinearLayout) findViewById(R.id.linear);
        mXlableCharterXLabels = (CharterXLabels) findViewById(R.id.xlable);
        valueX = fillRandomValues(15,200,0);
        valueY = fillRandomValues(7,500,10);
        valueLine = fillRandomValues(15,500,10);
 
        mXlableCharterXLabels.setValues(valueX);
        mYlableCharterYLabels.setValues(valueY);
        mLineCharterLine.setIndicatorStyle(CharterLine.INDICATOR_TYPE_SQUARE);
        mLineCharterLine.setIndicatorType(CharterLine.INDICATOR_STYLE_STROKE);
        mLineCharterLine.setValues(valueLine);
        mLineCharterLine.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                valueX = fillRandomValues(15,200,0);
                valueY = fillRandomValues(7,500,10);
                valueLine = fillRandomValues(15,500,10);
                mXlableCharterXLabels.setValues(valueX);
                mYlableCharterYLabels.setValues(valueY);
                mLineCharterLine.setValues(valueLine);
                mLineCharterLine.show();
            }
        });
 
    }
    private float[] fillRandomValues(int length, int max, int min) {
        Random random = new Random();
        float[] newRandomValues = new float[length];
        for (int i = 0; i < newRandomValues.length; i++) {
            newRandomValues[i] = random.nextInt(max - min + 1) - min;
        }
        return newRandomValues;
    }
}
 
fillRandomValues方法就是产生一些模拟数据,分别产生X Y 折线图的数据,然后把数据设置进组件当中进行显示,折线图组件又定义了点击事件,可以刷新数据。
 
细心的朋友应该会看到其中这两句代码:
 
mLineCharterLine.setIndicatorStyle(CharterLine.INDICATOR_TYPE_SQUARE);
        mLineCharterLine.setIndicatorType(CharterLine.INDICATOR_STYLE_STROKE);
 
第一句设置Style的 但是吧TYPE类型值传递进去啦,设置成正方形的样式, 
第二句设置Type的,但是吧Stype类型值设置进去啦,设置成不填充,空心样式。
 
这不是不对啦吗?确实是这样,这个地方等到我写这篇文章的时候才发现的,瑕不掩瑜哈!!^_^ 
我提交的代码中已经更改,代码中不会存在这个问题哈。
 
好了,基本代码全部完成,文章刚开始的效果图有几个,这里只介绍这一个,布局用法是一样的。别的界面就不多说了,大家如果感兴趣,下载代码进行研究。
 
不过还是提一点,就是自定义的属性的用法。 
因为上面的自定义的四个组件:X轴 Y轴 折线图 柱状图 这四个组件作者是定义在自己的liabrary中的,看图: 

android-柱状图、折线图、x轴、y轴绘制以及实例代码

 
可以看到attrs.xml是定义在library中,如果在项目中使用各个组件的自定义的属性,需要把这个attrs.xml文件拷贝到自己的res/values文件夹下.看图: 
 android-柱状图、折线图、x轴、y轴绘制以及实例代码
拷贝进来之后,我就可以在布局文件中使用自定义组件的属性啦。
 
例如:
 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.hrules.charter.demo.XYBarActivity">
<LinearLayout
    android:id="@+id/linear"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    >
    <com.hrules.charter.CharterYLabels
        android:id="@+id/ylable"
        android:layout_width="20dp"
        android:layout_height="300dp"
        />
    <com.hrules.charter.CharterBar
        android:id="@+id/charter_bar"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        app:c_barColor="@color/colorAccent"
        />
</LinearLayout>
    <com.hrules.charter.CharterXLabels
        android:id="@+id/xlable"
        android:layout_below="@id/linear"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:layout_marginLeft="20dp"
        />
</RelativeLayout>
 
其中的app:c_barColor=”@color/colorAccent”这一行就是使用的自定义属性进行设置的。 
这个布局的效果图如下: 

android-柱状图、折线图、x轴、y轴绘制以及实例代码

activity界面的代码就没什么说的啦,自己下载代码看看就明白啦。
android-柱状图、折线图、x轴、y轴绘制以及实例代码 转载https://www.codesocang.com/appboke/40298.html

技术博客阅读排行

最新文章