preface

A skeleton screen is a blank version of the page, usually sketched out with gray blocks before the page is fully rendered, and replaced with real content after the data is loaded. In this article, we will focus on how to implement a simple and easy-to-use skeleton screen effect using Compose, which provides more information and provides a better user experience than traditional skeleton screen effects

rendering

First take a look at the final renderings

features

  1. Easy to use, reusable pagesUI, do not need to be customized for the skeleton screenUI
  2. You can set whether to display the skeleton screen, which is generally used in conjunction with the loading status
  3. Support setting skeleton screen background and highlight color
  4. Support setting skeleton screen height part width, gradient part width
  5. Supports setting the Angle and direction of skeleton screen animation
  6. Support to set the skeleton screen animation time and two animation interval

use

Access to the

Step 1: In your project build.gradle add:

allprojects {
	repositories {
		...
		mavenCentral()
	}
}
Copy the code

Step 2: In your application’s build.gradle add:

dependencies {
        implementation 'the IO. Making. Shenzhen2017: shimmer: 1.0.0'
}
Copy the code

Simple to use

@Composable
fun ShimmerSample(a) {
    var loading: Boolean by remember {
        mutableStateOf(true)
    }
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .shimmer(loading,config = ShimmerConfig())
    ) {
        repeat(3) {
            PlaceHolderItem()
            Spacer(modifier = Modifier.height(10.dp))
        }
    }
}
Copy the code

As shown above:

  1. Only need toColumntheModifieraddshimmer.ColumnUnder all components can achieve skeleton screen effect
  2. throughloadingParameter to control whether the skeleton screen effect is displayed
  3. If you need to customize the skeleton screen animation effect, you can also configure some parameters

There are mainly the following parameters

data class ShimmerConfig(
    // Unhighlighted parts of the color
    val contentColor: Color = Color.LightGray.copy(alpha = 0.3 f),
    // Highlight some colors
    val higLightColor: Color = Color.LightGray.copy(alpha = 0.9 f),
    // Gradient part width
    @floatrange (from = 0.0, to = 1.0)
    val dropOff: Float = 0.5 f.// Highlight part of the width
    @floatrange (from = 0.0, to = 1.0)
    val intensity: Float = 0.2 f.// Skeleton screen animation direction
    val direction: ShimmerDirection = ShimmerDirection.LeftToRight,
    // Animation rotation Angle
    val angle: Float = 20f.// Animation length
    val duration: Float = 1000f.// Two animation intervals
    val delay: Float = 200f
)
Copy the code

The main principle of

Reuse pages through image blending modeUI

If we want to implement frame screen effect, the first thought is need according to the structure of the page to write a set of UI, and then in the loading time, according to the UI, otherwise hidden effect in the general load are true, but it will bring a problem, different page structure is different, then we wouldn’t want a page to rewrite a UI? This is clearly unacceptable

We can think of, well, the structure of the page we’ve already written, wouldn’t it be nice if we could reuse the structure of the page we’ve written? We can do this through image blending mode

Image blending mode defines the final presentation of the image when two images are combined. inAndrodIn, there are correspondingAPIInterface to support image blending mode, i.eXfermode.

There are mainly 16 image mixing modes as follows. The following picture vividly illustrates the role of image mixing to some extent. Two figures, one circle and one side, produce different combination effects through certain calculation, as follows



We introduce a few commonly used, other interested students can consult by themselves

  • SRC_IN: Only draw the source image where it intersects the target image.
  • DST_IN: Only draw [target image] where the source image intersects the target image, and the rendering effect is affected by the transparency of the corresponding place of the source image
  • SRC_OUT: Only draw [source image] where the source image and the target image do not intersect, and the intersection is based on the corresponding place of the target imagealphaFilter. If the target image is completely opaque, it is completely filtered. If the target image is completely transparent, it is not filtered
  • DST_OUT: Only draw the [target image] where the source image and the target image do not intersect, and at the intersection according to the source imagealphaFilter. If the source image is completely opaque, it is completely filtered. If the source image is completely transparent, it is not filtered

If we use the UI structure of the page as the target image and the skeleton screen effect as the source image, then we can use SRC_IN blend mode to display the skeleton screen only on the structure of the page and not in the white space, thus avoiding the need to write the UI repeatedly

Animations are achieved by panning

We have already implemented the skeleton screen on the page structure, but the skeleton screen effect and the animation effect is also very simple, set the skeleton screen to a gradient effect, then do a pan animation, and then look like the skeleton screen flash animation

fun Modifier.shimmer(a): Modifier = composed {
    var progress: Float by remember { mutableStateOf(0f)}val infiniteTransition = rememberInfiniteTransition()
    progress = infiniteTransition.animateFloat().value  // Animation effect, calculate percentage
    ShimmerModifier(visible = visible, progress = progress, config = config)
}

internal class ShimmerModifier(progress:Float) : DrawModifier, LayoutModifier {
    private val paint = Paint().apply {
        blendMode = BlendMode.SrcIn // Set the blending mode
        shader = LinearGradientShader(Offset(0f.0f),toOffset,colors,colorStops)// Set gradient
    }

    override fun ContentDrawScope.draw(a) {
        drawContent()
        val (dx, dy) = getOffset(progress) // Set the pan position according to progresspaint.shader? .postTranslate(dx, dy)// Pan operation
        it.drawRect(Rect(0f.0f, size.width, size.height), paint = paint)// Draw the skeleton screen effect}}Copy the code

As shown above, there are mainly several steps:

  1. Start the animation to get the current progressprogressAnd according to theprogressGets the current pan position
  2. Set the background gradient color and blend mode of the skeleton screen
  3. Draws the skeleton screen effect

Custom skeleton screen effect

As described above, we provide some parameters to customize the effect of skeleton screen. Other parameters are easy to understand, mainly the following two parameters are a little difficult to understand

  1. dropOff: Gradient part width
  2. intensity: Highlight part width

We know that you can customize the normal part color with contentColor, and you can customize the highlighted part color with higLightColor but how do those two colors distribute? What is the ratio of gradients? Take a look at the following code:

    private val paint = Paint().apply {
        shader = LinearGradientShader(Offset(0f.0f),toOffset,colors,colorStops)// Set gradient
    }

    private val colors = listOf(
        config.contentColor,
        config.higLightColor,
        config.higLightColor,
        config.contentColor
    )

    private val colorStops: List<Float> = listOf(
        ((1f - intensity - dropOff) / 2f).coerceIn(0f.1f),
        ((1f - intensity - 0.001 f) / 2f).coerceIn(0f.1f),
        ((1f + intensity + 0.001 f) / 2f).coerceIn(0f.1f),
        ((1f + intensity + dropOff) / 2f).coerceIn(0f.1f))Copy the code

As you can see, our color gradient has the following characteristics:

  1. The gradient color distribution is:contentColor->higLightColor->higLightColor->contentColor
  2. LinearGradientShaderusecolorsDefine colors,colorStopsDefine the distribution of color gradients,colorStopsbyintensitywithdropoffcalculated
  3. intensityDetermines the width of the highlighted section, i.eintensityThe bigger the part, the bigger the highlight
  4. dropOffDetermines the width of the gradient section, i.edropOffThe bigger the gradient, the bigger the gradient

conclusion

Special thanks to

In the process of implementing the Compose version of the skeleton screen, we mainly refer to the following open source framework ideas. If you are interested, you can also learn about Facebook’s open source Shimmer-Android Habib Kazemi’s open source shimmer-Shimmer

The project address

The Compose version of the skeleton screen is not easy to open source, if the project is helpful to you, welcome to like,Star, favorite ~