android-柱狀圖、折線圖、x軸、y軸繪製以及實例代碼

首先上圖:
這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

看圖說話,不錯吧!!哈哈

給地址:項目的github地址

給自己打廣告:^_^
歡迎關注我的github

該項目不是我的獨創,我在原有作者的基礎上,把代碼進行了註釋、添加了上面三幅圖中的兩幅圖,也就是兩個界面。
本文的主要目的,就是對作者的代碼進行解讀,說明代碼的實現過程、用法、思路!!
感謝原作者!原作者的github地址在這裏

好啦,現在開始說明上面的圖形的實現思路!!

第一幅圖是幾個實例,折線圖、柱狀圖,同時還有x軸、y軸的實現。

下面的兩幅圖分別有橫軸、縱軸、和折線圖或者柱狀圖。

作者的思路非常明確,每幅圖分爲三個部分,x軸部分,y軸部分,和中間的圖形部分。每一部分都是一個自定義的view!!
這裏寫圖片描述

三個部分如圖所示,每個部分可有可無,可以組裝。
其中橫軸、縱軸都是數字的列表的顯示,只不過是橫向和縱向的區別,因此可以有一個基類。中間的柱狀圖部分和折線圖也可以有一個基類,整個代碼的結構如下圖:
這裏寫圖片描述
看類名稱就可以看出類之間的關係。在此不多說。

下面是實現思路:

  • 橫軸、縱軸實現思路
    因爲是座標圖,所以橫軸、縱軸數據應該從服務器或者本地獲取到的,應該提前知道要顯示的數據,此時,橫軸縱軸要顯示的數據的個數已知,最大致最小值也可以知道,由此,可以得到兩個數據之間顯示的間距,有了間距,就可以一個個的顯示數據啦。
    公式表示就是:
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語句中
這裏寫圖片描述
兩個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);
          }
        }
      }
    }

這個是最後一步,完成座標點的繪製,默認樣式是空心圓圈,否則,就是矩形的點進行繪製。
這裏寫圖片描述
這個圖所示的就是矩形點的繪製。
關於空心圓圈的繪製,就不多說了,主要思路上面已有,過程主要是計算座標點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,看效果圖:
這裏寫圖片描述
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中的,看圖:
這裏寫圖片描述

可以看到attrs.xml是定義在library中,如果在項目中使用各個組件的自定義的屬性,需要把這個attrs.xml文件拷貝到自己的res/values文件夾下.看圖:
這裏寫圖片描述
拷貝進來之後,我就可以在佈局文件中使用自定義組件的屬性啦。

例如:

<?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”這一行就是使用的自定義屬性進行設置的。
這個佈局的效果圖如下:
這裏寫圖片描述

activity界面的代碼就沒什麼說的啦,自己下載代碼看看就明白啦。

好啦 長篇大論!完成。

最後附上項目的github地址

最後爲自己代言^_^:歡迎關注我的github

CSDN下載地址,請猛戳這裏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章