美文网首页
16_Android数学曲线绘制(View、Compose双版本

16_Android数学曲线绘制(View、Compose双版本

作者: 刘加城 | 来源:发表于2023-01-26 19:42 被阅读0次

        很早就有一个想法:把数学中的各种曲线在Android上绘制出来。本文将从Android的View和Jetpack Compose双版本出发,绘制一元一次直线、二次方曲线、三次方曲线、指数曲线、对数曲线、正余弦曲线(及变形)、贝塞尔曲线。View版本使用Java语言编写,Compose版本使用Kotlin语言编写。

    (1)坐标系绘制

        “工欲善其事,必先利其器”,在绘制各种曲线之前,首先要有一个坐标系,本小节就先来绘制它。
        目标效果图:



        View版本中的实现:将View的中心作为原点,画x轴、y轴、箭头及标识。

    //坐标轴自定义View
    public class AxisView extends View {
        Paint paint;
        static final int X_MARGIN = 40;
        static final int Y_MARGIN = 80;
        static final int DEFAULT_MARGIN = 50;
        final int DP_10 = dipToPixel(10);
        final int DP_5 = dipToPixel(5);
        final int DP_20 = dipToPixel(20);
    
        Point leftPoint; //坐标x轴起始点
        Point rightPoint;//坐标x轴终止点,箭头方向
        Point xArrowUpPoint; //x轴上箭头起始点坐标
        Point xArrowDownPoint;//x轴下箭头起始点坐标
    
        Point bottomPoint;//左边y轴起始点
        Point topPoint;//坐标y轴终止点,箭头方向
        Point yArrowLeftPoint;// y轴左箭头起始点坐标
        Point yArrowRightPoint;//y轴右箭头起始点坐标
    
        Point originPoint;//原点
        String originTxt = "0";
    
        Point xLabelPoint;//x字符坐标
        String xStr = "x";
    
        Point yLabelPoint;//y字符坐标
        String yStr = "y";
    
        public AxisView(Context context) {
            super(context);
            init();
        }
    
        public AxisView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        private int dipToPixel(int dip) {
            float scale = getResources().getDisplayMetrics().scaledDensity;
            return (int) (dip * scale + 0.5f);
        }
    
        private void init() {
            paint = new Paint();
            paint.setStrokeWidth(6.0f);
            paint.setAntiAlias(true);
            paint.setColor(Color.BLACK);
            paint.setTextSize(dipToPixel(15));
        }
    
        private void initPoint() {
            //View高宽
            int width = getWidth();
            int height = getHeight();
    
            //x轴起始点计算
            int leftX = getLeft() + X_MARGIN;
            int leftY = getTop() + height / 2;
            leftPoint = new Point(leftX, leftY);
    
            //x轴终止点计算
            int rightX = getRight() - X_MARGIN;
            int rightY = leftY;//一条水平线,y坐标相等
            rightPoint = new Point(rightX, rightY);
    
            //x轴箭头的上、下点计算
            xArrowUpPoint = new Point(rightX - DP_10, rightY - DP_5);
            xArrowDownPoint = new Point(rightX - DP_10, rightY + DP_5);
    
            //x label计算
            xLabelPoint = new Point(rightX - DP_20, rightY + DP_20);
    
            //y轴终止点计算
            int topX = getLeft() + width / 2;
            int topY = getTop() + Y_MARGIN;
            topPoint = new Point(topX, topY);
    
            //y轴箭头的左右点计算
            yArrowLeftPoint = new Point(topX - DP_5, topY + DP_10);
            yArrowRightPoint = new Point(topX + DP_5, topY + DP_10);
    
            //y轴起始点计算
            int bottomX = topX;//一条垂直线,x坐标相等
            int bottomY = getBottom() - Y_MARGIN;
            bottomPoint = new Point(bottomX, bottomY);
    
            //y label计算
            yLabelPoint = new Point(topX + DP_10, topY + DP_10 + DP_5);
    
            //坐标原点
            int originX = getLeft() + width / 2 - DEFAULT_MARGIN - dipToPixel(3);
            int originY = getTop() + height / 2 + DEFAULT_MARGIN + dipToPixel(8);
            originPoint = new Point(originX, originY);
    
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            initPoint();
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            drawX(canvas); //绘制x轴
    
            drawY(canvas);//绘制y轴
    
            drawOrigin(canvas);//绘制坐标原点
    
        }
    
        //绘制x轴
        private void drawX(Canvas canvas) {
            //绘制水平线
            canvas.drawLine(leftPoint.x, leftPoint.y, rightPoint.x, rightPoint.y, paint);
    
            //绘制箭头
            canvas.drawLine(xArrowUpPoint.x, xArrowUpPoint.y, rightPoint.x, rightPoint.y, paint);
            canvas.drawLine(xArrowDownPoint.x, xArrowDownPoint.y, rightPoint.x, rightPoint.y, paint);
    
            //绘制x标识
            canvas.drawText(xStr,xLabelPoint.x,xLabelPoint.y,paint);
        }
    
        //绘制y轴
        private void drawY(Canvas canvas) {
            //绘制y轴
            canvas.drawLine(bottomPoint.x, bottomPoint.y, topPoint.x, topPoint.y, paint);
    
            //绘制箭头
            canvas.drawLine(yArrowLeftPoint.x, yArrowLeftPoint.y, topPoint.x, topPoint.y, paint);
            canvas.drawLine(yArrowRightPoint.x, yArrowRightPoint.y, topPoint.x, topPoint.y, paint);
    
            //绘制y标识
            canvas.drawText(yStr,yLabelPoint.x,yLabelPoint.y,paint);
        }
    
        //绘制坐标原点
        private void drawOrigin(Canvas canvas) {
            //绘制原点
            canvas.drawText(originTxt, originPoint.x, originPoint.y, paint);
        }
    }
    

        在xml中的引用:

        <com.xxx.xxx.curve.AxisView
            android:layout_width="match_parent"
            android:layout_height="match_parent"></com.xxx.xxx.curve.AxisView>
    

        再来看Compose版本,首先在build.gradle中添加各种配置项:

        buildFeatures {
            compose true
        }
    
        composeOptions {
            kotlinCompilerExtensionVersion = "1.3.2" 
        }
    
    dependencies {
        def composeBom = platform('androidx.compose:compose-bom:2022.12.00')
        implementation composeBom
        androidTestImplementation composeBom
        // Material Design 3
        implementation 'androidx.compose.material3:material3'
        // or Material Design 2
        implementation 'androidx.compose.material:material'
        // or skip Material Design and build directly on top of foundational components
        implementation 'androidx.compose.foundation:foundation'
        // or only import the main APIs for the underlying toolkit systems,
        // such as input and measurement/layout
        implementation 'androidx.compose.ui:ui'
    
        implementation 'androidx.activity:activity-compose:1.6.1'
    
        // Android Studio Preview support
        implementation 'androidx.compose.ui:ui-tooling-preview'
        debugImplementation 'androidx.compose.ui:ui-tooling'
    }
    

        Compose版本是通过函数Canvas()来实现自定义View的。这里需要区分,是Canvas类还是Canvas()函数,导入包时它们非常的相似,如下:

    import androidx.compose.foundation.Canvas
    import androidx.compose.ui.graphics.Canvas
    import android.graphics.Canvas
    

        这三者中,第一个其实是Canvas()函数,也是我们将要使用的,它定义在文件androidx.compose.foundation.CanvasKt.kt中;第二个是compose ui包中的一个Canvas类,一开始被它误导了,以为它是正主,但又一直报错,找了很久的资料才纠正过来;第三个是Android 原始的Canvas。
        如果从Java虚拟机的角度,第一个导入会被当作CanvasKt类中的static方法Canvas(...)。Kotlin的这种设计,在一定程度上,非常地误导人。
        代码如下:

    import android.os.Bundle
    import android.graphics.Paint
    import androidx.activity.ComponentActivity
    
    import androidx.compose.runtime.Composable
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.Canvas
    import androidx.compose.foundation.layout.*
    import androidx.compose.ui.tooling.preview.Preview
    import androidx.compose.ui.geometry.Offset
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.graphics.nativeCanvas
    import androidx.compose.ui.Modifier
    
    class MainActivity : ComponentActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                AxisView()
            }
        }
    }
    
    @Preview
    @Composable
    fun AxisView() {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val canvasWidth = size.width
            val canvasHeight = size.height
            val X_MARGIN = 40f
            val Y_MARGIN = 80f
            val MARGIN_30 = 30
            val MARGIN_15 = 15
            val MARGIN_50 = 50
    
            //绘制水平线
            drawLine(
                start = Offset(x = X_MARGIN, y = canvasHeight / 2),
                end = Offset(x = canvasWidth - X_MARGIN, y = canvasHeight / 2),
                color = Color.Black,
                strokeWidth = 6F
            )
    
            //绘制x轴箭头
            drawLine(
                start = Offset(
                    x = canvasWidth - X_MARGIN - MARGIN_30,
                    y = canvasHeight / 2 - MARGIN_15
                ),
                end = Offset(x = canvasWidth - X_MARGIN, y = canvasHeight / 2),
                color = Color.Black,
                strokeWidth = 6F
            )
            drawLine(
                start = Offset(
                    x = canvasWidth - X_MARGIN - MARGIN_30,
                    y = canvasHeight / 2 + MARGIN_15
                ),
                end = Offset(x = canvasWidth - X_MARGIN, y = canvasHeight / 2),
                color = Color.Black,
                strokeWidth = 6F
            )
    
            //绘制垂直线
            drawLine(
                start = Offset(x = canvasWidth / 2, y = canvasHeight - Y_MARGIN),
                end = Offset(x = canvasWidth / 2, y = Y_MARGIN),
                color = Color.Black,
                strokeWidth = 6F
            )
    
            //绘制垂直箭头
            drawLine(
                start = Offset(x = canvasWidth / 2 - MARGIN_15, y = Y_MARGIN + MARGIN_30),
                end = Offset(x = canvasWidth / 2, y = Y_MARGIN),
                color = Color.Black,
                strokeWidth = 6F
            )
            drawLine(
                start = Offset(x = canvasWidth / 2 + MARGIN_15, y = Y_MARGIN + MARGIN_30),
                end = Offset(x = canvasWidth / 2, y = Y_MARGIN),
                color = Color.Black,
                strokeWidth = 6F
            )
    
            //绘制原点"0"
            drawContext.canvas.nativeCanvas.apply {
                drawText("0", canvasWidth / 2 - MARGIN_50, canvasHeight / 2 + MARGIN_50 + MARGIN_15, Paint().apply {
                    textSize = 60F
                    color = 0xFF000000.toInt()
                })
            }
    
            //绘制"x"
            drawContext.canvas.nativeCanvas.apply {
                drawText("x", canvasWidth - X_MARGIN - MARGIN_50, canvasHeight / 2 + MARGIN_50 + MARGIN_15, Paint().apply {
                    textSize = 60F
                    color = 0xFF000000.toInt()
                })
            }
    
            //绘制"y"
            drawContext.canvas.nativeCanvas.apply {
                drawText("y", canvasWidth/2 + MARGIN_50, Y_MARGIN + MARGIN_50, Paint().apply {
                    textSize = 60F
                    color = 0xFF000000.toInt()
                })
            }
        }
    }
    

    (2)一元一次直线

        一元一次直线的函数表示是:y = ax + b;这里,为了简化,取a = 1,b = 300,即变成y = x + 300 。
        目标效果图:


        这里不再贴和坐标系有关的代码,只贴入口和一元一次直线的实现,入口在AxisView的onDraw()方法里,如下:

        @Override
        protected void onDraw(Canvas canvas) {
          ......
            //绘制一元一次直线
            drawOneVariantOnePowLine(canvas, new Rect(leftPoint.x, topPoint.y, rightPoint.x, bottomPoint.y), paint);
        }
    

        方法drawOneVariantOnePowLine()的实现:

        public static void drawOneVariantOnePowLine(Canvas canvas, Rect rect, Paint paint) {
            final int MARGIN_20 = 20;
            final int MARGIN_80 = 80;
            final int MARGIN_60 = 60;
            final int B_VALUE = 300;
    
            int centerX = rect.left + (rect.right - rect.left) / 2;
            int centerY = rect.top + (rect.bottom - rect.top) / 2;
    
            int startX = rect.left + MARGIN_80;
            //将startX转换为坐标系中的点
            int startAxisX = startX - centerX;
            //根据y = x + 300,计算坐标系中startAxisY
            int startAxisY = startAxisX + B_VALUE;
            //将坐标系中的startAxisY转换为屏幕y坐标
            int startY = centerY - startAxisY;
    
            int endX = centerX + centerX / 2;
            int endAxisX = endX - centerX;
            int endAxisY = endAxisX + B_VALUE;
            int endY = centerY - endAxisY;
    
            //绘制直线
            canvas.drawLine(startX, startY, endX, endY, paint);
    
            //绘制y = x + 300
            canvas.drawText("y = x + 300", endX + MARGIN_20, endY + MARGIN_80, paint);
    
            //绘制y轴上的相交点"300"
            canvas.drawText("300", centerX + MARGIN_20, centerY - 300 + MARGIN_20 + MARGIN_20, paint);
    
            //绘制x轴上的相交点"-300"
            canvas.drawText("-300", centerX - 300, centerY + MARGIN_60, paint);
        }
    

        上述方法里涉及到了屏幕坐标和坐标系坐标的相互转换,屏幕坐标是以左上角为原点,x轴向右是正方向,y轴向下是正方向;而在坐标系中,除了原点不同外,y轴向上才是正方向,这一点需要留意。
        对于Compose版本,入口在MainActivity的onCreate()方法中,如下:

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                AxisView()
                OneVariantOnePowLine()
            }
        }
    

         OneVariantOnePowLine()方法实现:

    /**
     * 绘制一元一次直线: y = x + 300
     */
    @Preview
    @Composable
    fun OneVariantOnePowLine() {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val canvasWidth = size.width
            val canvasHeight = size.height
            val MARGIN_80 = 80f
            val MARGIN_20 = 20f
            val MARGIN_60 = 60f
            val B_VALUE = 300f
    
            val centerX = canvasWidth / 2;
            val centerY = canvasHeight / 2
    
            val startX = MARGIN_80;
            //将startX转换为坐标系中的点
            val startAxisX = startX - centerX;
            //根据y = x + 300,计算坐标系中startAxisY
            val startAxisY = startAxisX + B_VALUE
            //将坐标系中的startAxisY转换为屏幕y坐标
            val startY = centerY - startAxisY;
    
            val endX = centerX + centerX / 2
            val endAxisX = endX - centerX;
            val endAxisY = endAxisX + B_VALUE;
            val endY = centerY - endAxisY;
    
            //绘制直线
            drawLine(
                start = Offset(x = startX, y = startY),
                end = Offset(x = endX, y = endY),
                color = Color.Black,
                strokeWidth = 6F
            )
    
            //绘制y = x + 300
            drawContext.canvas.nativeCanvas.apply {
                drawText("y = x + 300", endX + MARGIN_20, endY + MARGIN_80, Paint().apply {
                    textSize = 60F
                    color = 0xFF000000.toInt()
                })
            }
    
            //绘制y轴上的相交点"300"
            drawContext.canvas.nativeCanvas.apply {
                drawText("300", centerX + MARGIN_20, centerY - 300 + MARGIN_20 + MARGIN_20, Paint().apply {
                    textSize = 60F
                    color = 0xFF000000.toInt()
                })
            }
    
            //绘制x轴上的相交点"-300"
            drawContext.canvas.nativeCanvas.apply {
                drawText("-300", centerX - 300, centerY + MARGIN_60, Paint().apply {
                    textSize = 60F
                    color = 0xFF000000.toInt()
                })
            }
    
        }
    }
    

        可以看到,Compose版坐标系和一元一次直线之间没有任何的耦合,互不影响。

    (3)二次方曲线

        一元二次曲线的函数表示是:y = ax^2 + bx + c。这里的x可以是小数,但对于手机屏幕来说,是不存在小数的。因为屏幕由众多的像素点组成,像素点不可再分。而且像素点也是有限制的,例如一个2560x1440分辨率的屏幕,x、y的取值都必须在此范围内,否则就显示不下。如果选一个函数y = x^2来绘制,当x = 100时,y = 10000,已经超出范围太多了。横向不到屏幕的\frac{1}{14},纵向已经超出屏幕3~4倍,非常影响展示效果。因此,这里取函数y = 0.003x^2来绘制。
        本小节将采取较为原始的方式,以实现功能为主:根据x轴的取值范围,用函数依次计算y值,然后绘制各个点。等介绍完贝塞尔曲线后,会提供一个更高效的版本。两者之间再进行性能对比。
        目标效果图:

    二次方曲线

        先来看View版本,入口仍然在AxisView类的onDraw()方法中:

        @Override
        protected void onDraw(Canvas canvas) {
          ......
            //绘制二次方曲线
            quadraticCurve(canvas, new Rect(leftPoint.x, topPoint.y, rightPoint.x, bottomPoint.y), paint);
        }
    

         quadraticCurve()方法如下:

    static void quadraticCurve(Canvas canvas, Rect rect, Paint paint) {
    
            final int MARGIN_20 = 20;
            final int MARGIN_80 = 80;
            final int MARGIN_60 = 60;
    
            int centerX = rect.left + (rect.right - rect.left) / 2;
            int centerY = rect.top + (rect.bottom - rect.top) / 2;
            //原点
            Point point = new Point(centerX, centerY);
    
            // y = 0.003x·x
            int startX = rect.left + MARGIN_80;
            int endX = rect.right - MARGIN_80;
    
            long beforeDrawPoint = System.nanoTime();
            int count = 0;
            for (int i = startX; i <= endX; i++) {
                count++;
                int tmpAxisX = transferAxisX(i, point); //坐标系转换
                int tmpAxisY = (int) (0.003d * tmpAxisX * tmpAxisX);
                int tmpY = transferAxisY(tmpAxisY, point);//坐标系转屏幕
                canvas.drawPoint(i, tmpY, paint);
            }
            long afterDrawPoint = System.nanoTime();
            Log.d("MathCurve", "[ " + startX + ", " + endX + " ]" + "time = " + (afterDrawPoint - beforeDrawPoint) + "纳秒,drawPoint次数 = " + count);
    
            Paint textPaint = new Paint();
            textPaint.setStrokeWidth(6f);
            textPaint.setColor(Color.BLACK);
            textPaint.setTextSize(60);
            textPaint.setAntiAlias(true);
    
            //绘制y = 0.003x
            int labelX = centerX + centerX / 2 - MARGIN_80;
            int labelAxisX = transferAxisX(labelX, point);
            int labelAxisY = (int) (0.003d * labelAxisX * labelAxisX);
            int labelY = transferAxisY(labelAxisY, point);
            float width = textPaint.measureText("y = 0.003x");
            canvas.drawText("y = 0.003x", labelX + MARGIN_20, labelY + MARGIN_60, textPaint);
    
            Paint paintPow = new Paint();
            paintPow.setStrokeWidth(2f);
            paintPow.setColor(Color.BLACK);
            paintPow.setTextSize(30);
            paintPow.setAntiAlias(true);
            //绘制平方
            canvas.drawText("2", labelX + MARGIN_20 + width, labelY + MARGIN_20 + 10, paintPow);
        }
    
        //坐标系转换,屏幕->坐标系,将屏幕x坐标转为以point为原点的坐标系x坐标
        static int transferAxisX(int screenValue, Point point) {
            return screenValue - point.x;
        }
    
        //坐标系转换,坐标系->屏幕,将以point为原点的坐标系y坐标转为屏幕y坐标
        static int transferAxisY(int axisValue, Point point) {
            return point.y - axisValue;
        }
    

        上面的代码中,打印了每次需要绘制点的个数和以纳秒为单位的耗时。我使用的是2015年的、分辨率为2560x1440的Android测试机,输出数据如下:

    01-28 12:26:11.525 15209 15209 D MathCurve: [ 120, 1320 ]time = 1533333纳秒,drawPoint次数 = 1201
    01-28 12:26:11.732 15209 15209 D MathCurve: [ 120, 1320 ]time = 1663906纳秒,drawPoint次数 = 1201

        可以看到,绘制的屏幕水平范围是[ 120, 1320 ],且绘制了1201个点,耗时大约1.5ms。后面会介绍一个性能高它两个量级、使用贝塞尔曲线实现的版本。
        再来看Compose实现,入口MainActivity的onCreate()方法:

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                AxisView()
                QuadraticCurve()
            }
        }
    

        QuadraticCurve()实现:

    /**
     * 绘制二次方曲线: y = 0.003 * x * x
     */
    @Preview
    @Composable
    fun QuadraticCurve() {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val canvasWidth = size.width
            val canvasHeight = size.height
            val MARGIN_80 = 80F
            val MARGIN_20 = 20F
            val MARGIN_60 = 60F
    
            val centerX = canvasWidth / 2
            val centerY = canvasHeight / 2
    
            //原点
            val point = Offset(centerX, centerY)
    
            val startX = MARGIN_80 + MARGIN_20 * 2
            val endX = canvasWidth - startX
    
            val beforeDrawPoint = System.nanoTime()
            var count = 0
            var i = startX
            while (i <= endX) {
                val tmpAxisX = transferAxisX(i, point)
                val tmpAxisY = (0.003 * tmpAxisX * tmpAxisX).toFloat()
                val tmpY = transferAxisY(tmpAxisY, point)
                drawContext.canvas.nativeCanvas.apply {
                    drawPoint(i, tmpY, Paint().apply {
                        textSize = 60F
                        color = 0xFF000000.toInt()
                        strokeWidth = 6F
                    })
                }
                i += 1
                count++
            }
            val afterDrawPoint = System.nanoTime();
            Log.d("MathCurve", "[ " + startX + ", " + endX + " ]" + "time = " + (afterDrawPoint - beforeDrawPoint) + "纳秒,drawPoint次数 = " + count);
    
    
            val textPaint = Paint()
            textPaint.textSize = 60F
            textPaint.color = 0xFF000000.toInt()
            textPaint.isAntiAlias = true
            textPaint.strokeWidth = 6F
    
            //绘制y = 0.003x·x
            val labelX = centerX + centerX / 2 - MARGIN_80
            val labelAxisX = transferAxisX(labelX, point);
            val labelAxisY = (0.003 * labelAxisX * labelAxisX).toFloat()
            val labelY = transferAxisY(labelAxisY, point)
            val width = textPaint.measureText("y = 0.003x");
            drawContext.canvas.nativeCanvas.apply {
                drawText("y = 0.003x", labelX + MARGIN_20, labelY + MARGIN_60, textPaint)
            }
    
            //绘制平方
            val powPaint = Paint()
            powPaint.textSize = 30F
            powPaint.color = 0xFF000000.toInt()
            powPaint.isAntiAlias = true
            powPaint.strokeWidth = 2F
            drawContext.canvas.nativeCanvas.apply {
                drawText("2", labelX + MARGIN_20 + width, labelY + MARGIN_20 + 10, powPaint)
            }
        }
    }
    
    //坐标系转换,屏幕->坐标系,将屏幕x坐标转为以point为原点的坐标系x坐标
    fun transferAxisX(i: Float, point: Offset): Float {
        return i - point.x
    }
    
    //坐标系转换,坐标系->屏幕,将以point为原点的坐标系y坐标转为屏幕y坐标
    fun transferAxisY(tmpAxisY: Float, point: Offset): Float {
        return point.y - tmpAxisY
    }
    

        打印的log如下:

    01-28 15:35:39.368 28239 28239 D MathCurve: [ 120.0, 1320.0 ]time = 33650416纳秒,drawPoint次数 = 1201
    01-28 15:36:21.563 28239 28239 D MathCurve: [ 120.0, 1320.0 ]time = 39323229纳秒,drawPoint次数 = 1201

        耗时约36ms,这个就太长了,无法接受。
        Compose Canvas 的DrawScope提供了一个drawPoints(...)方法,批量绘制点,下面使用它来代替上面的方式(while前后):

            val beforeDrawPoint = System.nanoTime()
            var i = startX
            val points = ArrayList<Offset>()
            while (i <= endX) {
                val tmpAxisX = transferAxisX(i, point)
                val tmpAxisY = (0.003 * tmpAxisX * tmpAxisX).toFloat()
                val tmpY = transferAxisY(tmpAxisY, point)
                i += 1;
                points.add(Offset(x = i,y = tmpY))
            }
            drawPoints(points = points, strokeWidth = 6F, pointMode = PointMode.Points, color = Color.Black)
    
            val afterDrawPoint = System.nanoTime();
            Log.d(
                "MathCurve",
                "[ " + startX + ", " + endX + " ]" + "time = " + (afterDrawPoint - beforeDrawPoint) + "纳秒"
            );
    

        耗时情况如下:

    01-28 17:11:47.430 1427 1427 D MathCurve: [ 120.0, 1320.0 ]time = 8098646纳秒
    01-28 17:12:15.930 1427 1427 D MathCurve: [ 120.0, 1320.0 ]time = 4807552纳秒
    01-28 17:12:21.137 1427 1427 D MathCurve: [ 120.0, 1320.0 ]time = 5836875纳秒

        平均约5-6ms,比上面的36ms要好多了。

    (4)贝塞尔曲线简介

        贝塞尔(Bezier)曲线是应用于计算机图形及相关领域的参数化曲线。由一系列的控制点(Control Point)P_0P_1、······、P_n组成平滑的、连续的、公式化的曲线。它由法国的Bezier于1960年左右发明,可以扩展到任意阶数。一阶的贝塞尔曲线是一条直线,二阶的贝塞尔曲线是二次方曲线,三阶的贝塞尔曲线是三次方曲线,以此类推。本小节主要介绍二阶和三阶的贝塞尔曲线。
        点P_0P_n是曲线的起点和终点,其他的点并不在曲线上,而是起控制作用。
        二阶的贝塞尔曲线由三个点组成,一个是起点,一个是终点,最后一个是控制点,起点和终点在曲线上的切线相交点即为控制点。如下图黑叉所示:

    二阶贝塞尔曲线
        对于起点为P_0、终点为P_2和控制点为P_1的二阶贝塞尔曲线,有如下的公式:
            B(t) = {(1-t)}^2*P_0 + 2(1-t) t *P_1 + t^2 *P_2,0\leqt\leq1

        三阶贝塞尔曲线由4个点组成,一个起点,一个终点,另外两个是控制点。根据控制点的位置,有两种情况,如下:

    三阶贝塞尔曲线的2种形式
        对于起点为P_0、终点为P_3和控制点为P_1P_2的三阶贝塞尔曲线,有如下的公式:
            B(t) = {(1-t)}^3*P_0 + 3(1-t)^2 t *P_1 + 3(1-t) t^2 *P_2 +t^3 *P_3,0\leqt\leq1
        需要注意的是,贝塞尔曲线的公式,都是针对平面上的点来定义的,和坐标系中定义的y = f(x)不同。点的x、y坐标,都属于因变量,t是自变量。例如,一个三阶贝塞尔曲线,起点是(110,150),控制点是(25,190)、(210,250),终点是(210,30),那么通过贝塞尔曲线,可以得到方程组:
    三阶贝塞尔具体点公式

    (5)再绘二次方曲线

        Android提供了贝塞尔曲线的实现,本小节通过二阶贝塞尔曲线来绘制y = 0.003x^2
        在绘制前,按照第(4)小节的内容,先要确定3个点:起点、终点和控制点。起点和终点很好选择,关键是如何确定控制点呢?如果只是一般的贝塞尔曲线,可以随意指定控制点,但要保证绘制出的曲线就是y = 0.003x^2,就不能随意指定了。

        控制点是起点、终点的切线相交点,因此,需要根据切线方程来求得控制点坐标。
        假设P_0的切线方程为y = k_0x + b_0P_2的切线方程为y = k_2x + b_2 。这两条直线的交点,就是要找的控制点的坐标。第一步就是要确定k_0b_0k_2b_2的值,然后再求解方程组的解。
        将原曲线换一种表示法:f(x) = 0.003x^2;对它求导数得到:f^{\prime}(x) = 0.006x。
        取起始点P_0(-600,1080),终点P_2(600,1080)。那么f^{\prime}(-600) = -3.6,即P_0处的切线斜率为-3.6,所以k_0 = -3.6,P_0是切线y = k_0x + b_0 上的点,将它代入,求得b_0 = -1080。使用同样的方式也可以求得k_2b_2的值,于是得到下面的方程组:
        \begin{cases}y=-3.6x-1080 \\y = 3.6x-1080\end{cases}
        求解方程组得到:
        \begin{cases}x=0 \\y = -1080\end{cases}
        即控制点坐标为(0,-1080),使用它作为控制点,就能保证画出的贝塞尔曲线刚好是y = 0.003x^2
        进一步扩展,先说结论:对于以y轴互相对称且在y = 0.003x^2上的起点、终点,假设它们的纵坐标是b,那么控制点坐标必然是(0,-b)。
        证明:设起始点为(-a,b),终点为(a,b),其中b = 0.003a^2,那么可以得到切线方程组:
        \begin{cases}y=-0.006ax+b-0.006a*a \\y=0.006ax+b-0.006a*a\end{cases}
        进而得到:
        \begin{cases}x=0 \\y = b-0.006a*a\end{cases}
        而b = 0.003a^2,那么y = b - 2b = -b,得到坐标(0,-b)。证毕!
        这个结论还可以再扩展:不要求以y轴对称,如果以x = x_0对称,起点、终点纵坐标依旧是b,那么控制点坐标是( x_0,-b)。证明的方式是相同的,在此就不再重复。
        如果是更一般的方程呢?如 y = 0.003x^2 + 0.9x + 60,它与x轴的交点坐标分别为(-200, 0)、(-100, 0),上面的结论不再适用,不过仍然可以使用切线方程组来求解控制点坐标。

        View版本:

        Path path;
        Paint beizerPaint;
    
        path = new Path();
        beizerPaint = new Paint();
        beizerPaint.setStrokeWidth(6.0f);
        beizerPaint.setAntiAlias(true);
        beizerPaint.setColor(Color.BLACK);
        beizerPaint.setStyle(Paint.Style.STROKE);
    
        //二次方曲线,贝塞尔
        public void quadraticCurve(Canvas canvas, Rect rect, Paint beizerPaint, Path path) {
            final int MARGIN_20 = 20;
            final int MARGIN_80 = 80;
            final int MARGIN_60 = 60;
    
            int centerX = rect.left + (rect.right - rect.left) / 2;
            int centerY = rect.top + (rect.bottom - rect.top) / 2;
    
            //原点
            Point point = new Point(centerX, centerY);
    
            //先取起始点的x坐标
            int startX = rect.left + MARGIN_80;
            int startAxisX = transferAxisX(startX, point);
            int startAxisY = (int)(0.003d * startAxisX * startAxisX);
            int startY = transferAxisY(startAxisY,point);
    
            int endX = rect.right - MARGIN_80;
            int endY = startY;
    
            Log.d("MathCurve", "屏幕起始点是:( "+startX+", "+startY+" ),转换后的坐标系坐标:( " + startAxisX  + ", "+startAxisY +" )");
    
    
            int controlX = centerX; //控制点屏幕坐标x
            int controlY = centerY + startAxisY; //控制点屏幕坐标y,为什么+ startAxisY,看上面的分析
            long beforequad = System.nanoTime();
            path.reset();
            path.moveTo(startX, startY);
            path.quadTo(controlX, controlY, endX, endY);
            canvas.drawPath(path, beizerPaint);
            long afterDrawquad = System.nanoTime();
    
            Log.d("MathCurve", "[ " + startX + ", " + endX + " ]" + "time = " + (afterDrawquad - beforequad) + "纳秒");
    
            Paint textPaint = new Paint();
            textPaint.setStrokeWidth(6f);
            textPaint.setColor(Color.BLACK);
            textPaint.setTextSize(60);
            textPaint.setAntiAlias(true);
    
            //绘制y = 0.003x·x
            int labelX = centerX + centerX / 2 - MARGIN_80;
            int labelAxisX = transferAxisX(labelX, point);
            int labelAxisY = (int) (0.003d * labelAxisX * labelAxisX);
            int labelY = transferAxisY(labelAxisY, point);
            float width = textPaint.measureText("y = 0.003x");
            canvas.drawText("y = 0.003x", labelX + MARGIN_20, labelY + MARGIN_60, textPaint);
    
            Paint paintPow = new Paint();
            paintPow.setStrokeWidth(2f);
            paintPow.setColor(Color.BLACK);
            paintPow.setTextSize(30);
            paintPow.setAntiAlias(true);
            //绘制平方
            canvas.drawText("2", labelX + MARGIN_20 + width, labelY + MARGIN_20 + 10, paintPow);
    
        }
    
        //坐标系转换,屏幕->坐标系,将屏幕x坐标转为以point为原点的坐标系x坐标
        static int transferAxisX(int screenValue, Point point) {
            return screenValue - point.x;
        }
    
        //坐标系转换,坐标系->屏幕,将以point为原点的坐标系y坐标转为屏幕y坐标
        static int transferAxisY(int axisValue, Point point) {
            return point.y - axisValue;
        }
    
    

        打印的log如下:

    01-28 19:27:14.033 9497 9497 D MathCurve: 屏幕起始点是:( 120, 158 ),转换后的坐标系坐标:( -600, 1080 )
    01-28 19:27:14.034 9497 9497 D MathCurve: [ 120, 1320 ]time = 73333纳秒
    01-28 19:27:14.200 9497 9497 D MathCurve: 屏幕起始点是:( 120, 158 ),转换后的坐标系坐标:( -600, 1080 )
    01-28 19:27:14.200 9497 9497 D MathCurve: [ 120, 1320 ]time = 43229纳秒

        可以看到,画同样的曲线,所需时间约为0.055ms。从性能上,比(3)中的View版1.5ms提高了约2个量级。
        再来看Compose版,入口依然在MainActivity的onCreate()方法里,这里不再贴了,重复的函数如坐标转换也没有贴(见上面的小节),只看具体实现:

    /**
     * 贝塞尔 绘制二次方曲线: y = 0.003 * x * x
     */
    @Preview
    @Composable
    fun QuadraticCurve2() {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val canvasWidth = size.width
            val canvasHeight = size.height
            val MARGIN_80 = 80F
            val MARGIN_20 = 20F
            val MARGIN_60 = 60F
    
            val centerX = canvasWidth / 2
            val centerY = canvasHeight / 2
    
            //原点
            val point = Offset(centerX, centerY)
    
            val startX = MARGIN_80 + MARGIN_20 * 2
            val endX = canvasWidth - startX
    
            val startAxisX = transferAxisX(startX, point)
            val startAxisY = (0.003 * startAxisX * startAxisX).toFloat();
            val startY = transferAxisY(startAxisY,point);
            val endY = startY
    
            val controlX = centerX; //控制点屏幕坐标x
            val controlY = centerY + startAxisY; //控制点屏幕坐标y
    
            val path = Path()
    
            val beforequad = System.nanoTime()
            path.reset()
            path.moveTo(startX, startY)
            path.quadraticBezierTo(controlX, controlY, endX, endY)
            drawPath(path=path, color = Color.Black, style = Stroke(width = 6F))
    
            val afterDrawquad = System.nanoTime();
            Log.d("MathCurve", "[ " + startX + ", " + endX + " ]" + "time = " + (afterDrawquad - beforequad) + "纳秒");
    
            val textPaint = Paint();
            textPaint.setStrokeWidth(6f);
            textPaint.setColor(0xFF000000.toInt());
            textPaint.setTextSize(60F);
            textPaint.setAntiAlias(true);
    
            //绘制y = 0.003x
            val labelX = centerX + centerX / 2 - MARGIN_80;
            val labelAxisX = transferAxisX(labelX, point);
            val labelAxisY = (0.003 * labelAxisX * labelAxisX).toFloat();
            val labelY = transferAxisY(labelAxisY, point);
            val width = textPaint.measureText("y = 0.003x");
            drawContext.canvas.nativeCanvas.apply {
                drawText("y = 0.003x", labelX + MARGIN_20, labelY + MARGIN_60, textPaint)
            }
    
            val paintPow = Paint();
            paintPow.setStrokeWidth(2f);
            paintPow.setColor(0xFF000000.toInt());
            paintPow.setTextSize(30F);
            paintPow.setAntiAlias(true);
            //绘制平方
            drawContext.canvas.nativeCanvas.apply {
                drawText("2", labelX + MARGIN_20+ width, labelY + MARGIN_60 +10, paintPow)
            }
        }
    }
    

        log信息如下:

    01-29 09:46:03.393 7238 7238 D MathCurve: [ 120.0, 1320.0 ]time = 58698纳秒
    01-29 09:46:06.365 7238 7238 D MathCurve: [ 120.0, 1320.0 ]time = 65729纳秒
    01-29 09:46:08.949 7238 7238 D MathCurve: [ 120.0, 1320.0 ]time = 62135纳秒

        可以看到,所需时间约0.06ms,比(3)中的Compose版5-6ms提高了约2个量级。

    (6)三次方曲线

        三次方曲线也可以通过贝塞尔来绘制。本小节将绘制曲线y = -x^3/30000,先看目标效果图:

    三次方曲线
        在用贝塞尔绘制二次方曲线时,起点、终点切线相交点即是控制点。但从上面的第(4)小节得知,三阶的贝塞尔曲线有两个控制点,从直观上看,它们除了在切线上并没有什么其他规律。如何保证绘制的贝塞尔曲线刚好是我们的目标y = -x^3/30000呢?换句话说,如何选取两个控制点,使得贝塞尔曲线满足y = -x^3/30000呢?这个过程涉及到繁琐的数学运算,下面来说说大体思路:

        从第(4)小节得知,x、y是自变量t的因变量,对于固定的起始点、二个控制点、终点,可以获得一个方程组。而y = -x^3/30000,将这一个方程加入,就组成了包含3个方程的方程组。起点和终点很容易选择,选起点(-300, 900),终点(300, -900),两个控制点设为(a, b)、(c, d)。两个控制点应该分别在起点、终点的切线上,它们的切线方程为:y = -9x -1800,y = -9x + 1800,a和b、c和d应该满足这样的关系。因此,只要求得a、c的值,就可以得到b、d的值。
        将(-300, 900)、(a, b)、(c, d)、(300, -900)这组数据,代入方程组的第一个方程x = f(t)。从y = -x^3/30000可以知道,x 和t的关系最终必须是一次方的,因为如果是多次方,那么y和t的关系必然超过了3次方,这和另一个方程y = f(t)冲突。因此,将 x = f(t)这个方程展开,t^3t^2这两项前面的系数必须为0。这样就可以得到关于a、c的二个方程,从而获得解a = -100,c = 100。
        到此,两个控制点坐标就计算出了:(-100, -900)、(100, 900) 。最终得到的四个点:(-300, 900)、(-100, -900)、(100, 900)、(300, -900)。这组数据将会在下面的代码中使用。

        View版本:

        //贝塞尔曲线 绘制 y = - x * x * x / 30000
        public static void cubicCurve(Canvas canvas, Rect rect, Paint beizerPaint, Path path) {
            final int MARGIN_20 = 20;
            final int MARGIN_60 = 60;
    
            int centerX = rect.left + (rect.right - rect.left) / 2;
            int centerY = rect.top + (rect.bottom - rect.top) / 2;
    
            //原点
            Point point = new Point(centerX, centerY);
    
            //起点
            int startAxisX = -300;
            int startAxisY = 900;
    
            //终点
            int endAxisX = 300;
            int endAxisY = -900;
    
            //控制点1
            int ctrOnePointX = -100;
            int ctrOnePointY = -900;
    
            //控制点2
            int ctrTwoPointX = 100;
            int ctrTwoPointY = 900;
    
            //将上述点转换到屏幕坐标系
            int startX = transferToScreenX(startAxisX,point);
            int startY = transferToScreenY(startAxisY,point);
    
            int endX = transferToScreenX(endAxisX,point);
            int endY = transferToScreenY(endAxisY,point);
    
            int ctrOneX = transferToScreenX(ctrOnePointX,point);
            int ctrOneY = transferToScreenY(ctrOnePointY,point);
    
            int ctrTwoX = transferToScreenX(ctrTwoPointX,point);
            int ctrTwoY = transferToScreenY(ctrTwoPointY,point);
    
            long beforequad = System.nanoTime();
            path.reset();
            path.moveTo(startX, startY);
            path.cubicTo(ctrOneX, ctrOneY,ctrTwoX, ctrTwoY, endX, endY);
            canvas.drawPath(path, beizerPaint);
            long afterDrawquad = System.nanoTime();
    
            Log.d("MathCurve", "[ " + startX + ", " + endX + " ]" + "time = " + (afterDrawquad - beforequad) + "纳秒");
    
            Paint textPaint = new Paint();
            textPaint.setStrokeWidth(6f);
            textPaint.setColor(Color.BLACK);
            textPaint.setTextSize(60);
            textPaint.setAntiAlias(true);
    
            //绘制y = 0.003x
            int labelX = endX + MARGIN_20;
            int labelY = centerY + centerY/2;
            float width = textPaint.measureText("y = -x");
            canvas.drawText("y = -x", labelX + MARGIN_20, labelY + MARGIN_60, textPaint);
    
            Paint paintPow = new Paint();
            paintPow.setStrokeWidth(2f);
            paintPow.setColor(Color.BLACK);
            paintPow.setTextSize(30);
            paintPow.setAntiAlias(true);
            float width2 = paintPow.measureText("3");
            //绘制立方
            canvas.drawText("3", labelX + MARGIN_20 + width, labelY + MARGIN_20 + 10, paintPow);
    
            canvas.drawText("/30000", labelX + MARGIN_20 + width + width2, labelY + MARGIN_60, textPaint);
        }
    
        //坐标系转换,坐标系->屏幕,
        static int transferToScreenX(int axisX, Point point) {
            return axisX + point.x;
        }
    
        //坐标系转换,屏幕->坐标系,将屏幕x坐标转为以point为原点的坐标系x坐标
        static int transferToAxisX(int screenValue, Point point) {
            return screenValue - point.x;
        }
    
        //坐标系转换,坐标系->屏幕,将以point为原点的坐标系y坐标转为屏幕y坐标
        static int transferToScreenY(int axisY, Point point) {
            return point.y - axisY;
        }
    
    

        Compose版本:

    @Composable
    fun CubicCurve() {
        Canvas(modifier = Modifier.fillMaxSize()) {
            val canvasWidth = size.width
            val canvasHeight = size.height
            val MARGIN_80 = 80F
            val MARGIN_20 = 20F
            val MARGIN_60 = 60F
    
            val centerX = canvasWidth / 2
            val centerY = canvasHeight / 2
    
            //原点
            val point = Offset(centerX, centerY)
    
            //起点
            val startAxisX = -300F
            val startAxisY = 900F
    
            //终点
            val endAxisX = 300f
            val endAxisY = -900f
    
            //控制点1
            val ctrOnePointX = -100f
            val ctrOnePointY = -900f
    
            //控制点2
            val ctrTwoPointX = 100f
            val ctrTwoPointY = 900f
    
            //将上述点转换到屏幕坐标系
            val startX = transferToScreenX(startAxisX, point)
            val startY = transferToScreenY(startAxisY, point)
    
            val endX = transferToScreenX(endAxisX, point)
            val endY = transferToScreenY(endAxisY, point)
    
            val ctrOneX = transferToScreenX(ctrOnePointX, point)
            val ctrOneY = transferToScreenY(ctrOnePointY, point)
    
            val ctrTwoX = transferToScreenX(ctrTwoPointX, point)
            val ctrTwoY = transferToScreenY(ctrTwoPointY, point)
    
            val path = Path()
            val beforequad = System.nanoTime()
    
            path.reset()
            path.moveTo(startX, startY)
            path.cubicTo(ctrOneX, ctrOneY, ctrTwoX, ctrTwoY, endX, endY)
            drawPath(path = path, color = Color.Black, style = Stroke(width = 6F))
    
            val afterDrawquad = System.nanoTime()
            Log.d(
                "MathCurve",
                "[ " + startX + ", " + endX + " ]" + "time = " + (afterDrawquad - beforequad) + "纳秒"
            )
    
            val textPaint = Paint();
            textPaint.setStrokeWidth(6f);
            textPaint.setColor(0xFF000000.toInt());
            textPaint.setTextSize(60F);
            textPaint.setAntiAlias(true);
    
            //绘制y = -x
            val labelX = centerX + centerX / 2 - MARGIN_80;
            val labelY = centerY + centerY / 2
            val width = textPaint.measureText("y = -x");
            drawContext.canvas.nativeCanvas.apply {
                drawText("y = -x", labelX + MARGIN_20, labelY + MARGIN_60, textPaint)
            }
    
            val paintPow = Paint();
            paintPow.setStrokeWidth(2f);
            paintPow.setColor(0xFF000000.toInt());
            paintPow.setTextSize(30F);
            paintPow.setAntiAlias(true);
            val width2 = paintPow.measureText("3");
            //绘制3次方
            drawContext.canvas.nativeCanvas.apply {
                drawText("3", labelX + MARGIN_20 + width, labelY + MARGIN_60 + 10, paintPow)
            }
    
            //绘制剩余的
            drawContext.canvas.nativeCanvas.apply {
                drawText("/30000", labelX + MARGIN_20 + width + width2, labelY + MARGIN_60, textPaint)
            }
        }
    }
    
    //坐标系转换,坐标系->屏幕,x轴
    fun transferToScreenX(axisX: Float, point: Offset): Float {
        return axisX + point.x
    }
    
    //坐标系转换,坐标系->屏幕,y轴
    fun transferToScreenY(axisY: Float, point: Offset): Float {
        return point.y - axisY
    }
    

    (7)指数曲线

        贝塞尔方程决定了它只能绘制1到n次方的曲线,往后的曲线就不能再它来绘制了。
        本小节将绘制指数曲线y = {1.018}^{x}/(2 * 10^9) 。之所以选择它,是因为指数增长实在太快了,一般的指数曲线如y = 2^x, x轴不到十几个像素,y轴已经飞出了屏幕范围。为了让曲线能适配手机屏幕,视觉效果更好,才选择了该函数。目标效果图如下:

    指数曲线
        View版本实现如下:
        // 指数曲线,y = 1.018^x/a ,a = 2 * pow(10,9)
        public static void powCurve(Canvas canvas, Rect rect, Paint paint) {
            final int MARGIN_20 = 20;
            final int MARGIN_60 = 60;
    
            int centerX = rect.left + (rect.right - rect.left) / 2;
            int centerY = rect.top + (rect.bottom - rect.top) / 2;
    
            //原点
            Point point = new Point(centerX, centerY);
    
            //起点
            int startAxisX = 0;
    
            //中间点
            int middleAxis = 320; //380
    
            Path path = new Path();
            
            long beforeDrawPoint = System.nanoTime();
            int count = 0;
            for (int i = startAxisX; i <= middleAxis; i++) {
                count++;
                int tmpX = transferToScreenX(i, point);
                BigDecimal tmp1 = BigDecimal.valueOf(1.018).pow(i);
                int tmpAxisY = tmp1.intValue();
                int tmpY = transferToScreenY(tmpAxisY, point);
                canvas.drawPoint(tmpX, tmpY, paint);
                if (i == middleAxis) {
                    path.moveTo(tmpX, tmpY);
                }
            }
            long afterDrawPoint = System.nanoTime();
            Log.d("MathCurve", "time = " + (afterDrawPoint - beforeDrawPoint) + "纳秒,drawPoint次数 = " + count);
    
            int endAxis = 380;
    
            //使得后面的线连贯
            for (int i = middleAxis; i <= endAxis; i++) {
                count++;
                int tmpX = transferToScreenX(i, point);
                BigDecimal tmp1 = BigDecimal.valueOf(1.018).pow(i);
                int tmpAxisY = tmp1.intValue();
                int tmpY = transferToScreenY(tmpAxisY, point);
                path.lineTo(tmpX, tmpY);
            }
            canvas.drawPath(path, paint);
    
            Paint textPaint = new Paint();
            textPaint.setStrokeWidth(6f);
            textPaint.setColor(Color.BLACK);
            textPaint.setTextSize(60);
            textPaint.setAntiAlias(true);
    
            //绘制y = 1.018
            int labelX = centerX + 2 * MARGIN_20;
            int labelY = rect.top + 2 * MARGIN_60;
            float width = textPaint.measureText("y = 1.018");
            canvas.drawText("y = 1.018", labelX + MARGIN_20, labelY + MARGIN_60, textPaint);
    
            Paint paintPow = new Paint();
            paintPow.setStrokeWidth(2f);
            paintPow.setColor(Color.BLACK);
            paintPow.setTextSize(40);
            paintPow.setAntiAlias(true);
            float width2 = paintPow.measureText("x");
            //绘制x次方
            canvas.drawText("x", labelX + MARGIN_20 + width, labelY + MARGIN_20 + 10, paintPow);
    
            float width3 = textPaint.measureText("/(2 * 10");
            canvas.drawText("/(2 * 10", labelX + MARGIN_20 + width + width2, labelY + MARGIN_60, textPaint);
    
            float width4 = textPaint.measureText("9");
            //9次方
            canvas.drawText("9", labelX + MARGIN_20 + width + width2 + width3, labelY + MARGIN_20 + 10, paintPow);
    
            canvas.drawText(")", labelX + MARGIN_20 + width + width2 + width3 + width4, labelY + MARGIN_60, textPaint);
        }
    

        为了不损失精度,代码中使用了BigDecimal类来处理小数的指数运算及中间结果。

    (8)对数曲线

        对数曲线:y = log_2x 。

    (9)正弦曲线

        正弦曲线:y = sin(x)。

    (10)余弦曲线

        余弦曲线:y = cos(x)。

    (11)其他曲线

        y = tan(x)、y= cot(x)、y = arcsin(x)、y = arctan(x)等;

        未完待续!

    相关文章

      网友评论

          本文标题:16_Android数学曲线绘制(View、Compose双版本

          本文链接:https://www.haomeiwen.com/subject/ksnjhdtx.html