Android自定义-曲线利用和认识

在这里插入图片描述

  • 曲线开发中并不常用,但是学会曲线绘制能让你的软件更具创造性无穷的魅力

其他API是绘制基础,我认为曲线是绘画的灵魂。有了它直接起飞。这节课我们学习曲线以及曲线的应用等。



一、曲线认识与理解

曲线常见的API
1.一阶曲线
2.二阶曲线
3.三阶曲线

我们在初中高中学习中学习了各种直线,,椭圆,正玄曲线等对应的坐标系方程吧,接下来我们回顾一下我们的直线和曲线等方程。

  • 第一步我们还是定义一个类新建坐标系,屏幕可旋转横屏显示
package com.example.android_draw.view

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View

/**
 *
 *  ┌─────────────────────────────────────────────────────────────┐
 *  │┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│
 *  ││Esc│!1 │@2 │#3 │$4 │%5 │^6 │&7 │*8 │(9 │)0 │_- │+= │|\ │`~ ││
 *  │├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴───┤│
 *  ││ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{[ │}] │ BS  ││
 *  │├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤│
 *  ││ Ctrl │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter  ││
 *  │├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────┬───┤│
 *  ││ Shift  │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│Shift │Fn ││
 *  │└─────┬──┴┬──┴──┬┴───┴───┴───┴───┴───┴──┬┴───┴┬──┴┬─────┴───┘│
 *  │      │Fn │ Alt │         Space         │ Alt │Win│   HHKB   │
 *  │      └───┴─────┴───────────────────────┴─────┴───┘          │
 *  └─────────────────────────────────────────────────────────────┘
 * 版权:渤海新能 版权所有
 *
 * @author feiWang
 * 版本:1.5
 * 创建日期:2/8/21
 * 描述:Android_Draw
 * E-mail : [email protected]
 * CSDN:https://blog.csdn.net/m0_37667770/article
 * GitHub:https://github.com/luhenchang
 */
class LHC_Cubic_View @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    
    

    init {
    
    

    }

    override fun onDraw(canvas: Canvas?) {
    
    
        super.onDraw(canvas)
    }

}

为了方便观察和绘制进行了网格和坐标轴的绘制。我相信学过上一篇文章的对于画布的变换操作已经熟练掌握了,网格坐标轴的代码我就不再讲解,看图。

1.方程式映射到坐标系

记得我们初中学过Y(x)=ax+b的直线方程吧。我们来看看这个方程映射到坐标系中的图像。
首先定义一个函数 y=2x-80获取一段点集合,为了看效果我们x偶数时候绘制,然后绘制点即可。代码如下:

    private var number=0..420
    //直线方程y=2x-80
    private fun drawzxLine(canvas: Canvas) {
    
    
        pointList= ArrayList()
        //绘制方程式y=10x+20
        val gPaint = getPaint()
        number.forEach {
    
     t ->
            val point=PointF()
            if (t%2==0) {
    
    //x轴偶数点进行绘制
                point.x = t.toFloat()
                point.y = 2f * t - 80
                pointList.add(point)
                canvas.drawPoint(point.x, point.y, gPaint)
            }
        }
    }

1.同样圆的方程,椭圆的方程等都可以这样进行映射到坐标系。

2. 所表示的曲线是以O(a,b)为圆心,以r为半径的圆。

3.例如:(x-10)2+(y-10)2=1602我们进行映射到坐标系内。

  • 解方程应该没问题吧。我们来变换为我们熟悉的方程式:

(x-10)2+(y-10)2=1602

1.移项 (y-10)2=1602-(x-10)2

2.开方 转换为函数Math方程式如下:

开方之后有正负值需要注意

  1. y=sqrt(160.0.pow(2.0).toFloat() - ((point.x - 10).toDouble()).pow(2.0)).toFloat() + 10
  2. y=-sqrt(160.0.pow(2.0).toFloat() - ((pointDown.x - 10).toDouble()).pow(2.0)).toFloat() + 10
 //绘制圆圈
        number.forEach {
    
     t ->
            val point = PointF()
            val pointDown = PointF()

            //(x-10)2+(y-10)2=1602
            point.x = t.toFloat()
            pointDown.x = t.toFloat()
            //y计算应该不用我说吧。
            point.y =
                sqrt(160.0.pow(2.0).toFloat() - ((point.x - 10).toDouble()).pow(2.0)).toFloat() + 10
            pointDown.y = -sqrt(
                160.0.pow(2.0).toFloat() - ((pointDown.x - 10).toDouble()).pow(2.0)
            ).toFloat() + 10
            canvas.drawPoint(point.x, point.y, gPaint)
            canvas.drawPoint(pointDown.x, pointDown.y, gPaint)

        }

2.贝塞尔曲线

通过上面我们发现凡是函数都可以和坐标系绘制进行一一映射,当然了贝塞尔曲线也是有方程式的。有如下:

线性贝塞尔曲线

  • 给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:
         

二次方贝塞尔曲线

  • 二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
         

三次方贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;公式如下:

当然在Android端的Native层已经封装好了方法,二次方贝塞尔曲线三次方贝塞尔曲线,已知函数当然可以进行封装。

在Android端提供了二阶和三阶
 `二次方贝塞尔曲线`:
    public void quadTo(float x1, float y1, float x2, float y2)
    public void rQuadTo(float dx1, float dy1, float dx2, float dy2) 
 `三次方贝塞尔曲线`:
    public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
    public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3) 

接下来我们绘制一个二阶曲线,控制点可以随着手势的移动和下按进行对应的屏幕移动,对于手势坐标系和屏幕坐标系的映射转换上节折线里面说很明白了,这里不多做解释。

  • quadTo(float x1, float y1, float x2, float y2)
    //记录移动的canvas画布坐标,不是手势坐标,由手势坐标转换为canvas坐标进行刷新
    private var moveX: Float = 160f
    private var moveY: Float = 160f
   private fun drawQuz(canvas: Canvas) {
    
    
        controllRect = Rect(
            (moveX - 30f).toInt(),
            (moveY + 30f).toInt(),
            (moveX + 30).toInt(),
            (moveY - 30f).toInt()
        )
        val quePath = Path()
        canvas.drawCircle(0f, 0f, 10f, getPaintCir(Paint.Style.FILL))
        canvas.drawCircle(320f, 0f, 10f, getPaintCir(Paint.Style.FILL))
        //第一个点和控制点的连线到最后一个点链线。为了方便观察
        val lineLeft = Path()
        lineLeft.moveTo(0f, 0f)
        lineLeft.lineTo(moveX, moveY)
        lineLeft.lineTo(320f, 0f)
        canvas.drawPath(lineLeft, getPaint(Paint.Style.STROKE))
        //第一个p0处画一个圆。第二个p1处画一个控制点圆,最后画一个。
        canvas.drawCircle(moveX, moveY, 10f, getPaintCir(Paint.Style.FILL))
        quePath.quadTo(moveX, moveY, 320f, 0f)
        canvas.drawPath(quePath, getPaint(Paint.Style.STROKE))
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
        when (event.action) {
    
    
            ACTION_DOWN,
            ACTION_MOVE -> {
    
    
                //在控制点附近范围内部,进行移动
                Log.e("x=", "onTouchEvent: (x,y)"+(event.x - width / 2).toInt()+":"+(-(event.y - height / 2)).toInt())
                //将手势坐标转换为屏幕坐标
                moveX = event.x - width / 2
                moveY = -(event.y - height / 2)
                invalidate()
            }
        }
        return true
    }

上图可以拖动控制点,在起点和结尾之间的曲线随着控制点发生了变形。控制点靠近那一侧弧度的凸起就偏向那一侧,初步的认识这一个规律即可,而练习中不断的去调节控制点达到我们的需求。但是在上图中我们回发现弧度不够圆圈,在三阶函数里面可以很好的调节弧度。接下来我们来看看三阶函数

三阶曲线

  • public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
    同样我们在坐标系内绘制三阶曲线。
    为了很好的看到效果我们这次进行来精细的控制,我们可以拖动任意我们想要拖动的控制点进行观察我们的三阶曲线。在上章节折线中对于手势配合Rect的contains方法可以进行局部的点击,当然了拖动也是没问题的。
    如下图:我们只需要在控制点附近进行绘制距形包裹住控制点,手势滑动时时刷新控制点对应的距形即可。

  private fun drawCubic(canvas: Canvas) {
    
    
       val cubicPath=Path()
       cubicPath.moveTo(0f,0f)
       cubicLeftRect= Rect(
           (moveCubiX - 30f).toInt(),
           (moveCubiY - 30f).toInt(),
           (moveCubiX + 30).toInt(),
           (moveCubiY + 30f).toInt()
       )
       cubicRightRect=Rect(
           (moveCubiXX - 30f).toInt(),
           (moveCubiYY - 30f).toInt(),
           (moveCubiXX + 30).toInt(),
           (moveCubiYY + 30f).toInt()
       )
        val lineLeft = Path()
        lineLeft.moveTo(0f, 0f)
        lineLeft.lineTo(moveCubiX, moveCubiY)
        lineLeft.lineTo(moveCubiXX, moveCubiYY)
        lineLeft.lineTo(320f, 0f)
        canvas.drawPath(lineLeft, getPaint(Paint.Style.STROKE,Color.GRAY))

        //canvas.drawRect(cubicLeftRect, getPaint(Paint.Style.FILL,Color.RED))
        //canvas.drawRect(cubicRightRect, getPaint(Paint.Style.FILL,Color.RED))
        canvas.drawCircle(moveCubiX, moveCubiY, 10f, getPaintCir(Paint.Style.FILL))
        canvas.drawCircle(moveCubiXX, moveCubiYY, 10f, getPaintCir(Paint.Style.FILL))

        cubicPath.cubicTo(moveCubiX,moveCubiY,moveCubiXX,moveCubiYY,320f,0f)
        canvas.drawPath(cubicPath, getPaint(Paint.Style.STROKE,Color.RED))
    }
    
   override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
        when (event.action) {
    
    
            ACTION_DOWN,
            ACTION_MOVE -> {
    
    
                //在控制点附近范围内部,进行移动
                Log.e(
                    "x=",
                    "onTouchEvent: (x,y)" + (event.x - width / 2).toInt() + ":" + (-(event.y - height / 2)).toInt()
                )
                //二阶曲线
                if (controllRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())) {
    
    
                    Log.e("点击来","对" )
                    moveX = event.x - width / 2
                    moveY = -(event.y - height / 2)
                    invalidate()
                //三阶曲线控制点1
                }else if(cubicLeftRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())){
    
    
                    moveCubiX= event.x - width / 2
                    moveCubiY= -(event.y - height / 2)
                    invalidate()
                 //三阶曲线控制点2
                }else if(cubicRightRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())){
    
    
                    moveCubiXX= event.x - width / 2
                    moveCubiYY= -(event.y - height / 2)
                    invalidate()

                }
            }
        }
        return true
    }

到这里我想我们应该大概的明白二阶和三阶曲线对于弧度的大致方向控制了吧。你以为这样就结束了么。接下来下来开始正式的进入曲线应用。

二、曲线-简单应用

在qq群里么有人问过这样的效果如何去做。我想到这里对于大家来说应该很简单了吧。我们接下来就利用上面学习的曲线内容进行编写。

首先准备

        1.一张图片

        2.绘制矩形框

        3.绘制直线和曲线

        4.拖动曲线

        5.改变色相饱和度亮度等

1.图片色相饱和度等的了解

图片在Android中并没有提供直接操作Bitmap来改变色相饱和度的API,所以我们需要将bitmap绘制到画布上。接下来我们开始写代码。首先新建类LHC_Image_View,attrs里面添加属性。

属性

<declare-styleable name="LHC_Image_View">
  <attr name="defaultImag" format="reference" />
</declare-styleable>

获取属性图片,进行绘制到canvas上面,在这里我们需要知道一点关于颜色的API。ColorMatrix
图像是由无数的像素点组成,每一个像素点用RGBA四个值来描述,具体是由一个4*5的矩阵来控制每个像素的显示,点的RGBA又共同组成了位图的外观表现形式,所以改变控制像素点的RGBA值,就可以改变位图展示的效果

`ColorMatrix`
  用4x5矩阵来表示,用于转换位图的颜色和alpha分量。
  [ a, b, c, d, e,
    f, g, h, i, j,
    k, l, m, n, o,
    p, q, r, s, t ]
    计算如下:
    R = a*R + b*G + c*B + d*A + e;
    G = f*R + g*G + h*B + i*A + j;
    B = k*R + l*G + m*B + n*A + o;
    A = p*R + q*G + r*B + s*A + t; 
通常见的默认标准矩阵    
[ 1 0 0 0 0               R=225            R1=225 + 0 + 0 + 0+偏移量=225
  0 1 0 0 0       x       G=225     =      G1=225 + 0 + 0 + 0+偏移量=225  =  RGBA=[225,225,225,225]
  0 0 1 0 0               B=225            B1=225 + 0 + 0 + 0+偏移量=225  
  0 0 0 1 0 ]             A=225            A1=225 + 0 + 0 + 0+偏移量=225
  
 我想降低绿色和蓝色,那么我们可以将矩阵中的2行2列1变为0.5,3行3列的1变为0.5等操作。
 ColorMatrix构造方法:我们使用ColorMatrix()
     public ColorMatrix() {
        reset();
    }
    /**
     * Create a new colormatrix initialized with the specified array of values.
     */
    public ColorMatrix(float[] src) {
        System.arraycopy(src, 0, mArray, 0, 20);
    }
    /**
     * Create a new colormatrix initialized with the specified colormatrix.
     */
    public ColorMatrix(ColorMatrix src) {
        System.arraycopy(src.mArray, 0, mArray, 0, 20);
    }

在ColorMatrix中常见的使用方法:setRotate(int axis, float degrees)中用到了degrees * Math.PI / 180d这里为了控制矩阵数组开发者便于编写使用来正余玄函数,将-1到1放大到了0-360度,当然这是我的猜想,毕竟-1到1之间的小数不好把握。为了便于大家使用不妨附上一副正玄图像:


由图我们可以看到0度和90度分别是0和1,那接下来我们进行绘制一个图片设置为int=0代表红色…degrees=90度,源码中修改了矩阵的为

[ 1 0 0 0 0               R    
  0 1 0 0 0       x       G
  0 0 1 0 0               B  =R G B A
  0 0 0 1 0 ]             A  
  
[ 1 0 0 0 0               R
  0 0 1 0 0       x       G =R B -G A 
  0 -1 0 0 0              B
  0 0 0 1 0 ]             A    

结果红没变,绿变为蓝,蓝色变为负,我们知道红和蓝成黄色。那么我们接下来验证一下我们的结果。
    public void setRotate(int axis, float degrees) {
    
    
        reset();
        double radians = degrees * Math.PI / 180d;
        float cosine = (float) Math.cos(radians);
        float sine = (float) Math.sin(radians);
        switch (axis) {
    
    
        // Rotation around the red color
        case 0:
            mArray[6] = mArray[12] = cosine;
            mArray[7] = sine;
            mArray[11] = -sine;
            break;
        // Rotation around the green color
        case 1:
            mArray[0] = mArray[12] = cosine;
            mArray[2] = -sine;
            mArray[10] = sine;
            break;
        // Rotation around the blue color
        case 2:
            mArray[0] = mArray[6] = cosine;
            mArray[1] = sine;
            mArray[5] = -sine;
            break;
        default:
            throw new RuntimeException();
        }
    }


验证结果:

class LHC_Image_View @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    
    
    private val MIDDLE_VALUE=127
    private var mdrawable: Drawable?
    lateinit var bitmap:Bitmap
    init {
    
    
        val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.LHC_Image_View)
        mdrawable = array.getDrawable(R.styleable.LHC_Image_View_defaultImag)

    }

    override fun onDraw(canvas: Canvas) {
    
    
        super.onDraw(canvas)
        if (mdrawable!=null){
    
    
            bitmap =mdrawable!!.toBitmap(width,height, Bitmap.Config.ARGB_8888)
        }else{
    
    
          return
        }
        //实例化一支画笔
        val mPaint = Paint()
        mPaint.strokeWidth=10f
        //实例化处理色相的颜色矩阵
        val colorMatrix =  ColorMatrix()
        //获得色相的计算公式
        val gress=90f
        //源码里面0表示红色
        colorMatrix.setRotate(0, gress)

        //将调好的颜色设置给画笔
        mPaint.colorFilter = ColorMatrixColorFilter(colorMatrix)
        //然后我们用调整好的颜色画笔将原来的图片bmp画到新的bitmap上
        canvas.drawBitmap(bitmap, 0f, 0f, mPaint)
    }

}

验证成功。那么我们继续进行我们曲线绘制学习,有点跑题了。

2.绘制矩形框

这个我想超级简单。

   private fun drawGrid(canvas: Canvas) {
    
    
        //1.我们左下角为屏幕的圆点进行操作
        canvas.translate(0f, height.toFloat())
        canvas.scale(1f,-1f)
        canvas.save()

        val xpath=Path()
        xpath.moveTo(0f,0f)
        xpath.lineTo(width.toFloat(), 0f)

        val paint=Paint()
        paint.color=Color.GRAY
        paint.strokeWidth=2f
        paint.style= Paint.Style.STROKE
        for (index in 0 until 6){
    
    
            canvas.translate(0f,160f)
            canvas.drawPath(xpath,paint)
        }
        canvas.restore()

        val ypath=Path()
        ypath.moveTo(0f,0f)
        ypath.lineTo(0f, width.toFloat()-120)

        val painty=Paint()
        painty.color=Color.GRAY
        painty.strokeWidth=2f
        painty.style= Paint.Style.STROKE
        canvas.save()

        for (index in 0 until 6){
    
    
            canvas.translate(160f,0f)
            canvas.drawPath(ypath,paint)
        }
        canvas.restore()



    }

3.直线和曲线加个手势

如下绘制直线和曲线以及圆圈加个手势。还讲个辣椒,上面都学过了对吧,我想努力点的小伙伴只有想不到没有做不到对不对。如果你还不会我相信你没有仔细阅读我的文章了,代码如下:

      private fun drawLineAndCubit(canvas: Canvas) {
    
    
        val paint=Paint()
        paint.color=Color.GRAY
        paint.strokeWidth=2f
        paint.style= Paint.Style.STROKE
        //斜线段
        val xpath=Path()
        xpath.moveTo(10f,10f)
        xpath.lineTo(width.toFloat(), width.toFloat()-120)
        canvas.drawPath(xpath,paint)
        //起点和终点圆圈
        paint.style= Paint.Style.FILL
        paint.color=Color.RED
        canvas.drawCircle(15f,15f,15f,paint)
        canvas.drawCircle(width.toFloat()-15, width.toFloat()-120-15,15f,paint)
        val cubicPath=Path()
        cubicPath.moveTo(0f,0f)
        paint.style= Paint.Style.STROKE
        paint.strokeWidth=5f
        cubicPath.quadTo(moveX, moveY,width.toFloat()-15, width.toFloat()-120-15)

        //绘制曲线
        canvas.drawPath(cubicPath,paint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
        when (event.action) {
    
    
            MotionEvent.ACTION_DOWN,
            MotionEvent.ACTION_MOVE -> {
    
    
                //在控制点附近范围内部,进行移动
                moveX= event.x 
                moveY= -(event.y - height)
                invalidate()
            }
        }
        return true
    }

三、曲线、贴图、裁剪的使用

很多能量球或者进度球等悬浮球体等都有着白塞尔曲线的身影。我们接下来进行简单的应用,写个鱼儿游动的小动画。这个过程我们需要的就是简单的数学计算画布的裁剪绘制图片到画布等基本常用操作。下面小案例我们结合曲线动画和相关API进行逐步分析。

  • 绘制分析
1.海浪的绘制->曲线的绘制
2.会动的海浪->画布的平移
3.始终运动的动画
4.随着海浪游动的鱼儿->画布上图片的绘制

1.海浪的绘制

如上图我们可以看到不断运动着的曲线海浪。那么海浪是如何绘制而成的呢?白塞尔曲线完全可以完成下面的绘制吧。


代码:

//绘制波浪
    private fun drawWave(canvas: Canvas) {
    
    
        val wavePath=Path()
        wavePath.moveTo(0f, -waveWidth * 6)
        wavePath.lineTo(0f, 0f)
        wavePath.quadTo(waveWidth, waveHeight, waveWidth * 2, 0f)
        wavePath.quadTo(waveWidth * 3, -waveHeight, waveWidth * 4, 0f)
        wavePath.quadTo(waveWidth * 5, waveHeight, waveWidth * 6, 0f)
        wavePath.quadTo(waveWidth * 7, -waveHeight, waveWidth * 8, 0f)
        wavePath.lineTo(waveWidth * 8, -waveWidth * 6)
        canvas.drawPath(wavePath, getPaintBefor(Paint.Style.FILL))

2.会动的海浪

海浪的绘制仅仅是曲线的绘制闭合之后填充颜色而已,曲线的运动简单的可以通过移动画布来完成。

//海浪通过画布的平移随之而动。
canvas.translate(animal.animatedValue as Float, 0f) 

海浪在一个或者矩形或者各种形状容器中运动的效果。
我们只需要将画布进行裁剪出一个想要的形状然后让海浪在裁剪过的部分观看即可。


代码:

    private fun clipCanvas(canvas: Canvas,type:Int) {
    
    
        if (type==0) {
    
    
            val rect = Rect(
                (waveWidth * 4).toInt(),
                160,
                (waveWidth * 8).toInt(),
                (-waveWidth * 4).toInt()
            )
            canvas.clipRect(rect)
        }else  if (type==1) {
    
    
            val circlePath = Path()
            circlePath.addCircle(480f, 0f, 160f, Path.Direction.CCW)
            canvas.drawCircle(480f, 0f, 160f, getPaint(Paint.Style.STROKE))
            canvas.clipPath(circlePath)
        }else  if (type==2){
    
    
            val rundRect = Path()
            rundRect.addRoundRect(waveWidth * 4,waveWidth*3,waveWidth* 8,-waveWidth*3,60f,60f,Path.Direction.CCW)
            //canvas.drawPath(rundRect,getPaint(Paint.Style.STROKE))
            canvas.clipPath(rundRect)

        }
    }

但是动画的值设置是一个简单的数学计算。

动画的值只需要平移一整个波长即可= waveWidth * 4

 init {
    
    
        animal=ObjectAnimator.ofFloat(0f, waveWidth * 4)
        animal.duration = 2000
        animal.repeatCount = ValueAnimator.INFINITE;
        animal.interpolator = LinearInterpolator()
        animal.addUpdateListener {
    
    
            invalidate()
        }
    }

3.随着海浪游动的鱼儿

画布平移,如果在画布上固定位置贴图,那么图也会随着画布的移动而移动。

//可以阅读相关的API,将一个图片绘制到画布坐标系(left,top)的位置。
drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)

素材可以在阿里巴巴图片素材库进行下载等。

  val arrList= arrayListOf( R.drawable.yu, R.drawable.ziyuan, R.drawable.jingyu)
    private fun drawFish(canvas: Canvas) {
    
    

        val bmp = BitmapFactory.decodeResource(resources, arrList[0])
        canvas.drawBitmap(bmp, waveWidth * 3, -waveHeight * 2.2f, getPaint(Paint.Style.FILL))


        val bmpzy = BitmapFactory.decodeResource(resources,arrList[1])
        canvas.drawBitmap(bmpzy, waveWidth, -waveHeight *3f, getPaint(Paint.Style.FILL))

        val bmpjy= BitmapFactory.decodeResource(resources,arrList[2])
        canvas.drawBitmap(bmpjy, waveWidth* 6, -waveHeight * 3.5f, getPaint(Paint.Style.FILL))

    }

最终效果:

                                       
代码:

package com.example.android_draw.view

import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_MOVE
import android.view.View
import android.view.animation.LinearInterpolator
import com.example.android_draw.R
import kotlin.math.max
import kotlin.random.Random

/**
 *
 *  ┌─────────────────────────────────────────────────────────────┐
 *  │┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│
 *  ││Esc│!1 │@2 │#3 │$4 │%5 │^6 │&7 │*8 │(9 │)0 │_- │+= │|\ │`~ ││
 *  │├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴───┤│
 *  ││ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{[ │}] │ BS  ││
 *  │├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤│
 *  ││ Ctrl │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter  ││
 *  │├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────┬───┤│
 *  ││ Shift  │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│Shift │Fn ││
 *  │└─────┬──┴┬──┴──┬┴───┴───┴───┴───┴───┴──┬┴───┴┬──┴┬─────┴───┘│
 *  │      │Fn │ Alt │         Space         │ Alt │Win│   HHKB   │
 *  │      └───┴─────┴───────────────────────┴─────┴───┘          │
 *  └─────────────────────────────────────────────────────────────┘
 * 版权:渤海新能 版权所有
 *
 * @author feiWang
 * 版本:1.5
 * 创建日期:2/8/21
 * 描述:Android_Draw
 * E-mail : [email protected]
 * CSDN:https://blog.csdn.net/m0_37667770/article
 * GitHub:https://github.com/luhenchang
 */
@Suppress("DEPRECATION")
class LHC_wave_View @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    
    
    private var moveX: Float = 0f
    private var moveY: Float = 0f
    private var hCount: Int = 0
    private var wCount: Int = 0
    private lateinit var pointList: ArrayList<PointF>



    //网格的宽度
    var gridWidth = 80
    //半波长
    val waveWidth=80f
    val waveHeight=30f
    lateinit var animal:ValueAnimator
    init {
    
    
        animal=ObjectAnimator.ofFloat(0f, waveWidth * 4)
        animal.duration = 2000
        animal.repeatCount = ValueAnimator.INFINITE;
        animal.interpolator = LinearInterpolator()
        animal.addUpdateListener {
    
    
            invalidate()
        }
    }

    override fun onDraw(canvas: Canvas) {
    
    
        super.onDraw(canvas)
        //绘制网格线
        drawGridLine(canvas)
        //绘制文字x和y轴的
        drawTextXAndY(canvas)
        //裁剪画布
        clipCanvas(canvas,2)
        //绘制波浪
        drawWave(canvas)
        //绘制
        drawFish(canvas)



    }
    val arrList= arrayListOf( R.drawable.yu, R.drawable.ziyuan, R.drawable.jingyu)
    private fun drawFish(canvas: Canvas) {
    
    

        val bmp = BitmapFactory.decodeResource(resources, arrList[0])
        canvas.drawBitmap(bmp, waveWidth * 3, -waveHeight * 2.2f, getPaint(Paint.Style.FILL))


        val bmpzy = BitmapFactory.decodeResource(resources,arrList[1])
        canvas.drawBitmap(bmpzy, waveWidth, -waveHeight *3f, getPaint(Paint.Style.FILL))

        val bmpjy= BitmapFactory.decodeResource(resources,arrList[2])
        canvas.drawBitmap(bmpjy, waveWidth* 6, -waveHeight * 3.5f, getPaint(Paint.Style.FILL))

    }

    private fun clipCanvas(canvas: Canvas,type:Int) {
    
    
        if (type==0) {
    
    
            val rect = Rect(
                (waveWidth * 4).toInt(),
                160,
                (waveWidth * 8).toInt(),
                (-waveWidth * 4).toInt()
            )
            canvas.clipRect(rect)
        }else  if (type==1) {
    
    
            val circlePath = Path()
            circlePath.addCircle(480f, 0f, 160f, Path.Direction.CCW)
            canvas.drawCircle(480f, 0f, 160f, getPaint(Paint.Style.STROKE))
            canvas.clipPath(circlePath)
        }else  if (type==2){
    
    
            val rundRect = Path()
            rundRect.addRoundRect(waveWidth * 4,waveWidth*3,waveWidth* 8,-waveWidth*3,60f,60f,Path.Direction.CCW)
            //canvas.drawPath(rundRect,getPaint(Paint.Style.STROKE))
            canvas.clipPath(rundRect)

        }
    }


    //绘制波浪
    private fun drawWave(canvas: Canvas) {
    
    
        canvas.translate(animal.animatedValue as Float, 0f) //内层海浪
        val wavePath=Path()
        wavePath.moveTo(0f, -waveWidth * 6)
        wavePath.lineTo(0f, 0f)
        wavePath.quadTo(waveWidth, waveHeight, waveWidth * 2, 0f)
        wavePath.quadTo(waveWidth * 3, -waveHeight, waveWidth * 4, 0f)
        wavePath.quadTo(waveWidth * 5, waveHeight, waveWidth * 6, 0f)
        wavePath.quadTo(waveWidth * 7, -waveHeight, waveWidth * 8, 0f)
        wavePath.lineTo(waveWidth * 8, -waveWidth * 6)
        canvas.drawPath(wavePath, getPaintBefor(Paint.Style.FILL))

        canvas.translate(animal.animatedValue as Float, 0f) //内层海浪
        val wavePath_out=Path()
        wavePath_out.moveTo(-waveWidth * 7, -waveWidth * 6)
        wavePath_out.lineTo(-waveWidth * 7, 0f)
        wavePath_out.quadTo(-waveWidth * 7, waveHeight, -waveWidth * 6, 0f)
        wavePath_out.quadTo(-waveWidth * 5, -waveHeight, -waveWidth * 4, 0f)
        wavePath_out.quadTo(-waveWidth * 3, waveHeight, -waveWidth * 2, 0f)
        wavePath_out.quadTo(-waveWidth, -waveHeight, 0f, 0f)
        wavePath_out.quadTo(waveWidth, waveHeight, waveWidth * 2, 0f)
        wavePath_out.quadTo(waveWidth * 3, -waveHeight, waveWidth * 4, 0f)
        wavePath_out.quadTo(waveWidth * 5, waveHeight, waveWidth * 6, 0f)
        wavePath_out.quadTo(waveWidth * 7, -waveHeight, waveWidth * 8, 0f)
        wavePath_out.lineTo(waveWidth * 8, -waveWidth * 6)
        canvas.drawPath(wavePath_out, getPaint(Paint.Style.FILL))


    }

    private fun getPaintIn(style: Paint.Style): Paint {
    
    
        val gPaint = Paint()
        gPaint.color = Color.BLUE
        gPaint.strokeWidth = 2f
        gPaint.isAntiAlias = true
        gPaint.style = style
        gPaint.textSize = 26f
        gPaint.color = Color.argb(255, 75, 151, 79)
        var linearGradient = LinearGradient(
            waveWidth * 4, -waveWidth * 8,
            waveWidth * 4,
            80f,
            Color.argb(255, 47, 26, 253),
            Color.argb(255, 24, 220, 253),

            Shader.TileMode.CLAMP
        )
        gPaint.shader=linearGradient
        return gPaint
    }


    override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
        when (event.action) {
    
    
            ACTION_DOWN,
            ACTION_MOVE -> {
    
    
                animal.start()
                //在控制点附近范围内部,进行移动
                Log.e(
                    "x=",
                    "onTouchEvent: (x,y)" + (event.x - width / 2).toInt() + ":" + (-(event.y - height / 2)).toInt()
                )
                moveX = event.x - width / 2
                moveY = -(event.y - height / 2)
                invalidate()

            }
        }
        return true
    }




    private fun drawTextXAndY(canvas: Canvas) {
    
    

        val gPaint = Paint()
        gPaint.color = Color.BLUE
        gPaint.strokeWidth = 2f
        gPaint.isAntiAlias = true
        gPaint.style = Paint.Style.STROKE
        gPaint.textSize = 26f
        gPaint.color = Color.argb(255, 75, 151, 79)
        canvas.scale(-1f, 1f)


        canvas.save()

        canvas.scale(1f, -1f)

        //x轴正方形文字
        for (index in 1 until wCount / 2) {
    
    
            val rectText = Rect()
            canvas.translate(160f, 0f)
            gPaint.getTextBounds(
                (80 * index * 2).toString(),
                0,
                (80 * index * 2).toString().length,
                rectText
            )
            canvas.drawText(
                (80 * index * 2).toString(),
                -(rectText.width() / 2).toFloat(), rectText.height().toFloat() * 2f, gPaint
            )
        }
        canvas.restore()
        canvas.save()
        //x轴负方向文字绘制
        canvas.scale(1f, -1f)
        for (index in 1 until wCount / 2) {
    
    
            val rectText = Rect()
            canvas.translate(-160f, 0f)
            gPaint.getTextBounds(
                "-${
      
      (80 * index * 2)}",
                0,
                (80 * index * 2).toString().length,
                rectText
            )
            canvas.drawText(
                "-${
      
      (80 * index * 2)}",
                -(rectText.width() / 2).toFloat(), rectText.height().toFloat() * 2f, gPaint
            )
        }
        canvas.restore()

        canvas.save()
        //x轴负方向文字绘制
        canvas.scale(1f, -1f)
        canvas.translate(20f, 0f)
        //y轴负方向
        for (index in 1 until hCount / 2) {
    
    
            val rectText = Rect()
            canvas.translate(0f, 160f)
            gPaint.getTextBounds(
                "-${
      
      (80 * index * 2)}",
                0,
                (80 * index * 2).toString().length,
                rectText
            )
            canvas.drawText(
                "-${
      
      (80 * index * 2)}",
                0f, rectText.height().toFloat(), gPaint
            )
        }
        canvas.restore()

        canvas.save()
        canvas.scale(1f, 1f)
        canvas.translate(20f, 0f)
        //y轴正方向
        for (index in 1 until hCount / 2) {
    
    
            val rectText = Rect()
            canvas.translate(0f, 160f)
            canvas.save()
            canvas.scale(1f, -1f)
            gPaint.getTextBounds(
                "${
      
      (80 * index * 2)}",
                0,
                (80 * index * 2).toString().length,
                rectText
            )
            canvas.drawText(
                "${
      
      (80 * index * 2)}",
                0f, rectText.height().toFloat(), gPaint
            )
            canvas.restore()
        }

        canvas.restore()


    }

    private fun drawGridLine(canvas: Canvas) {
    
    
        //初始化一个画笔
        val gPaint = Paint()
        gPaint.color = Color.BLUE
        gPaint.strokeWidth = 2f
        gPaint.isAntiAlias = true
        gPaint.style = Paint.Style.FILL
        gPaint.shader = RadialGradient(
            0f,
            0f,
            max(width, height) / 2f,
            Color.BLUE,
            Color.YELLOW,
            Shader.TileMode.CLAMP
        )
        //onDraw中已经知道屏幕宽度和高度
        val screenWidth = width
        val screenHeight = height
        //宽的格子个数
        wCount = screenWidth / gridWidth
        //高的格子个数
        hCount = screenHeight / gridWidth

        //1.将坐标点移动到屏幕的中点
        canvas.translate((screenWidth / 2).toFloat(), (screenHeight / 2).toFloat())
        //整体坐标系方向顺时针进行变化
        //2.修改y轴上方为正方向。
        canvas.scale(1f, -1f)
        //绘制x轴和y轴
        canvas.drawLine(-screenWidth / 2f, 0f, screenWidth / 2f, 0f, gPaint)
        canvas.drawLine(0f, -screenHeight / 2f, 0f, screenHeight / 2f, gPaint)
        gPaint.color = Color.argb(61, 111, 111, 111)
        drawGridCode(canvas, screenWidth, gPaint, hCount, screenHeight, wCount)
        //2.修改y轴下方为正方向。
        canvas.scale(1f, -1f)
        drawGridCode(canvas, screenWidth, gPaint, hCount, screenHeight, wCount)
        //3.修改x轴左正方向。
        canvas.scale(-1f, 1f)
        drawGridCode(canvas, screenWidth, gPaint, hCount, screenHeight, wCount)
        //4.修改x作为正y上为正
        canvas.scale(1f, -1f)
        drawGridCode(canvas, screenWidth, gPaint, hCount, screenHeight, wCount)


    }

    private fun drawGridCode(
        canvas: Canvas,
        screenWidth: Int,
        gPaint: Paint,
        hCount: Int,
        screenHeight: Int,
        wCount: Int
    ) {
    
    
        //这里保存好坐标圆点为屏幕中心的快照到堆栈里面。方便后期操作。
        canvas.save()
        //绘制一条横着的线条,重圆点(0,0)开始
        //canvas.drawLine(0f, 0f, (screenWidth / 2).toFloat(), 0f, gPaint)

        //3.绘制完成第一象限的平行x轴的线
        for (index in 0 until hCount) {
    
    
            //坐标系圆点不断向上平移gridWidth的高度
            canvas.translate(0f, gridWidth.toFloat())
            //在平移完的圆点直接画直线即可
            canvas.drawLine(0f, 0f, (screenWidth / 2).toFloat(), 0f, gPaint)
        }
        //恢复到快照状态。即圆点在中心
        canvas.restore()
        canvas.save()
        //4.绘制平行y轴的
        //canvas.drawLine(0f, 0f, 0f, screenHeight / 2f, gPaint)
        for (index in 0 until wCount) {
    
    
            //坐标系圆点不断向上平移gridWidth的高度
            canvas.translate(gridWidth.toFloat(), 0f)
            //在平移完的圆点直接画直线即可
            canvas.drawLine(0f, 0f, 0f, screenHeight / 2f, gPaint)
        }
        //恢复到快照状态。即圆点在中心
        canvas.restore()
    }
    private fun getPaintBefor(style: Paint.Style): Paint {
    
    
        val gPaint = Paint()
        gPaint.color = Color.BLUE
        gPaint.strokeWidth = 2f
        gPaint.isAntiAlias = true
        gPaint.style = style
        gPaint.textSize = 26f
        gPaint.color = Color.argb(255, 75, 151, 79)
        var linearGradient = LinearGradient(
            waveWidth * 4, -waveWidth * 8,
            waveWidth * 4,
            80f,
            Color.argb(155, 27, 134, 244),
            Color.argb(195, 24, 220, 253),
            Shader.TileMode.CLAMP
        )
        gPaint.shader=linearGradient
        return gPaint
    }
    private fun getPaint(style: Paint.Style): Paint {
    
    
        val gPaint = Paint()
        gPaint.color = Color.BLUE
        gPaint.strokeWidth = 2f
        gPaint.isAntiAlias = true
        gPaint.style = style
        gPaint.textSize = 26f
        gPaint.color = Color.argb(255, 75, 151, 79)
        var linearGradient = LinearGradient(
            waveWidth * 4, -waveWidth * 2,
            waveWidth * 4,
            80f,
            Color.argb(255, 27, 134, 244),
            Color.argb(255, 24, 220, 253),
            Shader.TileMode.CLAMP
        )
        gPaint.shader=linearGradient
        return gPaint
    }

}



四、曲线图表

图表最常用而最简单,无非绘制API简单的数学计算手势即可。接下来我们进行绘制市面上常见的曲线图表。

1.变换坐标

首先我们进行左边变换为我们常见的坐标系:将左边和底部文字空白处进行留出来。如下进行变换如果没看懂那就看看上一节折线里面讲解的坐标系变换。

        //y轴向上为负方向
        canvas.scale(1f, -1f)
        //坐标系向右平移leftWidth,向下平移动 height-bootomHeight,这里向上是正哦。所以向下平移是负数
        canvas.translate(leftWidth, -(height - bottomHeight))
//新建类LHC_curve_View
class LHC_curve_View @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    
    
    //距离左边的距离
    val leftWidth = 180f
    //距离最底部的高度
    val bottomHeight = 240f
    //x轴的宽度
    var x_scaleWidth=0f
    //方格的宽高
    private var grid_width=0f
    init {
    
    

    }

    override fun onDraw(canvas: Canvas) {
    
    
        super.onDraw(canvas)
        //2.画布平移
        translateCanvas(canvas)
    }

    //平移画布
    private fun translateCanvas(canvas: Canvas) {
    
    
        //y轴向上为负方向
        canvas.scale(1f, -1f)
        //坐标系向右平移leftWidth,向下平移动 height-bootomHeight,这里向上是正哦。所以向下平移是负数
        canvas.translate(leftWidth, -(height - bottomHeight))
     }
    
}

2.绘制平行x轴的线

绘制平行x轴的线,上面我们已经转换坐标系接下来我们绘制平行x轴的线

//绘制平行x轴的线
    private fun drawLine(canvas: Canvas) {
    
    
        //canvas.drawColor(Color.argb(22, 255, 255, 111))
        val line_paint = Paint()
        line_paint.strokeWidth = 2f
        line_paint.style = Paint.Style.STROKE
        line_paint.color = Color.argb(100, 188, 188, 188)

        x_scaleWidth = (width - leftWidth - 80f)
        grid_width = x_scaleWidth / 6
        //绘制最底部一条线
        val x_path = Path()
        x_path.moveTo(0f, 0f)
        x_path.lineTo(x_scaleWidth, 0f)
        canvas.drawPath(x_path, line_paint)

        canvas.save()
        //通过平移画布绘制剩余的平行x轴线
        (0 until 3).forEach {
    
     index ->
            canvas.translate(0f, grid_width-40f)
            canvas.drawPath(x_path, line_paint)
        }
        canvas.restore()

    }

效果如下:

3.绘制文字

  • 文字绘制x轴下方的文字,无非涉及到文字的测量和画布的变换。
1.Paint的getTextBounds结合Rect进行测量
2.利用canvas的save和restore进行画布随意的变换

 //水平方向的x轴下面的文字
    private fun drawDownOfXLineText(canvas: Canvas) {
    
    
        val text_paint = Paint()
        text_paint.strokeWidth = 2f
        text_paint.style = Paint.Style.STROKE
        text_paint.color = Color.argb(100, 111, 111, 111)
        text_paint.textSize=24f

        val rectText=Rect()
        canvas.save()
        //将文字旋转摆正,此时坐标系y向下是正
        canvas.scale(1f,-1f)
        (0 until 7).forEach {
    
     index ->
            if(index>0) {
    
    
                canvas.translate(grid_width, 0f)
            }
            val strTx= "11.${
      
      11+index}"
            text_paint.getTextBounds(strTx,0,strTx.length,rectText)
            canvas.drawText(strTx, -rectText.width().toFloat()/2, rectText.height().toFloat()*2.5f,text_paint)
        }
        canvas.restore()

    }
    
    
     private fun drawLeftOfYLineText(canvas: Canvas) {
    
    
        val text_paint = Paint()
        text_paint.strokeWidth = 2f
        text_paint.style = Paint.Style.STROKE
        text_paint.color = Color.argb(100, 111, 111, 111)
        text_paint.textSize=24f

        val rectText=Rect()
        canvas.save()
        //将文字旋转摆正,此时坐标系y向下是正
        (0 until 4).forEach {
    
     index ->
            if(index>0) {
    
    
                canvas.translate(0f, grid_width-40f)
            }
            var strTx=""
            if(index==0){
    
    
                strTx="${
      
      index}"
            }else if(index==1){
    
    
                strTx="${
      
      500}"
            }else if(index==2){
    
    
                strTx="1k"
            }else{
    
    
                strTx="1.5k"
            }

            canvas.save()
            canvas.scale(1f,-1f)
            text_paint.getTextBounds(strTx,0,strTx.length,rectText)
            canvas.drawText(strTx, -rectText.width().toFloat()-42f, rectText.height().toFloat()/2,text_paint)
            canvas.restore()
        }
        canvas.restore()

    }

效果

3.曲线的绘制

  • 二阶曲线绘制,我们会发现缺少控制点。下面我们来进行控制点的计算

如上图我们不难得出控制点 controX=( x 1 x_1 x1+ x 2 x_2 x2)/2、controY=( y 1 y_1 y1+ y 2 y_2 y2)/2。接下来我们进行绘制曲线。x轴grid_width=1天的时间。不难计算出中间控制点的x。y轴500=grid_width-40f。

代码如下:

    private fun drawCaves(canvas: Canvas) {
    
    
        val text_paint = Paint()
        text_paint.strokeWidth = 2f
        text_paint.style = Paint.Style.STROKE
        text_paint.color = Color.argb(100, 111, 111, 111)

       val caves_path=Path()
        //500=grid_width-40 每个单位的长度的=像素长度
       val danweiY=(grid_width-40)/500
       val danweiX=(grid_width)
       for (index in 0 until dataList.size-1){
    
    
            if (dataList[index]==dataList[index+1]){
    
    
               caves_path.quadTo(
                   (grid_width * index + grid_width * (1 + index)) / 2,
                   0f,
                   grid_width * (index + 1),
                   (dataList[index + 1].toFloat()) * danweiY
               )
           }else {
    
    
               caves_path.quadTo(
                   (grid_width * index + grid_width * (1 + index)) / 2,
                   (dataList[index].toFloat() * danweiY + dataList[index + 1].toFloat() * danweiY) / 2 +100,
                   grid_width * (index + 1),
                   (dataList[index + 1].toFloat()) * danweiY
               )
           }
       }
       canvas.drawCircle(0f,0f,10f,text_paint)
       canvas.drawPath(caves_path,text_paint)
    }


但是我们会发现在两个焦点处没有弧度。很生硬,接下来我们继续分析和解决达到完美。至于最终效果我们可以更酷更炫。

4.三阶曲线的拯救

y 1 y_1 y1< y 2 y_2 y2如上图1.求出中点坐标x轴下部分控制点x+40px,上部分x-40px,y轴也可以调整来搞搞平滑度下部分控制点y-40x,上部分y+40。
1.获取中点的坐标( X 中 X_中 X Y 中 Y_中 Y)= (( x 1 x_1 x1+ x 2 x_2 x2)/2、( y 1 y_1 y1+ y 2 y_2 y2)/2)
2. x 1 x_1 x1 X 中 X_中 X之间的坐标=(( x 1 x_1 x1+ x 中 x_中 x)/2、( y 1 y_1 y1+ y 中 y_中 y)/2)
3. x 中 x_中 x X 2 X_2 X2之间的坐标=(( x 中 x_中 x+ x 2 x_2 x2)/2、( y 中 y_中 y+ y 2 y_2 y2)/2)

y 1 y_1 y1> y 2 y_2 y2如上图2.求出中点坐标x轴上部分+40px,下部分x-40px,y轴也可以调整,y轴也可以调整来搞搞平滑度上部分控制点y+40x,下部分y-40。
1.获取中点的坐标( X 中 X_中 X Y 中 Y_中 Y)= (( x 1 x_1 x1+ x 2 x_2 x2)/2、( y 1 y_1 y1+ y 2 y_2 y2)/2)
2. x 1 x_1 x1 X 中 X_中 X之间的坐标=(( x 1 x_1 x1+ x 中 x_中 x)/2、( y 1 y_1 y1+ y 中 y_中 y)/2)
3. x 中 x_中 x X 2 X_2 X2之间的坐标=(( x 中 x_中 x+ x 2 x_2 x2)/2、( y 中 y_中 y+ y 2 y_2 y2)/2)

接下来我们堆代码:

   //绘制曲线
    private fun drawCaves(canvas: Canvas) {
    
    
        val text_paint = Paint()
        text_paint.strokeWidth = 2f
        text_paint.style = Paint.Style.STROKE
        text_paint.color = Color.argb(100, 111, 111, 111)

        val caves_path = Path()
        //500=grid_width-40 每个单位的长度的=像素长度
        val danweiY = (grid_width - 40) / 500
        val danweiX = (grid_width)
        for (index in 0 until dataList.size - 1) {
    
    
            //二阶曲线显示比较尴尬
            //           if (dataList[index]==dataList[index+1]){
    
    
            //               caves_path.quadTo(
            //                   (grid_width * index + grid_width * (1 + index)) / 2,
            //                   0f,
            //                   grid_width * (index + 1),
            //                   (dataList[index + 1].toFloat()) * danweiY
            //               )
            //           }else {
    
    
            //               caves_path.quadTo(
            //                   (grid_width * index + grid_width * (1 + index)) / 2,
            //                   (dataList[index].toFloat() * danweiY + dataList[index + 1].toFloat() * danweiY) / 2 +100,
            //                   grid_width * (index + 1),
            //                   (dataList[index + 1].toFloat()) * danweiY
            //               )
            //           }
            //三阶曲线来显示
            val xMoveDistance=40
            val yMoveDistance=40

            if (dataList[index] == dataList[index + 1]) {
    
    
                caves_path.lineTo(danweiX*(index+1),0f)
            } else if(dataList[index] < dataList[index + 1]){
    
    //y1<y2情况
                val centerX=(grid_width * index + grid_width * (1 + index)) / 2
                val centerY=(dataList[index].toFloat() * danweiY + dataList[index + 1].toFloat() * danweiY) / 2
                val controX0=(grid_width * index+centerX)/2
                val controY0=(dataList[index].toFloat() * danweiY+centerY)/2
                val controX1=(centerX+ grid_width * (1 + index))/2
                val controY1=(centerY+dataList[index+1].toFloat() * danweiY)/2
                caves_path.cubicTo(controX0+xMoveDistance,controY0-yMoveDistance,controX1-xMoveDistance,controY1+yMoveDistance,grid_width * (1 + index),dataList[index + 1].toFloat() * danweiY)
            }else{
    
    
                val centerX=(grid_width * index + grid_width * (1 + index)) / 2
                val centerY=(dataList[index].toFloat() * danweiY + dataList[index + 1].toFloat() * danweiY) / 2
                val controX0=(grid_width * index+centerX)/2
                val controY0=(dataList[index].toFloat() * danweiY+centerY)/2
                val controX1=(centerX+ grid_width * (1 + index))/2
                val controY1=(centerY+dataList[index+1].toFloat() * danweiY)/2
                caves_path.cubicTo(controX0+xMoveDistance,controY0+yMoveDistance,controX1-xMoveDistance,controY1-yMoveDistance,grid_width * (1 + index),dataList[index + 1].toFloat() * danweiY)

            }
        }
        canvas.drawCircle(0f, 0f, 10f, text_paint)
        canvas.drawPath(caves_path, text_paint)
    }

看看效果吧

  • 是不是有点感觉了,无非控制点进行调节罢了,我们看到人家的比较尖,我们对于的xMoveDistance,yMoveDistance进行调整

val xMoveDistance=20
val yMoveDistance=40

到这里我们最大的困难也简单的解决了吧?无非简单的加减乘除是不是?接下来我们进行装饰美丽的曲线,学过之前的文章对于这几点技能我想我们都熟能生巧的创作了吧?渐变填充动画点击手势贴图

6.美化曲线

  • 我记得说过要画比它更骚的,我想我们应该能办到,多和骚不一定美,但是处处是技术,那下来我们在多和骚方面入手,至于结果我们不敢说比UI设计更美对吧。

1.美化技能之渐变填充

  • 我们发现设计图有渐变从上到下逐渐变淡。我们选择线性渐变最高点(0,y)到(0,0)即可。心里没数的遍历求最大值。我数据为了方便写死的。
 val linearGradient = LinearGradient(
            0f, 1500 * danweiY,
            0f,
            0f,
            Color.argb(255,229,160,144),
            Color.argb(255,251,244,240),
            Shader.TileMode.CLAMP
        )
        text_paint.shader = linearGradient

2.美化技能之环绕Path

  • 我们发现人家的UI有个红色线进行了环绕。我们直接拿着曲线进行设置画笔即可。每个顶点需要加个圈。
         //绘制闭合渐变曲线
        canvas.drawPath(caves_path, text_paint)
        val line_paint = Paint()
        line_paint.strokeWidth = 3f
        line_paint.style = Paint.Style.STROKE
        line_paint.color = Color.argb(255, 212, 100, 77)
        //绘制外环红色线
        canvas.drawPath(caves_path, line_paint)
        line_paint.style = Paint.Style.FILL
        //画圈。
        for (index in 0 until dataList.size ) {
    
    
           canvas.drawCircle(grid_width*index,danweiY*dataList[index],6f,line_paint)
        }

看看效果:

绘制文字

 //绘制前7天和后7天文字按钮
    private fun drawTextButton(canvas: Canvas) {
    
    
        val line_paint = Paint()
        line_paint.strokeWidth = 2f
        line_paint.style = Paint.Style.STROKE
        line_paint.color = Color.argb(188, 76, 126, 245)
        line_paint.textSize=38f
        val buttonPath = Path()
        buttonPath.addRoundRect(100f, -120f, 320f, -200f, 80f, 80f, Path.Direction.CCW)
        canvas.drawPath(buttonPath,line_paint)
        canvas.save()
        canvas.scale(1f,-1f)
        canvas.drawText("前 七 天",140f,175f,line_paint)
        canvas.restore()

        canvas.save()
        canvas.translate(260f,0f)
        canvas.drawPath(buttonPath,line_paint)
        canvas.scale(1f,-1f)
        canvas.drawText("后 七 天",140f,175f,line_paint)

        canvas.restore()

    }

效果如下:

3.美化技能之贴图

贴图之前博客就用过了,这里不多讲了。加个裁剪就完事了。

  //绘制头像在顶部
    private fun drawHeaderToCanvas(canvas: Canvas) {
    
    
        val bitmap_paint = Paint()
        bitmap_paint.strokeWidth = 2f
        bitmap_paint.style = Paint.Style.STROKE
        bitmap_paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        bitmap_paint.isAntiAlias =true
        canvas.save()
        val srcRect1=Rect(0, 0, 80, 80)
        val dstRect1=Rect(0, 0, 40, 40)
        val danweiY = (grid_width - 40) / 500
        for (index in 0 until dataList.size) {
    
    
            val mdrawable = ContextCompat.getDrawable(context, imgList[index])
            val bitmap = getBitmap(bitmap_paint, mdrawable!!)
            canvas.save()
            canvas.translate(
                grid_width * index - bitmap.width / 4,
                danweiY * dataList[index] + 20
            )
            //这里绘制图片到画布上
            val circlePath = Path()
            circlePath.addCircle(20f,20f, 20f, Path.Direction.CCW)
            canvas.clipPath(circlePath)
            canvas.drawBitmap(bitmap, srcRect1, dstRect1, bitmap_paint)
            canvas.restore()
        }
        canvas.restore()


    }

到这里我想已经达到UI设计图了吧。我们多加了个图片。文字这么简单的东西我就不啰嗦了。

4.强迫症

为了达到一致效果,自己强迫症没法子,绘制完最终效果。那些写死的文字和距离不是一个专业开发人员的素质,希望你们写的时候自己多测量多计算。我这里为了速度都是写死的数据。切勿模仿。

 private fun drawTopTextToCanvas(canvas: Canvas) {
    
    
        val text_paint = Paint()
        text_paint.strokeWidth = 2f
        text_paint.style = Paint.Style.FILL
        text_paint.color = Color.argb(255, 0, 0, 0)
        text_paint.textSize =66f
        val rectText = Rect()
        val rectTextYuan = Rect()

        canvas.save()
        canvas.scale(1f, -1f)
        canvas.translate((width/2).toFloat()-100,-500f)
        val text="1347"
        val textyu="元"

        text_paint.getTextBounds(text, 0,text.length, rectText)

        canvas.drawText(
            text,
            -rectText.width().toFloat() - 42f,
            rectText.height().toFloat() / 2,
            text_paint
        )
        text_paint.color = Color.argb(111, 111, 111, 111)
        text_paint.getTextBounds(textyu, 0,textyu.length, rectTextYuan)
        text_paint.textSize =33f
        canvas.drawText(
            textyu,
             80+ -rectTextYuan.width().toFloat() - 42f,
            rectTextYuan.height().toFloat() / 2,
            text_paint
        )

        canvas.translate(0f,50f)
        canvas.drawText(
            "较前天",
            -rectTextYuan.width().toFloat() - 180f,
            rectTextYuan.height().toFloat() / 2,
            text_paint
        )
        canvas.translate(100f,0f)
        text_paint.color = Color.argb(255, 223, 129, 120)
        canvas.drawText(
            "+971.99(251.19%)",
            -rectTextYuan.width().toFloat() - 180f,
            rectTextYuan.height().toFloat() / 2,
            text_paint
        )
        canvas.translate(-100f,50f)
        text_paint.color = Color.argb(111, 111, 111, 111)
        canvas.drawText(
            "对应图中虚线部分进行最高评奖",
            -rectTextYuan.width().toFloat() - 180f,
            rectTextYuan.height().toFloat() / 2,
            text_paint
        )
        //暂时没找到canvas绘制富文本的方法。只能一个个测量绘制文字了。别学我,好好测量测量有待提高自己的小学计算。

//        val textSpanned1 =  SpannableString("Hello World");
//        textSpanned1.setSpan(ForegroundColorSpan(Color.RED), 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
//        text_paint.reset()
//        text_paint.textSize=44f
//        canvas.drawText(textSpanned1,0,10,0f,0f,text_paint)
        canvas.restore()
    }

7.美化技能之手势动画

如果不明白手势滑动缩放动画的多看看之前的几篇文章,这个作业留给你们

五、曲线-游戏

明天再写吧睡觉

猜你喜欢

转载自blog.csdn.net/m0_37667770/article/details/113885095
今日推荐