Android - 折线图
使用Android的canvas,画折线图:代码为:
package spt.view; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathEffect; import android.util.Log; import android.view.View; /** * 为了扩展不同分辨率手机的兼容性,百分比和一些重要变量设置为final,其他可变变量设置为成员变量. * * @author Administrator * */ @SuppressLint("DrawAllocation") public class BrokenLineView extends View { // 计算相对比例时,均使用边距margin为依照(值与比例结果成反比). // 画轴线时,箭头所占比例. private static final int ARROW_PECENT = 10; // x,y轴坐标点文字的比例. private static final int X_TEXT_PECENT = 10; private static final int Y_TEXT_PECENT = 10; // 标题文字的比例. private static final int TITLE_TEXT_PECENT = 5; // 默认坐标轴值. private static final String[] X_LABLE = { "a", "b", "c", "d", "e", "f", "g" }; private static final String[] Y_LABLE = { "0", "50", "100", "150", "200", "250", "300" }; // 数据点圆的半径. private static final int dataRadius = 10; // x,y轴坐标点数字的位置偏离轴线的距离. private int xTextDistanceAxis = 20; private int yTextDistanceAxis = 30; // 数据点数值文字相对于数据点的高度值. private int dataTextAboveCircle = 25; // 边距(也不能为final,因为可能用户可能根据不同条件设置不同间距. private int margin = 100; // X,Y轴的单位长度 private int xScale = 20; private int yScale = 20; // 标题的高度. private String title; // 标题距离最顶行线的y距离. private int titleHeight = 20; // 原点坐标 private int x0Point; private int y0Point; // X,Y轴上面的显示文字 private String[] xLabel = null; private String[] yLabel = null; // 曲线数据 private int[] data = null; public BrokenLineView(Context context, String title, String[] xLabel, String[] yLabel, int[] data) { super(context); this.title = title; // 若传递空值,则使用默认的值. this.xLabel = (xLabel == null ? X_LABLE : xLabel); this.yLabel = (yLabel == null ? Y_LABLE : yLabel); if (data == null) throw new RuntimeException("data cannot null:"); this.data = data; } public BrokenLineView(Context context) { this(context, null, X_LABLE, Y_LABLE, null); } // 设置坐标原点位置和轴线上的单位长度. public void init() { x0Point = margin; // x0. y0Point = getHeight() - margin; // y0. xScale = (getWidth() - 2 * margin) / (this.xLabel.length - 1); yScale = (getHeight() - 2 * margin) / (this.yLabel.length - 1); } public int getMargin() { return margin; } public void setMargin(int margin) { if (margin < 0) throw new RuntimeException("间距不能为负数:" + margin); this.margin = margin; } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.BLACK); // 背景色. Paint p = new Paint(); p.setStyle(Paint.Style.STROKE); // 设置轴线的的外框的样式“空心”(STROKE). p.setAntiAlias(true); // 抗锯齿 p.setColor(Color.WHITE); p.setStrokeWidth(2); // 设置轴线的的外框的宽度. init(); drawYAxis(canvas, p); drawXAxis(canvas, p); drawHorizontalLine(canvas); drawData(canvas); } // x向线 private void drawHorizontalLine(Canvas canvas) { Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setColor(Color.GRAY); Path path = new Path(); // 先画长度为1的实线,然后长度为10的空白,再画长度为1实线,再画长度为10的空白;最后一个是是偏移量,可不理会. PathEffect effects = new DashPathEffect(new float[] { 1, 10, 1, 10 }, 1); paint.setPathEffect(effects); for (int i = 1; (y0Point - i * yScale) >= margin; i++) { int startX = x0Point; int startY = y0Point - i * yScale; int stopX = x0Point + (xLabel.length - 1) * xScale; path.moveTo(startX, startY); path.lineTo(stopX, startY); paint.setColor(Color.DKGRAY); canvas.drawPath(path, paint); } } /** * 画y轴线 * * @param canvas * @param p */ private void drawYAxis(Canvas canvas, Paint p) { // y轴. canvas.drawLine(x0Point, y0Point, margin, margin, p); // y轴箭头的左部分. canvas.drawLine(x0Point, margin, x0Point - x0Point / ARROW_PECENT, margin + margin / ARROW_PECENT, p); // y轴箭头的右部分. canvas.drawLine(x0Point, margin, x0Point + x0Point / ARROW_PECENT, margin + margin / ARROW_PECENT, p); } /** * 画x轴线. * * @param canvas * @param p */ private void drawXAxis(Canvas canvas, Paint p) { // x轴. canvas.drawLine(x0Point, y0Point, getWidth() - margin, y0Point, p); // x轴箭头的上部分. canvas.drawLine(getWidth() - margin, y0Point, getWidth() - margin - margin / ARROW_PECENT, y0Point - margin / ARROW_PECENT, p); // x轴箭头的下部分. canvas.drawLine(getWidth() - margin, y0Point, getWidth() - margin - margin / ARROW_PECENT, y0Point + margin / ARROW_PECENT, p); } // 画数据 private void drawData(Canvas canvas) { Paint p = new Paint(); p.setAntiAlias(true); // 抗锯齿. p.setColor(Color.RED); p.setTextSize(margin / X_TEXT_PECENT); for (int x = 0; x < data.length; x++) { int startX = x0Point + x * xScale; // 轴坐标点文字的显示. canvas.drawText(xLabel[x], startX, y0Point + xTextDistanceAxis, p); // 数据点的圆. canvas.drawCircle(startX, calY(data[x]), dataRadius, p); // 在数据点上标数据值. canvas.drawText(data[x] + "", startX, calY(data[x]) + dataRadius - dataTextAboveCircle, p); // 在数据点圆间画变化线. // 画线时,最后数据点不执行操作. if (x != data.length - 1) canvas.drawLine(startX, calY(data[x]), startX + xScale, calY(data[x + 1]), p); } // for x. // 画y轴坐标点文字. Paint py = new Paint(); py.setAntiAlias(true); // 抗锯齿. py.setColor(Color.RED); py.setTextSize(margin / Y_TEXT_PECENT); for (int y = 0; y < yLabel.length; y++) { int startY = y0Point - y * yScale; // y轴坐标点文字的显示. canvas.drawText(yLabel[y], x0Point - yTextDistanceAxis, startY, py); } // for y. // 画标题. Paint pTitle = new Paint(); pTitle.setAntiAlias(true); // 抗锯齿. pTitle.setColor(Color.RED); pTitle.setTextSize(margin / TITLE_TEXT_PECENT); // 通过Paint.measureText计算标题长度的像素,进而将标题水平居中. canvas.drawText(title, (getWidth() - pTitle.measureText(title)) / 2, margin - titleHeight, pTitle); } /** * 计算数据值在坐标系中y的位置. * * @param y * @return */ private int calY(int y) { int y0 = 0; int y1 = 0; try { y0 = Integer.parseInt(yLabel[0]); y1 = Integer.parseInt(yLabel[1]); return y0Point - ((y - y0) * yScale / (y1 - y0)); } catch (NumberFormatException e) { Log.d("sysout", "y轴label必须是数字:" + e.getMessage()); return -1; } } }
然后在Activity中调用即可:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //test: int[] data = { 0, 50, 51, 78, 200, 121, 31 }; final String title = "最近一周AQI变化图"; setContentView(new BrokenLineView(this, title, null, null, data)); initView(); }