In this Article we are going to fly a plane or the screen

###### Path()

Path is an android class it is very useful or many time only choice in android for creating arbitrary shapes and drawings arbitrary figures , Path is also used in animations. But often times working with paths is very painful. If you don’t want to bear that pain anymore bear with me for rest of the article.

So we will fly a plane on a random path using path animations.

###### Taking a Random Path in Life

I am going to generate a random path on screen, our plane will fly on that path only.

private fun initPlanePath() { | |

planePath.reset() | |

var startX = randomX() | |

var startY = randomY() | |

repeat(3) { | |

planePath.moveTo(startX, startY) | |

val controlX = randomX() | |

val controlY = randomY() | |

val endX = randomX() | |

val endY = randomY() | |

planePath.quadTo(controlX, controlY, endX, endY) | |

startX = endX | |

startY = endY | |

} | |

} | |

//generate a random number between 0 and view width | |

private fun randomX() = random.nextInt(0, width).toFloat() | |

//generate a random number between 0 and view height | |

private fun randomY() = random.nextInt(0, height).toFloat() |

result of drawing random path for a random run of app

here the key method is `quadTo()`

which is used to generate a quadratic bezier curve , I am creating multiple bezier curves which are connected to each other the starting point of each path (bezier curve) is called a contour in paths language, there is also a `cubicTo()`

which is used to create a cubic bezier curve which has 2 control points.

I encourage you to learn more about bezier curve so that you understand what are control points and how above code works under the hood.

###### let’s Fly a plane

We want to fly a plane along a path to be able to do that we need to know about x and y co-ordinates of points which forms this path so that we can keep setting x and y or our plane on those co-ordinates to produce an animation to know about those co-ordinates we need to understand a really important class.

**PathMeasure()**

**PathMeasure** is best friend class of a Path class , a path is kind of shy it won’t directly tell you about its personal things like it’s length , tangent of a line at any point on it’s body , length of a segment between two points , whether or not it is closed etc.

To handle paths you’d like as much information as you can so you may want PathMeasure as common friend , I’ll familiarize you to it along the way.

###### getPosTan

`getPosTan(float distance, float[] pos, float[] tan)`

this method is used to get co-ordinates of point on a path a given distance.

It takes three arguments

`distance`

we will get the co-ordinates at this distance

`pos`

the x and y co-ordinates are written in this float array

`tan`

the tangent in radian is written in this array

###### nextContour

Our path is made of several `quadTo()`

commands `getposTan()`

or other methods of **PathMeasure** do not operate of entire path at once rather they operate on one command at a time in our case you can say a contour is one path which is created by one `quadTo()`

call for moving to the next we call `nextContour()`

this method returns true if there is a next path false if we are done with entire path.

So if we continuously keep getting these co-ordinates for subsequent distances on the path we can put an object at those co-ordinates and it will appear moving , that is what we are going to do now

private fun flyPlane() { | |

var distance = 0f | |

val tan = floatArrayOf(0f,0f) | |

val pos = floatArrayOf(0f, 0f) | |

val valueAnimator = ValueAnimator.ofFloat(0f, 1f) | |

valueAnimator.duration = (pathMeasure.length * 5).toLong() | |

valueAnimator.interpolator = AccelerateDecelerateInterpolator() | |

valueAnimator.addUpdateListener { | |

distance = it.animatedValue as Float | |

pathMeasure.getPosTan(distance * pathMeasure.length, pos, tan) | |

} | |

valueAnimator.doOnEnd { | |

if (pathMeasure.nextContour()) { | |

flyPlane() | |

} | |

} | |

valueAnimator.start() | |

} |

explanation of above animation

here we are animating from `0f`

to `1f`

it will give us co-ordinates at different lengths which is tried to explain in image above after getting a co-ordinate we set the x and y of our plane at those co-ordinates when we are done with a contour on a curve we jump to the next one using `nextContour()`

until the entire path is done.

private fun flyPlane() { | |

var distance = 0f | |

val tan = floatArrayOf(0f,0f) | |

val pos = floatArrayOf(0f, 0f) | |

val valueAnimator = ValueAnimator.ofFloat(0f, 1f) | |

valueAnimator.duration = (pathMeasure.length * 5).toLong() | |

valueAnimator.interpolator = AccelerateDecelerateInterpolator() | |

valueAnimator.addUpdateListener { | |

distance = it.animatedValue as Float | |

pathMeasure.getPosTan(distance * pathMeasure.length, pos,null) | |

val planeX = pos[0] | |

val planeY = pos[1] | |

plane.x = planeX | |

plane.y = planeY | |

} | |

valueAnimator.doOnEnd { | |

if (pathMeasure.nextContour()) { | |

flyPlane() | |

} | |

} | |

valueAnimator.start() | |

} |

## Job Offers

now we just take the `pos`

in which co-ordinates were written during animation and place our plane on these co-ordinates since we are invalidating in value animation on each animated value the animation will look smooth.

but hey planes do not fly backwards or sideways we can not go against nature and break the laws of physics we need to do something about this.

###### Taking a right direction in Life

Do you remember the third argument to `getPostTan()`

which gives as tangent at any point on the curve also described in image above, it will help us to align the tip of the plane in the right direction.

private fun flyPlane() { | |

var distance = 0f | |

val tan = floatArrayOf(0f,0f) | |

val pos = floatArrayOf(0f, 0f) | |

val valueAnimator = ValueAnimator.ofFloat(0f, 1f) | |

valueAnimator.duration = (pathMeasure.length * 5).toLong() | |

valueAnimator.interpolator = AccelerateDecelerateInterpolator() | |

valueAnimator.addUpdateListener { | |

distance = it.animatedValue as Float | |

pathMeasure.getPosTan(distance * pathMeasure.length, pos, tan) | |

val planeX = pos[0] | |

val planeY = pos[1] | |

plane.x = planeX | |

plane.y = planeY | |

val degrees = atan2(tan[1], tan[0]) * 180.0 / Math.PI | |

plane.rotation = degrees.toFloat() - 180 | |

} | |

valueAnimator.doOnEnd { | |

if (pathMeasure.nextContour()) { | |

flyPlane() | |

} | |

} | |

valueAnimator.start() | |

} |

here we are getting tangent to `pos`

then converting this angle to degrees are setting rotation of plane accordingly.

###### Flying Multiple Planes

One Plane is not enough to defeat our enemy we need more fighters , I am just going to use and array of planes and and array of pathmeasures one path measure for each plane then just modify our fly plane function a bit to accept a plane and a pathmeasure.

private fun flyPlane(plane: ImageView, pathMeasure: PathMeasure) { | |

var distance = 0f | |

val tan = floatArrayOf(0f,0f) | |

val pos = floatArrayOf(0f, 0f) | |

val valueAnimator = ValueAnimator.ofFloat(0f, 1f) | |

// fly planes with different speed so that they look separately on the screen | |

valueAnimator.duration = (pathMeasure.length * random.nextInt(3,8)).toLong() | |

valueAnimator.interpolator = AccelerateDecelerateInterpolator() | |

valueAnimator.addUpdateListener { | |

distance = it.animatedValue as Float | |

pathMeasure.getPosTan(distance * pathMeasure.length, pos, tan) | |

val planeX = pos[0] | |

val planeY = pos[1] | |

plane.x = planeX | |

plane.y = planeY | |

val degrees = atan2(tan[1], tan[0]) * 180.0 / Math.PI | |

plane.rotation = degrees.toFloat() - 180 | |

} | |

valueAnimator.doOnEnd { | |

if (pathMeasure.nextContour()) { | |

flyPlane(plane, pathMeasure) | |

} | |

} | |

valueAnimator.start() | |

} |

If you find it interesting you can find complete code on my *Github*

This article was originally published on proandroiddev.com on June 30, 2022