先看效果
项目开发中,被安排去调研实现 坐标曲线图,网上第三方的库很多,可以实现,但是有些样式无法做到符合自己要求,Android 与iOS效果上也存在差异,所以自己自定义了一个;
其实比较简单,就是画点,画线,画虚线,画曲线,添加点击事件即可;这里面需要涉及到的知识点主要是有:对自定义View有一点基础,比如onMeasure()、onLayout()、onDraw();至少得了解这三个方法;
另外就是需要会用画笔Paint、点Point、路径Path等,至少会使用这三个API,那基本就没有问题了,画点、点与点连接成线、然后闭合起来就是一个多边形、再给多边形填充颜色即可;
另外横坐标纵坐标,以及点的数据,都是外部传入,具体情况具体考虑;
先看使用方法:
在布局中添加
<com.test.jsontouijson.weight.LineGraphicView
android:id="@+id/lineGraphicView"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
在activity或fragment中初始化,添加数据
lineGraphicView = findViewById(R.id.lineGraphicView);
lineGraphicView.setData(pointData);
//添加点击事件
lineGraphicView.setListener((x, y) -> initPopupWindow(lineGraphicView, x, y));
点击后的弹框是外部实现的,这个自行使用popupWindow去实现,点击已经有返回点的坐标x、y了,弹框的显示位置可通过这个坐标点定位。
大概就是这些,下面是LineGraphicView具体代码,代码有比较详细的备注形式
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LineGraphicView extends View {
private static final int CIRCLE_SIZE = 40;
private static enum LineStyle {LINE, CURVE}
private static enum YLineStyle {DASHES_LINE, FULL_LINE}
private static enum ShaderOrientationStyle {ORIENTATION_H, ORIENTATION_V}
private Context mContext;
private Resources res;
private DisplayMetrics dm;
private OnClickListener listener;
private LineStyle mStyle = LineStyle.LINE;
private YLineStyle mYLineStyle = YLineStyle.DASHES_LINE;
private ShaderOrientationStyle mShaderOrientationStyle = ShaderOrientationStyle.ORIENTATION_V;
private int canvasHeight;
private int canvasWidth;
private int bHeight = 0;
private int bWidth = 0;
private int marginLeft;
private boolean isMeasure = true;
private boolean isShowFirstXContent = false;
private int xTextWidth = 0;//Y轴内容宽度
private int spacingHeight;
private double averageValue;
private int marginTop = 0;
private int marginBottom = 0;
/**
* data
*/
private Point[] mPoints;//点
private List<String> yRawDatas;//y轴数据
private PointData pointData;//外部传入数据
private List<String> xRawDatas;//x轴数据
private List<Double> dataList = new ArrayList<>();//点的数据
private List<Integer> xList = new ArrayList<>();// 记录每个x的值
private Map<String, Integer> xMap = new HashMap<>();//用于保存 点-X坐标 对应起来
/**
* paint color
*/
private int xTextPaintColor;
private int yTextPaintColor;
private int startShaderColor;
private int endShaderColor;
private int mCanvasColor;
/**
* paint size
*/
private int xTextSize = 12;
private int yTextSize = 12;
private Point mSelPoint;
public LineGraphicView(Context context) {
this(context, null);
}
public LineGraphicView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
initView();
}
private void initView() {
this.res = mContext.getResources();
xTextPaintColor = res.getColor(R.color.black);
yTextPaintColor = res.getColor(R.color.black);
startShaderColor = res.getColor(R.color.colorYellow);
endShaderColor = res.getColor(R.color.colorYellow1);
mCanvasColor = res.getColor(R.color.colorGray1);
dm = new DisplayMetrics();
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(dm);
}
public void setData(PointData pointData) {
this.pointData = pointData;
this.averageValue = pointData.getyAverageValue();
this.xRawDatas = pointData.getxAxis();
this.yRawDatas = pointData.getyAxis();
for (int i = 0; i < pointData.getPointInfo().size(); i++) {
dataList.add(pointData.getPointInfo().get(i).getPrice());
}
if (null != dataList) {
this.mPoints = new Point[dataList.size()];
}
if (null != yRawDatas) {
this.spacingHeight = yRawDatas.size();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (isMeasure) {
marginLeft = dip2px(30);
marginTop = dip2px(30);
marginBottom = dip2px(80);
this.canvasHeight = getHeight();//获取画布高度
this.canvasWidth = getWidth();//获取画布宽度
if (bHeight == 0) {
bHeight = getHeight() - marginBottom;//实际坐标图高度
}
if (bWidth == 0) {
bWidth = canvasWidth - marginLeft;//实际坐标图宽度
}
isMeasure = false;
}
}
@Override
protected void onDraw(Canvas canvas) {
//draw X line
drawAllXLine(canvas);
if (YLineStyle.DASHES_LINE == mYLineStyle) {
drawPathYDashesLine(canvas);//draw Y dashes line
} else {
drawAllYLine(canvas);// draw Y ine)
}
// point init
mPoints = getPoints();
//draw cure line
drawCurve(canvas);
//draw Polygon bg color
drawPolygonBgColor(canvas);
// is click point
if (null == mSelPoint) {
drawDot(canvas);// draw dot
} else {
clickUpdateDot(canvas);// update dot after click
}
canvas.drawColor(mCanvasColor);//canvas color
}
private void drawCurve(Canvas c) {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(res.getColor(R.color.colorPrimaryDark));
p.setStrokeWidth(dip2px(0.5f));
p.setStyle(Paint.Style.STROKE);
if (mStyle == LineStyle.CURVE) {
drawScrollLine(c, p);
} else {
drawLine(c, p);
}
}
//画点
private void drawDot(Canvas c) {
if (null == mPoints || mPoints.length == 0) {
return;
}
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Paint.Style.FILL);
for (Point point : mPoints) {
p.setColor(res.getColor(R.color.colorYellow2));
c.drawCircle(point.x, point.y, CIRCLE_SIZE / 2, p);
p.setColor(res.getColor(R.color.colorYellow3));
c.drawCircle(point.x, point.y, CIRCLE_SIZE / 3, p);
}
}
//更新点
private void clickUpdateDot(Canvas c) {
if (null == mPoints || mPoints.length == 0) {
return;
}
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Paint.Style.FILL);
for (Point point : mPoints) {
if (null != mSelPoint && mSelPoint.x == point.x && mSelPoint.y == point.y) {
p.setColor(res.getColor(R.color.colorPrimary));
c.drawCircle(point.x, point.y, CIRCLE_SIZE, p);
p.setColor(res.getColor(R.color.colorYellow3));
c.drawCircle(point.x, point.y, (float) (CIRCLE_SIZE / 1.5), p);
} else {
p.setColor(res.getColor(R.color.colorYellow2));
c.drawCircle(point.x, point.y, CIRCLE_SIZE / 2, p);
p.setColor(res.getColor(R.color.colorYellow3));
c.drawCircle(point.x, point.y, CIRCLE_SIZE / 3, p);
}
}
}
//填充曲线闭合起来的图
private void drawPolygonBgColor(Canvas c) {
if (null == mPoints || mPoints.length == 0) {
return;
}
Path p = new Path();
float startX = 0;
float endX = 0;
int endPoint = mPoints.length - 1;
for (int i = 0; i < mPoints.length; i++) {
if (i == 0) {
startX = mPoints[i].x;
p.moveTo(mPoints[i].x, 0);
p.lineTo(mPoints[i].x, mPoints[i].y);
} else {
p.lineTo(mPoints[i].x, mPoints[i].y);
if (i == endPoint) {
endX = mPoints[i].x;
}
}
}
p.lineTo(endX, bHeight + marginTop);
p.lineTo(startX, bHeight + marginTop);
p.close();
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
Shader shader = null;
if (mShaderOrientationStyle == ShaderOrientationStyle.ORIENTATION_H) {
shader = new LinearGradient(endX, bHeight + marginTop, startX, bHeight + marginTop,
startShaderColor, endShaderColor, Shader.TileMode.REPEAT);
} else {
Point point = getYBiggestPoint();
if (null != point) {
shader = new LinearGradient(point.x, point.y, endX, bHeight + marginTop,
startShaderColor, endShaderColor, Shader.TileMode.REPEAT);
}
}
paint.setShader(shader);
c.drawPath(p, paint);
}
//获取Y坐标最高的点
private Point getYBiggestPoint() {
Point p = null;
if (null != mPoints && mPoints.length > 0) {
p = mPoints[0];
for (int i = 0; i < mPoints.length - 1; i++) {
if (p.y > mPoints[i + 1].y) {
p = mPoints[i + 1];
}
}
}
return p;
}
//画Y方向虚线
private void drawPathYDashesLine(Canvas canvas) {
if (null == xRawDatas || xRawDatas.size() == 0) {
return;
}
Path path = new Path();
int dashLength = 16;
int blankLength = 16;
Paint p = new Paint();
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(4);
p.setColor(res.getColor(R.color.colorGray));
p.setPathEffect(new DashPathEffect(new float[]{dashLength, blankLength}, 0));
if (!isShowFirstXContent && null == mSelPoint) {
xRawDatas.add(0, "");
}
for (int i = 0; i < xRawDatas.size(); i++) {
drawTextY(xRawDatas.get(i), (getMarginWidth() + getBWidth() / xRawDatas.size() * i) - dip2px(8), bHeight + marginTop + dip2px(26),
canvas);
if (null != xMap) {
xMap.put(xRawDatas.get(i), getMarginWidth() + getBWidth() / xRawDatas.size() * i);
}
float startX = getMarginWidth() + getBWidth() / xRawDatas.size() * i;
float startY = marginTop;
float endY = bHeight + marginTop;
path.moveTo(startX, startY);
path.lineTo(startX, endY);
canvas.drawPath(path, p);
}
getPointX();
}
/**
* 画所有Y方向实线
*/
private void drawAllYLine(Canvas canvas) {
if (null == xRawDatas || xRawDatas.size() == 0) {
return;
}
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(res.getColor(R.color.colorBlack));
for (int i = 0; i < xRawDatas.size(); i++) {
canvas.drawLine(getMarginWidth() + getBWidth() / xRawDatas.size() * i, marginTop, getMarginWidth()
+ getBWidth() / xRawDatas.size() * i, bHeight + marginTop, p);
drawTextY(xRawDatas.get(i), getMarginWidth() + getBWidth() / xRawDatas.size() * i - dip2px(8), bHeight + marginTop + dip2px(26),
canvas);
if (null != xMap) {
xMap.put(xRawDatas.get(i), getMarginWidth() + getBWidth() / xRawDatas.size() * i);
}
}
getPointX();
}
//获取点的X坐标
private void getPointX() {
if (null == xMap || xMap.size() == 0) {
return;
}
if (null != pointData && pointData.getPointInfo().size() > 0) {
for (PointData.PointInfo info : pointData.getPointInfo()) {
for (String key : xMap.keySet()) {
if (key.equals(info.getMouth())) {
xList.add(xMap.get(key));
}
}
}
}
}
/**
* 画所有X方向的线
*/
private void drawAllXLine(Canvas canvas) {
if (null == yRawDatas || yRawDatas.size() == 0) {
return;
}
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(res.getColor(R.color.colorBlack));
p.setStyle(Paint.Style.FILL);
for (int i = 0; i < yRawDatas.size(); i++) {
drawTextX(yRawDatas.get(i), marginLeft / 2,
bHeight - (bHeight / spacingHeight) * i + marginTop + dip2px(2), canvas);
canvas.drawLine(getMarginWidth(), bHeight - (bHeight / spacingHeight) * i + marginTop, canvasWidth,
bHeight - (bHeight / spacingHeight) * i + marginTop, p);// Y坐标
}
}
//画圆滑的曲线
private void drawScrollLine(Canvas canvas, Paint paint) {
if (null == mPoints || mPoints.length == 0) {
return;
}
Point startP;
Point endP;
for (int i = 0; i < mPoints.length - 1; i++) {
startP = mPoints[i];
endP = mPoints[i + 1];
int wt = (startP.x + endP.x) / 2;
Point p3 = new Point();
Point p4 = new Point();
p3.y = startP.y;
p3.x = wt;
p4.y = endP.y;
p4.x = wt;
Path path = new Path();
path.moveTo(startP.x, startP.y);
path.cubicTo(p3.x, p3.y, p4.x, p4.y, endP.x, endP.y);
canvas.drawPath(path, paint);
}
}
//画笔直的曲线
private void drawLine(Canvas canvas, Paint paint) {
if (null == mPoints || mPoints.length == 0) {
return;
}
Point startP;
Point endP;
for (int i = 0; i < mPoints.length - 1; i++) {
startP = mPoints[i];
endP = mPoints[i + 1];
canvas.drawLine(startP.x, startP.y, endP.x, endP.y, paint);
}
}
//添加X轴方向的文字
private void drawTextY(String text, int x, int y, Canvas canvas) {
if (null == yRawDatas || yRawDatas.size() == 0) {
return;
}
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setTextSize(dip2px(yTextSize));
p.setColor(yTextPaintColor);
p.setTextAlign(Paint.Align.LEFT);
canvas.drawText(text, x, y, p);
}
//添加Y轴方向的文字
private void drawTextX(String text, int x, int y, Canvas canvas) {
if (null == xRawDatas || xRawDatas.size() == 0) {
return;
}
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setTextSize(dip2px(xTextSize));
p.setColor(xTextPaintColor);
p.setTextAlign(Paint.Align.LEFT);
xTextWidth = (int) p.measureText(text);
canvas.drawText(text, x, y, p);
}
//获取所有曲线点
private Point[] getPoints() {
Point[] points = new Point[dataList.size()];
for (int i = 0; i < dataList.size(); i++) {
int ph = bHeight - (int) (((dataList.get(i) - pointData.getyAxisSmallValue()) / averageValue) * (bHeight / spacingHeight));
points[i] = new Point(xList.get(i), ph + marginTop);
}
return points;
}
//获取实际的左边距
private int getMarginWidth() {
if (xTextWidth == 0) {
return marginLeft;
} else {
return xTextWidth + marginLeft;
}
}
//获取实际的坐标图宽度
private int getBWidth() {
if (xTextWidth == 0) {
return bWidth;
} else {
return bWidth - xTextWidth;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
dealClick(x, y);
break;
}
return true;
}
//添加点击事件,并更新点
private void dealClick(int x, int y) {
if (null != mPoints && mPoints.length > 0) {
for (int i = 0; i < mPoints.length; i++) {
if ((mPoints[i].x - CIRCLE_SIZE) < x && x < (mPoints[i].x + CIRCLE_SIZE) &&
(mPoints[i].y - CIRCLE_SIZE) < y && y < (mPoints[i].y + CIRCLE_SIZE)) {
mSelPoint = mPoints[i];
invalidate();
if (null != listener) {
listener.onClick(mPoints[i].x, mPoints[i].y);
}
Toast.makeText(mContext, "点击了第" + i + "个", Toast.LENGTH_SHORT).show();
}
}
}
}
public void setAverageValue(int averageValue) {
this.averageValue = averageValue;
}
public void setMarginTop(int marginTop) {
this.marginTop = marginTop;
}
public void setMarginBottom(int marginBottom) {
this.marginBottom = marginBottom;
}
public void setMStyle(LineStyle mStyle) {
this.mStyle = mStyle;
}
public void setMYLineStyle(YLineStyle style) {
this.mYLineStyle = style;
}
public void setShaderOrientationStyle(ShaderOrientationStyle shaderOrientationStyle) {
this.mShaderOrientationStyle = shaderOrientationStyle;
}
public void setBHeight(int bHeight) {
this.bHeight = bHeight;
}
public void setXTextPaintColor(int xTextPaintColor) {
this.xTextPaintColor = xTextPaintColor;
}
public void setYTextPaintColor(int yTextPaintColor) {
this.yTextPaintColor = yTextPaintColor;
}
public void setXTextSize(int xTextSize) {
this.xTextSize = xTextSize;
}
public void setYTextSize(int yTextSize) {
this.yTextSize = yTextSize;
}
public void setShaderColor(int startColor, int endColor) {
this.startShaderColor = startColor;
this.endShaderColor = endColor;
}
public void setIsShowFirstXContent(boolean isShowFirstXContent) {
this.isShowFirstXContent = isShowFirstXContent;
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
private int dip2px(float dpValue) {
return (int) (dpValue * dm.density + 0.5f);
}
public interface OnClickListener {
void onClick(int x, int y);
}
public void setListener(OnClickListener listener) {
this.listener = listener;
}
}
that's all