1. Knowledge points involved in this article

Bessel curve
The View map
Usage extension of Path (Region)

2. Plot irregular continuous waves (Bezier curves)

Drawing waves, in my opinion, whether you use semicircles, semicircles or arcs, is OK, mainly depends on how you use them, and how the various coordinates you draw should be combined with the animation.

(I’m using a second-order Bezier curve here.)

The first is the definition and initialization of the parameters:

class WaveBoatView(context: Context, attrs: AttributeSet) : View(context, attrs) {


    // A wave length is equivalent to the length of two second-order Bezier curves
    //var mWaveLength: Float = 200f

    // There are several waves in an interface (either mWavaLength or mWavaLength)
    var mWaveNumber = 5

    // Start point
    var mStartX = 0.0 f
    var mStartY = 0.0 f

    // Wave brushes
    private var mPaint: Paint? = Paint().apply {
    	// Since it is a wave, it must be arranged in blue
        color = Color.BLUE
        style = Paint.Style.FILL_AND_STROKE
        strokeJoin = Paint.Join.ROUND
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // Get the length of a wave based on the number of waves to display (if not using a fixed length, but based on the page adaptation)
        mWaveLength = width.toFloat() / mWaveNumber
        // Select 1/2 height as the starting position
        mStartY = height.toFloat() / 2}}Copy the code

The next step is to draw a continuous wave.

Before you draw a wave, you might want to think about, how do you splice together a continuous wave from multiple Bezier curves

First, we need to understand the difference between path.rquadto () and path.quadto (). By understanding this, it is possible that rQuadTo is more suitable for us because it saves a record of multiple connected coordinate points in Path. Here is one of our Bessel curve anchor point and curve implementation effect:

And here you can see that Bessel’s last coordinate point is drawn out of the screen, right?

Because of the limitations of the Bezier curve, we can not say that the end is at the right end, we can only choose to draw out of the screen, so in the loop, we use to determine whether the current length is longer than the parent layout length, to determine whether we need to draw again (next wave or two).

override fun onDraw(canvas: Canvas?) {super.ondraw (canvas) // Every time you initialize the Path mPath!! . Reset () val halfWaveLen = mWaveLength / 2 // .moveto (0, mStartY) // The starting position of the wave // Each for loop adds the length of a wave to the path, Var I = 0f while (I <= widthtoFloat()){mPath!! .rQuadTo( halfWaveLen / 2.toFloat(), -200f, halfWaveLen.toFloat(), 0f ) mPath!! .rQuadTo( halfWaveLen / 2.toFloat(), 200f, halfWaveLen.toFloat(), 0f ) i += mWaveLength } mPath!! .lineTo(width.toFloat(), height.toFloat()) mPath!! .lineTo(0f, height.toFloat()) mPath!! .close() // Close the path to canvas!! .drawPath(mPath!! , mPaint!!) }Copy the code

3. Wave animation

A static wave has been drawn, now think about how to make this wave wave:

The wave moves to the right, which is essentially the starting point of the Path, moves to the right, so we just animate the Path to move to the right


//Path moves the coordinate point
    var dx = 0f
    	// Override the set method to refresh the interface each time a new value is set
        set(value) {
            field = value
            postInvalidate()
        }
    //  
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
            .......
            // Start animation
			startAnim()
			
        }
        
    override fun onDraw(canvas: Canvas?). {
        super.onDraw(canvas)
        / /.. Omit duplicate code
        //mPath!! MoveTo (0, mStartY) changesmPath!! .moveTo(dx, mStartY)// The starting position of the wave
        / /.. Omit duplicate code
    }   
    // Start animation
    fun startAnim(a) {
        // From 0 to mItemWaveLength dx, you can change the initial position of the wave by increasing the length of the wave.
        // So you can create a full wave animation. If the dx is at most smaller than the mItemWaveLength, you can draw a full wave shape
        var anim: ObjectAnimator =
            ObjectAnimator.ofFloat(this."dx".0f, mWaveLength.toFloat())
        anim.duration = 2000
		anim.repeatCount = ValueAnimator.INFINITE
        anim.start()
    }
Copy the code

Here we see that the wave has moved back, but the front part of the wave has also moved away with the wave, so we need to draw one more wave off the screen, which is the same length of movement as our animation, to achieve what looks like a continuous wave. That is, the starting point of our Path, and the starting point for calculating the wave length, needs to be shifted to the left by one wave length.

override fun onDraw(canvas: Canvas?). {
        / /.. Omit duplicate code
        //mPath!! .moveTo(dx, mStartY)mPath!! .moveTo(-mWaveLength + dx, mStartY)// The starting position of the wave
        // Each for loop adds the length of a wave to the path
     	// Calculate how many wave lengths can be added based on the width of the view
        // var i = 0f
        var i = -mWaveLength
		/ /.. Omit duplicate code
    }

Copy the code

Look, this animation is starting to come together

What about random waves?

In fact, the principle of Random wave is very simple. Originally, our wave bezier height was set to 200, but now we just need to change 200 to Random number. (Of course, you can directly use Random to generate Random number and put it directly in Bezier function, it will not work.

We need a data structure that holds each set of waves (one up and one down), subtracting that set of waves from the last one as a wave passes, and adding a new set of waves to the head

????? This data structure, isn’t it just a linked list? Yes, we use LinkedList.

As mentioned above, when a wave passes, it corresponds to the animation, that is, when the animation ends. Since we adopt Repeat mode animation, we only need to update the linked list in Repeat monitor

	// Save random waves
    var heightList =  LinkedList<Float> ()override fun onDraw(canvas: Canvas?). {
        super.onDraw(canvas)
       	/ /.. Omit duplicate code
        // What wave is it now
		var waveIndex =0
        while(i <= width.toFloat()) { mPath!! .rQuadTo( halfWaveLen /2.toFloat(),
                // Use a random wave height
                -heightList.get(waveIndex),
                halfWaveLen.toFloat(),
                0f) mPath!! .rQuadTo( halfWaveLen /2.toFloat(),
                // Use a random wave height
                heightList.get(waveIndex),
                halfWaveLen.toFloat(),
                0f
            )
            i += mWaveLength
        }
        / /.. Omit duplicate code

    }
    
    fun startAnim(a) {
        // From 0 to mItemWaveLength dx, you can change the initial position of the wave by increasing the length of the wave.
        // So you can create a full wave animation. If the dx is at most smaller than the mItemWaveLength, you can draw a full wave shape
        var anim: ObjectAnimator =
            ObjectAnimator.ofFloat(BezierView@ this."dx".0f, mItemWaveLength.toFloat())
        anim.duration = 1000
        // Why not repeat? Repeat can be used by itself, which will lead to an animated flash screen
        // You need to put it in hanlder, otherwise start is invalid
        anim.doOnEnd {
            handler.post {
                heightList.addFirst(Random.nextInt(500).toFloat())
                heightList.removeAt(mWaveNumber + 6)
                anim.start()
            }
        }
        anim.start()
    }
    

Copy the code

In this case, a wave of random height would be fine

4. Draw the boat (use of Path and Region)

What is Region for?

Region is actually used to get the Region, the Region that intersects the Path and let’s say I want to get the height of the wave in the middle?

I can intersect Path through the middle Region and then get the height of the intersecting Region. That’s it.

 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        
		/ /.. Omit duplicate code
        // The boat is centered by default
        var x = width.toFloat() / 2
        var region = Region()
        
        var clip = Region((x - 0.1).toInt(), 0, x.toInt(), height)
        
		// Get the height area of the boat
        var rect = region.getBounds()
        
        canvas.drawBitmap(
            boat,
            // Put the bottom of the boat against the waves
            rect.right.toFloat() - boat.width / 2,
            rect.top.toFloat() - boat.height / 4 * 3,
            mPaint
        )
    }
Copy the code

And then the boat can go up and down, with the waves

The boat rolled with the waves

How does the boat swing work? Some people here may ask, is the Matrix directly obtained by PathMeasure?

Since there is no way to get the length of the midpoint of Bezier curve, we have to use Region again to get the rotation Angle of the boat. If we can’t get trigonometric function, we can skip it and the boat can’t turn (dog).Here I’m using two regions to get two points to get angles to get the rotation of the boatThe two red areas, the two height points, you can get the trigonometric values and figure out the angles

override fun onDraw(canvas: Canvas?). {
        / /... Omit duplicate code

        // The boat is centered by default
        // Get two points on the Path
        var x = width.toFloat() / 2
        var region = Region()
        var region2 = Region()
        var clip = Region((x - 0.1).toInt(), 0, x.toInt(), height)
        var clip2 = Region((x - 10).toInt(), 0, (x - 9).toInt(), height) region.setPath(mPath!! , clip) region2.setPath(mPath!! , clip2)var rect = region.getBounds()
        var rect2 = region2.getBounds()
		// Calculate the Angle value
        val fl =
            -atan2(-rect.top.toFloat() + rect2.top.toFloat(), 9.5 f) * 180 / Math.PI.toFloat()

        canvas.save()
		// Perform rotation
        canvas.rotate(
            fl, rect.right.toFloat(),
            rect.top.toFloat()
        )
        canvas.drawBitmap(
            boat,
            rect.right.toFloat() - boat.width / 2,
            rect.top.toFloat() - boat.height / 4 * 3,
            mPaint
        )
        canvas.restore()

    }
Copy the code

After this, our animation effect can be realized

5. Extension

Through this project, I hope you can not only learn the Bezier curve, but also with different animation effects, to achieve better animation

Git project address github.com/ZhuHuaming/…