用Bezier绘制圆滑曲线

版权声明:本文为博主原创文章,未经博主允许不得转载。 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设计得需求。

猜你喜欢

转载自blog.csdn.net/haoyuegongzi/article/details/80149550