首先上圖:
看圖說話,不錯吧!!哈哈
給地址:項目的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下載地址,請猛戳這裏。