Android 根据一系列坐标点绘制贝塞尔曲线

我的博客: 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值

你可能感兴趣的:(Android 根据一系列坐标点绘制贝塞尔曲线)