第一幅图是几个实例,折线图、柱状图,同时还有x轴、y轴的实现。 下面的两幅图分别有横轴、纵轴、和折线图或者柱状图。 作者的思路非常明确,每幅图分为三个部分,x轴部分,y轴部分,和中间的图形部分。
第一幅图是几个实例,折线图、柱状图,同时还有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界面的代码就没什么说的啦,自己下载代码看看就明白啦。
android-柱状图、折线图、x轴、y轴绘制以及实例代码
转载https://www.codesocang.com/appboke/40298.html