首先上图:

看图说话,不错吧!!哈哈

给地址:项目的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));//标签大小,默认10sppaintLabelSize = 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_CENTERy = (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();//返回包裹标签的最小矩形rectpaintLabel.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_LEFTx = 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()返回值fasleif (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()返回值fasleif (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.5private 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);//指示点大小  默认6dpindicatorSize = 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);//线宽 指的是折线的线宽 默认2dpstrokeSize = 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);//是否在自己中进行绘画  默认truesetWillNotDraw(!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;//折线pathpath = 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 indicatorif (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 barif (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 barif (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;@Overrideprotected 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() {@Overridepublic 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下载地址,请猛戳这里。

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

  1. Origin: 双Y轴 | 柱状图 | 折线图 | 垂线散点图的结合绘制

    origin | 双Y轴 | 添加图层 | 柱状图 | 折线图 | 散点图 一.前言 二.数据准备 三.绘图 3.1 图层1-绘制柱状图 3.2 图层2-折线图的绘制 3.2.1 添加新图层-折线图 ...

  2. python绘制双Y轴折线图以及单Y轴双变量柱状图

    近来实验室的师姐要发论文,由于论文交稿时间临近,有一些杂活儿需要处理,作为实验室资历最浅的一批,我这个实习生也就责无旁贷地帮忙当个下手.今天师姐派了一个小活,具体要求是: 给一些训练模型的迭代次数,训 ...

  3. 使用hellocharts绘制折线图 并自定义Y轴

    效果图 1.hellocharts 的引用 在app的   build.gradle 中添加   compile 'com.github.lecho:hellocharts-library:1.5.8 ...

  4. echarts折线图怎么从y轴开始_基于echarts的双y轴实时更新折线图

    一款基于echarts的双y轴实时更新折线图效果,页面加载后开始自动更新数据并绘制对应的折线图,可以点击右上角的按钮:显示数据视图.刷新数据和将数据存储为png的图片. 查看演示 下载资源: 46 次 ...

  5. echarts折线图不显示y轴值_Echarts 折线图y轴标签值太长时显示不全的解决办法

    问题 先看一下正常的情况 再看一下显示不全的情况 所有的数据都是从后台取的,也就是说动态变化的,一开始的时候数据量不大不会出现问题,后面y轴的值越来越大的时候就出现了这个显示不全的情况. 分析 先贴一 ...

  6. html折线图怎么控制y轴数值,echarts Y轴数据类型不同怎么让折线图显示差距不大...

    for(var j in this.initVal) { legendData.push(this.initVal[j]);var i = 0;var newSeriesData =[];//遍历店铺 ...

  7. Echarts笔记-折线图定制(Y轴百分数,鼠标移动显示百分数,显示X轴,Y轴值)

    本笔记记录时间:2022-02-19 12:18:42,估计发布到网上是一个月后了. 效果图如下: 对应Echart代码如下: <script type="text/javascrip ...

  8. jfreechart柱状图+折线图

    public static void main(String[] args) {//创建主题样式 ,以下代码用于解决中文乱码问题StandardChartTheme standardChartThem ...

  9. echarts图表折线图柱状图多个X轴Y轴以及一个Y轴反向

    echarts图表折线图柱状图多个X轴Y轴以及一个Y轴反向 option1: {color: ['#21E9F6', '#F2CE2E', '#EE2929', '#006DD9', '#1789FF ...

  10. 【python科研绘图】双y轴并列柱状图+折线图+数据表结合,并封装图形绘制函数

    双y轴并列柱状图+折线图+数据表结合 1. 论文原图 2 数据准备 3 代码实现步骤拆解 3.1 导入第三方库 3.2 数据赋值 3.3 数据绘图 4 函数封装 手动反爬虫: 原博地址 https:/ ...

最新文章

  1. 前后端分离的探索(三)
  2. c语言节点导入数据编程,编程小白。用C语言计算SR E2E ARQ模式下节点的数据分析...
  3. PIC单片机 按键检测识别
  4. 安装Mongodb并解决用户授权问题
  5. c++中的继承--2(继承中的析构函数和构造函数,继承中同名成员,继承中静态成员)
  6. python写程序注意事项(很重要)
  7. 安装rtx时报错因计算机中丢失lo,policy.3.1.IntervalZero.RTX64.dll
  8. 服务器如何用显示器更改ip,ip地址怎么改
  9. 带外设引脚选择(PPS)的I/O端口
  10. 浏览器刷新页面导致vuex数据丢失问题如何解决?
  11. 利用MATLAB仿真实现交通红绿灯识别的目的
  12. android+农历月份大小压缩表,部分日期农历新历转换会崩溃
  13. 微信授权APP第三方登陆(Android)
  14. form表单AJAX提交
  15. 计算机无误的英语,“开电脑”的英语正确表示是哪个?说错了就尴尬
  16. (转)flex dataGrid 编辑
  17. Windows常用快捷键及运行命令
  18. 百分点10周年乔迁新址 数据智能服务更上一层楼
  19. 全网最全微服务架构—Spring Cloud详解,没有比这更详细的了!
  20. HDU 1248 寒冰王座(完全背包)

热门文章

  1. 前端学习-HTML5
  2. URI URL区别及转换
  3. 揭开程序员身上的「专业面纱」:非科班程序员,都来自哪些专业?
  4. Python爬虫编程思想(133):项目实战--利用Appium抓取微信朋友圈信息
  5. CSS——CSS盒子模型(重点※)
  6. JavaScript网页滚动距离
  7. kubeadm部署k8s集群最全最详细
  8. 用百度ai的人流量统计(动态版)来统计固定场景的人流
  9. 爬取豆瓣电影排行榜,并制作柱状图与3d柱状图
  10. 子佩录音盒,winform实现拨打电话,接听电话,显示客户信息,并录音,写入一条记录