项目开发中经常用到统计图表,网上也有很多的图表类库,比如 :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;
}