我的博客: https://devallever.github.io/
原文链接: Android 根据一系列坐标点绘制贝塞尔曲线
写在前面
该控件是项目中用到的,业务逻辑较多。但作为一个自定义View离不开那几部分
- 重写onMeasure,测量控件大小
- 重写onDraw,绘制界面,其中可能用到部分数学几何知识,但不难。绘制就那么几个drawXXX的方法,只要一步一步走,还是相对简单。
- 重写onTouch,处理交互。
最后附上项目地址
https://github.com/devallever/AndroidUIKit
- https://github.com/devallever/AndroidUIKit/tree/master/app/src/main/java/com/allever/app/ui/widget/linechartview
功能概述:
给定一系列坐标,绘制它的贝塞尔曲线,并有渐变效果,如图:
[图片上传失败...(image-419da5-1582547444804)]
详细实现
把一般坐标转为 Android中的视图坐标
因为现实中的坐标和Android中的坐标是不同的,Android中向下为Y轴正方向
List oldPointF1 = new ArrayList<>();
oldPointF1.add(new PointF(1, 100));
oldPointF1.add(new PointF(2, 200));
oldPointF1.add(new PointF(3, 150));
oldPointF1.add(new PointF(4, 200));
oldPointF1.add(new PointF(5, 50));
oldPointF1.add(new PointF(6, 150));
oldPointF1.add(new PointF(7, 100));
oldPointF1.add(new PointF(8, 200));
oldPointF1.add(new PointF(9, 100));
oldPointF1.add(new PointF(10, 150));
oldPointF1.add(new PointF(11, 50));
oldPointF1.add(new PointF(12, 100));
List pointFs1 = changePoint(oldPointF1);
/**
* 把一般坐标转为 Android中的视图坐标**/
private List changePoint(List oldPointFs){
List pointFs = new ArrayList<>();
//间隔,减去某个值是为了空出多余空间,为了画线以外,还要写坐标轴的值,除以坐标轴最大值(这里设为定值)
//相当于缩小图像
int intervalX = (getMeasuredWidth()-20)/12;
int intervalY = (getMeasuredHeight()-20)/250;
int height = getMeasuredHeight();
PointF p;
float x;
float y;
for (PointF pointF: oldPointFs){
//最后的正负值是左移右移
x = pointF.x * intervalX + 0;
y = height - pointF.y * intervalY - 100;
p = new PointF(x, y);
pointFs.add(p);
}
return pointFs;
}
计算控制点
参考文章:根据多个点使用canvas贝赛尔曲线画一条平滑的曲线
在这里仅把求控制点公式转换成java代码
首先封装控制点类:ControlPoint.java
package com.allever.bicycle.chartTest;
import android.graphics.PointF;
import java.util.ArrayList;
import java.util.List;
/**
* Created by allever on 17-7-27.
*/
public class ControlPoint {
private PointF conPoint1;
private PointF conPoint2;
public ControlPoint(PointF p1, PointF p2){
this.conPoint1 = p1;
this.conPoint2 = p2;
}
public PointF getConPoint1() {
return conPoint1;
}
public void setConPoint1(PointF conPoint1) {
this.conPoint1 = conPoint1;
}
public PointF getConPoint2() {
return conPoint2;
}
public void setConPoint2(PointF conPoint2) {
this.conPoint2 = conPoint2;
}
public static List getControlPointList(List pointFs){
List controlPoints = new ArrayList<>();
PointF p1;
PointF p2;
float conP1x;
float conP1y;
float conP2x;
float conP2y;
for (int i=0; i
其中getControlPointList()方法用来求每一断贝塞尔曲线的控制点,需要传入点坐标的List。一段曲线有两个控制点,因为使用三阶贝塞尔曲线,把每一段的控制点(两个)放到List中返回。
这里进行了判断,是因为第一段和左后一段求控制点与其他断有区别。可以查看上面文章公式。
绘制贝塞尔曲线
Path mPath1 = new Path();
//贝塞尔曲线获取控制点
List controlPoints1 = ControlPoint.getControlPointList(pointFs1);
for (int i=0; i
颜色渐变
把画笔paint设置线性渐变LinearGradient,查看该构造方法,可以知道,
第一个参数,0,为x中开始渐变
第二个参数,0,为y轴开始渐变
第三个参数。0,为x轴结束渐变
第四个参数,为y轴结束渐变,
这里这样设置就是只在y轴上渐变
颜色数组:
依次是从上到下的渐变颜色
LinearGradient mLinearGradient = new LinearGradient(
0,
0,
0,
getMeasuredHeight(),
new int[]{
0xffffffff,
getResources().getColor(R.color.cyan),
getResources().getColor(R.color.colorPrimary),
getResources().getColor(R.color.colorPrimary),
getResources().getColor(R.color.colorPrimary)},
null,
Shader.TileMode.CLAMP
);
mPaint.setShader(mLinearGradient);
绘制坐标和分割线
//画分割线和纵坐标
Paint linePaint = new Paint();
linePaint.setColor(0x66cccccc);
linePaint.setTextSize(40f);
//画5条分割线
for (int i=0; i<5; i++){
canvas.drawLine(90, getMeasuredHeight()-((getMeasuredHeight()/6) * i) - 140, getMeasuredWidth(), getMeasuredHeight()-((getMeasuredHeight()/6) * i) - 140,linePaint);
canvas.drawText(45*i+"",10, getMeasuredHeight()-((getMeasuredHeight()/6) * i) - 130,linePaint);
canvas.save();
}
//画横坐标
linePaint.setTextSize(30f);
for (int i=0; i<12; i++){
canvas.drawText((i+1)+":00", (getMeasuredWidth())/13*i + 100, getMeasuredHeight()-((getMeasuredHeight()/5) * 0) - 60, linePaint);
canvas.save();
}
第二版更新
可以设置数据,曲线数量
曲线数据封装
LineDataSet.java
public class LineDataSet {
private int color;
private int[] gradientColors;
private List oldPointFsList;
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int[] getGradientColors() {
return gradientColors;
}
public void setGradientColors(int[] gradientColors) {
this.gradientColors = gradientColors;
}
public List getOldPointFsList() {
return oldPointFsList;
}
public void setOldPointFsList(List oldPointFsList) {
this.oldPointFsList = oldPointFsList;
}
}
color: 指定曲线颜色
grandientColors: 颜色数组,用来设定渐变效果
oldPointFsList: 坐标点的集合
定义公开方法
/**
* 添加数据
* @param lineDataSet 曲线参数集**/
public void addLineData(LineDataSet lineDataSet){
mLineDataSetList.add(lineDataSet);
invalidate();
}
public void removeLine(LineDataSet lineDataSet){
mLineDataSetList.remove(lineDataSet);
invalidate();
}
mLineDataSetList: 是曲线参数集合, 用来存储每一条曲线.
循环绘制曲线
//循环绘制曲线------------------------------------------------------------------------
mBezierPaint.setStyle(Paint.Style.STROKE);
mBezierPaint.setStrokeWidth(10f);//设置线宽
mBezierPaint.setAntiAlias(true);//去除锯齿
/**
* 根据给定参数绘制曲线
* **/
for (int position=0; position < mLineDataSetList.size(); position++){
List pointFList = changePoint(mLineDataSetList.get(position).getOldPointFsList());
List controlPoints = ControlPoint.getControlPointList(pointFList);
Path linePath = new Path();
for (int i =0; i
首先根据mLineDataSetList.size()数量决定绘制多少条曲线
首先, 把原来坐标转换为Android视图坐标
List pointFList = changePoint(mLineDataSetList.get(position).getOldPointFsList());
更新后的转换方法如下:
/**
* 把一般坐标转为 Android中的视图坐标**/
private List changePoint(List oldPointFs){
List pointFs = new ArrayList<>();
//间隔,减去某个值是为了空出多余空间,为了画线以外,还要写坐标轴的值,除以坐标轴最大值
//相当于缩小图像
float intervalX = (getMeasuredWidth() - mMarginLeftRight * 2f)/11f;
//float intervalY = (getMeasuredHeight() - mMarginTopBottom * 2f)/200f; 原始
float intervalY = (getMeasuredHeight() - mMarginTopBottom * 2f)/200f-0.4f;
Log.d(TAG, "intervalX = " + intervalX);
Log.d(TAG, "intervalY = " + intervalY);
int height = getMeasuredHeight();
Log.d(TAG, "width = " + getMeasuredWidth());
Log.d(TAG, "height = " +getMeasuredHeight());
PointF p;
float x;
float y;
for (int i = 0; i< oldPointFs.size(); i++){
PointF pointF = oldPointFs.get(i);
//最后的正负值是左移右移
x = (pointF.x-1) * intervalX + mMarginLeftRight;
//y = height - mMarginTopBottom - intervalY*pointF.y; 原始
y = height - mMarginTopBottom - intervalY*pointF.y - 30f;
p = new PointF(x, y);
pointFs.add(p);
Log.d(TAG, "oldX = " + pointF.x + "\toldY = " + pointF.y);
Log.d(TAG, "newX = " + x + "\tnewY = " + y);
}
/* for (PointF pointF: oldPointFs){
}*/
return pointFs;
}
然后根据转换后的坐标算出每一段贝塞尔曲线的控制点,以三阶曲线为例,一段曲线有两个控制点
然后绘制贝塞尔曲线路径
for (int i =0; i
cubicT()的方法为
/**
* Add a cubic bezier from the last point, approaching control points
* (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
* made for this contour, the first point is automatically set to (0,0).
*
* @param x1 The x-coordinate of the 1st control point on a cubic curve
* @param y1 The y-coordinate of the 1st control point on a cubic curve
* @param x2 The x-coordinate of the 2nd control point on a cubic curve
* @param y2 The y-coordinate of the 2nd control point on a cubic curve
* @param x3 The x-coordinate of the end point on a cubic curve
* @param y3 The y-coordinate of the end point on a cubic curve
*/
public void cubicTo(float x1, float y1, float x2, float y2,
float x3, float y3) {
isSimplePath = false;
native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}
看参数解析是需要两个控制点,和一个结束点,因为一段曲线有两个点连接.一个起点,一个终点,这里的第三个坐标值就是终点坐标.
接着根据LineDataSet设置曲线的渐变和颜色
//设置颜色和渐变
int lineColor = mLineDataSetList.get(position).getColor();
mBezierPaint.setColor(lineColor);
LinearGradient mLinearGradient = null;
if (mLineDataSetList.get(position).getGradientColors() != null){
//设置渐变
mLinearGradient = new LinearGradient(
0,
0,
0,
getMeasuredHeight(),
mLineDataSetList.get(position).getGradientColors(),
null,
Shader.TileMode.CLAMP
);
}else {
mLinearGradient = new LinearGradient(
0,
0,
0,
getMeasuredHeight(),
new int[]{lineColor,lineColor,lineColor,lineColor,lineColor},
null,
Shader.TileMode.CLAMP);
}
mBezierPaint.setShader(mLinearGradient);
因为可能不需要设置曲线渐变,所以对数组进行判空处理,有设置渐变的就用设置的,没有设置渐变的就去原来颜色做渐变
最后绘制贝塞尔曲线
canvas.drawPath(linePath,mBezierPaint);
canvas.save();
使用
在布局中引用控件
在代码中设置曲线参数:坐标,颜色,渐变色数组
public void initData(){
List speedPoint = new ArrayList<>();
speedPoint.add(new PointF(1, 190));
speedPoint.add(new PointF(2, 70));
speedPoint.add(new PointF(3, 190));
speedPoint.add(new PointF(4, 70));
speedPoint.add(new PointF(5, 190));
speedPoint.add(new PointF(6, 70));
speedPoint.add(new PointF(7, 190));
speedPoint.add(new PointF(8, 70));
speedPoint.add(new PointF(9, 190));
speedPoint.add(new PointF(10, 70));
speedPoint.add(new PointF(11, 190));
speedPoint.add(new PointF(12, 70));
mSpeedLineDataSet = new LineDataSet();
mSpeedLineDataSet.setOldPointFsList(speedPoint);
mSpeedLineDataSet.setColor(getResources().getColor(R.color.colorPrimary));
mSpeedLineDataSet.setGradientColors(new int[]{
0xffffffff,
getResources().getColor(R.color.cyan),
getResources().getColor(R.color.colorPrimary),
getResources().getColor(R.color.colorPrimary),
getResources().getColor(R.color.colorPrimary)});
mLineChartView.addLineData(mSpeedLineDataSet);
List runSpeedPoint = new ArrayList<>();
runSpeedPoint.add(new PointF(1, 50));
runSpeedPoint.add(new PointF(2, 200));
runSpeedPoint.add(new PointF(3, 10));
runSpeedPoint.add(new PointF(4, 200));
runSpeedPoint.add(new PointF(5, 10));
runSpeedPoint.add(new PointF(6, 200));
runSpeedPoint.add(new PointF(7, 10));
runSpeedPoint.add(new PointF(8, 200));
runSpeedPoint.add(new PointF(9, 10));
runSpeedPoint.add(new PointF(10, 200));
runSpeedPoint.add(new PointF(11, 10));
runSpeedPoint.add(new PointF(12, 50));
mRunSpeedLineDataSet = new LineDataSet();
mRunSpeedLineDataSet.setOldPointFsList(runSpeedPoint);
mRunSpeedLineDataSet.setColor(Color.YELLOW);
mRunSpeedLineDataSet.setGradientColors(new int[]{
0xffffffff,
Color.GREEN,
Color.YELLOW,
Color.YELLOW,
Color.YELLOW});
mLineChartView.addLineData(mRunSpeedLineDataSet);
List oppositionPoint = new ArrayList<>();
oppositionPoint.add(new PointF(1, 100));
oppositionPoint.add(new PointF(2, 150));
oppositionPoint.add(new PointF(3, 30));
oppositionPoint.add(new PointF(4, 60));
oppositionPoint.add(new PointF(5, 80));
oppositionPoint.add(new PointF(6, 130));
oppositionPoint.add(new PointF(7, 200));
oppositionPoint.add(new PointF(8, 160));
oppositionPoint.add(new PointF(9, 20));
oppositionPoint.add(new PointF(10, 85));
oppositionPoint.add(new PointF(11, 200));
oppositionPoint.add(new PointF(12, 70));
mOppositionLineDataSet = new LineDataSet();
mOppositionLineDataSet.setOldPointFsList(oppositionPoint);
mOppositionLineDataSet.setColor(getResources().getColor(R.color.colorAccent));
mLineChartView.addLineData(mOppositionLineDataSet);
List powerPoint = new ArrayList<>();
powerPoint.add(new PointF(1, 100));
powerPoint.add(new PointF(2, 140));
powerPoint.add(new PointF(3, 100));
powerPoint.add(new PointF(4, 140));
powerPoint.add(new PointF(5, 100));
powerPoint.add(new PointF(6, 140));
powerPoint.add(new PointF(7, 100));
powerPoint.add(new PointF(8, 140));
powerPoint.add(new PointF(9, 100));
powerPoint.add(new PointF(10, 140));
powerPoint.add(new PointF(11, 100));
powerPoint.add(new PointF(12, 140));
mPowerLineDataSet = new LineDataSet();
mPowerLineDataSet.setOldPointFsList(powerPoint);
mPowerLineDataSet.setColor(Color.RED);
mPowerLineDataSet.setGradientColors(new int[]{
0xffffffff,
Color.RED,
Color.RED,
Color.RED,
Color.RED});
mLineChartView.addLineData(mPowerLineDataSet);
List heartPoint = new ArrayList<>();
heartPoint.add(new PointF(1, 30));
heartPoint.add(new PointF(2, 170));
heartPoint.add(new PointF(3, 30));
heartPoint.add(new PointF(4, 170));
heartPoint.add(new PointF(5, 30));
heartPoint.add(new PointF(6, 170));
heartPoint.add(new PointF(7, 30));
heartPoint.add(new PointF(8, 170));
heartPoint.add(new PointF(9, 30));
heartPoint.add(new PointF(10, 170));
heartPoint.add(new PointF(11, 30));
heartPoint.add(new PointF(12, 170));
mHeartLineDataSet = new LineDataSet();
mHeartLineDataSet.setOldPointFsList(heartPoint);
mHeartLineDataSet.setColor(Color.BLUE);
mHeartLineDataSet.setGradientColors(new int[]{
0xffffffff,
Color.BLUE,
Color.BLUE,
Color.BLUE,
Color.BLUE});
mLineChartView.addLineData(mHeartLineDataSet);
}
效果图:
[图片上传失败...(image-eb618d-1582547444805)]
后续更新
可以设置坐标, 触控显示竖线和弹出Y值