Android实现自定义曲线图

一般来说应用中比较常见的是折线图,直方图这种比较多,今天来写一个项目中的需求曲线图,也是在之前的折线图基础上改进而来,看效果图
Android实现自定义曲线图
文章图片

主要考虑曲线的实现以及阴影部分的实现
先看代码:

import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import java.util.ArrayList; import java.util.List; /** * Created by wangchang on 2019/4/29. */public class StudyBendLine extends View {private float sumHeight; //总控件的高度private float sumWidth; //总空间的宽度private float maxTime; //最大的时间 用来划分单位的 最小就是20 X1.2是为了给上方和下方预留空间private Paint linePaint; //线的画笔private Paint shadowPaint; //阴影的画笔private Paint mPaint; //曲线画笔private Paint circlePaint; //圆点画笔private Paint circlePaint2; //圆点画笔private Paint textPaint; //文字的画笔private ArrayList timeList; //读书时间private ArrayList dataList; //底部的时间private float oneHeight; //每一个小段所要分成的高private float oneWidth; //每一个小段所要分成的宽private float buttomHeiht; //给底部一排日期预留出的时间private Path baseLinePath; //折线路径private float smoothness = 0.36f; //折线的弯曲率private Paint baseShadow; //折线下的阴影的画笔private ArrayList xyList; //储存定好的坐标点的集合public StudyBendLine(Context context) {super(context); initPaint(); }public StudyBendLine(Context context, AttributeSet attrs) {super(context, attrs); initPaint(); }/*** 初始化画笔** @linpaint 线条画笔* @shadowPaint 阴影画笔*/private void initPaint() {//画线的画笔linePaint = new Paint(); linePaint.setColor(Color.parseColor("#17CAAA")); linePaint.setAntiAlias(true); linePaint.setTextSize(dp2px(getContext(), 12)); linePaint.setStrokeWidth(dp2px(getContext(), 1)); //画背景的画笔shadowPaint = new Paint(); shadowPaint.setColor(Color.parseColor("#CBF2ED")); shadowPaint.setAntiAlias(true); //画最下方文字的画笔textPaint = new Paint(); textPaint.setColor(Color.parseColor("#999999")); textPaint.setAntiAlias(true); textPaint.setTextSize(dp2px(getContext(), 11)); circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); circlePaint.setColor(Color.parseColor("#8BE4D4")); circlePaint.setStrokeWidth(dp2px(getContext(), 2)); circlePaint.setStyle(Paint.Style.STROKE); circlePaint2 = new Paint(Paint.ANTI_ALIAS_FLAG); circlePaint2.setColor(Color.WHITE); circlePaint2.setStyle(Paint.Style.FILL); baseShadow = new Paint(); baseShadow.setAntiAlias(true); baseShadow.setColor((Color.WHITE & 0x40FFFFFF) | 0x10000000); baseShadow.setStyle(Paint.Style.FILL); buttomHeiht = dp2px(35); //线距离底部高度mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.parseColor("#17CAAA")); mPaint.setStrokeWidth(dp2px(getContext(), 2)); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setColor(Color.parseColor("#17CAAA")); }@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec); sumHeight = getMeasuredHeight(); sumWidth = getMeasuredWidth(); }@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh); }private void measure() {maxTime = 100; //最大分数为100for (int i = 0; i < 7; i++) {if (maxTime <= timeList.get(i)) {maxTime = timeList.get(i); }}if (maxTime < 20) {maxTime = 20; }String text = "分"; Rect rect = new Rect(); textPaint.getTextBounds(text, 0, text.length(), rect); oneHeight =((sumHeight-buttomHeiht-2*rect.height()) / maxTime); oneWidth = sumWidth / 28; }/*** 更新阅读时间*/public void updateTime(ArrayList timeList, ArrayList bottomList) {this.timeList = timeList; this.dataList = bottomList; if (this.timeList!=null&&this.timeList.size()>0&&dataList!=null&&dataList.size()>0) {invalidate(); }}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas); if (timeList == null || dataList == null) return; maxTime = getMaxTime(timeList); measure(); toGetXy(); //获取x和y的坐标toDrawLine(canvas); }private void toGetXy() {int leftWidth = dp2px(13); //距离右边宽度xyList = new ArrayList<>(); for (int i = 0; i < 7; i++) {float x = oneWidth + i * 4 * oneWidth; float time = timeList.get(i); //每点时间float y = (sumHeight - (oneHeight * time)); xyList.add(new PointF(x + leftWidth, y - buttomHeiht)); }}/*** 画线*/private void toDrawLine(Canvas canvas) {if (xyList == null || xyList.size() == 0) {return; }List NewPoints = new ArrayList<>(); NewPoints.addAll(xyList); float lX = 0; float lY = 0; baseLinePath = new Path(); baseLinePath.moveTo(NewPoints.get(0).x, NewPoints.get(0).y); for (int i = 1; i < NewPoints.size(); i++) {PointF p = NewPoints.get(i); PointF firstPointF = NewPoints.get(i - 1); float x1 = firstPointF.x + lX; float y1 = firstPointF.y + lY; PointF secondPointF = NewPoints.get(i + 1 < NewPoints.size() ? i + 1 : i); lX = (secondPointF.x - firstPointF.x) / 2 * smoothness; lY = (secondPointF.y - firstPointF.y) / 2 * smoothness; float x2 = p.x - lX; float y2 = p.y - lY; if (y1 == p.y) {y2 = y1; }baseLinePath.cubicTo(x1, y1, x2, y2, p.x, p.y); }canvas.drawPath(baseLinePath, mPaint); for (int i = 0; i < NewPoints.size(); i++) {float x = NewPoints.get(i).x; float y = NewPoints.get(i).y; int time = timeList.get(i); String mThumbText = String.valueOf(time); Rect rect = new Rect(); linePaint.getTextBounds(mThumbText, 0, mThumbText.length(), rect); canvas.drawText(mThumbText, x - rect.width() / 2, y - dp2px(6), linePaint); //画最上面字体canvas.drawCircle(x, y, dp2px(3), circlePaint); canvas.drawCircle(x, y, dp2px(2), circlePaint2); Rect rectf = new Rect(); textPaint.getTextBounds(dataList.get(i), 0, dataList.get(i).length(), rectf); canvas.drawText(dataList.get(i), x - rectf.width() / 2, sumHeight - dp2px(2), textPaint); //画最下方的字体}drawArea(canvas, NewPoints); drawBottomLine(canvas); drawLine(canvas); }/*** 分割线** @param canvas*/private void drawLine(Canvas canvas) {mPaint.setColor(Color.parseColor("#DDDDDD")); mPaint.setStrokeWidth(dp2px(0.5f)); //String text = "0"; Rect rect = new Rect(); mPaint.getTextBounds(text, 0, text.length(), rect); //0canvas.drawText(text, 0, sumHeight - buttomHeiht - rect.height() / 2, textPaint); //60text = "60"; float y = (sumHeight - (oneHeight * 60)); canvas.drawText(text, 0, y - buttomHeiht - rect.height() / 2 - dp2px(4), textPaint); canvas.drawLine(0, y - buttomHeiht, sumWidth, y - buttomHeiht, mPaint); //100text = "100(分)"; float y2 = (sumHeight - (oneHeight * 100)); canvas.drawText(text, 0, y2 - buttomHeiht - rect.height() / 2 - dp2px(4), textPaint); canvas.drawLine(0, y2 - buttomHeiht, sumWidth, y2 - buttomHeiht, mPaint); }/*** 底部标线** @param canvas*/private void drawBottomLine(Canvas canvas) {mPaint.setColor(Color.parseColor("#EBEBEB")); mPaint.setStrokeWidth(dp2px(1)); Rect rect = new Rect(); mPaint.getTextBounds("0", 0, "0".length(), rect); canvas.drawLine(0, sumHeight - dp2px(15) - rect.height() / 2, sumWidth, sumHeight - dp2px(15) - rect.height() / 2, mPaint); //画底部灰线}/*** 阴影层叠** @param canvas* @param Points*/private void drawArea(Canvas canvas, List Points) {LinearGradient mShader = new LinearGradient(0, 0, 0, getMeasuredHeight(), new int[]{Color.parseColor("#BAEFE6"), Color.parseColor("#D7F5F0"), Color.parseColor("#F9FEFD")}, new float[]{0.5f, 0.65f, 0.85f}, Shader.TileMode.REPEAT); baseShadow.setShader(mShader); if (Points.size() > 0 && xyList != null && xyList.size() > 0) {baseLinePath.lineTo(xyList.get(Points.size() - 1).x, sumHeight - buttomHeiht); baseLinePath.lineTo(xyList.get(0).x, sumHeight - buttomHeiht); baseLinePath.close(); canvas.drawPath(baseLinePath, baseShadow); }}public int dp2px(float dp) {final float scale = this.getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); }/*** 取出时间里面的最大的一个用来计算总长度** @param timeList* @return*/public float getMaxTime(List timeList) {maxTime = 0; for (int i = 0; i < 7; i++) {if (maxTime < timeList.get(i)) {maxTime = timeList.get(i); }}if (maxTime <= 10) {maxTime = 10; }maxTime *= 1.2; return maxTime; }publicint dp2px(Context context, float dp) {final float scale = context.getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); }publicfloat dp2px(Resources resources, float dp) {final float scale = resources.getDisplayMetrics().density; returndp * scale + 0.5f; }publicfloat sp2px(Resources resources, float sp){final float scale = resources.getDisplayMetrics().scaledDensity; return sp * scale; }}

首先在onMeasure获取宽高,这里主要说一下getMeasuredHeight,getMeasuredWidth与 getWidth(),getHeight()的区别,getMeasuredHeight表示view的测量高度,getHeight表示view在屏幕中的高度,当屏幕能包含view时,二者大小是一致的,当超出屏幕时,getHeight只表示在屏幕中可见部分的高度。
首先将数据整理成PointF坐标点集合,遍历坐标点集合,通过path.moveto,移动到起始点位置,通过计算贝塞尔曲线的控制点然后调用cubicTo方法实现贝塞尔曲线。
然后调用drawpath绘制整个曲线
阴影层叠通过LinearGradient 来实现
private void drawArea(Canvas canvas, List Points) {LinearGradient mShader = new LinearGradient(0, 0, 0, getMeasuredHeight(), new int[]{Color.parseColor("#BAEFE6"),Color.parseColor("#D7F5F0"), Color.parseColor("#F9FEFD")}, new float[]{0.5f, 0.65f, 0.85f}, Shader.TileMode.REPEAT); baseShadow.setShader(mShader); if (Points.size() > 0 && xyList != null && xyList.size() > 0) {baseLinePath.lineTo(xyList.get(Points.size() - 1).x, sumHeight - buttomHeiht); baseLinePath.lineTo(xyList.get(0).x, sumHeight - buttomHeiht); baseLinePath.close(); canvas.drawPath(baseLinePath, baseShadow); }}

然后你以为这就完了?no,可以看到ondraw方法中为了处理一些问题,在ondraw中进行了实例化的对象创建,这么做是错误的。
so,需要把所有的初始化操作都放置在构造方法中,因为刷新的时候会调用ondraw方法,就会导致对象重复创建,这也是自定义view优化的点。看优化之后的代码
package com.example.android.testviewpager; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import java.util.ArrayList; import java.util.List; /** * Created by wangchang on 2019/4/29. */public class StudyBendLine extends View {private float sumHeight; //总控件的高度private float sumWidth; //总空间的宽度private float maxTime; //最大的时间 用来划分单位的 最小就是20 X1.2是为了给上方和下方预留空间private Paint linePaint; //线的画笔private Paint shadowPaint; //阴影的画笔private Paint mPaint; //曲线画笔private Paint circlePaint; //圆点画笔private Paint circlePaint2; //圆点画笔private Paint scorePaint; private Paint textPaint; //文字的画笔private ArrayList timeList; //读书时间private ArrayList dataList; //底部的时间private float oneHeight; //每一个小段所要分成的高private float oneWidth; //每一个小段所要分成的宽private float buttomHeiht; //给底部一排日期预留出的时间private Path baseLinePath; //折线路径private float smoothness = 0.36f; //折线的弯曲率private Paint baseShadow; //折线下的阴影的画笔private ArrayList xyList; //储存定好的坐标点的集合public StudyBendLine(Context context) {super(context); initPaint(); }public StudyBendLine(Context context, AttributeSet attrs) {super(context, attrs); initPaint(); }/*** 初始化画笔** @linpaint 线条画笔* @shadowPaint 阴影画笔*/private void initPaint() {//画线的画笔linePaint = new Paint(); linePaint.setColor(Color.parseColor("#17CAAA")); linePaint.setAntiAlias(true); linePaint.setTextSize(dp2px(getContext(), 12)); linePaint.setStrokeWidth(dp2px(getContext(), 1)); //画背景的画笔shadowPaint = new Paint(); shadowPaint.setColor(Color.parseColor("#CBF2ED")); shadowPaint.setAntiAlias(true); //画最下方文字的画笔textPaint = new Paint(); textPaint.setColor(Color.parseColor("#999999")); textPaint.setAntiAlias(true); textPaint.setTextSize(dp2px(getContext(), 11)); circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); circlePaint.setColor(Color.parseColor("#8BE4D4")); circlePaint.setStrokeWidth(dp2px(getContext(), 2)); circlePaint.setStyle(Paint.Style.STROKE); circlePaint2 = new Paint(Paint.ANTI_ALIAS_FLAG); circlePaint2.setColor(Color.WHITE); circlePaint2.setStyle(Paint.Style.FILL); baseShadow = new Paint(); baseShadow.setAntiAlias(true); baseShadow.setColor((Color.WHITE & 0x40FFFFFF) | 0x10000000); baseShadow.setStyle(Paint.Style.FILL); buttomHeiht = dp2px(35); //线距离底部高度mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.parseColor("#17CAAA")); mPaint.setStrokeWidth(dp2px(getContext(), 2)); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeCap(Paint.Cap.ROUND); scorePaint = new Paint(Paint.ANTI_ALIAS_FLAG); scorePaint.setStyle(Paint.Style.STROKE); scorePaint.setStrokeCap(Paint.Cap.ROUND); scorePaint.setColor(Color.parseColor("#DDDDDD")); scorePaint.setStrokeWidth(dp2px(0.5f)); baseLinePath = new Path(); }@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec); sumHeight = getMeasuredHeight(); sumWidth = getMeasuredWidth(); }private void measure() {maxTime = 100; //最大分数为100for (int i = 0; i < 7; i++) {if (maxTime <= timeList.get(i)) {maxTime = timeList.get(i); }}if (maxTime < 20) {maxTime = 20; }String text = "分"; Rect rect = new Rect(); textPaint.getTextBounds(text, 0, text.length(), rect); oneHeight =((sumHeight-buttomHeiht-2*rect.height()) / maxTime); oneWidth = sumWidth / 28; }/*** 更新阅读时间*/public void updateTime(ArrayList timeList, ArrayList bottomList) {this.timeList = timeList; this.dataList = bottomList; if (this.timeList!=null&&this.timeList.size()>0&&dataList!=null&&dataList.size()>0) {invalidate(); }}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas); if (timeList == null || dataList == null) return; maxTime = getMaxTime(timeList); measure(); toGetXy(); //获取x和y的坐标toDrawLine(canvas); }private void toGetXy() {int leftWidth = dp2px(13); //距离右边宽度xyList = new ArrayList<>(); for (int i = 0; i < 7; i++) {float x = oneWidth + i * 4 * oneWidth; float time = timeList.get(i); //每点时间float y = (sumHeight - (oneHeight * time)); xyList.add(new PointF(x + leftWidth, y - buttomHeiht)); }}/*** 画线*/private void toDrawLine(Canvas canvas) {if (xyList == null || xyList.size() == 0) {return; }List NewPoints = new ArrayList<>(); NewPoints.addAll(xyList); float lX = 0; float lY = 0; baseLinePath.reset(); baseLinePath.moveTo(NewPoints.get(0).x, NewPoints.get(0).y); for (int i = 1; i < NewPoints.size(); i++) {PointF p = NewPoints.get(i); PointF firstPointF = NewPoints.get(i - 1); float x1 = firstPointF.x + lX; float y1 = firstPointF.y + lY; PointF secondPointF = NewPoints.get(i + 1 < NewPoints.size() ? i + 1 : i); lX = (secondPointF.x - firstPointF.x) / 2 * smoothness; lY = (secondPointF.y - firstPointF.y) / 2 * smoothness; float x2 = p.x - lX; float y2 = p.y - lY; if (y1 == p.y) {y2 = y1; }baseLinePath.cubicTo(x1, y1, x2, y2, p.x, p.y); }canvas.drawPath(baseLinePath, mPaint); for (int i = 0; i < NewPoints.size(); i++) {float x = NewPoints.get(i).x; float y = NewPoints.get(i).y; int time = timeList.get(i); String mThumbText = String.valueOf(time); Rect rect = new Rect(); linePaint.getTextBounds(mThumbText, 0, mThumbText.length(), rect); canvas.drawText(mThumbText, x - rect.width() / 2, y - dp2px(6), linePaint); //画最上面字体canvas.drawCircle(x, y, dp2px(3), circlePaint); canvas.drawCircle(x, y, dp2px(2), circlePaint2); Rect rectf = new Rect(); textPaint.getTextBounds(dataList.get(i), 0, dataList.get(i).length(), rectf); canvas.drawText(dataList.get(i), x - rectf.width() / 2, sumHeight - dp2px(2), textPaint); //画最下方的字体}drawArea(canvas, NewPoints); drawBottomLine(canvas); drawLine(canvas); }/*** 分割线** @param canvas*/private void drawLine(Canvas canvas) {//String text = "0"; Rect rect = new Rect(); scorePaint.getTextBounds(text, 0, text.length(), rect); //0canvas.drawText(text, 0, sumHeight - buttomHeiht - rect.height() / 2, textPaint); //60text = "60"; float y = (sumHeight - (oneHeight * 60)); canvas.drawText(text, 0, y - buttomHeiht - rect.height() / 2 - dp2px(4), textPaint); canvas.drawLine(0, y - buttomHeiht, sumWidth, y - buttomHeiht, scorePaint); //100text = "100(分)"; float y2 = (sumHeight - (oneHeight * 100)); canvas.drawText(text, 0, y2 - buttomHeiht - rect.height() / 2 - dp2px(4), textPaint); canvas.drawLine(0, y2 - buttomHeiht, sumWidth, y2 - buttomHeiht, scorePaint); }/*** 底部标线** @param canvas*/private void drawBottomLine(Canvas canvas) {Rect rect = new Rect(); scorePaint.getTextBounds("0", 0, "0".length(), rect); canvas.drawLine(0, sumHeight - dp2px(15) - rect.height() / 2, sumWidth, sumHeight - dp2px(15) - rect.height() / 2, scorePaint); //画底部灰线}/*** 阴影层叠** @param canvas* @param Points*/private void drawArea(Canvas canvas, List Points) {LinearGradient mShader = new LinearGradient(0, 0, 0, getMeasuredHeight(), new int[]{Color.parseColor("#BAEFE6"), Color.parseColor("#D7F5F0"), Color.parseColor("#F9FEFD")}, new float[]{0.5f, 0.65f, 0.85f}, Shader.TileMode.REPEAT); baseShadow.setShader(mShader); if (Points.size() > 0 && xyList != null && xyList.size() > 0) {baseLinePath.lineTo(xyList.get(Points.size() - 1).x, sumHeight - buttomHeiht); baseLinePath.lineTo(xyList.get(0).x, sumHeight - buttomHeiht); baseLinePath.close(); canvas.drawPath(baseLinePath, baseShadow); }}public int dp2px(float dp) {final float scale = this.getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); }/*** 取出时间里面的最大的一个用来计算总长度** @param timeList* @return*/public float getMaxTime(List timeList) {maxTime = 0; for (int i = 0; i < 7; i++) {if (maxTime < timeList.get(i)) {maxTime = timeList.get(i); }}if (maxTime <= 10) {maxTime = 10; }maxTime *= 1.2; return maxTime; }publicint dp2px(Context context, float dp) {final float scale = context.getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); }publicfloat dp2px(Resources resources, float dp) {final float scale = resources.getDisplayMetrics().density; returndp * scale + 0.5f; }publicfloat sp2px(Resources resources, float sp){final float scale = resources.getDisplayMetrics().scaledDensity; return sp * scale; }}

实现的效果图
【Android实现自定义曲线图】Android实现自定义曲线图
文章图片

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

    推荐阅读