本来是想写一篇点赞效果的自定义View实例的,后来好基友因为项目紧急叫我帮他撸一个条形柱状图,大致瞄了一眼原型图以后便开撸了。最后做出来的效果与原型图八九不离十,看一下最后实现的效果图:
一个普通的条形柱状图,统计的是12个月份两种状态的数值,选中的长条背景颜色会加深,并且显示当前两种状态的数值。看看怎么实现的吧:
一.准备工作:
1.数据准备:
myChartView = (MyChartView) findViewById(R.id.my_chart_view);
relativeLayout = (RelativeLayout) findViewById(R.id.linearLayout);
Random random = new Random();
while (list.size() < 24) {
int randomInt = random.nextInt(100);
list.add(randomInt);
}
myChartView.setList(list);
这里生成了24个100以内的随机整数,所以第二次进入的时候,柱状图的高度数值会有所改变。
2.设置属性,计算宽高,进行必要的初始化:
public MyChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取我们自定义的样式属性
TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyChartView, defStyleAttr, 0);
int n = array.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.MyChartView_leftColor:
// 默认颜色设置为黑色
leftColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.MyChartView_rightColor:
rightColor = array.getColor(attr, Color.BLACK);
break;
case R.styleable.MyChartView_xyColor:
lineColor = array.getColor(attr, Color.BLACK);
break;
}
}
array.recycle();
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mBound = new Rect();
mChartPaint = new Paint();
mChartPaint.setAntiAlias(true);
mShadowPaint = new Paint();
mShadowPaint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width;
int height;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = widthSize * 1 / 2;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = heightSize * 1 / 2;
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mWidth = getWidth();
mHeight = getHeight();
mStartWidth = getWidth() / 13;
mSize = getWidth() / 39;
mChartWidth = getWidth() / 13 - mSize;
}
相信看过前几篇博客的小伙伴,对这些代码以及非常熟悉了,这也是自定义View前必不可少的准备工作。
二.绘制柱状图:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setColor(lineColor);
//画坐标轴
canvas.drawLine(0, mHeight - 100, mWidth, mHeight - 100, mPaint);
for (int i = 0; i < 12; i++) {
//画刻度线
canvas.drawLine(mStartWidth, mHeight - 100, mStartWidth, mHeight - 80, mPaint);
//画数字
mPaint.setTextSize(30);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.getTextBounds(String.valueOf(i + 1) + "月", 0, String.valueOf(i).length(), mBound);
canvas.drawText(String.valueOf(i + 1) + "月", mStartWidth - mBound.width() * 1 / 2, mHeight - 60 + mBound.height() * 1 / 2, mPaint);
mStartWidth += getWidth() / 13;
}
//画柱状图
for (int i = 0; i < list.size(); i++) {
int size = mHeight / 150;
//偶数
if (i % 2 == 0) {
mChartPaint.setColor(leftColor);
} else {
mChartPaint.setColor(rightColor);
}
mChartPaint.setStyle(Paint.Style.FILL);
//画阴影
if (i == number * 2 || i == number * 2 + 1) {
mShadowPaint.setColor(Color.LTGRAY);
} else {
mShadowPaint.setColor(Color.WHITE);
}
canvas.drawRect(mChartWidth, 0, mChartWidth + mSize, mHeight - 100, mShadowPaint);
//画柱状图
canvas.drawRect(mChartWidth, mHeight - 100 - list.get(i) * size, mChartWidth + mSize, mHeight - 100, mChartPaint);// 长方形
mChartWidth += (i % 2 == 0) ? (getWidth() / 39) : (getWidth() / 13 - mSize);
}
}
从上到下,循序渐进,这里分享一个小技巧:自定义View之前先在本子上把大致位置画出来,坐标在纸上算一遍,这样既不容易出错,也对自己写代码有帮助。实现不是很复杂,代码注释也比较清楚,就不详细分析了。
三.点击事件的实现:
示例图中可以看到,点击柱状图的不同位置,会加深点击区域的背景,并且显示当前数值,看看怎么实现的:
@Override
public boolean onTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
int left = 0;
int top = 0;
int right = mWidth / 12;
int bottom = mHeight - 100;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
for (int i = 0; i < 12; i++) {
rect = new Rect(left, top, right, bottom);
left += mWidth / 12;
right += mWidth / 12;
if (rect.contains(x, y)) {
listener.getNumber(i, x, y);
number = i;
invalidate();
}
}
break;
}
return true;
}
public interface getNumberListener {
void getNumber(int number, int x, int y);
}
通过计算得到点击的区域代表的值,调用invalidate()方法进行重新绘制,加深背景颜色。
示例图中显示的数值会以一个小方块的形式显示出来,其实是通过接口回调得到点击区域代表的值,以及点击的x,y坐标。然后怎样在Activity中使用呢:
myChartView.setListener(new MyChartView.getNumberListener() {
@Override
public void getNumber(int number, int x, int y) {
relativeLayout.removeView(showText);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
params.leftMargin = x - 100;
if (x - 100 < 0) {
params.leftMargin = 0;
} else if (x - 100 > relativeLayout.getWidth() - showText.getWidth()) {
params.leftMargin = relativeLayout.getWidth() - showText.getWidth();
}
params.topMargin = 100;
showText.setLayoutParams(params);
showText.setTextColor(getResources().getColor(R.color.white));
showText.setTextSize(10);
showText.setText("选择的数字为:" + list.get(number * 2) + "," + list.get(number * 2 + 1));
showText.setBackground(getResources().getDrawable(R.drawable.flag_01));
relativeLayout.addView(showText);
}
});
实现接口,每次将这个显示的小方块动态添加到布局中去,显示点击的数值,加上x坐标左右的限制,防止出界。
四.注意事项:
之前几篇自定义View,有小伙伴和我反应,把应用切换至手机后台时,再打开应用,发现自定义的View会消失,这是因为Activity的生命周期影响了自定义View的生命周期。
可参考我之前一篇博客:Android自定义View探索(一)—生命周期
解决的办法有很多,推荐两个常用的:
1.在Activity中重写onResume()方法,设置自定义View需要的初始值
2.在自定义View方法中重写onWindowVisibilityChanged()方法,设置初始值
这篇博客我采用的是第二种方法:
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == VISIBLE) {
mSize = getWidth() / 39;
mStartWidth = getWidth() / 13;
mChartWidth = getWidth() / 13 - mSize - 3;
}
}
这样只要你不退出当前界面,将应用切换至后台,过一段时间再次进入时,自定义View还是会保持最初的样子,不会消失的。
源码地址:
欢迎star,fork,提issues,一起进步!
希望能对你有所帮助,下一篇再见~~~