效果:
跟上一篇手撸一个线形图表 做法基本一样,柱形图主要是用到canvas的绘制矩形的api 。剩下的就是坐标的计算了。
(1)
按照自定义View的步骤,先重写他的几个构造方法。在构造方法中初始化我们需要用到的一些变量,画笔,path路径,间距,绘制柱形图的Rect等。
(2)
在onSizeChanged方法中初始化控件的宽和高,绘图区域的高度等尺寸值
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mTotalWidth = getMeasuredWidth();
mTotalHeight = getMeasuredHeight();
setNeedHeight();
leftWhiteRect = new Rect(0, 0, 0, mTotalHeight);
rightWhiteRect = new Rect(mTotalWidth - leftMargin * 2, 0, mTotalWidth, mTotalHeight);
topWhiteRect = new Rect(0, 0, mTotalWidth, topMargin / 2);
bottomWhiteRect = new Rect(0, (int) yStartIndex, mTotalWidth, mTotalHeight);
super.onSizeChanged(w, h, oldw, oldh);
}
(3)
在onDraw里面进行绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(BG_COLOR);
if (mData == null) return;
//得到每个bar的宽度
getItemsWidth();
checkTheLeftMoving();
canvas.drawRect(bottomWhiteRect, bgPaint);
canvas.drawRect(topWhiteRect, bgPaint);
//画中间的白线
drawWhiteLine(canvas);
//绘制矩形柱子
drawBars(canvas);
//画左边和右边的遮罩层
leftWhiteRect.right = (int) xStartIndex;
canvas.drawRect(leftWhiteRect, bgPaint);
canvas.drawRect(rightWhiteRect, bgPaint);
//画左边的Y轴
canvas.drawLine(xStartIndex, yStartIndex, xStartIndex, topMargin / 2, axisPaint);
//左边Y轴的单位
canvas.drawText(leftAxisUnit, xStartIndex, topMargin / 2 - 14, textPaint);
//画右边的Y轴
canvas.drawLine(mTotalWidth - leftMargin * 2, yStartIndex, mTotalWidth - leftMargin * 2, topMargin / 2, axisPaint);
//画左边的Y轴text
drawLeftYAxis(canvas);
//画X轴 下面的和上面
canvas.drawLine(xStartIndex, yStartIndex, mTotalWidth - leftMargin * 2, yStartIndex, axisPaint);
canvas.drawLine(xStartIndex, topMargin / 2, mTotalWidth - leftMargin * 2, topMargin / 2, axisPaint);
//画X轴的text
drawXAxisText(canvas);
}
(4)
当数据很多的时候 我们希望可以通过拖拽来滑动视图,这就需要我们在绘制的时候先加上一个变量leftMoving,在onTouchEvent中改变leftMoving的值然后invalidate();重新绘制达到拖动的效果。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastPointX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
float movex = event.getRawX();
movingThisTime = lastPointX - movex;
leftMoving = leftMoving + movingThisTime;
lastPointX = movex;
invalidate();
break;
case MotionEvent.ACTION_UP:
new Thread(new SmoothScrollThread(movingThisTime)).start();
break;
default:
return super.onTouchEvent(event);
}
if (mGestureListener != null) {
mGestureListener.onTouchEvent(event);
}
return true;
}
(5)
当手指停下的时候 动画不立即停止,先滑动一会慢慢的停止。这样体验比较好。 做法就是起一个线程动态改变leftMoving的值刷新界面
/**
* 左右滑动的时候 当手指抬起的时候 使滑动慢慢停止 不会立刻停止
*/
private class SmoothScrollThread implements Runnable {
float lastMoving;
boolean scrolling = true;
private SmoothScrollThread(float lastMoving) {
this.lastMoving = lastMoving;
scrolling = true;
}
@Override
public void run() {
while (scrolling) {
long start = System.currentTimeMillis();
lastMoving = (int) (0.9f * lastMoving);
leftMoving += lastMoving;
checkTheLeftMoving();
postInvalidate();
if (Math.abs(lastMoving) < 5) {
scrolling = false;
}
long end = System.currentTimeMillis();
if (end - start < 20) {
try {
Thread.sleep(20 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
(6)
柱形图经常会用到点击每个柱子来展示一些详情,作为给它加上点击效果,重要用到了GestureDetector 手势监听 onSingleTapUp()就是监听我们的手指点击一下的方法。在该方法中判断我们点击的是不是这个柱子。
判断方法就是,我们绘制柱子的时候,将每个柱子的左边和右边的X轴的坐标存起来,遍历数据,判断我们点击的地方的坐标是不是在其左边和右边的坐标之间。是的话返回当前list的position。否则就是点击的无效位置。
demo只是判断了X轴的坐标 所以只要X轴在范围内 Y轴不再范围内页会触发点击。如果要求严格的话,可以自行加上Y轴的判断。方法一样。
/**
* 手势监听器
*
* @author A Shuai
*/
private class RangeBarOnGestureListener implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
int position = identifyWhichItemClick(e.getX(), e.getY());
if (position != INVALID_POSITION && mOnItemBarClickListener != null) {
mOnItemBarClickListener.onClick(position);
setClicked(position);
invalidate();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
(6)滑动优化
上面的第五步,当手指抬起的时候视图继续滑动一段距离。这种fling效果是通过一个子线程动态减少滑动的值然后更新视图实现的。效果虽然实现了,但是不够流畅,其实安卓已经提供了速度相关的api供我们使用。体验也会更好。
速度相关:VelocityTracker
滑动相关:Scroller
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastPointX = event.getX();
scroller.abortAnimation();//如果在滑动终止动画
initOrResetVelocityTracker();//初始化速度跟踪器
velocityTracker.addMovement(event);//将用户的action添加到跟踪器中。
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_UP:
// new Thread(new SmoothScrollThread(movingThisTime)).start();
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();
break;
case MotionEvent.ACTION_CANCEL:
recycleVelocityTracker();//回收速度跟踪器
break;
default:
return super.onTouchEvent(event);
}
if (mGestureListener != null) {
mGestureListener.onTouchEvent(event);
}
return true;
}
使用Scroller需要重写计算滑动的方法
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
movingThisTime = (scroller.getCurrX() - lastPointX);
leftMoving = leftMoving + movingThisTime;
lastPointX = scroller.getCurrX();
postInvalidate();
}
}
OK这样在滑动的时候就会感觉更流畅了