I am participating in the Mid-Autumn Festival Creative Submission contest, please see: Mid-Autumn Festival Creative Submission Contest for details

Inspired by the article that wave animations are common, but this wave component is definitely not, I wrote a progress loading library for Compose. The API is designed to conform to the Compose development specification and is very easy to use.

1. Usage

Add the jitPack to root’s build.gradle,

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io'}}}Copy the code

Introduce the latest version of ComposeWaveLoading in module build.gradle

dependencies {
    implementation 'com.github.vitaviva:ComposeWaveLoading:$latest_version'
}
Copy the code

2. API design idea


Box {
    WaveLoading (
        progress = 0.5 f // 0f ~ 1f
    ) {
        Image(
          painter = painterResource(id = R.drawable.logo_tiktok),
          contentDescription = "")}}Copy the code

In traditional UI development, such a wave control would be designed using a custom View and passed in images as properties. In Compose, we make the API more flexible by making WaveLoding and Image used in combination. The WaveLoding inside can be an Image, a Text, or some other Composable. Wave animation is not limited to a particular Composable, and any Composable can be presented in the form of wave animation. The combined use of the Composable extends the coverage of the “ability”.

3. API parameters

@Composable
fun WaveLoading(
    modifier: Modifier = Modifier,
    foreDrawType: DrawType = DrawType.DrawImage,
    backDrawType: DrawType = rememberDrawColor(color = Color.LightGray).@floatrange (from = 0.0, to = 1.0) progress: Float = 0f.@floatrange (from = 0.0, to = 1.0) amplitude: Float = defaultAmlitude,
    @floatrange (from = 0.0, to = 1.0) velocity: Float = defaultVelocity,
    content: @Composable BoxScope.() -> Unit) {... }Copy the code

The parameters are described as follows:

parameter instructions
progress Loading schedule
foreDrawType Type of wave drawing:DrawColororDrawImage
backDrawType Wave graph background drawing
amplitude Wave amplitude, 0F ~ 1f represents the proportion of amplitude in the whole drawing area
velocity The speed at which waves move
content The child Composalble

Let’s focus on DrawType.

DrawType

The progress of the wave is reflected in the visual difference between foreDrawType and backDrawType. We can specify different drawTypes for foreground and backDrawType respectively to change the style of the wave.

sealed interface DrawType {
    object None : DrawType
    object DrawImage : DrawType
    data class DrawColor(val color: Color) : DrawType
}
Copy the code

As above, there are three types of DrawType:

  • None: No drawing is performed
  • DrawColor: Draw with a single color
  • DrawImage: Draw as is

Take the following Image as an example to see the combination of different drawtypes

index backDrawType foreDrawType instructions
1 DrawImage DrawImage Background gray scale, foreground original
2 DrawColor(Color.LightGray) DrawImage Background monochrome, foreground original
3 DrawColor(Color.LightGray) DrawColor(Color.Cyan) Background monochrome, foreground monochrome
4 None DrawColor(Color.Cyan) No background, monochrome foreground

In the image below, the second row is the foreground original and the third row is the foreground monochrome

The figure below shows the situation without background color

Note that when backDrawType is set to DrawImage, a grayscale image is displayed.

4. Principle Analysis

Briefly introduce the implementation principle. The code has been simplified for ease of understanding, and the full code can be viewed on Github

The key to this library is that WaveLoading {… } take out the content and display it in the form of wave animation. Therefore, the sub-Composalbe needs to be converted into Bitmap for subsequent processing.

4.1 obtain Bitmap

I didn’t find a way to get the bitmap in Compose, so I used the trick of displaying the Composalbe in The AndroidView via Compose’s good interoperability with the Android native view. Then get the Bitmap in native mode:

@Composable
fun WaveLoading (...).
{
    Box {

        var _bitmap by remember {
            mutableStateOf(Bitmap.createBitmap(1.1, Bitmap.Config.RGB_565))
        }
        
        AndroidView(
            factory = { context ->
                // Creates custom view
                object : AbstractComposeView(context) {

                    @Composable
                    override fun Content(a) {
                        Box(Modifier.wrapContentSize(){
                            content()
                        }
                    }


                    override fun dispatchDraw(canvas: Canvas?). {
                        val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
                        val canvas2 = Canvas(source)
                        super.dispatchDraw(canvas2)
                        _bitmap = bmp
                        
                    }

                }
            }

        )


        WaveLoadingInternal(bitmap = _bitmap)

    }
}
Copy the code

AndroidView is a native control that can draw Composable, we put WaveLoading’s sub-Composable in its Content, and then draw in dispatchDraw, Draw the content into our prepared Bitmap.

4.2 Draw wavy lines

We draw wavy lines based on Compose’s Canvas, and the wavy lines are defined by the Path bearer WaveAnim to draw wavy lines

internal data class WaveAnim(
    val duration: Int.val offsetX: Float.val offsetY: Float.val scaleX: Float.val scaleY: Float,) {private val _path = Path()

    // Draw wavy lines
    internal fun buildWavePath(
        dp: Float,
        width: Float,
        height: Float,
        amplitude: Float,
        progress: Float
    ): Path {

        var wave = (scaleY * amplitude).roundToInt() // Calculate the amplitude after stretching

        _path.reset()
        _path.moveTo(0f, height)
        _path.lineTo(0f, height * (1 - progress))

        // Draw waves with a sine curve
        if (wave > 0) {
                var x = dp
                while (x < width) {
                    _path.lineTo(
                        x,
                        height * (1 - progress) - wave / 2f * Math.sin(4.0 * Math.PI * x / width)
                            .toFloat()
                    )
                    x += dp
                }
        }
            
        _path.lineTo(width, height * (1 - progress))
        _path.lineTo(width, height)
        _path.close()
        return _path
    }

}
Copy the code

As above, the wavy line Path is drawn by a sine function.

4.3 Wave filling

With Path, we also need to populate the content. Fill it with something that we’ve already described, either DrawColor or DrawImage. To draw a Path, you need to define Paint

    val forePaint = remember(foreDrawType, bitmap) {
        Paint().apply {
            shader = BitmapShader(
                when (foreDrawType) {
                    is DrawType.DrawColor -> bitmap.toColor(foreDrawType.color)
                    is DrawType.DrawImage -> bitmap
                    else -> alphaBitmap
                },
                Shader.TileMode.CLAMP,
                Shader.TileMode.CLAMP
            )
        }
    } 
Copy the code

Paint uses the Shader Shader to draw a Bitmap. When DrawType is monochromatic, the Bitmap is single-valued:

/** * bitmap monochrome */
fun Bitmap.toColor(color: androidx.compose.ui.graphics.Color): Bitmap {
    val bmp = Bitmap.createBitmap(
        width, height, Bitmap.Config.ARGB_8888
    )
    val oldPx = IntArray(width * height) // Store the color information of each pixel of the original image
    getPixels(oldPx, 0, width, 0.0, width, height) // Get the pixel information in the original image

    val newPx = oldPx.map {
        color.copy(Color.alpha(it) / 255f).toArgb()
    }.toTypedArray().toIntArray()
    bmp.setPixels(newPx, 0, width, 0.0, width, height) // Assign the processed pixel information to the new image
    return bmp
}
Copy the code

4.4 Wave Animation

Finally, make the waves move with the Compose animation

    val transition = rememberInfiniteTransition()

    val waves = remember(Unit) {
        listOf(
            WaveAnim(waveDuration, 0f.0f, scaleX, scaleY),
            WaveAnim((waveDuration * 0.75 f).roundToInt(), 0f.0f, scaleX, scaleY),
            WaveAnim((waveDuration * 0.5 f).roundToInt(), 0f.0f, scaleX, scaleY)
        )
    }

    val animates :  List<State<Float>> = waves.map { transition.animateOf(duration = it.duration) }

Copy the code

To make the waves more layered, we define three WaveAnim to animate as sets

Finally, use WaveAnim to draw the Path of the wave to Canvas

 Canvas{

        drawIntoCanvas { canvas ->

            // Draw the back scene
            canvas.drawRect(0f.0f, size.width, size.height, backPaint)


            // Draw foreground
            waves.forEachIndexed { index, wave ->

                canvas.withSave {

                    val maxWidth = 2 * scaleX * size.width / velocity.coerceAtLeast(0.1 f)
                    val maxHeight = scaleY * size.height
                  
                    canvas.drawPath (
                        wave.buildWavePath(
                            width = maxWidth,
                            height = maxHeight,
                            amplitude = size.height * amplitude,
                            progress = progress
                        ), forePaint
                    )
                }

            }
        }
    }
Copy the code

Source: github.com/vitaviva/Co…