继续练习自定义View,这次带来的是简易折线图,支持坐标点点击监听,效果如下:
画坐标轴、画刻度、画点、连线。。x、y轴的数据范围是写死的 1 <= x <= 7 ,1 <= y <= 70 。。写活的话涉及到坐标轴刻度的动态计算、坐标点的坐标修改,想想就头大,这里只练习自定义View。
1、在res/values文件夹下新建attrs.xml文件,编写自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LineChartView">
<attr name="textColor" format="color" />
<attr name="lineColor" format="color" />
<attr name="pointColor" format="color" />
</declare-styleable>
</resources>
2、新建LineChartView继承View,重写构造方法:
public LineChartView(Context context) {
this(context, null);
}
public LineChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LineChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
3、在第三个构造方法中获取自定义属性的值:
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LineChartView, defStyleAttr, 0);
mTextColor = ta.getColor(R.styleable.LineChartView_textColor, 0xff381a59);
mLineColor = ta.getColor(R.styleable.LineChartView_lineColor, 0xff8e29fa);
mPointColor = ta.getColor(R.styleable.LineChartView_pointColor, 0xffff5100);
mPointRadius = DensityUtils.dp2px(context, 3);
ta.recycle();
4、创建画图所使用的对象,如Paint、Path:
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(40);
mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mLinePaint.setStyle(Paint.Style.STROKE);
mLinePaint.setColor(mLineColor);
mLinePaint.setStrokeWidth(DensityUtils.dp2px(context, 2));
mLinePaint.setStrokeCap(Paint.Cap.ROUND);
mXyPath = new Path();
mPointPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPointPaint.setStyle(Paint.Style.FILL);
mPointPaint.setColor(mPointColor);
mPointCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPointCirclePaint.setStyle(Paint.Style.STROKE);
mPointCirclePaint.setStrokeWidth(DensityUtils.dp2px(context, 2));
mPointCirclePaint.setColor(mLineColor);
5、重写onMeasure()方法,计算自定义View的宽高:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measuredDimension(widthMeasureSpec), measuredDimension(heightMeasureSpec));
}
private int measuredDimension(int measureSpec) {
int result;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
if (mode == MeasureSpec.EXACTLY) {
result = size;
} else {
result = 500;
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
}
return result;
}
6、暴露一个设置x、y数据集合的方法:
public void setDataList(List<Integer> xList, List<Integer> yList) {
if (xList == null || yList == null || xList.size() == 0 || yList.size() == 0) {
throw new IllegalArgumentException("没有数据");
}
if (xList.size() != yList.size()) {
throw new IllegalArgumentException("x、y轴数据长度不一致");
}
setPointData(xList, yList);
setPointAnimator();
}
private void setPointData(List<Integer> xList, List<Integer> yList) {
mPointList = new ArrayList<>();
for (int i = 0; i < xList.size(); i++) {
ChartPoint point = new ChartPoint();
//设置坐标点的xy数据
point.setxData(xList.get(i));
point.setyData(yList.get(i));
//计算坐标点的横纵坐标
point.setX(xyMargin + xList.get(i) * (getWidth() - 2 * xyMargin) / maxX);
point.setY(getHeight() - xyMargin - (getHeight() - 2 * xyMargin) * yList.get(i) / maxY);
mPointList.add(point);
}
}
private void setPointAnimator() {
for (int i = 0; i < mPointList.size(); i++) {
final ChartPoint point = mPointList.get(i);
ValueAnimator anim;
if (mLastPointList != null && mLastPointList.size() > 0) {
anim = ValueAnimator.ofInt(mLastPointList.get(i).getY(), point.getY());
} else {
anim = ValueAnimator.ofInt(getHeight() - xyMargin, point.getY());
}
anim.setDuration(500);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
point.setY(value);
invalidate();
}
});
anim.start();
}
//储存坐标点集合
mLastPointList = mPointList;
}
7、重写onDraw()方法,绘制坐标轴、刻度,画点连线,注意坐标的计算:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mPointList == null || mPointList.size() == 0) {
return;
}
mXyPath.reset();
mXyPath.moveTo(xyMargin, 0);
mXyPath.lineTo(xyMargin, getHeight() - xyMargin);
mXyPath.lineTo(getWidth(), getHeight() - xyMargin);
canvas.drawPath(mXyPath, mLinePaint);//画x、y坐标轴
for (int i = 0; i < mPointList.size(); i++) {
//画x轴刻度线
int x = xyMargin + (i + 1) * (getWidth() - 2 * xyMargin) / mPointList.size();
canvas.drawLine(x, getHeight() - xyMargin - graduatedLineLength, x, getHeight() - xyMargin, mLinePaint);
//画y轴刻度线
int y = getHeight() - xyMargin - (i + 1) * (getHeight() - 2 * xyMargin) / mPointList.size();
canvas.drawLine(xyMargin, y, xyMargin + graduatedLineLength, y, mLinePaint);
//画坐标轴刻度文本
canvas.drawText(String.valueOf(mPointList.get(i).getxData()), x, getHeight() - mTextPaint.getTextSize() / 4, mTextPaint);
canvas.drawText(String.valueOf((i + 1) * 10), 0, y + mTextPaint.getTextSize() / 2, mTextPaint);
}
//画连接线
for (int i = 0; i < mPointList.size(); i++) {
if (i != mPointList.size() - 1) {
ChartPoint lastP = mPointList.get(i);
ChartPoint nextP = mPointList.get(i + 1);
canvas.drawLine(lastP.getX(), lastP.getY(), nextP.getX(), nextP.getY(), mLinePaint);
}
}
//画坐标点
for (int i = 0; i < mPointList.size(); i++) {
ChartPoint point = mPointList.get(i);
canvas.drawCircle(point.getX(), point.getY(), mPointRadius, mPointPaint);
canvas.drawCircle(point.getX(), point.getY(), mPointRadius, mPointCirclePaint);
}
}
8、设置坐标点点击事件:
private OnPointClickListener mOnPointClickListener;
public interface OnPointClickListener {
void onPointClick(int index, ChartPoint point);
}
public void setOnPointClickListener(OnPointClickListener onPointClickListener) {
mOnPointClickListener = onPointClickListener;
}
9、重写onTouchEvent()方法,判断当前点击的点是不是在坐标点范围内:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//判断当前点击的点是否在坐标点范围内
int curX = (int) event.getX();
int curY = (int) event.getY();
for (int i = 0; i < mPointList.size(); i++) {
ChartPoint point = mPointList.get(i);
double d1 = Math.pow(curX - point.getX(), 2);
double d2 = Math.pow(curY - point.getY(), 2);
//√ ̄(curX - cx)² + (curY - cy)² < R
if (Math.sqrt(d1 + d2) < mPointRadius + 10) {//为了方便点击,把坐标点范围增大了10像素
if (mOnPointClickListener != null) {
mOnPointClickListener.onPointClick(i, point);
}
}
}
break;
}
return super.onTouchEvent(event);
}
10、在activity_main.xml布局文件中使用该View:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:lcv="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<com.monkey.linechartview.LineChartView
android:id="@+id/chartView"
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_marginTop="@dimen/activity_vertical_margin"
lcv:lineColor="#8e29fa"
lcv:pointColor="#ff5100"
lcv:textColor="#000000" />
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:text="set data"
android:textAllCaps="false" />
</LinearLayout>
11、在MainActivity.java中传入数据集合,并设置坐标点点击监听:
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
List<Integer> xList = new ArrayList<>();
List<Integer> yList = new ArrayList<>();
for (int i = 0; i < 7; i++) {
xList.add(i + 1);
int y = (int) (Math.random() * 70 + 1);
yList.add(y);
}
chartView.setDataList(xList, yList);
}
});
chartView.setOnPointClickListener(new LineChartView.OnPointClickListener() {
@Override
public void onPointClick(int position, ChartPoint point) {
tv.setText("position:" + position + "\nx:" + point.getxData() + "\ny:" + point.getyData());
}
});
致此大致步骤完成了,发现和上一篇步骤差不多。。代码已上传github:
https://github.com/MonkeyMushroom/LineChartView/tree/master