Android自定义折线图
来源:互联网 发布:游戏美工原画招聘 编辑:程序博客网 时间:2024/06/05 15:03
前言
折线图在比较数据、天气等方面的时候会用到,网上也不乏大佬将其封装成完整控件。自己也简单写写看一下效果。先上效果图,毕竟无图何以言diao。
嗯,效果就这样,下面将介绍其从无到有的过程!!
重写View的onMeasure方法
在自定义控件的时候,控件的宽高(最终的测量尺寸)由控件本身和其父容器共同决定的,既然是共同决定的我们得知道父容器的“意愿”对吧!
父容器的“意愿”也就三种,在MeasureSpec类中的三个常量表明了其三种意愿:
1、UNSPECIFIED:父容器对控件的大小不在意,你想多大都可以;
2、AT_MOST:父容器给控件的大小设置了一个最大值,表示你大小的取值最好在这个范围内;
3、EXACTLY: 父容器已经给控件计算出了显示的大小,控件就显示这么大得了;
onMeasure方法具体的代码:
int resultWidth; //获取父容器的意愿 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { resultWidth = sizeWidth; } else { if (widthMode == MeasureSpec.AT_MOST) { resultWidth = Math.min(screenWidth, sizeWidth); } else { resultWidth = screenWidth; } } int resultHeight; int heightMode = MeasureSpec.getMode(heightMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); if (heightMode == MeasureSpec.EXACTLY) { resultHeight = sizeHeight; } else { if (heightMode == MeasureSpec.AT_MOST) { resultHeight = Math.min(screenHeight, sizeHeight); } else { resultHeight = screenHeight; } } //设置测量出的宽高 setMeasuredDimension(resultWidth, resultHeight);
因为折线图是一个显示出来较占位置的控件,所以在父容器对大小无要求的时候,就以屏幕的大小作为宽高。获取屏幕的宽高:
DisplayMetrics metrics = context.getResources().getDisplayMetrics(); screenWidth = metrics.widthPixels; screenHeight = metrics.heightPixels;
现在展现在你面前的就是一张“白纸”,想怎么画全看意愿了,哈
理一下画图的顺序
drawTextY(Canvas canvas) //绘制y轴上需要显示的文字
drawTextX(Canvas canvas) //绘制x轴上的文字
drawCoordinate(Canvas canvas) //绘制坐标轴
drawBrokenLine(Canvas canvas) //绘制折线、顶点及顶点文字
差不多就是这个从上到下的顺序!
看到这儿估计有人会说:你特么逗我么,绘制折线图不先画坐标轴?
道友别急,请听我狡辩:绘制折线图有一个关键点是确定点的坐标(特么不是废话么),之所以将坐标轴放到文字之后绘制是因为y上绘制文字的宽度和x轴上文字的高度会对坐标轴的定位有一定影响。因为这两个数据是未知的,所以我们得先确定下来才好进行下一步操作!
drawTextY(Canvas canvas)
先上代码
//y轴文字的最大宽度private int maxTextWidth;//y轴每一刻度的高度private int singleHeight;//文字的高度private int textHeight;//y轴文字listprivate List<String> textY;/** * draw Y轴上的文字 * * @param canvas */private void drawTextY(Canvas canvas) { if (textY == null) return; // 文字的高度:mTextPaint.descent() - mTextPaint.ascent() textHeight = (int) (mTextPaint.descent() - mTextPaint.ascent()); singleHeight = (getHeight() - paddingBottom - paddingTop - textHeight - spacing) / (textY.size() - 1); int xPos = paddingLeft; int yPos; for (int i = 0; i < textY.size(); i++) { //找出文字所需的最大宽度 if (mTextPaint.measureText(textY.get(textY.size() - i - 1)) > maxTextWidth) { maxTextWidth = (int) mTextPaint.measureText(textY.get(textY.size() - i - 1)); } yPos = paddingTop + singleHeight * i + textHeight / 2; canvas.drawText(textY.get(textY.size() - i - 1), xPos, yPos, mTextPaint); }}
变量注释应该还算详细,抽两个简单介绍一下吧
textHeight:文字的高度获取,mTextPaint.descent() - mTextPaint.ascent()表达式前后的顺序不要反了,不然得到是一个负数(别问我是怎么知道的)。至于为什么是这样获取,问神奇的www.google.cn吧。
singleHeight:每一刻度的高度,通过控件的高度减去上下边距、文字高度、文字与坐标轴的间距(固定值)除以份数得到,聪明的你一定了然于心了!
drawTextX(Canvas canvas)
//x轴每一刻度的宽度private int singleWidth;//x轴文字listprivate List<String> textX;/** * draw X轴上的文字 * * @param canvas */private void drawTextX(Canvas canvas) { if (textX == null) return; singleWidth = (getWidth() - paddingLeft - paddingRight - spacing - maxTextWidth) / (textX.size() - 1); int xPos; int yPos = getHeight() - paddingBottom; for (int i = 0; i < textX.size(); i++) { xPos = paddingLeft + maxTextWidth + spacing + singleWidth * i - (int) mTextPaint.measureText(textX.get(i)) / 2; canvas.drawText(textX.get(i), xPos, yPos, mTextPaint); }}
singleWidth:每一刻度的高度,得到方式与singleHeight差不多,不过减去的值由文字(textHeight)的高度变为文字的宽度(maxTextWidth)
然后对应点上绘制对应的文字!!
drawCoordinate(Canvas canvas)绘制坐标轴
/** * 画坐标轴及点 * * @param canvas 画布 */int spacing = 6;//文字与坐标轴的间距private void drawCoordinate(Canvas canvas) { //移到画线的起始点(左上角),Y轴顶点 mPath.moveTo(paddingLeft + maxTextWidth + spacing, paddingTop / 2); //然后移到坐标轴原点 mPath.lineTo(paddingLeft + maxTextWidth + spacing, getHeight() - paddingBottom - textHeight - spacing); //最后移到X轴顶点 mPath.lineTo(getWidth() - paddingRight / 2, getHeight() - paddingBottom - textHeight - spacing); //绘制坐标轴 canvas.drawPath(mPath, mPaint); if (textY == null) { return; } int x0 = paddingLeft + maxTextWidth + spacing; int y0 = getHeight() - paddingBottom - textHeight - spacing; //画y轴坐标点 for (int i = 1; i < textY.size(); i++) { canvas.drawLine(x0, y0 - singleHeight * i, x0 + dp2px(context, 4), y0 - singleHeight * i, mPaint); } if (textX == null) { return; } //画x轴坐标点 for (int i = 1; i < textX.size(); i++) { canvas.drawLine(x0 + singleWidth * i, y0, x0 + singleWidth * i, y0 - dp2px(context, 4), mPaint); }}
使用Path绘制的坐标轴,一个点确定线的工具类;
然后绘制坐标点,根据singleWidth和singleHeight确定其位置。
drawBrokenLine(Canvas canvas) 绘制折线
/** * 画折线、圆点、顶点文字 * * @param canvas */private void drawBrokenLine(Canvas canvas) { //确定坐标轴原点的xy值,以此作为依据绘图 int startX = paddingLeft + maxTextWidth + spacing; int startY = getHeight() - paddingBottom - textHeight - spacing; //开起循环确定折线经过的坐标点,然后采用Path绘制 for (int i = 0; i < brokenData.length; i++) { if (i == 0) { if (brokenData[i] > maxY) { mBrokenPath.moveTo(startX, startY - singleHeight * (maxY - minY) / singleY); } else { mBrokenPath.moveTo(startX, startY - singleHeight * (brokenData[i] - minY) / singleY); } } else { if (brokenData[i] > maxY) { mBrokenPath.lineTo(startX + singleWidth * i, startY - singleHeight * (maxY - minY) / singleY); } else { mBrokenPath.lineTo(startX + singleWidth * i, startY - singleHeight * (brokenData[i] - minY) / singleY); } } } canvas.drawPath(mBrokenPath, mBrokenLinePaint); //在画线之后调用,不然会存在线覆盖点的问题(看着怪) for (int i = 0; i < brokenData.length; i++) { canvas.drawCircle(startX + singleWidth * i, startY - singleHeight * (brokenData[i] - minY) / singleY, circleRadius, mCirclePaint); canvas.drawText(String.valueOf(brokenData[i]), startX + singleWidth * i + spacing, startY - singleHeight * (brokenData[i] - minY) / singleY - spacing, mTextVerticesPaint); } invalidate();}
到此我们得绘制就差不多完成了。
然后定制控件的所需的属性值:
//线的颜色private int linePaintColor;//折线的颜色private int brokenLinePaintColor;//字的颜色private int textPaintColor;//字的大小private int textPaintSize;//顶点圆点的颜色private int circlePaintColor;//圆点半径private float circleRadius;//顶点文字的颜色private int textVerticesPaintColor;//顶点文字的大小private int textVerticesPaintSize;//线宽private float lineWidth;
最后暴露出设置数据的公共方法:
setTextY(…)//设置Y轴显示的数据
setTextX(…)//设置X轴显示的数据
setBrokenData(…)//设置折线顶点的值
到此差不多完成,然后再修一下边角就大功告成了,给自己点个赞!哈
完整代码
public class BrokenLineView extends View {private Context context;public BrokenLineView(Context context) { this(context, null);}public BrokenLineView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0);}public BrokenLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); obtainAttribute(context, attrs); init(context); this.context = context;}//画线的笔private Paint mPaint;//画折线的笔private Paint mBrokenLinePaint;//写坐标轴上文字的笔private TextPaint mTextPaint;//顶点文字的笔private TextPaint mTextVerticesPaint;//画圆点的笔private Paint mCirclePaint;//坐标的pathprivate Path mPath;//折线path,与上面用同一个会存在颜色互串的问题private Path mBrokenPath;//屏幕宽度private int screenWidth;//屏幕高度private int screenHeight;//padding系列private int paddingTop;private int paddingBottom;private int paddingRight;private int paddingLeft;private void init(Context context) { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//抗锯齿 mPaint.setColor(linePaintColor); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(lineWidth); mBrokenLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBrokenLinePaint.setColor(brokenLinePaintColor); mBrokenLinePaint.setStyle(Paint.Style.STROKE); mBrokenLinePaint.setStrokeWidth(lineWidth); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(textPaintColor); mTextPaint.setTextSize(textPaintSize); mTextVerticesPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mTextVerticesPaint.setColor(textVerticesPaintColor); mTextVerticesPaint.setTextSize(textVerticesPaintSize); mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCirclePaint.setColor(circlePaintColor); mPath = new Path(); mBrokenPath = new Path(); //获取屏幕的宽高 DisplayMetrics metrics = context.getResources().getDisplayMetrics(); screenWidth = metrics.widthPixels; screenHeight = metrics.heightPixels; paddingTop = Math.max(dp2px(context, 16), getPaddingTop()); paddingBottom = Math.max(dp2px(context, 12), getPaddingBottom()); paddingLeft = Math.max(dp2px(context, 16), getPaddingLeft()); paddingRight = Math.max(dp2px(context, 24), getPaddingRight());}/** * 得到设置的属性 *///线的颜色private int linePaintColor;//折线的颜色private int brokenLinePaintColor;//字的颜色private int textPaintColor;//字的大小private int textPaintSize;//顶点圆点的颜色private int circlePaintColor;//圆点半径private float circleRadius;//顶点文字的颜色private int textVerticesPaintColor;//顶点文字的大小private int textVerticesPaintSize;//线宽private float lineWidth;private void obtainAttribute(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BrokenLineView); try { linePaintColor = typedArray.getColor(R.styleable.BrokenLineView_linePaintColor, Color.BLUE); brokenLinePaintColor = typedArray.getColor(R.styleable.BrokenLineView_brokenLinePaintColor, Color.BLACK); textPaintColor = typedArray.getColor(R.styleable.BrokenLineView_textPaintColor, Color.BLACK); textPaintSize = typedArray.getDimensionPixelSize(R.styleable.BrokenLineView_textPaintSize, sp2px(context, 10)); circlePaintColor = typedArray.getColor(R.styleable.BrokenLineView_circlePaintColor, Color.BLACK); circleRadius = typedArray.getDimensionPixelSize(R.styleable.BrokenLineView_circleRadius, dp2px(context, 1)); textVerticesPaintColor = typedArray.getColor(R.styleable.BrokenLineView_textVerticesPaintColor, Color.BLACK); textVerticesPaintSize = typedArray.getDimensionPixelSize(R.styleable.BrokenLineView_textVerticesPaintSize, sp2px(context, 10)); lineWidth = typedArray.getDimensionPixelSize(R.styleable.BrokenLineView_lineWidth, dp2px(context, 1)); } finally { typedArray.recycle(); }}@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); drawTextY(canvas); drawTextX(canvas); drawCoordinate(canvas); drawBrokenLine(canvas);}//y轴文字的最大宽度private int maxTextWidth;//y轴每一刻度的高度private int singleHeight;//文字的高度private int textHeight;//y轴文字listprivate List<String> textY;/** * draw Y轴上的文字 * * @param canvas */private void drawTextY(Canvas canvas) { if (textY == null) return; // 文字的高度:mTextPaint.descent() - mTextPaint.ascent() textHeight = (int) (mTextPaint.descent() - mTextPaint.ascent()); singleHeight = (getHeight() - paddingBottom - paddingTop - textHeight - spacing) / (textY.size() - 1); Log.i("tag", "height: " + getHeight() + " singleHeight: " + singleHeight + " bottom: " + paddingBottom + " top: " + paddingTop + " textHeight: " + textHeight); int xPos = paddingLeft; int yPos; for (int i = 0; i < textY.size(); i++) { //找出文字所需的最大宽度 if (mTextPaint.measureText(textY.get(textY.size() - i - 1)) > maxTextWidth) { maxTextWidth = (int) mTextPaint.measureText(textY.get(textY.size() - i - 1)); } yPos = paddingTop + singleHeight * i + textHeight / 2; canvas.drawText(textY.get(textY.size() - i - 1), xPos, yPos, mTextPaint); }}/** * 设置y轴显示的数值 * * @param minY y轴最小值 * @param maxY y轴最大值 * @param number 坐标轴分为几部分 * @param unit 数字后面所跟的单位 *///每一部分y轴的取值private int singleY;//y轴最大值private int maxY;//y轴最小值private int minY;public void setTextY(int minY, int maxY, int number, String unit) { if (maxY - minY <= 0 || number < 1) { return; } this.maxY = maxY; this.minY = minY; textY = new ArrayList<>(); singleY = (maxY - minY) / number; for (int i = 0; i < number + 1; i++) { textY.add(String.valueOf(i * singleY + minY) + unit); } invalidate();}//x轴每一刻度的宽度private int singleWidth;//x轴文字listprivate List<String> textX;/** * draw X轴上的文字 * * @param canvas */private void drawTextX(Canvas canvas) { if (textX == null) return; singleWidth = (getWidth() - paddingLeft - paddingRight - spacing - maxTextWidth) / (textX.size() - 1); int xPos; int yPos = getHeight() - paddingBottom; Log.i("tag", "canvasHeight: " + canvas.getHeight() + " height: " + getHeight()); Log.i("tag", "textHeight: " + textHeight); for (int i = 0; i < textX.size(); i++) { xPos = paddingLeft + maxTextWidth + spacing + singleWidth * i - (int) mTextPaint.measureText(textX.get(i)) / 2; canvas.drawText(textX.get(i), xPos, yPos, mTextPaint); }}/** * 设置x轴显示的文字 * * @param textX x轴文字集合 */public void setTextX(List<String> textX) { if (textX == null) return; this.textX = textX; invalidate();}/** * 画坐标轴 * * @param canvas 画布 */int spacing = 6;//文字与坐标轴的间距private void drawCoordinate(Canvas canvas) { //移到画线的起始点(左上角) mPath.moveTo(paddingLeft + maxTextWidth + spacing, paddingTop / 2); mPath.lineTo(paddingLeft + maxTextWidth + spacing, getHeight() - paddingBottom - textHeight - spacing); mPath.lineTo(getWidth() - paddingRight / 2, getHeight() - paddingBottom - textHeight - spacing); canvas.drawPath(mPath, mPaint); if (textY == null) { return; } int x0 = paddingLeft + maxTextWidth + spacing; int y0 = getHeight() - paddingBottom - textHeight - spacing; //画y轴坐标点 for (int i = 1; i < textY.size(); i++) { canvas.drawLine(x0, y0 - singleHeight * i, x0 + dp2px(context, 4), y0 - singleHeight * i, mPaint); } if (textX == null) { return; } //画x轴坐标点 for (int i = 1; i < textX.size(); i++) { canvas.drawLine(x0 + singleWidth * i, y0, x0 + singleWidth * i, y0 - dp2px(context, 4), mPaint); }}/** * 画折线、圆点、顶点文字 * * @param canvas */private void drawBrokenLine(Canvas canvas) { int startX = paddingLeft + maxTextWidth + spacing; int startY = getHeight() - paddingBottom - textHeight - spacing; Log.i("tag", "height: " + getHeight() + " singleHeight: " + singleHeight + " bottom: " + paddingBottom + " top: " + paddingTop + " textHeight: " + textHeight); for (int i = 0; i < brokenData.length; i++) { if (i == 0) { if (brokenData[i] > maxY) { mBrokenPath.moveTo(startX, startY - singleHeight * (maxY - minY) / singleY); } else { mBrokenPath.moveTo(startX, startY - singleHeight * (brokenData[i] - minY) / singleY); } } else { if (brokenData[i] > maxY) { mBrokenPath.lineTo(startX + singleWidth * i, startY - singleHeight * (maxY - minY) / singleY); } else { mBrokenPath.lineTo(startX + singleWidth * i, startY - singleHeight * (brokenData[i] - minY) / singleY); } } } canvas.drawPath(mBrokenPath, mBrokenLinePaint); //在画线之后调用,不然会存在线覆盖点的问题(看着怪) for (int i = 0; i < brokenData.length; i++) { canvas.drawCircle(startX + singleWidth * i, startY - singleHeight * (brokenData[i] - minY) / singleY, circleRadius, mCirclePaint); canvas.drawText(String.valueOf(brokenData[i]), startX + singleWidth * i + spacing, startY - singleHeight * (brokenData[i] - minY) / singleY - spacing, mTextVerticesPaint); } invalidate();}//折线图的值private float[] brokenData;/** * 设置折线图顶点的值 * * @param data 数值数组 */public void setBrokenData(float[] data) { if (data == null) return; this.brokenData = data; invalidate();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int resultWidth; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); if (widthMode == MeasureSpec.EXACTLY) { resultWidth = sizeWidth; } else { if (widthMode == MeasureSpec.AT_MOST) { resultWidth = Math.min(screenWidth, sizeWidth); } else { resultWidth = screenWidth; } } int resultHeight; int heightMode = MeasureSpec.getMode(heightMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); if (heightMode == MeasureSpec.EXACTLY) { resultHeight = sizeHeight; } else { if (heightMode == MeasureSpec.AT_MOST) { resultHeight = Math.min(screenHeight, sizeHeight); } else { resultHeight = screenHeight; } } setMeasuredDimension(resultWidth, resultHeight);}/** * dp转px * * @param context * @return */private int dp2px(Context context, float dpVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics());}/** * sp转px * * @param context * @return */private int sp2px(Context context, float spVal) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, context.getResources().getDisplayMetrics());}}
属性定义文件,values下attrs.xml
<resources><declare-styleable name="BrokenLineView"> <attr name="linePaintColor" format="color" /> <attr name="textPaintColor" format="color" /> <attr name="textPaintSize" format="dimension" /> <attr name="circlePaintColor" format="color" /> <attr name="circleRadius" format="dimension" /> <attr name="textVerticesPaintColor" format="color" /> <attr name="textVerticesPaintSize" format="dimension" /> <attr name="brokenLinePaintColor" format="color" /> <attr name="lineWidth" format="dimension" /></declare-styleable></resources>
有兴趣的可以看一下
Kotlin编写RecyclerView的Adapter
ARouter使用时build.gradle的配置
- android 自定义折线图
- android 自定义折线图
- android 自定义 折线图
- android 自定义折线图
- android自定义折线图
- Android自定义折线图
- android 自定义折线图
- android 自定义折线图
- android---自定义折线图
- android 自定义折线图
- Android自定义折线图
- Android自定义折线图
- 自定义控件-- 折线图--Android
- Android中自定义折线图
- Android自定义简单折线图
- Android 自定义View 折线图
- Android自定义折线图LineChart
- Android 自定义View,实现折线图
- Linux系统之ks脚本安装虚拟机与dhcp网络配置
- CentOS 7 下使用Systemctl来管理系统服务
- React Native布局之flex
- 在Openwrt上安装alar以便进行录音,播音功能
- 深度学习之基础模型-FractalNet
- Android自定义折线图
- mysql5.7.19版本数据表名不区分大小写
- word转出图片(使用免费插件)03
- 图形学基础:第1章 颜色
- Java用MyEclipse 连接数据库
- Google Map开发系列(七)——使用谷歌地图API实现自定义控件
- 续厨师与母亲的厨艺问题的一些想法
- Android常见动画以及分类
- 使用Python机器识别库pytesseract遇到的问题