詳解android繪制動畫效果的曲線圖的實現
安卓繪制統計圖可以用androidchart,也可以自己繪制,不像ios,android能找到的開源庫在UI方面都很差,要做出吸引人地方還是需要自己繪制。
本文給出最常用的曲線圖的繪制方法。
繪制曲線圖首先需要畫好橫豎坐標軸建立坐標系,比如坐標系中的100距離應該在canvas中繪制多長,這個是需要計算的,其實坐標體系的建立是最復雜的,我看過很多第三方庫的建立方法都不一樣,有的要靈活一些,有的比較死板。至于繪制曲線要么是用Canvas.drawLine方法,要么是用Path.lineTo方法,看你自己的習慣。
為了做出一個外觀良好的曲線圖,我參考了兩個開源代碼,第一個的曲線圖繪制限制較多,使用范圍太窄,但是有數據變化時的動畫效果。第二個的適用范圍很廣,他能根據數據集合自動計算橫縱坐標的個數,在canvas上單元格的距離,只需輸入坐標點就能自動建立坐標體系繪制曲線,但是沒有動畫效果。
先講第一個LineView。
LineView的demo可以在這里下載,lineview其實只是github項目的一部分,我是將其提取出來了的,個人覺得他的其他部分沒有參考價值。作者好像是個韓國人。
LineView的曲線繪制沒有什么可取的部分,我想學習的是他實現動畫效果的方法,設計的很好,但具體實現還需要改進,讓動畫更流暢。
Lineview的調用方法:
在xml中添加lineview控件
<HorizontalScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/horizontalScrollView"
android:layout_alignParentRight="true"
android:layout_above="@+id/line_button">
<view
android:layout_width="wrap_content"
android:layout_height="200dp"
class="com.example.widget.LineView"
android:id="@+id/line_view"/>
</HorizontalScrollView>
在activity代碼中獲取lineview對象:
finalLineView lineView = (LineView)findViewById(R.id.line_view);
添加橫坐標:
int randomint = 9;
ArrayList<String>test =newArrayList<String>();
for (int i=0;i<randomint; i++){
test.add(String.valueOf(i+1));
}
lineView.setBottomTextList(test);
允許繪制坐標點:
lineView.setDrawDotLine(true);
lineView.setShowPopup(LineView.SHOW_POPUPS_NONE);
ArrayList<Integer> dataList = newArrayList<Integer>();
intrandom = (int)(Math.random()*9+1);
for (int i=0;i<randomint; i++){
dataList.add((int)(Math.random()*random));
}
添加縱坐標的值:
ArrayList<ArrayList<Integer>>dataLists = newArrayList<ArrayList<Integer>>();
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(int widthMeasureSpec, 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<Dot> 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類,他不僅僅包含了坐標值,還有目標值:
class Dot{
intx;
inty;
intdata;
inttargetX;
inttargetY;
intlinenumber;
intvelocity =MyUtils.dip2px(getContext(),2);
Dot(int x,int y,inttargetX,int targetY,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,int targetY,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(int origin, inttarget, int velocity){
if(origin < target) {
origin += velocity;
} elseif(origin > target){
origin-= velocity;
}
if(Math.abs(target-origin)<velocity){
origin = target;
}
returnorigin;
}
}