手擼一個Android柱形圖和線型圖的組合圖表
專案開發中經常用到統計圖表,網上也有很多的圖表類庫,比如 :MPAndroidChart,XCL-chart,hellochart,AChartEngine等等,以前我最常用的就是MPAndroidChart,這個庫做的非常細緻用起來也簡單。
但是用別人的東西好處就是快方便,壞處就是不好維護了,而且它們也只是實現了一些主流的效果,當我們面對產品經理天馬行空的想法的時候,總有一些效果是這些庫無法實現的。所以掌握Android中的一些繪製的基本技能是非常重要的。因為當需求來臨的時候,只能自己擼啦。
前幾天有個需求,要求柱形圖和線型圖組合,柱形圖的資料依賴左邊Y軸,還得分成3段,線型圖有3條,右邊的Y軸分成3份分別對應3條線的資料。好吧,找不到輪子只能自己造了。
先上個圖:
首先在onSizeChanged()方法中初始化寬高。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
screenWidth = getMeasuredWidth();
screenHeight = getMeasuredHeight();
//設定矩形的頂部 底部 右邊Y軸的3部分每部分的高度
getStatusHeight();
leftWhiteRect = new Rect(0, 0, 0 , screenHeight);
rightWhiteRect = new Rect(screenWidth - leftMargin * 2 - 10, 0, screenWidth, screenHeight);
topWhiteRect = new Rect(0,0,screenWidth,topMargin/2);
bottomWhiteRect = new Rect(0, (int) yStartIndex,screenWidth,screenHeight);
super.onSizeChanged(w, h, oldw, oldh);
}
然後就是在onDraw()中繪製,先繪製柱形圖,因為線型圖的X座標在柱形圖的中間,在畫線型圖的路徑就好畫多了。每個柱形圖有3個部分,從最下面的開始繪製矩形,第二個的底部座標就是第一個的頂部,以此類推。其實onDraw()中其實就是一些對座標的計算,只要計算對了 使用android提供的繪製的api繪製就可以了。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
leftPoints.clear();
rightPoints.clear();
canvas.drawColor(BG_COLOR);
if (winds == null || mBarData == null || humidity == null || temperature == null) return;
//重置3條線
linePathW.reset();
linePathW.incReserve(winds.size());
linePathH.reset();
linePathH.incReserve(winds.size());
linePathT.reset();
linePathT.incReserve(winds.size());
checkTheLeftMoving();
textPaint.setTextSize(DensityUtil.dip2px(getContext(), 10));
barPaint.setColor(Color.WHITE);
canvas.drawRect(bottomWhiteRect, barPaint);
canvas.drawRect(topWhiteRect, barPaint);
//畫矩形
drawBars(canvas);
canvas.save();
//畫線型圖
canvas.drawPath(linePathW, linePaint);
canvas.drawPath(linePathH, linePaint);
canvas.drawPath(linePathT, linePaint);
//畫線上的點
drawCircles(canvas);
// linePath.rewind();
//畫X軸 下面的和上面的
canvas.drawLine(xStartIndex, yStartIndex, screenWidth - leftMargin, yStartIndex, axisPaint);
canvas.drawLine(xStartIndex, topMargin / 2, screenWidth - leftMargin, topMargin / 2, axisPaint);
//畫左邊和右邊的遮罩層
int c = barPaint.getColor();
leftWhiteRect.right = (int) xStartIndex;
barPaint.setColor(Color.WHITE);
canvas.drawRect(leftWhiteRect, barPaint);
canvas.drawRect(rightWhiteRect, barPaint);
barPaint.setColor(c);
//畫左邊的Y軸
canvas.drawLine(xStartIndex, yStartIndex, xStartIndex, topMargin / 2, axisPaint);
//畫左邊的Y軸text
drawLeftYAxis(canvas);
//左邊Y軸的單位
canvas.drawText(leftAxisUnit, xStartIndex - textPaint.measureText(leftAxisUnit) - 5, topMargin/2, textPaint);
//畫右邊的Y軸
canvas.drawLine(screenWidth - leftMargin * 2 - 10, yStartIndex, screenWidth - leftMargin * 2 - 10, topMargin / 2, axisPaint);
//畫右邊Y軸text
drawRightYText(canvas);
}
然後是當我們的資料太多繪製超出了view的寬度之後,可以通過手勢滑動來看到後面的資料。這部分可以通過Scroller來滑動通過VelocityTracker和Scroller來處理手擡起後再滑動一段距離停下
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastPointX = event.getX();
scroller.abortAnimation();//如果在滑動終止動畫
initOrResetVelocityTracker();//初始化速度跟蹤器
break;
case MotionEvent.ACTION_MOVE:
float movex = event.getX();
movingThisTime = lastPointX - movex;
leftMoving = leftMoving + movingThisTime;
lastPointX = movex;
invalidate();
velocityTracker.addMovement(event);//將使用者的action新增到跟蹤器中。
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000, maxVelocity);//根據已經到達的點計算當前速度。
int initialVelocity = (int) velocityTracker.getXVelocity();//獲得最後的速度
velocityTracker.clear();
//通過scroller讓它飛起來
scroller.fling((int) event.getX(), (int) event.getY(), -initialVelocity / 2,
0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
invalidate();
lastPointX = event.getX();
recycleVelocityTracker();//回收速度跟蹤器
break;
default:
return super.onTouchEvent(event);
}
if (mGestureListener != null) {
mGestureListener.onTouchEvent(event);
}
return true;
}
然後是新增動畫,使用屬性動畫
private float percent = 1f;
private TimeInterpolator pointInterpolator = new DecelerateInterpolator();
public void startAnimation(int duration){
ValueAnimator mAnimator = ValueAnimator.ofFloat(0,1);
mAnimator.setDuration(duration);
mAnimator.setInterpolator(pointInterpolator);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
percent = (float) animation.getAnimatedValue();
invalidate();
}
});
mAnimator.start();
}
上面是一個0-1的屬性動畫來改變percent 的值,所以percent 就是從0慢慢增長到1的值,我們繪製的時候對於Y座標的處理就可以用Y座標的值乘以percent 。這樣Y值就是從0到它本來的值慢慢增長完成動畫。
最後是給每個柱形圖新增點選事件。通過GestureDetector中的onGestureListener中的onSingleTapUp實現,關鍵是計算我們點選的座標的範圍是不是在一個柱形圖的範圍內。demo中只計算了X軸的座標。原理,當我們繪製柱形圖的時候,每繪製一個,將講它的x座標的左邊的座標和右邊的座標存起來。迴圈判斷我們點選的位置是不是在左邊和右邊的座標之間如果是就返回當前的i,反之就是點選的無效的位置。
private int identifyWhichItemClick(float x, float y) {
float leftx = 0;
float rightx = 0;
for (int i = 0; i < mBarData.size(); i++) {
leftx = leftPoints.get(i);
rightx = rightPoints.get(i);
if (x < leftx) {
break;
}
if (leftx <= x && x <= rightx) {
return i;
}
}
return INVALID_POSITION;
}