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:DrawColor orDrawImage |
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…