自定义控件-精美的心电图
2016-12-11 23:16
316 查看
大家好,最近项目上要优化一些Ui,发下之前的一些界面不太美观,比如说关于心电的一些折线图展示等问题。于是就乘着一些空余时间谢了个心电图的控件。来与大家一起分享。
由于需要根据实际情况设置不同的目标线会有不同的情况。
有一个目标线的效果图:
有两个目标线的效果图:
对与自定义组件比较陌生童鞋们可以访问我之前的一些博客:
http://blog.csdn.net/androidstarjack/article/list/5
接下来主要是绘制一些主要关键点:
1.绘制底部的日期
3.绘制目标线
4,.设置目标线
最重要的还是每次更新完毕后要精确计算其:目标线和集合中的最大值和最小值:
好,最后附上源码:
项目地址:http://download.csdn.net/detail/androidstarjack/9708431
老于的博客http://blog.csdn.net/androidstarjack
另外你觉得此篇文章对您有所帮助 请关注终端研发部,QQ交流群 :232203809
微信公众号:
由于需要根据实际情况设置不同的目标线会有不同的情况。
有一个目标线的效果图:
/** * 设置目标值1 * * @param goal */ public void setGoal_one(int goal) { this.goal = goal; maxValue = getListOgMax(y_array); minValus = getListOgMin(y_array); if (goal > maxValue) { maxValue = goal; } if (goal < minValus) { minValus = goal; } drawFullDistance = maxValue - minValus;//最大值-最小值 perunit_valus = drawFullDistance / UNIT_NUM; postInvalidate(); }
有两个目标线的效果图:
/** * 设置目标值 * * @param goal_min,goal_max */ public void setMyGoadToLine(int goal_min, int goal_max) { this.goal_min = goal_min; this.goal_max = goal_max; maxValue = getListOgMax(y_array); maxValue = Math.max(maxValue, goal_max); minValus = getListOgMin(y_array); minValus = Math.min(minValus, goal_min); drawFullDistance = maxValue - minValus;//最大值-最小值 perunit_valus = drawFullDistance / UNIT_NUM; postInvalidate(); }
对与自定义组件比较陌生童鞋们可以访问我之前的一些博客:
http://blog.csdn.net/androidstarjack/article/list/5
接下来主要是绘制一些主要关键点:
1.绘制底部的日期
/** * 绘制底部的日期 * * @param canvas */ private void drawBottomDateTypeText(Canvas canvas) { if (xValues == null) { return; } canvas.translate(0, myHeightCell); // canvas.drawRect(0,0,getMeasuredWidth() * 1F,perUnitCell * 2f,paint_date); for (int i = 0; i < xValues.size(); i++) { float centerD = perBottomTextLehght * i + perBottomTextLehght; String text = Utils.date2String(Long.parseLong(xValues.get(i)), "MM月dd日"); float textLength = paint_date.measureText(text) / 2; float startX = centerD - textLength; //float startY = perUnitCell * 2; Rect rectf = new Rect(); paint_date.getTextBounds(text, 0, text.length(), rectf); float startY = perUnitCellHeifht * 2 - rectf.height() / 2; canvas.drawText(text, startX, startY, paint_date); } }2. 绘制单元格
/** * 绘制单元格 * * @param canvas */ private void drawUniteCell(Canvas canvas) { for (int i = 0; i < myHeightCell / perUnitCellHeifht; i++) { float startX = 0; float startY = i * perUnitCellHeifht; float endX = getMeasuredWidth(); float endY = i * perUnitCellHeifht; canvas.drawLine(startX, startY, endX, endY, framPanint); } for (int i = 0; i < getMeasuredWidth() / perUnitCellHeifht; i++) { float startX = i * perUnitCellHeifht; float startY = 0; float endX = i * perUnitCellHeifht; float endY = myHeightCell; canvas.drawLine(startX, startY, endX, endY, framPanint); } }
3.绘制目标线
/** * 绘制目标线 * * @param canvas */ private void drawGoalLine(Canvas canvas) { canvas.save(); if (goal_min > 0) { float getCurrenY = (goal_min - minValus) / perunit_valus * perUnitCellHeifht; float startX1 = 0; float startY1 = (getCurrenY == 0 ? (10) : getCurrenY); float endX1 = currentWidth - 2; float endY1 = startY1; canvas.drawLine(startX1, -startY1, endX1, -endY1, paint_gold1); String resultGroal = "目标:" + goal_min; canvas.drawText(resultGroal, startX1 + getDrawMyBottomTextDefaultPadding(resultGroal, paint_date), -startY1 - 10, paint_date); LogUtil.e("yuyahao", "goal_min: " + goal_min + " " + startX1 + " -" + startY1 + " " + endX1 + " -" + endY1); } if (goal_max > 0) { float getCurrenY = (goal_max - minValus) / perunit_valus * perUnitCellHeifht; float startX2 = 0; float startY2 = (getCurrenY == 0 ? (10) : getCurrenY); ; float endX2 = currentWidth - 2; float endY2 = startY2; canvas.drawLine(startX2, -startY2, endX2, -endY2, paint_gold1); String resultGroal = "目标:" + goal_max; canvas.drawText(resultGroal, startX2 + getDrawMyBottomTextDefaultPadding(resultGroal, paint_date), -startY2 - 10, paint_date); LogUtil.e("yuyahao", "goal_max: " + goal_max + " " + startX2 + " -" + startY2 + " " + endX2 + " -" + endY2); } if (goal > 0) { float getCurrenY = (goal - minValus) / perunit_valus * perUnitCellHeifht; float startX = 0; float startY = (getCurrenY == 0 ? (10) : getCurrenY); float endX = currentWidth - 2; float endY = startY; canvas.drawLine(startX, -startY, endX, -endY, paint_gold1); String resultGroal = "目标:" + goal; canvas.drawText("目标:" + goal, startX + getDrawMyBottomTextDefaultPadding(resultGroal, paint_date), -startY - 10, paint_date); LogUtil.e("yuyahao", "goal: " + goal + " " + startX + " -" + startY + " " + endX + " -" + endY); } }
4,.设置目标线
/** * 设置目标值 * * @param goal_min,goal_max */ public void setMyGoadToLine(int goal_min, int goal_max) { this.goal_min = goal_min; this.goal_max = goal_max; maxValue = getListOgMax(y_array); maxValue = Math.max(maxValue, goal_max); minValus = getListOgMin(y_array); minValus = Math.min(minValus, goal_min); drawFullDistance = maxValue - minValus;//最大值-最小值 perunit_valus = drawFullDistance / UNIT_NUM; postInvalidate(); }
最重要的还是每次更新完毕后要精确计算其:目标线和集合中的最大值和最小值:
maxValue = getListOgMax(y_array); maxValue = Math.max(maxValue, goal_max); minValus = getListOgMin(y_array); minValus = Math.min(minValus, goal_min); if (goal > 0) { if (goal > maxValue) { maxValue = goal; } if (goal < minValus) { minValus = goal; } } drawFullDistance = maxValue - minValus;//最大值-最小值 perunit_valus = drawFullDistance / UNIT_NUM;
好,最后附上源码:
package com.example.administrator.myapplication.view;Activity中:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.view.ViewTreeObserver;
import com.example.administrator.myapplication.R;
import com.example.administrator.myapplication.utils.DensityUtil;
import com.example.administrator.myapplication.utils.LogUtil;
import com.example.administrator.myapplication.utils.Utils;
import java.util.Collections;
import java.util.List;
/**
* 折线图
*
* @author yuyahao
*/
public class MyBrokenLineView extends View {
/*********
* 最大值和最小值
*********/
private Context context;
private int MARGIN_VALUE = 0;//margin值为10
private Paint paint_date;//绘制底部日期
private Paint paint_brokenLine;//绘制底部的一根线
private Paint paint_dot;//绘制圆点的一根线
private Paint paint_gold1;//目标线
private Paint framPanint;//绘制单元格
private Paint paintBg;
private int perUnitCellHeifht = 10;
private float tb;
private List<String> xValues;//底部日期
private List<Float> y_array;//y轴坐标的值
private final int MAX_SHOW_TEXT = 6;//一屏幕上最多显示的日期的个数
private int perBottomTextLehght = 0;//每个单元格的高度
private int myHeightCell = 0;//单元格所在总高度
private float maxValue = 0, minValus = 0;//y轴方向上的最大值和最小值
private float drawFullDistance;//最大值-最小值
private float perunit_valus = 0;
private int currentWidth;
private int UNIT_NUM;
private Path path;
private boolean isDrawRectBackgrodund = false;//是否绘制底部的背景小方格
private int goal_min;//设置目标值1(2个goal的目标范围的最小值)
private int goal_max;//设置目标值2
private int goal;//设置目标值2
private boolean isDrawRectBackgrodundColor = false;
/*public MyBrokenLineView(Context context) {
super(context);
this.context = context;
}*/
public MyBrokenLineView(Context context, List<String> xValues, List<Float> y_array) {
super(context);
this.context = context;
this.xValues = xValues;
this.y_array = y_array;
initData();
initDefaltData();
}
public MyBrokenLineView(Context context, List<String> xValues, List<Float> y_array, int goal_min, int goal_max) {
super(context);
this.context = context;
this.xValues = xValues;
this.y_array = y_array;
this.goal_min = goal_min;
this.goal_max = goal_max;
initData();
initDefaltData();
}
public MyBrokenLineView(Context context, List<String> xValues, List<Float> y_array, int goal) {
super(context);
this.context = context;
this.xValues = xValues;
this.y_array = y_array;
this.goal = goal;
initData();
initDefaltData();
}
private void initDefaltData() {
perBottomTextLehght = DensityUtil.getScreenIntWidth(context) / MAX_SHOW_TEXT;
if (xValues != null) {
currentWidth = xValues.size() * perBottomTextLehght;
}
/* maxValue = getListOgMax(y_array);
minValus = getListOgMin(y_array);
drawFullDistance = maxValue - minValus;//最大值-最小值*/
maxValue = getListOgMax(y_array);
maxValue = Math.max(maxValue, goal_max);
minValus = getListOgMin(y_array);
minValus = Math.min(minValus, goal_min);
if (goal > 0) {
if (goal > maxValue) {
maxValue = goal;
}
if (goal < minValus) {
minValus = goal;
}
}
drawFullDistance = maxValue - minValus;//最大值-最小值
perunit_valus = drawFullDistance / UNIT_NUM;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
currentWidth = widthSize;
if (currentWidth < widthMeasureSpec) {
currentWidth = DensityUtil.getScreenIntWidth(context);
}
} else {
if (currentWidth < DensityUtil.getScreenIntWidth(context)) {
currentWidth = DensityUtil.getScreenIntWidth(context);
}
}
if (heightMode == MeasureSpec.EXACTLY) {
heightMeasureSpec = heightSize;
} else {
heightMeasureSpec = DensityUtil.dip2px(context, 200);
}
currentWidth = currentWidth - DensityUtil.dip2px(context, 10);//出去左右填充后padding
setMeasuredDimension(currentWidth, heightMeasureSpec);
myHeightCell = heightMeasureSpec - perUnitCellHeifht * 4;
UNIT_NUM = myHeightCell / perUnitCellHeifht;//竖向一共有几个单元格
perunit_valus = drawFullDistance / UNIT_NUM;
}
private void initData() {
tb = context.getResources().getDimension(R.dimen.historyscore_tb);
paint_date = new Paint();
paint_date.setStrokeWidth(tb * 0.1f);
paint_date.setTextSize(tb * 1f);
paint_date.setAntiAlias(true);
paint_date.setTextAlign(Paint.Align.CENTER);
paint_date.setColor(ContextCompat.getColor(context, R.color.black));
paint_brokenLine = new Paint();
paint_brokenLine.setAntiAlias(true);
paint_brokenLine.setStrokeWidth(tb * 0.1f);
paint_brokenLine.setColor(ContextCompat.getColor(context, R.color.theme_color));
paint_brokenLine.setStyle(Paint.Style.STROKE);
paint_dot = new Paint();
paint_dot.setAntiAlias(true);
paint_dot.setStrokeWidth(tb * 0.1f);
paint_dot.setColor(ContextCompat.getColor(context, R.color.theme_color));
paint_dot.setStyle(Paint.Style.FILL);
paint_gold1 = new Paint();
paint_gold1.setAntiAlias(true);
paint_gold1.setStrokeWidth(tb * 0.1f);
paint_gold1.setColor(ContextCompat.getColor(context, R.color.titlebar_bg_color));
paint_gold1.setStyle(Paint.Style.STROKE);
framPanint = new Paint();
framPanint.setAntiAlias(true);
framPanint.setStrokeWidth(tb * 0.1f);
framPanint.setColor(ContextCompat.getColor(context, R.color.black));
framPanint.setStyle(Paint.Style.FILL);
framPanint.setAlpha(40);
paintBg = new Paint();
paintBg.setAntiAlias(true);
paintBg.setColor(ContextCompat.getColor(context, R.color.black4));
perUnitCellHeifht = DensityUtil.dip2px(context, 10);
path = new Path();
MARGIN_VALUE = DensityUtil.dip2px(context, 10);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(0, MARGIN_VALUE * 2);
if (isDrawRectBackgrodundColor) {//绘制背景颜色
canvas.drawColor(paintBg.getColor());
}
if (isDrawRectBackgrodund) {//绘制底部单元格
drawUniteCell(canvas);
}
canvas.save();
if (xValues == null || xValues.size() == 0) {
return;
}
//绘制底部文字
drawBottomDateTypeText(canvas);
//绘制目标线
drawGoalLine(canvas);
//绘制 描点绘制折线图
drawPointAndLine(canvas);
}
/**
* 绘制 描点绘制折线图
*
* @param canvas
*/
private void drawPointAndLine(Canvas canvas) {
if (y_array == null || y_array.size() == 0) {
return;
}
canvas.save();
//canvas.drawLine(0,0,getMeasuredWidth(),5,paint_date);
for (int i = 0; i < y_array.size(); i++) {
float yalue = y_array.get(i);
float cx = perBottomTextLehght * i + perBottomTextLehght / 2 + perUnitCellHeifht / 2;
float cy = (yalue - minValus) / perunit_valus * perUnitCellHeifht;
canvas.drawCircle(cx, -cy, 5, paint_dot);
LogUtil.e("yuyahao", "(" + cx + "," + (-cy) + ")" + " yalue: " + yalue + " 最小值:" + minValus + "最大值:" + maxValue + " perunit_valus: " + perunit_valus + " perUnitCellHeifht: " + perUnitCellHeifht);
//canvas.drawCircle(cx, y1, 3, paint_dot);
if (i >= 1) {
path.lineTo(cx, -cy);
canvas.drawPath(path, paint_brokenLine);
if (i == (y_array.size() - 1)) {
path.close();
}
LogUtil.e("yuyahao", "(" + cx + "," + (-cy) + ")");
} else if (i == 0) {
path.reset();
path.moveTo(cx, -cy);
}
}
}
/** * 绘制底部的日期 * * @param canvas */ private void drawBottomDateTypeText(Canvas canvas) { if (xValues == null) { return; } canvas.translate(0, myHeightCell); // canvas.drawRect(0,0,getMeasuredWidth() * 1F,perUnitCell * 2f,paint_date); for (int i = 0; i < xValues.size(); i++) { float centerD = perBottomTextLehght * i + perBottomTextLehght; String text = Utils.date2String(Long.parseLong(xValues.get(i)), "MM月dd日"); float textLength = paint_date.measureText(text) / 2; float startX = centerD - textLength; //float startY = perUnitCell * 2; Rect rectf = new Rect(); paint_date.getTextBounds(text, 0, text.length(), rectf); float startY = perUnitCellHeifht * 2 - rectf.height() / 2; canvas.drawText(text, startX, startY, paint_date); } }
/**
* 去一个集合的最大值
*
* @param y_array
* @return
*/
private float getListOgMax(List<Float> y_array) {
if (y_array == null) {
return 0;
}
return Collections.max(y_array);
}
/**
* 绘制单元格
*
* @param canvas
*/
private void drawUniteCell(Canvas canvas) {
for (int i = 0; i < myHeightCell / perUnitCellHeifht; i++) {
float startX = 0;
float startY = i * perUnitCellHeifht;
float endX = getMeasuredWidth();
float endY = i * perUnitCellHeifht;
canvas.drawLine(startX, startY, endX, endY, framPanint);
}
for (int i = 0; i < getMeasuredWidth() / perUnitCellHeifht; i++) {
float startX = i * perUnitCellHeifht;
float startY = 0;
float endX = i * perUnitCellHeifht;
float endY = myHeightCell;
canvas.drawLine(startX, startY, endX, endY, framPanint);
}
}
/**
* 绘制目标线
*
* @param canvas
*/
private void drawGoalLine(Canvas canvas) {
canvas.save();
if (goal_min > 0) {
float getCurrenY = (goal_min - minValus) / perunit_valus * perUnitCellHeifht;
float startX1 = 0;
float startY1 = (getCurrenY == 0 ? (10) : getCurrenY);
float endX1 = currentWidth - 2;
float endY1 = startY1;
canvas.drawLine(startX1, -startY1, endX1, -endY1, paint_gold1);
String resultGroal = "目标:" + goal_min;
canvas.drawText(resultGroal, startX1 + getDrawMyBottomTextDefaultPadding(resultGroal, paint_date), -startY1 - 10, paint_date);
LogUtil.e("yuyahao", "goal_min: " + goal_min + " " + startX1 + " -" + startY1 + " " + endX1 + " -" + endY1);
}
if (goal_max > 0) {
float getCurrenY = (goal_max - minValus) / perunit_valus * perUnitCellHeifht;
float startX2 = 0;
float startY2 = (getCurrenY == 0 ? (10) : getCurrenY);
;
float endX2 = currentWidth - 2;
float endY2 = startY2;
canvas.drawLine(startX2, -startY2, endX2, -endY2, paint_gold1);
String resultGroal = "目标:" + goal_max;
canvas.drawText(resultGroal, startX2 + getDrawMyBottomTextDefaultPadding(resultGroal, paint_date), -startY2 - 10, paint_date);
LogUtil.e("yuyahao", "goal_max: " + goal_max + " " + startX2 + " -" + startY2 + " " + endX2 + " -" + endY2);
}
if (goal > 0) {
float getCurrenY = (goal - minValus) / perunit_valus * perUnitCellHeifht;
float startX = 0;
float startY = (getCurrenY == 0 ? (10) : getCurrenY);
float endX = currentWidth - 2;
float endY = startY;
canvas.drawLine(startX, -startY, endX, -endY, paint_gold1);
String resultGroal = "目标:" + goal;
canvas.drawText("目标:" + goal, startX + getDrawMyBottomTextDefaultPadding(resultGroal, paint_date), -startY - 10, paint_date);
LogUtil.e("yuyahao", "goal: " + goal + " " + startX + " -" + startY + " " + endX + " -" + endY);
}
}
/**
* 去一个集合的最小值
*
* @param y_array
* @return
*/
private float getListOgMin(List<Float> y_array) {
if (y_array == null) {
return 0;
}
return Collections.min(y_array);
}
/**
* 是否绘制底部的小方格
*
* @param drawRectBackgrodund
*/
public void setDrawRectBackgrodund(boolean drawRectBackgrodund) {
isDrawRectBackgrodund = drawRectBackgrodund;
invalidate();
}
/**
* 设置绘制底部的小方格的颜色
*
* @param resColorId
*/
public void setDrawRectBackgrodundPaintColor(int resColorId) {
framPanint.setColor(ContextCompat.getColor(context, resColorId));
invalidate();
}
/**
* 设置目标值
*
* @param goal_min,goal_max
*/
public void setMyGoadToLine(int goal_min, int goal_max) {
this.goal_min = goal_min;
this.goal_max = goal_max;
maxValue = getListOgMax(y_array);
maxValue = Math.max(maxValue, goal_max);
minValus = getListOgMin(y_array);
minValus = Math.min(minValus, goal_min);
drawFullDistance = maxValue - minValus;//最大值-最小值
perunit_valus = drawFullDistance / UNIT_NUM;
postInvalidate();
}
/**
* 重新设置数据进行刷新
*
* @param
*/
public void setMyChangedData(List<String> xValues, List<Float> y_array) {
this.xValues = xValues;
this.y_array = y_array;
initData();
initDefaltData();
postInvalidate();
}
/**
* 设置目标值1
*
* @param goal
*/
public void setGoal_one(int goal) {
this.goal = goal;
maxValue = getListOgMax(y_array);
minValus = getListOgMin(y_array);
if (goal > maxValue) {
maxValue = goal;
}
if (goal < minValus) {
minValus = goal;
}
drawFullDistance = maxValue - minValus;//最大值-最小值
perunit_valus = drawFullDistance / UNIT_NUM;
postInvalidate();
}
/**
* 适配 绘制目标文字的左边的距离
*
* @param string
* @param paint
* @return
*/
private float getDrawMyBottomTextDefaultPadding(String string, Paint paint) {
float length = paint.measureText(string);
float ff = paint.measureText("目标:" + 20);
if (length == ff) {
return perUnitCellHeifht * 2.2F;
} else {
return perUnitCellHeifht * 2.5F;
}
}
/**
* 设置背景颜色
*
* @param resColor
*/
public void setMyBackgroundColor(int resColor) {
isDrawRectBackgrodundColor = true;
if (resColor != 0) {
paintBg.setColor(ContextCompat.getColor(context, resColor));
}
}
}
private LinearLayout ll_content; private MyBrokenLineView myBrokenLineView; private java.util.List<String> xValues; private List<Float> y_array; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ll_content = (LinearLayout) findViewById(R.id.ll_content); xValues = new ArrayList<>(); y_array = new ArrayList<>(); for (int i = 0; i < 6; i++) { xValues.add(System.currentTimeMillis()+""); } y_array.add(30F); y_array.add(70F); y_array.add(80F); y_array.add(90F); y_array.add(60F); y_array.add(150F); ; myBrokenLineView = new MyBrokenLineView(MainActivity.this,xValues,y_array); ll_content.addView(myBrokenLineView); //myBrokenLineView.setGoal_one(75); myBrokenLineView.setMyGoadToLine(75,120);//绘制两根目标线 myBrokenLineView.setMyBackgroundColor(R.color.black4); myBrokenLineView.setDrawRectBackgrodund(true); //handler.sendEmptyMessageDelayed(100,1000); } private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what){ case 100: y_array.clear(); xValues.clear(); for (int i = 0; i < 6; i++) { y_array.add((float)Math.round(Math.random()*(40)+60)); xValues.add((""+Math.round(Math.random()*(System.currentTimeMillis()- 60 * 60 *24 *30) + System.currentTimeMillis()- 60 * 60 *24 *30))); } myBrokenLineView.setGoal_one((int) Math.round(Math.random()*(40)+60)); myBrokenLineView.setMyChangedData(xValues,y_array); handler.sendEmptyMessageDelayed(100,1000); break; } } };
项目地址:http://download.csdn.net/detail/androidstarjack/9708431
老于的博客http://blog.csdn.net/androidstarjack
另外你觉得此篇文章对您有所帮助 请关注终端研发部,QQ交流群 :232203809
微信公众号:
相关文章推荐
- Apache HTTP Server通过mod_cluster模块与Tomcat连接
- shpfexcel
- 拓扑排序
- 基本概念学习(7000)--P2P对等网络
- Linux软件安装与管理之apt-get安装方式
- 基于无监督学习的自编码器实现
- c#学习一:类的基本概念
- python笔记
- 152. Maximum Product Subarray (Array; DP)
- 树莓派如何定时关机或定时执行任务-Cron
- 戏说春秋一窃符救赵
- CentOS6.X 查看、配置网络的方法
- Linux学习之路(2)-ssh配置和SecureCRT远程连接
- MySQL 5.7.11 的密码
- 重写TreeView模板来实现数据分层展示(一)
- postgresql常用功能
- 银行卡号每隔4位插入空格
- PHP 变量知识整理
- 戏说春秋一纸上谈兵
- Codeforces 741C 图论