You can find the final code for this blog post on GitHub.
Our Goal Link to heading
Let’s create some fun animations with the Airbnb logo using Jetpack Compose on Android. After we’re done, we’ll have a reusable Canvas based vector animation. Here’s our final product:
Drawing on the Canvas
creates an extremely performant, resizeable Compose animation with no
loading time, that can be included in an app for a few KB. The above animation is only 18 KB when
compiled with no optimizations.
Breaking Down Our Approach Link to heading
- Parse the Logo Vector Points - Extract the logo’s vector coordinates from an svg, and draw it using a Jetpack Compose Canvas.
- Animate the Logo - Use points we extract from the svg to create a custom model that can be animated.
- Split the Logo Animation - Split the Animation into a Left and Right animation
- Fade the Tip of the Stroke - The tip of the stroke is a sharp line. By adding a gradient to the tip of the stroke, we can soften it and make it feel a bit better.
Parse the Logo Vector Points Link to heading
I found an Airbnb SVG Logo online that I’ll use:
After looking at the SVG, it’s just a single path
, and only uses 3 path
commands:
L - Line To
- Draws a simple straight line connecting two points.C - Curve To
- Curves to a specified point using an endpoint and two different control points to change the direction of the curve.S - Smooth Curve To
- Similar to Curve To, but it only has one control point.
Let’s create a simple model for this:
/**
* Represents a command in an SVG path.
*/
sealed interface SvgPathCommand {
/**
* The end point of each SVG command
*/
val p: Point
/**
* Represents a "Line To" command in an SVG path.
* Draws a line from the current point to the given point.
*
* @property p The target point.
*/
data class LineTo(override val p: Point) : SvgPathCommand
/**
* Represents a "Curve To" command in an SVG path.
* Draws a cubic Bézier curve from the current point to the given point using two control points.
*
* @property p1 The first control point.
* @property p2 The second control point.
* @property p The target point.
*/
data class CurveTo(
val p1: Point,
val p2: Point,
override val p: Point,
) : SvgPathCommand
/**
* Represents a "Smooth Curve To" command in an SVG path.
* Draws a cubic Bézier curve from the current point to the given point using one control point.
*
* @property p2 The control point.
* @property p The target point.
*/
data class SmoothCurveTo(val p2: Point, override val p: Point) : SvgPathCommand
}
We can then create a Simple SVG Parser which parses the SVG to our model, and then prints it out as code.
We’re left with something like this:
private val AIRBNB_LOGO_PATH_COMMANDS = listOf(
// Trace of the inside
SvgPathCommand.MoveTo(1851.6f, 1735.6f, false),
SvgPathCommand.CurveTo(1836.6f, 1847.2f, 1761.5f, 1943.7f, 1656.4f, 1986.6f, false),
SvgPathCommand.CurveTo(1604.9f, 2008.0f, 1549.1f, 2014.5f, 1493.3f, 2008.0f, false),
SvgPathCommand.CurveTo(1439.7001f, 2001.6f, 1386.0f, 1984.4f, 1330.3f, 1952.3f, false),
SvgPathCommand.CurveTo(1253.1001f, 1909.3f, 1175.8f, 1842.9f, 1085.7001f, 1744.2001f, false),
SvgPathCommand.CurveTo(1227.3f, 1570.4f, 1313.1001f, 1411.7001f, 1345.3f, 1270.1001f, false),
SvgPathCommand.CurveTo(1360.3f, 1203.6001f, 1362.5f, 1143.5001f, 1356.0f, 1087.7001f, false),
SvgPathCommand.CurveTo(1347.4f, 1034.1001f, 1328.1f, 984.7001f, 1298.1f, 941.80005f, false),
SvgPathCommand.CurveTo(1231.6f, 845.30005f, 1120.0f, 789.50006f, 995.6f, 789.50006f, false),
SvgPathCommand.CurveTo(871.19995f, 789.50006f, 759.6f, 847.4001f, 693.1f, 941.80005f, false),
SvgPathCommand.CurveTo(663.1f, 984.7001f, 643.8f, 1034.1001f, 635.19995f, 1087.7001f, false),
SvgPathCommand.CurveTo(626.6f, 1143.5001f, 628.7999f, 1205.7001f, 645.89996f, 1270.1001f, false),
SvgPathCommand.CurveTo(678.1f, 1411.7001f, 765.99994f, 1572.6001f, 905.5f, 1746.3f, false),
SvgPathCommand.CurveTo(817.5f, 1845.0f, 738.2f, 1911.5f, 660.9f, 1954.4f, false),
SvgPathCommand.CurveTo(605.10004f, 1986.6f, 551.5f, 2003.8f, 497.90002f, 2010.2001f, false),
SvgPathCommand.CurveTo(442.60004f, 2016.4f, 386.7f, 2009.0001f, 334.90002f, 1988.8f, false),
SvgPathCommand.CurveTo(229.80002f, 1945.9f, 154.70003f, 1849.3f, 139.70003f, 1737.8f, false),
SvgPathCommand.CurveTo(133.30003f, 1684.2001f, 137.60002f, 1630.6001f, 159.00003f, 1570.5f, false),
SvgPathCommand.CurveTo(165.40002f, 1549.0f, 176.20003f, 1527.6f, 186.90002f, 1501.9f, false),
SvgPathCommand.CurveTo(201.90002f, 1467.6f, 219.10002f, 1431.1f, 236.20003f, 1394.6f, false),
SvgPathCommand.LineTo(238.40002f, 1390.2999f, false),
SvgPathCommand.CurveTo(386.40002f, 1070.5999f, 545.2f, 744.49994f, 710.4f, 426.99994f, false),
SvgPathCommand.LineTo(716.80005f, 414.09995f, false),
SvgPathCommand.CurveTo(734.00006f, 381.99994f, 751.10004f, 347.59995f, 768.30005f, 315.39996f, false),
SvgPathCommand.CurveTo(785.50006f, 281.09998f, 804.80005f, 248.89996f, 828.4f, 220.99997f, false),
SvgPathCommand.CurveTo(873.5f, 169.49997f, 933.5f, 141.59998f, 1000.0f, 141.59998f, false),
SvgPathCommand.CurveTo(1066.5f, 141.59998f, 1126.6f, 169.49997f, 1171.6f, 220.99997f, false),
SvgPathCommand.CurveTo(1195.2f, 248.89996f, 1214.5f, 281.09998f, 1231.7f, 315.39996f, false),
SvgPathCommand.CurveTo(1248.8999f, 347.59998f, 1266.0f, 381.89996f, 1283.2f, 413.99997f, false),
SvgPathCommand.LineTo(1289.7f, 426.89996f, false),
SvgPathCommand.CurveTo(1452.7f, 746.5f, 1611.5f, 1072.6f, 1759.5f, 1392.3f, false),
SvgPathCommand.VerticalLineTo(1394.4f, false),
SvgPathCommand.CurveTo(1776.7f, 1428.7001f, 1791.7f, 1467.4f, 1808.8f, 1501.7001f, false),
SvgPathCommand.CurveTo(1819.5f, 1527.5001f, 1830.3f, 1548.9f, 1836.7001f, 1570.3f, false),
SvgPathCommand.CurveTo(1853.8f, 1626.2001f, 1860.2001f, 1679.8f, 1851.6001f, 1735.6001f, false),
SvgPathCommand.ClosePath,
//Trace of the internal loop
SvgPathCommand.MoveTo(995.6f, 1634.7f, false),
SvgPathCommand.CurveTo(879.8f, 1488.7999f, 804.69995f, 1351.5f, 778.89996f, 1235.7f, false),
SvgPathCommand.CurveTo(768.19995f, 1186.2999f, 765.99994f, 1143.3999f, 772.49994f, 1104.7999f, false),
SvgPathCommand.CurveTo(776.7999f, 1070.4999f, 789.69995f, 1040.3999f, 806.7999f, 1014.69995f, false),
SvgPathCommand.CurveTo(847.5999f, 956.7999f, 916.19995f, 920.2999f, 995.5999f, 920.2999f, false),
SvgPathCommand.CurveTo(1074.9999f, 920.2999f, 1145.7999f, 954.69995f, 1184.3999f, 1014.69995f, false),
SvgPathCommand.CurveTo(1201.5999f, 1040.5f, 1214.3999f, 1070.5f, 1218.7f, 1104.7999f, false),
SvgPathCommand.CurveTo(1225.1f, 1143.3999f, 1223.0f, 1188.4999f, 1212.2999f, 1235.7f, false),
SvgPathCommand.CurveTo(1186.6f, 1349.3999f, 1111.4999f, 1486.7f, 995.5999f, 1634.7f, false),
SvgPathCommand.ClosePath,
// Trace of the outside
SvgPathCommand.MoveTo(1963.2f, 1523.2f, false),
SvgPathCommand.CurveTo(1952.5f, 1497.5f, 1941.7f, 1469.6f, 1931.0f, 1446.0f, false),
SvgPathCommand.CurveTo(1913.8f, 1407.4f, 1896.7f, 1370.9f, 1881.6f, 1336.6f, false),
SvgPathCommand.LineTo(1879.5f, 1334.5f, false),
SvgPathCommand.CurveTo(1731.5f, 1012.7f, 1572.7f, 686.6f, 1405.4f, 364.8f, false),
SvgPathCommand.LineTo(1399.0f, 351.9f, false),
SvgPathCommand.CurveTo(1381.8f, 319.69998f, 1364.7f, 285.4f, 1347.5f, 251.09999f, false),
SvgPathCommand.CurveTo(1326.0f, 212.5f, 1304.6f, 171.69998f, 1270.3f, 133.09999f, false),
SvgPathCommand.CurveTo(1201.6001f, 47.19999f, 1102.9f, -1.5258789E-5f, 997.80005f, -1.5258789E-5f, false),
SvgPathCommand.CurveTo(890.50006f, -1.5258789E-5f, 794.00006f, 47.199986f, 723.10004f, 128.69998f, false),
SvgPathCommand.CurveTo(690.9f, 167.29999f, 667.30005f, 208.09998f, 645.9f, 246.69998f, false),
SvgPathCommand.CurveTo(628.7f, 280.99997f, 611.60004f, 315.3f, 594.4f, 347.5f, false),
SvgPathCommand.LineTo(588.0f, 360.3f, false),
SvgPathCommand.CurveTo(422.8f, 682.1f, 261.9f, 1008.2f, 113.899994f, 1330.0f, false),
SvgPathCommand.LineTo(111.799995f, 1334.3f, false),
SvgPathCommand.CurveTo(96.799995f, 1368.6001f, 79.59999f, 1405.1001f, 62.399994f, 1443.7001f, false),
SvgPathCommand.CurveTo(50.899994f, 1469.1001f, 40.199993f, 1494.9f, 30.199993f, 1520.9f, false),
SvgPathCommand.CurveTo(2.2999935f, 1600.3f, -6.300007f, 1675.4f, 4.399994f, 1752.6f, false),
SvgPathCommand.CurveTo(27.999994f, 1913.5f, 135.29999f, 2048.7f, 283.3f, 2108.7f, false),
SvgPathCommand.CurveTo(339.09998f, 2132.3f, 397.0f, 2143.0f, 457.09998f, 2143.0f, false),
SvgPathCommand.CurveTo(474.3f, 2143.0f, 495.69998f, 2140.9f, 512.89996f, 2138.7f, false),
SvgPathCommand.CurveTo(583.69995f, 2130.0999f, 656.6f, 2106.5999f, 727.39996f, 2065.8f, false),
SvgPathCommand.CurveTo(815.39996f, 2016.5f, 899.0f, 1945.7001f, 993.39996f, 1842.7001f, false),
SvgPathCommand.CurveTo(1087.7999f, 1945.7001f, 1173.6f, 2016.5001f, 1259.3999f, 2065.8f, false),
SvgPathCommand.CurveTo(1330.2f, 2106.6f, 1403.0999f, 2130.1f, 1473.8999f, 2138.7f, false),
SvgPathCommand.CurveTo(1491.0999f, 2140.9f, 1512.4999f, 2143.0f, 1529.7f, 2143.0f, false),
SvgPathCommand.CurveTo(1589.7999f, 2143.0f, 1649.7999f, 2132.3f, 1703.5f, 2108.7f, false),
SvgPathCommand.CurveTo(1853.7f, 2048.5999f, 1958.8f, 1911.2999f, 1982.4f, 1752.6f, false),
SvgPathCommand.CurveTo(1999.6f, 1677.6f, 1991.0f, 1602.6f, 1963.2001f, 1523.2f, false),
SvgPathCommand.ClosePath
)
We can then create a Composable that can draw the SVG path:
@Composable
fun AirbnbLogo(
modifier: Modifier = Modifier,
color: Color = AirbnbLogo.COLOR_RAUSCH,
) {
Canvas(
modifier = modifier
.fillMaxSize()
.aspectRatio(LOGO_VECTOR_WIDTH / LOGO_VECTOR_HEIGHT)
) {
val xScale = size.width / LOGO_VECTOR_WIDTH
val yScale = size.height / LOGO_VECTOR_HEIGHT
val path = svgCommandsToPath(xScale, yScale, AIRBNB_LOGO_PATH_COMMANDS)
drawPath(path = path, color = color, style = Fill)
}
}
private fun svgCommandsToPath(
xScale: Float,
yScale: Float,
commands: List<SvgPathCommand>,
): Path {
val path = Path()
// Start at the location right before the first command
commands.last().p.let {
path.moveTo(it.x * xScale, it.y * yScale)
currentX = it.x
currentY = it.y
}
commands.forEach { command ->
when (command) {
is SvgPathCommand.MoveTo -> {
path.moveTo(command.p.x * xScale, command.p.y * yScale)
}
is SvgPathCommand.Close -> {
path.close()
}
is SvgPathCommand.LineTo -> {
val scaledX = command.p.x * xScale
val scaledY = command.p.y * yScale
path.lineTo(scaledX, scaledY)
}
is SvgPathCommand.CurveTo -> {
val scaledX = command.p.x * xScale
val scaledY = command.p.y * yScale
path.cubicTo(
command.p1.x * xScale, command.p1.y * yScale,
command.p2.x * xScale, command.p2.y * yScale,
scaledX, scaledY
)
}
is SvgPathCommand.SmoothCurveTo -> {
val scaledX = command.p.x * xScale
val scaledY = command.p.y * yScale
path.quadraticBezierTo(
command.p2.x * xScale,
command.p2.y * yScale,
scaledX,
scaledY
)
}
}
}
return path
}
And we’re left with a pretty good result:
Understanding the SVG Link to heading
The SVG is coded with a fill
path. This means that it creates a shape by drawing a path, then the
Canvas fills the created shape of with a color when drawn. In the case of the Airbnb Logo, 3 shapes
are defined in the SVG path. This is more easily represented if I graph the points on the logo.
Above can see the outside path in green, and the inside path in blue. It’s less clear in the image, but there is a 3rd shape defined by the internal twist as well.
Animating a fill like this can be challenging. It’s possible because each point seems to have an
equivalent pair, but the data structure and algorithm would be complicated. It’d be much easier if
we could change this into a stroke
. Instead of two separate paths we’d find points representing
the center, and instead of drawing two sets of points, we’d draw one, telling the canvas how much of
a line to draw on either side.
We can achieve this by pairing each of the points, and then averaging them together. Lucky for us, this will also work for control points.
We can simplify everything if the model that we animate is able to look at everything as a single
kind of draw command. Lucky for us, SmoothCurveTo
, and LineTo
, can both be represented as a
CurveTo
. When we convert everything to our new model, everything will have an endpoint, and then
two control points.
Animating the Logo Link to heading
Let’s create an interface Point
to represent x
and y
coordinates. This will allow us to create
a PairPoint
implementation, that will automatically average the two paired off points and
represent them as a single x and y coordinate in our model. We could just use Offset
and average
the points together ourselves. By having a PairPoint
we can keep the original SVG data, and if
there are any issues, far more easily diagnose what’s going on by looking back at our original
source.
interface Point {
val x: Float
val y: Float
}
fun Point(x: Float, y: Float) : Point = SinglePoint(x = x, y = y)
data class SinglePoint(
override val x: Float,
override val y: Float
) : Point
/** A point that takes 2 separate points and outputs their average */
data class PairPoint(
val x1: Float,
val y1: Float,
val x2: Float,
val y2: Float,
) : Point {
override val x: Float = (x1 + x2) / 2f
override val y: Float = (y1 + y2) / 2f
}
Based on our previous learnings, we can represent all draw commands as CurveTo
, which means we
just need an end point, and then two control points. This allows us to draw any direction by using a
control point from where we are drawing to, along with the opposite control point from the last
drawn point. Let’s call this a VectorNode
.
internal data class VectorNode(
/** The first control point for the node */
val cp1: Point,
/** The second control point for the node */
val cp2: Point,
/** The end point we are drawing to for the node */
val p: Point,
/**
* If true this is a point that will be drawn on again.
*/
val isIntersection: Boolean = false,
)
We convert everything to a VectorNode
with PairPoint
s, and we’re left with
this.
Calculating Distance Link to heading
When animating the logo, there are several approaches that we can take. Given that each point is at varying distances apart, I think the best approach is going to be to track distance traveled from point to point. We can then use Compose to animate our logo based on percent, and as long as we know the total distance of all the points in the animation, we can then calculate how far to draw for each frame.
We could get really fancy calculating the distance of a Cubic Bezier Curve using integrals, but for this blog post, a simple magnitude calculation will suffice:
fun calculateDistance(node1: VectorNode, node2: VectorNode): Float {
return sqrt((node2.p.x - node1.p.x).pow(2) + (node2.p.y - node1.p.y).pow(2))
}
De Casteljau’s Algorithm Link to heading
What happens when our draw distance is somewhere in between two points in a CurveTo command? We have to calculate a new point, new control points, and draw our line to that. This was one of the more interesting and challenging problems to solve.
I found a solution in De Casteljau’s algorithm, which uses linear interpolation and allows the calculation of partial points in a Cubic Bezier Curve. The actual implementation was fairly simple:
private fun Path.partialCubicBezier(
start: Offset,
c1: Offset,
c2: Offset,
end: Offset,
drawPercent: Float,
): Offset {
// Calculate intermediate points using De Casteljau's algorithm
val partialControl1 = linearInterpolation(start, c1, drawPercent)
val p1_1 = linearInterpolation(c1, c2, drawPercent)
val p2_1 = linearInterpolation(c2, end, drawPercent)
val partialControl2 = linearInterpolation(partialControl1, p1_1, drawPercent)
val p1_2 = linearInterpolation(p1_1, p2_1, drawPercent)
val partialEnd = linearInterpolation(partialControl2, p1_2, drawPercent)
// Draw the cubic Bézier curve segment
cubicTo(
partialControl1.x, partialControl1.y,
partialControl2.x, partialControl2.y,
partialEnd.x, partialEnd.y,
)
return Offset(partialEnd.x, partialEnd.y)
}
private fun linearInterpolation(a: Offset, b: Offset, t: Float): Offset {
return Offset(
x = (1 - t) * a.x + t * b.x,
y = (1 - t) * a.y + t * b.y
)
}
Our Animation Algorithm Link to heading
Let’s put it all together, and animate over 1.2 seconds.
private fun drawVectorNodes(
canvas: Canvas,
xScale: Float,
yScale: Float,
nodes: List<VectorNode>,
distanceToDraw: Float,
color: Color,
strokeWidth: Float,
) {
val path = Path()
// The last node we were drawing
var lastNode: VectorNode = nodes.first()
// The total distance we've drawn.
var distanceDrawn = 0f
// Start at the location right before the first command
path.moveTo(lastNode.p.x * xScale, lastNode.p.y * yScale)
// Reset the paint
val paint = Paint().apply {
isAntiAlias = true
style = PaintingStyle.Stroke
this.strokeWidth = strokeWidth
this.color = color
}
for (i in 1 ..< nodes.size) {
val node: VectorNode = nodes[i]
val nodeDistance = calculateDistance(lastNode, node)
// Percent of line to the node to draw
val nodeDrawPercent: Float = if (nodeDistance < (distanceToDraw - distanceDrawn)) {
1f
} else {
if (distanceDrawn >= distanceToDraw) {
// This can happen when the drawPercent is 0
break
}
((distanceToDraw - distanceDrawn) / nodeDistance).coerceAtMost(1f)
}
val c1: Point = lastNode.cp1
val c2: Point = node.cp2
val end: Point = node.p
if (nodeDrawPercent == 1f) {
val endX = end.x * xScale
val endY = end.y * yScale
path.cubicTo(
c1.x * xScale, c1.y * yScale,
c2.x * xScale, c2.y * yScale,
endX, endY,
)
Offset(endX, endY)
} else {
// Use De Casteljau's Algorithm to animate to a partial point
path.partialCubicBezier(
Offset(lastNode.p.x * xScale, lastNode.p.y * yScale),
Offset(c1.x * xScale, c1.y * yScale),
Offset(c2.x * xScale, c2.y * yScale),
Offset(end.x * xScale, end.y * yScale),
nodeDrawPercent,
)
}
lastNode = node
distanceDrawn += nodeDistance
}
canvas.drawPath(path = path, paint)
}
We can bring it together with a simple composable that animates our logo based on percent:
@Composable
fun AirbnbLogo(
modifier: Modifier = Modifier,
color: Color = AirbnbLogo.COLOR_RAUSCH,
delayMillis: Int = 0,
) {
var targetDrawPercent: Float by remember { mutableFloatStateOf(0f) }
val drawPercent: Float by animateFloatAsState(
targetValue = targetDrawPercent,
animationSpec = tween(1200, easing = EaseOut, delayMillis = delayMillis),
label = "Draw Percent",
)
LaunchedEffect(Unit) {
delay(300)
// Trigger the animation to 1f
targetDrawPercent = 1f
}
Canvas(
modifier = modifier
.fillMaxSize()
.aspectRatio(LOGO_VECTOR_WIDTH / LOGO_VECTOR_HEIGHT)
) {
drawAirbnbLogo(color, drawPercent)
}
}
private fun DrawScope.drawAirbnbLogo(color: Color, drawPercent: Float) {
val xScale = size.width / LOGO_VECTOR_WIDTH
val yScale = size.height / LOGO_VECTOR_HEIGHT
val scaledStrokeWidth = min(xScale, yScale) * STROKE_WIDTH
drawIntoCanvas { canvas ->
drawVectorNodes(
canvas = canvas,
xScale = xScale,
yScale = yScale,
side = DrawSide.LEFT,
distanceToDraw = drawPercent * DrawSide.LEFT.distance,
color = color,
strokeWidth = scaledStrokeWidth
)
}
}
Our result:
Splitting the Logo Animation Link to heading
Instead of animating in a single direction, let’s split the animation so it animates to the left, and the right at the same time, and then joins together at the top.
We can achieve this by breaking up our list of VectorNode
s into a left and a right side. Since
we’re already drawing to the left, all we need to do is reverse the order of the Right vector nodes.
We’ll also need to track each side’s distance separately.
internal enum class DrawSide(
val distance: Float,
val nodes: List<VectorNode>,
) {
LEFT(
distance = 5817.2754f,
nodes = AIRBNB_LOGO_LEFT_SIDE,
),
RIGHT(
distance = 2758.3904f,
nodes = AIRBNB_LOGO_RIGHT_SIDE.asReversed(),
),
}
Now all we need to do is adjust our drawAirbnbLogo
method to draw the left side, and then the
right:
private fun DrawScope.drawAirbnbLogo(color: Color, drawPercent: Float) {
val xScale = size.width / LOGO_VECTOR_WIDTH
val yScale = size.height / LOGO_VECTOR_HEIGHT
val scaledStrokeWidth = min(xScale, yScale) * STROKE_WIDTH
drawIntoCanvas { canvas ->
drawVectorNodes(
canvas = canvas,
xScale = xScale,
yScale = yScale,
side = DrawSide.LEFT,
distanceToDraw = drawPercent * DrawSide.LEFT.distance,
color = color,
strokeWidth = scaledStrokeWidth
)
drawVectorNodes(
canvas = canvas,
xScale = xScale,
yScale = yScale,
side = DrawSide.RIGHT,
distanceToDraw = drawPercent * DrawSide.RIGHT.distance,
color = color,
strokeWidth = scaledStrokeWidth,
)
}
}
Now our logo animates on the left and right, then meets in the center:
Fading the Tip of the Stroke Link to heading
We can improve this further by adding a fade to the tip of our stroke to remove the harsh line. Explaining the full process could be a blog post in and of itself.
In short, while drawing the points of the line, I calculate the point x distance from the tip of the
stroke. Then using that point along with the last point that we draw for the path, I draw a new
stroke on top of our first one, using a BlendMode
and a LinearGradient
to create the desired
fade effect.
You can find most of the interesting code for this here.
The improvements are subtle, but noticeable:
Creating a Multi Color Logo Animation Link to heading
Now that we’ve got the ability to animate a logo, to create the multi color effect, all we need to do is adjust the color, and then overlay the animations on top of each other with a delay.
@Composable
fun MultiColorAirbnbLogo(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
) {
AirbnbLogo(
modifier = Modifier.fillMaxSize(),
)
AirbnbLogo(
modifier = Modifier.fillMaxSize(),
color = Color(0xFF6FC8EE),
delayMillis = 500,
)
AirbnbLogo(
modifier = Modifier.fillMaxSize(),
color = Color(0xFFF4D45A),
delayMillis = 800,
)
AirbnbLogo(
modifier = Modifier.fillMaxSize(),
delayMillis = 1200,
)
}
}
I’m sure we could have chosen colors that are more on-brand for Airbnb, but I’m pretty happy with the result:
You can find the final code for this blog post on GitHub.