<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: exyte</title>
    <description>The latest articles on DEV Community by exyte (@exytehq).</description>
    <link>https://dev.to/exytehq</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F331172%2F322d672a-d253-41f2-b848-b5aa0ba2b8ea.jpg</url>
      <title>DEV Community: exyte</title>
      <link>https://dev.to/exytehq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/exytehq"/>
    <language>en</language>
    <item>
      <title>Jetpack Compose Tutorial: Replicating the Water Level Widget</title>
      <dc:creator>exyte</dc:creator>
      <pubDate>Wed, 07 Jun 2023 09:55:24 +0000</pubDate>
      <link>https://dev.to/exytehq/jetpack-compose-tutorial-replicating-the-water-level-widget-54hg</link>
      <guid>https://dev.to/exytehq/jetpack-compose-tutorial-replicating-the-water-level-widget-54hg</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ujN2jVAj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jb8pf6u5rzolihx1ux7l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ujN2jVAj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jb8pf6u5rzolihx1ux7l.png" alt="Image description" width="800" height="512"&gt;&lt;/a&gt;&lt;br&gt;
Apple's apps and widgets always were a staple of design, and inspirations for our Replicating Series: &lt;a href="https://exyte.com/blog/swiftui-tutorial-replicating-activity-application?utm_source=github&amp;amp;utm_medium=referral&amp;amp;utm_campaign=website_blog"&gt;Activity Application &lt;/a&gt;and &lt;a href="https://exyte.com/blog/replicating-apple-card-application-using-swiftui?utm_source=github&amp;amp;utm_medium=referral&amp;amp;utm_campaign=website_blog"&gt;Card application&lt;/a&gt;. When they announced the new Apple Watch Ultra, the design of the depth gauge widget caught our eye, and we thought it would be cool to replicate it on Android! As usual for our Android replicating challenges, we used the Jetpack Compose framework.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NVAxHlFA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hprkwpeztfknjxh0e704.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NVAxHlFA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hprkwpeztfknjxh0e704.gif" alt="Image description" width="800" height="379"&gt;&lt;/a&gt;&lt;br&gt;
This article will tell you how we went about implementing it - creating a wave effect, having the water snap around the text, and blending colors. We feel like this will be useful both to beginners and those already acquainted with Jetpack Compose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Water Level&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, let's consider the most trivial problem - how to calculate and animate the water level.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TLwn4HnO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bl1gja3houzqfj1axc3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TLwn4HnO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bl1gja3houzqfj1axc3.gif" alt="Image description" width="800" height="300"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum class WaterLevelState {
    StartReady,
    Animating,
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we define the duration of the animation and the initial state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val waveDuration by rememberSaveable { mutableStateOf(waveDurationInMills) }
var waterLevelState by remember { mutableStateOf(WaterLevelState.StartReady) }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that we need to define how the water's progress should change. It's necessary for recording the progress on the screen as text and for drawing the water level.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val waveProgress by waveProgressAsState(
    timerState = waterLevelState,
    timerDurationInMillis = waveDuration
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a closer look at waveProgressAsState. We use animatable because it gives us a little more control and customization. For example, we can specify different animationSpec for different states.&lt;br&gt;
Now to calculate the coordinates of the water's edge that needs to be drawn on the screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val waterLevel by remember(waveProgress, containerSize.height) {
    derivedStateOf {
        (waveProgress * containerSize.height).toInt()
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After all this preliminary work we can move on to creating actual waves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waves&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most common way to simulate a wave is to use a sine graph that moves horizontally at a certain speed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Dxa628KO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wbwov9t5tonbds0qjfrd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Dxa628KO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wbwov9t5tonbds0qjfrd.png" alt="Image description" width="500" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We want it to look more realistic, and it will have to flow over the elements on the screen, so we need a more sophisticated approach. The main idea of the implementation is to define a set of points representing the height of the wave. The values are animated to create the wave effect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jzDWlEqj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wb1fxdluumlew819pthq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jzDWlEqj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wb1fxdluumlew819pthq.png" alt="Image description" width="500" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, we create a list with points to store the values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val points = remember(spacing, containerSize) {
    derivedStateOf {
        (-spacing..containerSize.width + spacing step spacing).map { x -&amp;gt;
            PointF(x.toFloat(), waterLevel)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the case of normal water flow when there are no obstacles in its path, we simply fill it with the values of the water level. We will consider the other cases later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LevelState.PlainMoving -&amp;gt; {
    points.value.map {
        it.y = waterLevel
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consider an animation that will change the height of each point. Animating all the points would take a heavy toll on the performance and battery. So, in order to save resources, we will only use a small number of Float animation values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun createAnimationsAsState1(
    pointsQuantity: Int,
): MutableList&amp;lt;State&amp;lt;Float&amp;gt;&amp;gt; {
    val animations = remember { mutableListOf&amp;lt;State&amp;lt;Float&amp;gt;&amp;gt;() }
    val random = remember { Random(System.currentTimeMillis()) }
    val infiniteAnimation = rememberInfiniteTransition()

    repeat(pointsQuantity / 2) {
        val durationMillis = random.nextInt(2000, 6000)
        animations += infiniteAnimation.animateFloat(
            initialValue = 0f,
            targetValue = 1f,
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis),
                repeatMode = RepeatMode.Reverse,
            )
        )
    }
    return animations
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To prevent the animation from repeating every 15 points and the waves from being identical, we can set the initialMultipliers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun createInitialMultipliersAsState(pointsQuantity: Int): MutableList&amp;lt;Float&amp;gt; {
    val random = remember { Random(System.currentTimeMillis()) }
    return remember {
        mutableListOf&amp;lt;Float&amp;gt;().apply {
            repeat(pointsQuantity) { this += random.nextFloat() }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now to add the waves - iterate through all the points and calculate the new heights.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;points.forEachIndexed { index, pointF -&amp;gt;
    val newIndex = index % animations.size

    var waveHeight = calculateWaveHeight(
        animations[newIndex].value,
        initialMultipliers[index],
        maxHeight
    )
    pointF.y = pointF.y - waveHeight
}
return points
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding initialMultipliers to currentSize will reduce the possibility of repeating values. And using linear interpolation will help smoothly change the height:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private fun calculateWaveHeight(
    currentSize: Float,
    initialMultipliers: Float,
    maxHeight: Float
): Float {
    var waveHeightPercent = initialMultipliers + currentSize
    if (waveHeightPercent &amp;gt; 1.0f) {
        val diff = waveHeightPercent - 1.0f
        waveHeightPercent = 1.0f - diff
    }

    return lerpF(maxHeight, 0f, waveHeightPercent)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the most interesting part - how to make the water flow around UI elements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive water movement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We start by defining 3 states that water has when its level decreases. The PlainMoving name speaks for itself,  WaveIsComing is for the moment when the water comes up to the UX element the water will flow around and you have to show it. FlowsAround is the actual moment of flowing around a UI element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sealed class LevelState {
    object PlainMoving : LevelState()
    object FlowsAround : LevelState()
    object WaveIsComing: LevelState()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We understand that the water level is higher than the item if the water level is less than the item position minus the buffer. This area is shown in red on the below picture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun isAboveElement(waterLevel: Int, bufferY: Float, position: Offset) = waterLevel &amp;lt; position.y - bufferY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TlbT4_qo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z4a0dozu5fwsipct5x9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TlbT4_qo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z4a0dozu5fwsipct5x9c.png" alt="Image description" width="500" height="814"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the water level is at the level of the element, it is too early to start flowing around yet. This area is shown in grey in the next picture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun atElementLevel(
    waterLevel: Int,
    buffer: Float,
    elementParams: ElementParams,
) = (waterLevel &amp;gt;= (elementParams.position.y - buffer)) &amp;amp;&amp;amp;
        (waterLevel &amp;lt; (elementParams.position.y + elementParams.size.height * 0.33))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PFJTYS_j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tc0ekui2bp236yi802zk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PFJTYS_j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tc0ekui2bp236yi802zk.png" alt="Image description" width="500" height="802"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun isWaterFalls(
    waterLevel: Int,
    elementParams: ElementParams,
) = waterLevel &amp;gt;= (elementParams.position.y + elementParams.size.height * 0.33) &amp;amp;&amp;amp;
        waterLevel &amp;lt;= (elementParams.position.y + elementParams.size.height)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another question we have to consider is this - how to calculate the timing of the water flow? The animations of the waterfall and of the wave increase occurs when the water level is in the blue zone. Thus, we need to calculate the time at which the water level passes 2/3 of the element's height.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Composable
fun rememberDropWaterDuration(
    elementSize: IntSize,
    containerSize: IntSize,
    duration: Long,
): Int {
    return remember(
        elementSize,
        containerSize
    ) { (((duration * elementSize.height * 0.66) / (containerSize.height))).toInt() }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a closer look at the flow around the element. The shape of the water flow is based on a parabola - we chose a simple shape for the sake of the tutorial. We use the points shown in the picture through which the parabola passes. We do not extend the parabola below the current water level (the horizontal dim red line).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JVuib59M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2fonumwti1g9h3ldpvfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JVuib59M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2fonumwti1g9h3ldpvfs.png" alt="Image description" width="512" height="543"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;is LevelState.FlowsAround -&amp;gt; {
    val point1 = PointF(
        position.x,
        position.y - buffer / 5
    )
    val point2 = point1.copy(x = position.x + elementSize.width)
    val point3 = PointF(
        position.x + elementSize.width / 2,
        position.y - buffer
    )
    val p = Parabola(point1, point2, point3)
    points.value.forEach {
        val pr = p.calculate(it.x)
        if (pr &amp;gt; waterLevel) {
            it.y = waterLevel
        } else {
            it.y = pr
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's look at the waterfall animation: we will use the same parabola, changing its height from the initial position, and the OvershootInterpolator for a softer fall effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val parabolaHeightMultiplier = animateFloatAsState(
    targetValue = if (levelState == LevelState.WaveIsComing) 0f else -1f,
    animationSpec = tween(
        durationMillis = dropWaterDuration,
        easing = { OvershootInterpolator(6f).getInterpolation(it) }
    )
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, we use the height multiplier animation so that eventually the height of the parabola becomes 0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val point1 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point2 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point3 by remember(position, elementSize, parabolaHeightMultiplier, waterLevel) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width / 2,
            waterLevel + (elementSize.height / 3f + buffer) * parabolaHeightMultiplier.value
        )
    )
}
return produceState(
    initialValue = Parabola(point1, point2, point3),
    key1 = point1,
    key2 = point2,
    key3 = point3
) {
    this.value = Parabola(point1, point2, point3)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition, we need to change the size of the waves in places that overlap the UI element, because at the moment of the water falling motion they increase, and then decrease to their normal size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val point1 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point2 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point3 by remember(position, elementSize, parabolaHeightMultiplier, waterLevel) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width / 2,
            waterLevel + (elementSize.height / 3f + buffer) * parabolaHeightMultiplier.value
        )
    )
}
return produceState(
    initialValue = Parabola(point1, point2, point3),
    key1 = point1,
    key2 = point2,
    key3 = point3
) {
    this.value = Parabola(point1, point2, point3)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wave's height is increased in a radius around the UI element for more realism.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gMbeXp9m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ymb6v3bdp51gns7lhxn0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gMbeXp9m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ymb6v3bdp51gns7lhxn0.png" alt="Image description" width="800" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val elementRangeX = (position.x - bufferX)..(position.x + elementSize.width + bufferX)
points.forEach { index, pointF -&amp;gt;
    if (levelState.value is LevelState.WaveIsComing &amp;amp;&amp;amp; pointF.x in elementRangeX) {
        waveHeight *= waveMultiplier
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it's time for combining everything we have, and add color blending.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combining all the elements&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are several ways you can paint on the canvas using a blend mode.&lt;br&gt;
The first method that came to mind is to use a bitmap to draw paths, and to draw the text using Blend modes on a bitmapCanvas. This approach uses an old implementation of the canvas from Android view, so we decided to go natively instead - applying BlendMode for color blending. First, we draw waves on the canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Canvas(
    modifier = Modifier
        .background(Water)
        .fillMaxSize()
) {
    drawWaves(paths)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the implementation we use drawIntoCanvas so that we can use paint.pathEffectCornerPathEffect to smooth out the waves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun DrawScope.drawWaves(
    paths: Paths,
) {
    drawIntoCanvas {
        it.drawPath(paths.pathList[1], paint.apply {
            color = Blue
        })
        it.drawPath(paths.pathList[0], paint.apply {
            color = Color.Black
            alpha = 0.9f
        })
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see how much space the text takes up, we put the Text element into a Box. Since Text does not support blendMode in the layout, we need to draw text on the Canvas using blendMode, so we use the drawWithContent modifier to only draw the text on the Canvas, but not the text element.&lt;br&gt;
To make blend mode work, a new layer needs to be created. To achieve this, we can use .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)*. The rendering of the content will always be rendered into an offscreen buffer first and then drawn to the destination, regardless of any other parameters configured on the graphics layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;(This is an update to our previous implementation that used a .graphicsLayer(alpha = 0.99f) hack. &lt;a class="mentioned-user" href="https://dev.to/romainguy"&gt;@romainguy&lt;/a&gt; helped us with a cleaner choice in the comments).
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Box(
    modifier = modifier
        .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
        .drawWithContent {
            drawTextWithBlendMode(
                mask = paths.pathList[0],
                textStyle = textStyle,
                unitTextStyle = unitTextStyle,
                textOffset = textOffset,
                text = text,
                unitTextOffset = unitTextProgress,
                textMeasurer = textMeasurer,
            )
        }
) {
    Text(
        modifier = content().modifier
            .align(content().align)
            .onGloballyPositioned {
                elementParams.position = it.positionInParent()
                elementParams.size = it.size
            },
        text = "46FT",
        style = content().textStyle
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;First we draw the text, then we draw a wave, which is used as a mask. Here's &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/BlendMode#SrcIn()"&gt;the official documentation&lt;/a&gt; regarding different blend modes available to developers&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4N-18j1V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0k2ifq43pajfg439z8al.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4N-18j1V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0k2ifq43pajfg439z8al.png" alt="Image description" width="800" height="732"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun DrawScope.drawTextWithBlendMode(
    mask: Path,
    textMeasurer: TextMeasurer,
    textStyle: TextStyle,
    text: String,
    textOffset: Offset,
    unitTextOffset: Offset,
    unitTextStyle: TextStyle,
) {
    drawText(
        textMeasurer = textMeasurer,
        topLeft = textOffset,
        text = text,
        style = textStyle,
    )
    drawText(
        textMeasurer = textMeasurer,
        topLeft = unitTextOffset,
        text = "FT",
        style = unitTextStyle,
    )

    drawPath(
        path = mask,
        color = Water,
        blendMode = BlendMode.SrcIn
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can see the whole result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ki2iLFfI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auzuuxvfb54y33mlnypu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ki2iLFfI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/auzuuxvfb54y33mlnypu.gif" alt="Image description" width="600" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This turned out to be quite a complex implementation, but that's expected given the source material. We were glad that a lot could be done using the native Compose tooling. You can also tweak the parameters to get a more compelling water effect, but we decided to stop at this proof of concept. As usual, the &lt;a href="https://github.com/exyte/android-waves-progressbar"&gt;repo&lt;/a&gt; contains the full implementation. If you like this tutorial, you can check how to implement the audio dribble app here or find more interesting stuff in our &lt;a href="https://exyte.com/blog"&gt;blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tutorial</category>
      <category>opensource</category>
      <category>android</category>
    </item>
    <item>
      <title>Jetpack Compose Tutorial: How to use FlowLayout</title>
      <dc:creator>exyte</dc:creator>
      <pubDate>Mon, 01 May 2023 12:36:21 +0000</pubDate>
      <link>https://dev.to/exytehq/jetpack-compose-tutorial-how-to-use-flowlayout-50lm</link>
      <guid>https://dev.to/exytehq/jetpack-compose-tutorial-how-to-use-flowlayout-50lm</guid>
      <description>&lt;p&gt;A short tutorial on how to use FlowLayout in Jetpack Compose&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t0SxKSSn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tze5bc93278tfjbra6cw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t0SxKSSn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tze5bc93278tfjbra6cw.png" alt="Image description" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://exyte.com/"&gt;Exyte&lt;/a&gt;, we strive to contribute to the open source engineering community by sharing our knowledge and expertise. We regularly write tutorials and release libraries to help other developers improve their skills and create better software. We expand our knowledge base with up-to-date trends and technologies in software development, such as &lt;a href="https://github.com/exyte/replicating"&gt;SwiftUI&lt;/a&gt; and &lt;a href="https://github.com/exyte/android-replicating"&gt;Jetpack Compose&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our latest tutorial is on Android FlowLayout, where we demonstrate how to create a flexible layout for displaying views in a flow-like manner.&lt;/p&gt;




&lt;p&gt;If you’ve been keeping up with the latest updates in Jetpack Compose, you might have heard about the new FlowRow and FlowColumn composables recently added to Jetpack Compose 1.4.1. Of course, it has already been published in the Accompanist for a while (a group of libraries designed to supplement Jetpack Compose with features often required by developers but not yet available). But now they are available in the base framework and it would be cool to see what they can be used for.&lt;/p&gt;

&lt;p&gt;The most important idea in this layout is that we can lay out items in a container when the size of the items or the container is unknown or dynamic. For example, here we have chips (tags) which we place in random order. “Plant-based food” chip does not fit and carries over to the next line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jxyPXtXX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/1682417330746-flowLayout_example_1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jxyPXtXX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/1682417330746-flowLayout_example_1.png" alt="Image description" width="800" height="371"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FlowRow {
    chipsInRow.forEach {
        Chip(text = it.text, pointColor = it.pointColor)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a FlowColumn, it could look like this: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ICtKsQmr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/635aeyvi4zr0kzvhdlvn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ICtKsQmr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/635aeyvi4zr0kzvhdlvn.png" alt="Image description" width="800" height="534"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FlowColumn {
    cardsInColumn.forEach { cardData -&amp;gt;
        Card(cardData)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a look at the API:&lt;br&gt;
horizontalArrangement helps to arrange the elements horizontally in a certain order.&lt;br&gt;
verticalAlignment helps align elements if they are of different heights (although this should not be a common case).&lt;br&gt;
maxItemsInEachRow indicates the maximum number of elements we can place in a row. This is very convenient to use in combination with weight modifiers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun FlowRow(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    maxItemsInEachRow: Int = Int.MAX_VALUE,
    content: @Composable RowScope.() -&amp;gt; Unit
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the FlowColumn the parameters work the same way, except rotated vertically.&lt;br&gt;
verticalAlignment offers 3 different options that are self-evident:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vtZi0g3Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/flowLayoutAlignmentExample.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vtZi0g3Y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/flowLayoutAlignmentExample.png" alt="Image description" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;horizontalArrangement, on the other hand, has more options with some non-obvious ones:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LkMBb_cU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/flowLayoutArrangement.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LkMBb_cU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/flowLayoutArrangement.png" alt="Image description" width="800" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The distinction between SpaceEvenly and SpaceAround might not be obvious. The main difference is that SpaceEvenly, as the name implies, provides a spacing that is even, including the leading and trailing margins. SpaceAround works the same way, but the margins will take half as much space as the distance between the inner elements. Alternatively, you can use an absolute value to keep the interface constant when selecting the RTL (Right to Left) layout: Arrangement.Absolute&lt;/p&gt;

&lt;p&gt;If you want to add distance between elements you can use this construction:&lt;br&gt;
Arrangement.spacedBy(5.dp, Alignment.CenterHorizontally)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--q_O9mWmJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/flowLayoutSpaceBy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--q_O9mWmJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://exyte.com/upload/flowLayoutSpaceBy.png" alt="Image description" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;maxItemsInEachRow can be used together with weight modifiers to make something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_7sZVvdE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eyoxm7sa03q3qltapbc9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_7sZVvdE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eyoxm7sa03q3qltapbc9.png" alt="Image description" width="340" height="716"&gt;&lt;/a&gt;&lt;br&gt;
All buttons have the weight set to 1 except the zero button, which has the weight set to 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FlowRow(maxItemsInEachRow = 4) {
    buttons.forEach {
        FlowButton(
            modifier = Modifier
                .aspectRatio(1 * it.weight)
                .clip(CircleShape)
                .background(it.color)
                .weight(it.weight),
            text = it.text,
            textColor = it.textColor,
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, for straight-forward examples the FlowRow and FlowColumn composables are very easy to use - they are a powerful and flexible layout tool for dynamic or unknown sizes. This makes them especially useful for dynamic and responsive interfaces that adapt to different screen sizes and orientations. We expect the examples above to cover most developers’ needs, but we still want to create a more complex example using these new layouts and animating transitions that could happen during adding, removing or repositioning elements. So keep an eye out for the next article on Flow Layout! Meanwhile you can check out our other articles on android development: &lt;a href="https://exyte.com/blog/android-dribbble-replicating-part-1"&gt;Dribbble replicating&lt;/a&gt; or &lt;a href="https://exyte.com/blog/androidview-jetpack-compose-tutorial"&gt;Androidview&amp;amp;Jetpack Compose tutorial&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
