版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haoyuegongzi/article/details/80149550
新项目需求里面,需要根据不同的数据,绘制一条圆滑曲线,最开始想到的是用二阶贝塞尔曲线来绘制。两个数据点,一个控制点,三个数据,从第0个开始,依次推进。实践的时候 发现不对,绘制出来的数据跟UI要求的或者跟预期中的差距太远了。
项目需求的曲线
在度娘哪里得到的信息,无外乎两种:
一种就是教你如何绘制三个点下的二阶贝塞尔,或者说4个点下的三阶贝塞尔。笑话,单单几个点独立成线并且将数据点和控制点都告诉你的绘制,我也会。问题是,我现在只告诉你两个数据点,没有控制点,你如何绘制出完美的贝塞尔曲线?
第二种就是装13的货,列出一大堆高深莫测的贝塞尔推导公式,能彻底理解这些公式的数学大牛有几个?话说我都这么牛逼的数学大牛了,还这里给你瞎BBB?一点也不实际。
另外一个共同点就是,无论第一种情况还是第二种情况,原创的都太少了,都是你抄过去,我抄过来,对于脸盲的我来说,一脸懵逼。
度娘没有,没大神指导,就自己研究呗。
后面研究发现,绘制这样的曲线其实需要更高阶的三阶贝塞尔曲线来完成。并且在两个数据点和两个控制点的坐标关系上,有如下规律。有了这个规律,就能大大减轻计算复杂程度了。具体规律是:
三阶贝塞尔曲线的绘制方法:linePath.moveTo()、linePth.cubicTo();
绘制三阶贝塞尔曲线,需要4个点:两端的两个数据点(startPoint,endPoint),中间两个控制点(controlAPoint、controlBPoint)。
这4个点的坐标值的确定方法(假如给定数据List<Integer> list):
一、startPoint:
1、当绘制第一个点的时候:start.x为曲线起始点到View左边界的距离,可以简单的理解为paddingLeft,如下图中红框框1的宽度;start.y为list.get(0);
2、当绘制第1+N个点的时候,startPoint = endPoint;endPoint后面会介绍。
二、controlAPoint:
controlA.x = start.x + L/2; controlA.y =start.y; L为如上图中红框框2的宽度。
三、controlBPoint:
controlB.x = controlA.x; controlB.y = end.y;
四、endPoint:
end.x = start.x + L;同上,L为如上图中红框框2的宽度。
End.y = list.get(i)(注意,这里的i>0,从1开始);
这是绘制三阶贝塞尔曲线的4个坐标点的关系计算。
具体绘制如下:
public class MyBezierChhar extends View {
/**曲线的画笔*/
private Paint linePaint;
/**锚点的画笔*/
private Paint pointPaint;
/**轴线文本,坐标文本的画笔*/
private Paint textPaint;
/**警示框文本的画笔*/
private Paint warnPaint;
/**轴线的画笔*/
private Paint shaftPaint;
private Paint warnTextPaint;
private Path warnPath;
private Path linePath;
private Path shaftPath;
private int viewWidth;
private int viewHight;
private float textPaintSize = 24f;
private float lineWidth = 4f;
private float pointWidth = 8f;
float maxValue = 0;
/**Y轴坐标偏移量*/
private int offSetShaftY = dpTopx(5);
/**X轴坐标偏移量*/
private int offSetShaftX = dpTopx(5);
/**在onDraw方法里面实例化*/
private PointF[] points;
private List<Float> list;
private List<String> dateList;
private void initNativeParams(){
linePaint = new Paint();
linePaint.setStrokeWidth(lineWidth);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeCap(Paint.Cap.ROUND);
linePaint.setColor(Color.parseColor("#0072ff"));
pointPaint = new Paint();
pointPaint.setStrokeWidth(pointWidth);
pointPaint.setAntiAlias(true);
pointPaint.setStyle(Paint.Style.FILL);
pointPaint.setStrokeCap(Paint.Cap.ROUND);
pointPaint.setColor(Color.parseColor("#0072ff"));
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setColor(Color.parseColor("#0072ff"));
textPaint.setTextSize(textPaintSize);
textPaint.setTextAlign(Paint.Align.CENTER);
warnPaint = new Paint();
warnPaint.setAntiAlias(true);
warnPaint.setStyle(Paint.Style.FILL);
warnPaint.setColor(Color.parseColor("#ffa200"));
float warnTextPaintSize = 24f;
warnTextPaint = new Paint();
warnTextPaint.setTextSize(warnTextPaintSize);
warnTextPaint.setAntiAlias(true);
warnTextPaint.setStyle(Paint.Style.FILL);
/**左对齐*/
warnTextPaint.setTextAlign(Paint.Align.LEFT);
warnTextPaint.setColor(Color.parseColor("#ffffff"));
shaftPaint = new Paint();
shaftPaint.setStrokeWidth(0.5f);
shaftPaint.setAntiAlias(true);
shaftPaint.setStyle(Paint.Style.STROKE);
shaftPaint.setColor(Color.parseColor("#0072ff"));
linePath = new Path();
shaftPath = new Path();
warnPath = new Path();
list = new ArrayList<>();
}
public MyBezierChhar(Context context) {
super(context);
initNativeParams();
}
public MyBezierChhar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initNativeParams();
}
public MyBezierChhar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initNativeParams();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = w;
viewHight = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**绘制背景色*/
canvas.drawColor(Color.parseColor("#ffffffff"));
/**offSetShaftY:Y轴坐标偏移量,分正负和方向*/
canvas.translate(0, offSetShaftY);
/**offSetShaftX:X轴坐标偏移量,分正负和方向*/
canvas.drawLine(offSetShaftX,0, viewWidth, 0, shaftPaint);
/**areaNumber个数据,areaNumber-1根线*/
int areaNumber = getList().size();
/**Y轴便宜后,水平方向还可以用的宽度*/
int shaftLineWidth = viewWidth - offSetShaftY;
/**平均每个区间的宽度*/
float averageWidth = shaftLineWidth / areaNumber;
/**areaNumber个数据, areaNumber-1根线,第0根线(也可以看做Y坐标),不画,作用从1开始*/
for (int i = 1; i < areaNumber; i++) {
float coordinates = offSetShaftY + averageWidth * i;
canvas.drawLine(coordinates, 0, coordinates, viewHight, shaftPaint);
}
/**Y轴方向偏移5dp的距离,绘制坐标文本*/
if (dateList.size() != areaNumber) {
return;
}
for (int i = 0; i < dateList.size(); i++) {
float locationX = averageWidth / 2 + averageWidth * i;
canvas.drawText(dateList.get(i), locationX, (offSetShaftY + textPaintSize), textPaint);
}
float drawAreaY = (viewHight - offSetShaftY - textPaintSize) * 0.75f;
/**绘制曲线*/
/**曲线绘制区域:
* X方向:offSetShaftX开始的整个水平方向
* Y方向:(viewWidth(View的整体高度) - offSetShaftY(垂直方向的偏移量) - textPaintSize(坐标值得尺寸))*0.75的区域内*/
/**最大的值*/
for (int i = 0; i < list.size(); i++) {
if(list.get(i) > maxValue){
maxValue = list.get(i);
}
}
/**所有点都位于自己区域内水平中心线上,因此所有点的X坐标公式:
* float locationX = averageWidth / 2 + averageWidth*i;
**/
/**起始点的坐标*/
PointF start = new PointF();
start.x = (averageWidth / 2) + offSetShaftX;
/**(list.get(0) / maxValue):算出给定的数据占总高度的百分比, 乘drawAreaY就得出新的高度*/
start.y = viewHight - ((list.get(0) / maxValue) * drawAreaY + offSetShaftY + textPaintSize + dpTopx(12));
points[0] = start;
linePath.moveTo(start.x, start.y);
for (int i = 1; i < list.size(); i++) {
PointF end = new PointF();
end.x = averageWidth / 2 + offSetShaftX + averageWidth * i;
end.y = viewHight - ((list.get(i) / maxValue) * drawAreaY + offSetShaftY + textPaintSize + dpTopx(12));
float controlPointA = start.x + averageWidth / 2;
/**控制点1*/
PointF controlA = new PointF();
controlA.set(controlPointA, start.y);
/**控制点2*/
PointF controlB = new PointF();
controlB.set(controlPointA, end.y);
linePath.cubicTo(controlA.x, controlA.y, controlB.x, controlB.y, end.x, end.y);
start = end;
points[i] = end;
}
canvas.drawPath(linePath, linePaint);
for (int i = 0; i < points.length; i++) {
PointF pointF = points[i];
if (pointF == null) {
return;
}
/**绘制锚点*/
canvas.drawCircle(pointF.x, pointF.y, pointWidth, pointPaint);
/**绘制坐标值*/
canvas.drawText(String.valueOf(list.get(i)), pointF.x + textPaintSize, pointF.y - textPaintSize, textPaint);
/**不合格数据标识*/
if (list.get(i) < 50) {
/**圆角矩形*/
float startX = pointF.x + (textPaintSize/2);
float startY = pointF.y + (textPaintSize/2);
float endX = pointF.x + textPaintSize + 80;
float endY = pointF.y + textPaintSize + 30;
RectF rectF = new RectF(startX, startY, endX, endY);
warnPath.addRoundRect(rectF, 10, 10, Path.Direction.CCW);
canvas.drawPath(warnPath, warnPaint);
/**不合格警示文本*/
String warnText = "未达标";
canvas.drawText(warnText, startX + 9, startY + 29, warnTextPaint);
}
}
}
public List<Float> getList() {
if (list == null) {
return new ArrayList<>();
}
return list;
}
public void setList(List<Float> list) {
this.list = list;
postInvalidate();
}
public List<String> getDateList() {
if (dateList == null) {
return new ArrayList<>();
}
return dateList;
}
public void setDateList(List<String> dateList) {
this.dateList = dateList;
if (dateList == null || dateList.size() < 1) {
return;
}
points = new PointF[dateList.size()];
postInvalidate();
}
private int dpTopx(float dipValue) {
final float scale = getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
private int spTopx(float spValue) {
final float fontScale = getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
Activity里面用法如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/back_ground"
tools:context="com.haoyue.demolist.bezier.MyBeizierActivity">
<FrameLayout
android:id="@+id/flAddNewView"
android:layout_width="match_parent"
android:layout_height="300dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_margin="10dp"
android:background="@drawable/view_click"
android:text="加载曲线图"
android:gravity="center"
android:onClick="loadBeizier"/>
</LinearLayout>
Java代码如下:
FrameLayout flAddNewView;
MyBezierChhar bezierChhar;
List<Float> list = new ArrayList<>();
List<String> dateList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_beizier);
flAddNewView = findViewById(R.id.flAddNewView);
bezierChhar = new MyBezierChhar(this);
}
public void loadBeizier(View view){
int viewNumber = flAddNewView.getChildCount();
if (viewNumber > 0) {
bezierChhar.postInvalidate();
return;
}
for (int i = 1; i < 11; i++) {
dateList.add(i + "日");
}
list.add(200f);
list.add(100f);
list.add(300f);
list.add(20f);
list.add(120f);
list.add(60f);
list.add(160f);
list.add(300f);
list.add(50f);
list.add(150f);
bezierChhar.setList(list);
bezierChhar.setDateList(dateList);
flAddNewView.addView(bezierChhar);
}
实际运行效果图:
完全满足了UI设计得需求。