安卓绘制统计图可以用androidchart,也可以自己绘制,不像ios,android能找到的开源库在UI方面都很差,要做出吸引人地方还是需要自己绘制。

本文给出最常用的曲线图的绘制方法。

绘制曲线图首先需要画好横竖坐标轴建立坐标系,比如坐标系中的100距离应该在canvas中绘制多长,这个是需要计算的,其实坐标体系的建立是最复杂的,我看过很多第三方库的建立方法都不一样,有的要灵活一些,有的比较死板。至于绘制曲线要么是用Canvas.drawLine方法,要么是用Path.lineTo方法,看你自己的习惯。

为了做出一个外观良好的曲线图,我参考了两个开源代码,第一个的曲线图绘制限制较多,使用范围太窄,但是有数据变化时的动画效果。第二个的适用范围很广,他能根据数据集合自动计算横纵坐标的个数,在canvas上单元格的距离,只需输入坐标点就能自动建立坐标体系绘制曲线,但是没有动画效果。

先讲第一个LineView。

LineView的demo可以在这里下载,lineview其实只是github项目的一部分,我是将其提取出来了的,个人觉得他的其他部分没有参考价值。作者好像是个韩国人。

LineView的曲线绘制没有什么可取的部分,我想学习的是他实现动画效果的方法,设计的很好,但具体实现还需要改进,让动画更流畅。

Lineview的调用方法:
在xml中添加lineview控件
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/horizontalScrollView"
android:layout_alignParentRight="true"
android:layout_above="@+id/line_button">
android:layout_width="wrap_content"
android:layout_height="200dp"
class="com.example.widget.LineView"
android:id="@+id/line_view"/>
在activity代码中获取lineview对象:
finalLineView lineView = (LineView)findViewById(R.id.line_view);
添加横坐标:
int randomint = 9;
ArrayListtest =newArrayList();
for(inti=0;i
test.add(String.valueOf(i+1));
}
lineView.setBottomTextList(test);
允许绘制坐标点:
lineView.setDrawDotLine(true);
lineView.setShowPopup(LineView.SHOW_POPUPS_NONE);
ArrayList dataList =newArrayList();
intrandom = (int)(Math.random()*9+1);
for(inti=0;i
dataList.add((int)(Math.random()*random));
}
添加纵坐标的值:
ArrayList>dataLists =newArrayList>();
dataLists.add(dataList);
lineView.setDataList(dataLists);

从其用法中可以看出,lineview需要提前设定横坐标的范围,而且纵坐标的值必须和lineView.setBottomTextList(test)中添加的值一一对应(读lineview源码可以知道),使用起来很不方便,我觉得作者仅仅是做出了一条曲线而已,而不太关注是否有用。和很多曲线图的开源代码一样lineview允许一次绘制几根颜色不同的曲线。

只需在上面的代码中为dataLists再添加一个list成员就行。

Lineview的实现

Lineview是view的直接子类public class LineView extends View,因为其绘制曲线的方法本身没什么亮点就不讲了,简单提一下其measure方法。重点讲他是如何实现动画效果的。

其实我也不是很明白measure过程中有些代码,自己看吧:

@Override
protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec) {
intmViewWidth = measureWidth(widthMeasureSpec);
mViewHeight=measureHeight(heightMeasureSpec);
refreshAfterDataChanged();
setMeasuredDimension(mViewWidth,mViewHeight);
}
privateintmeasureWidth(intmeasureSpec){
inthorizontalGridNum = getHorizontalGridNum();
intpreferred =backgroundGridWidth*horizontalGridNum+sideLineLength*2;
returngetMeasurement(measureSpec, preferred);
}
privateintmeasureHeight(intmeasureSpec){
intpreferred = 0;
returngetMeasurement(measureSpec,preferred);
}
privateintgetMeasurement(intmeasureSpec,intpreferred){
intspecSize = MeasureSpec.getSize(measureSpec);
intmeasurement;
switch(MeasureSpec.getMode(measureSpec)){
caseMeasureSpec.EXACTLY:
measurement = specSize;
break;
caseMeasureSpec.AT_MOST:
measurement = Math.min(preferred,specSize);
break;
default:
measurement = preferred;
break;
}
returnmeasurement;
}

动画:

如何才能展现出一条曲线的变化过程呢,曲线在波动其实是纵坐标y值在上下波动,横坐标x值是没有变化的,但是不要紧这个办法即使x值变化也应该可以展示出动画来,只是可能动画有点乱乱的感觉。

一条曲线的动画其实是多个个点的值在不断变化引起的,因此传统的android动画满足不了这么细致的需求,得自己想办法。

假如我们有这样的9个点(1,2),(2,0),(3,0),(4,2),(5,1),(6,0),(7,1),(8,2),(9,1)则用lineview可以得到如下的曲线图:


我们先预设最初的点为(1,0), (2,0), (3,0), (4,0), (5,0), (6,0), (7,0), (8,0), (9,0)

然后每隔一段时间就让每个点的y+1,这样就能得到动画效果。

private Runnable animator = new Runnable() {
@Override
public void run() {
boolean needNewFrame = false;
for(ArrayList data :drawDotLists){
for(Dotdot : data){
dot.update();
if(!dot.isAtRest()){
needNewFrame = true;
}
}
}
if (needNewFrame) {
postDelayed(this, 10);
}
invalidate();
}
};

其中postDelayed(this, 10);表示每隔10毫秒执行一次值变化,同时刷新曲线图;dot.update();表示更新该点的值。有意思的是表示坐标的dot类,他不仅仅包含了坐标值,还有目标值:

classDot{
intx;
inty;
intdata;
inttargetX;
inttargetY;
intlinenumber;
intvelocity=MyUtils.dip2px(getContext(),2);
Dot(intx,inty,inttargetX,inttargetY,Integer data,intlinenumber){
this.x= x;
this.y= y;
this.linenumber=linenumber;
setTargetData(targetX,targetY,data,linenumber);
}
Point getPoint(){
returnnewPoint(x,y);
}
Dot setTargetData(inttargetX,inttargetY,Integer data,intlinenumber){
this.targetX=targetX;
this.targetY=targetY;
this.data=data;
this.linenumber=linenumber;
returnthis;
}
booleanisAtRest(){
return(x==targetX)&&(y==targetY);
}
voidupdate(){
x=updateSelf(x,targetX,velocity);
y=updateSelf(y,targetY,velocity);
}
privateintupdateSelf(intorigin,inttarget,intvelocity){
if(origin < target) {
origin += velocity;
}elseif(origin > target){
origin-= velocity;
}
if(Math.abs(target-origin)
origin = target;
}
returnorigin;
}
}