Welcome 👋
In this article, we’ll explore how to craft a stunning animated arrow pointer using Jetpack Compose and enhance your app’s appearance in 5 minutes.
Stay tuned, and let’s dive in! 🚀
Defining the Function
Let’s start by declaring the AnimatedArrowPointer
function and go through its parameters.
@Composable
fun AnimatedArrowPointer(
modifier: Modifier,
color: Color,
isVisible: Boolean = true,
strokeWidth: Dp = 2.dp,
pointerSize: Dp = 12.dp,
dashLength: Dp? = 4.dp,
strokeCap: StrokeCap = StrokeCap.Round,
pointerShape: Shape = SimpleArrow,
animationSpec: AnimationSpec<Float> = tween(3000)
)
⚒️ ️️Parameters breakdown
⚡ modifier ➜ Modifier to be applied to the pointer layout.
⚡ color ➜ Color of the arrow pointer.
⚡ isVisible ➜ Determines if the arrow pointer is visible.
⚡ strokeWidth ➜ Width of the arrow pointer’s stroke.
⚡ pointerSize ➜ Size of the arrow pointer.
⚡ dashLength ➜ Length of the stroke dashes; null
means a solid stroke.
⚡ strokeCap ➜ Style of stroke endings in the arrow pointer.
⚡ pointerShape ➜ Shape of the arrow pointer.
⚡ animationSpec ➜ Specifies arrow animation behavior.
Paths
Alright, let’s proceed to the backbone of today's article.
ArrowPath
The createArrowPath
function takes the width and height of the canvas and returns a Path
drawn within these boundaries:
fun createArrowPath(width: Float, height: Float): Path {
return Path().apply {
moveTo(width * 0.2f, 0f) // 0
cubicTo(
x1 = 0f, y1 = height * 0.25f, // 1
x2 = width * 0.1f, y2 = height * 0.7f, // 2
x3 = width * 0.65f, y3 = height * 0.6f // 3
)
cubicTo(
x1 = width, y1 = height * 0.50f, // 4
x2 = width * 0.48f, y2 = height * 0.20f, // 5
x3 = width * 0.3f, y3 = height * 0.5f // 6
)
cubicTo(
x1 = width * 0.2f, y1 = height * 0.70f, // 7
x2 = width * 0.5f, y2 = height, // 8
x3 = width, y3 = height // 9
)
}
}
If the code looks confusing, check out the picture below 👇
SimplePointer
Next, we need to craft a pointer. Let’s define an object that implements the Shape
interface:
object SimpleArrow : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val width = size.width
val height = size.height
val path = Path().apply {
moveTo(0f, 0f) // 1
lineTo(width, height * 0.5f) // 2
lineTo(0f, height) // 3
lineTo(width * 0.5f, height * 0.5f) // 4
close() // line to 1
}
return Outline.Generic(path)
}
}
For clarity, you can check out the picture 👇
Drawing
Now, we are close to the implementation of the function. But before that, we need to define two support functions for drawing the path and the pointer head.
Arrow Path Drawing
Drawing the path is straightforward; we just draw a normal path on the canvas with specified properties:
fun DrawScope.drawPathSegment(
path: Path,
color: Color,
strokeWidth: Dp,
strokeCap: StrokeCap,
dashLength: Dp? = null
) {
drawPath(
path = path,
color = color,
style = Stroke(
width = strokeWidth.toPx(),
cap = strokeCap,
// Draw dashed strokes if dashLength is specified
// Otherwise, use a solid line
pathEffect = dashLength?.let { dash ->
PathEffect.dashPathEffect(
floatArrayOf(dash.toPx(), dash.toPx())
)
}
)
)
}
Pointer Head Drawing
To draw the head dynamically at the end of the current path segment, we will utilize getPosition
and getTangent
from PathMeasure
to get the position of the pointer and the angle to point in the right direction.
fun DrawScope.drawPointerHead(
pathMeasure: PathMeasure,
stopDistance: Float,
pointerSize: Dp,
color: Color,
pointerShape: Shape
) {
// Calculate the point and tangent at the specified distance
val headPoint = pathMeasure.getPosition(stopDistance)
val tangent = pathMeasure.getTangent(stopDistance)
// Calculate the rotation angle of the arrowhead based on the tangent
val angle = atan2(tangent.y.toDouble(), tangent.x.toDouble()).toFloat() * 180 / Math.PI.toFloat()
// Define the size and outline of the arrowhead
val headSize = Size(pointerSize.toPx(), pointerSize.toPx())
val headOutline = pointerShape.createOutline(headSize, layoutDirection, this)
// Translate and rotate the canvas to position and orient the arrowhead
translate(headPoint.x - (headSize.width / 2), headPoint.y - (headSize.height / 2)) {
rotate(angle, pivot = headSize.center) {
// Draw the arrowhead outline
drawOutline(headOutline, color = color)
}
}
}
The Implementation
Finally, let’s put it all together and apply the animation.
@Composable
fun AnimatedArrowPointer(
/* Parameters... */
) {
// Define an Animatable for the animation progress value
val pathCompletion = remember { Animatable(0f) }
// Launch the animation based on the visibility state change
LaunchedEffect(isVisible) {
if (isVisible) {
// Animate path
pathCompletion.animateTo(1f, animationSpec)
} else {
// Immediately hide the path
pathCompletion.snapTo(0f)
}
}
// Draw the animated arrow on a canvas
Canvas(
// Ensure appropriate ratio to avoid path distortion
modifier.aspectRatio(0.6f)
) {
// Create the path for the arrow based on the canvas size
val arrowPath = createArrowPath(size.width, size.height)
// Measure the path to get its length and segment information
val pathMeasure = PathMeasure().apply {
setPath(arrowPath, false)
}
// Create a path segment based on the current animation progress
val pathSegment = Path()
val stopDistance = pathCompletion.value * pathMeasure.length
pathMeasure.getSegment(0f, stopDistance, pathSegment, true)
// Draw the current segment of the arrow path with the specified properties
drawPathSegment(pathSegment, color, strokeWidth, strokeCap, dashLength)
// If the path has been drawn to some extent, draw the arrowhead
if (pathCompletion.value > 0) {
drawPointerHead(pathMeasure, stopDistance, pointerSize, color, pointerShape)
}
}
}
Congratulations 🥳! We’ve successfully built it 👏. For the complete code implementation, you can access it on GitHub Gist 🧑💻. Now, let’s explore how we can put it to use.
Advertisement
Are you learning a foreign language and struggling with new vocabulary? Then, I strongly recommend you check out this words-learning app, which will make your journey easy and convenient!
Usage
You can easily customize the position of the arrow pointer by applying simple transformation modifiers to the layout. Here are common scenarios:
Bottom-Right (Default)
The bottom-right position is the default pointing direction:
AnimatedArrowPointer(
modifier = Modifier.size(140.dp),
color = Color.Red
)
Top-Right
To point to the top-right, rotate the layout by -90 degrees:
AnimatedArrowPointer(
modifier = Modifier
.size(140.dp)
.rotate(-90f),
color = Color.Red
)
Bottom-Left
To point to the bottom-left, reflect the layout horizontally by scaling it with -1f
on the x-axis:
AnimatedArrowPointer(
modifier = Modifier
.size(140.dp)
.scale(-1f, 1f),
color = Color.Red
)
Top-Left
For the top-left direction, first reflect the layout horizontally, then rotate it by -90 degrees:
AnimatedArrowPointer(
modifier = Modifier
.size(140.dp)
.scale(-1f, 1f)
.rotate(-90f),
color = Color.Red
)
You might also like 👇
Thank you for reading this article! ❤️ I hope you’ve found it enjoyable and valuable. Feel free to show your appreciation by hitting the clap 👏 if you liked it and follow Kappdev for more exciting articles 😊
Happy coding!