Android 实现一个自定义曲线图
Posted 隔壁小王66
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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<Integer> timeList;//读书时间
private ArrayList<String> dataList;//底部的时间
private float oneHeight; //每一个小段所要分成的高
private float oneWidth;//每一个小段所要分成的宽
private float buttomHeiht; //给底部一排日期预留出的时间
private Path baseLinePath;//折线路径
private float smoothness = 0.36f; //折线的弯曲率
private Paint baseShadow;//折线下的阴影的画笔
private ArrayList<PointF> 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"));
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
sumHeight = getMeasuredHeight();
sumWidth = getMeasuredWidth();
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
super.onSizeChanged(w, h, oldw, oldh);
private void measure()
maxTime = 100;//最大分数为100
for (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<Integer> timeList, ArrayList<String> bottomList)
this.timeList = timeList;
this.dataList = bottomList;
if (this.timeList!=null&&this.timeList.size()>0&&dataList!=null&&dataList.size()>0)
invalidate();
@Override
protected 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<PointF> 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);
//0
canvas.drawText(text, 0, sumHeight - buttomHeiht - rect.height() / 2, textPaint);
//60
text = "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);
//100
text = "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<PointF> 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<Integer> 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;
public int dp2px(Context context, float dp)
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
public float dp2px(Resources resources, float dp)
final float scale = resources.getDisplayMetrics().density;
return dp * scale + 0.5f;
public float 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<PointF> 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<Integer> timeList;//读书时间
private ArrayList<String> dataList;//底部的时间
private float oneHeight; //每一个小段所要分成的高
private float oneWidth;//每一个小段所要分成的宽
private float buttomHeiht; //给底部一排日期预留出的时间
private Path baseLinePath;//折线路径
private float smoothness = 0.36f; //折线的弯曲率
private Paint baseShadow;//折线下的阴影的画笔
private ArrayList<PointF> 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();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
sumHeight = getMeasuredHeight();
sumWidth = getMeasuredWidth();
private void measure()
maxTime = 100;//最大分数为100
for (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<Integer> timeList, ArrayList<String> bottomList)
this.timeList = timeList;
this.dataList = bottomList;
if (this.timeList!=null&&this.timeList.size()>0&&dataList!=null&&dataList.size()>0)
invalidate();
@Override
protected 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<PointF> 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);
//0
canvas.drawText(text, 0, sumHeight - buttomHeiht - rect.height() / 2, textPaint);
//60
text = "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);
//100
text = "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<PointF> 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<Integer> 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;
public int dp2px(Context context, float dp)
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5f);
public float dp2px(Resources resources, float dp)
final float scale = resources.getDisplayMetrics().density;
return dp * scale + 0.5f;
public float sp2px(Resources resources, float sp)
final float scale = resources.getDisplayMetrics().scaledDensity;
return sp * scale;
实现的效果图
以上是关于Android 实现一个自定义曲线图的主要内容,如果未能解决你的问题,请参考以下文章