Skip to content

Shimmer for Jetpack Compose & Compose Multiplatform

A library which offers a shimmering effect for Compose.

Setup

The library is available on mavenCentral().

dependencies {
  implementation("com.valentinilk.shimmer:compose-shimmer:1.3.1")
}

Multiplatform Targets

Currently supported KMP targets are:

  • Android
  • iOS
  • JVM (Desktop)
  • JS (Browser)
  • Wasm (WebAssembly)

Quick Start

Simply apply the shimmer modifier to any UI of your choice. The code below will emit the shimmering UI that can be seen in the gif above.

@Composable
fun ShimmeringPlaceholder() {
    Row(
        modifier = Modifier
            .shimmer() // <- Affects all subsequent UI elements
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
    ) {
        Box(
            modifier = Modifier
                .size(80.dp, 80.dp)
                .background(Color.LightGray),
        )
        Column(
            verticalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(24.dp)
                    .background(Color.LightGray),
            )
            Box(
                modifier = Modifier
                    .size(120.dp, 20.dp)
                    .background(Color.LightGray),
            )
        }
    }
}

Modifier placement

As usual with modifiers the order matters. Every UI component defined after the shimmer modifier will be affected by the animation. This includes child views and other modifiers:

The first example demonstrates that only the inner Box is shimmering, even though the shimmer has been added to the outer Box's modifier stack. The blue background is not shimmering, as it's sitting above the shimmer modifier.

Box(
  modifier = Modifier
    .size(128.dp)
    .background(Color.Blue)
    .shimmer(),
  contentAlignment = Alignment.Center
) {
  Box(
    modifier = Modifier
      .size(64.dp)
      .background(Color.Red)
  )
}

If the modifier, however, is applied earlier, the outer Box's background modifier is affected by the shimmer as well.

Box(
  modifier = Modifier
    .size(128.dp)
    .shimmer()
    .background(Color.Blue),
  contentAlignment = Alignment.Center
) {
  Box(
    modifier = Modifier
      .size(64.dp)
      .background(Color.Red)
  )
}

Theming

The library includes a ShimmerTheme which can be provided as a local composition. A good practice would be to integrate the theming into your customized MaterialTheme. There is no need to wrap every single shimmer into a CompositionLocalProvider.

val yourShimmerTheme = defaultShimmerTheme.copy(...)

CompositionLocalProvider(
  LocalShimmerTheme provides yourShimmerTheme
) {
  [...]
}

The theme can also be passed as a parameter by using the rememberShimmer(...) function, which is explained further down below.

The theme itself offers a few simple configurations like the shimmer's rotation or width. Additionally a few unabstracted objects like an AnimationSpec or BlendMode are exposed. While this violates the principales of information hiding, it allows for some great customizations.

For further information have a look at documentation in data class itself and have a look at the ThemingSamples in the sample app.

Advanced Usage

The default shimmer() modifier creates a shimmering animation, which will traverse over the view in a certain time. That means that the animation will have a different velocity, depending on the size of the view.

If you apply the modifier to multiple views, each of a different size, then each shimmer will have its own velocity. This effect can be seen in the following gif:

That might not always be the desired effect, that's why the library offers a way to set the boundaries for the animation:

val shimmerInstance = rememberShimmer(shimmerBounds = ShimmerBounds.XXX)
Box(modifier = Modifier.shimmer(shimmerInstance))

ShimmerBounds.View (default)

The view's height and width will be used as the boundaries for the animation. This option was used to create the gifs shown above and should be sufficient for most use cases.

ShimmerBounds.Window

This option uses the window's height, with and coordinate system for the calculations. It will create a shimmer that travels over the whole window instead of only the views. But only views that have the shimmer modifier attached will be affected.

Be aware that this option might look odd on scrollable content, because the shimmer will be positioned relative to the window. So the shimmer will not be moved together with the content.

Column {
  val shimmerInstance = rememberShimmer(shimmerBounds = ShimmerBounds.Window)
  Text("Shimmering Text", modifier = Modifier.shimmer(shimmerInstance))
  Text("Non-shimmering Text")
  Text("Shimmering Text", modifier = Modifier.shimmer(shimmerInstance))
}

ShimmerBounds.Custom

The downsides of the Window option is why the ShimmerBounds.Custom option exists. By using this option, the shimmer and its content will not be drawn until the bounds are set manually by using the updateBounds method on the Shimmer. This can be used to attach the shimmer to a scrollable list for example. Or simply use the default ShimmerBounds.View option.

val shimmerInstance = rememberShimmer(ShimmerBounds.Custom)
Column(
  modifier = Modifier
    .fillMaxSize()
    .verticalScroll(rememberScrollState())
    .onGloballyPositioned { layoutCoordinates ->
      // Util function included in the library
      val position = layoutCoordinates.unclippedBoundsInWindow()
      shimmerInstance.updateBounds(position)
    },
) {
  Text("Shimmering Text", modifier = Modifier.shimmer(shimmerInstance))
  Text("Non-shimmering Text")
  Text("Shimmering Text", modifier = Modifier.shimmer(shimmerInstance))
}

Sample Apps

Sample apps for different platforms are included in the project. To use them, clone the repository into a folder locally.

Android

To run the Android app, simply open the project in Android Studio. Select the app configuration and run it.

iOS

The iOS app can be build and launched by using XCode. Open the iosApp folder with XCode as a project. Adapt the signing in XCode to match your needs and launch the app on an emulator or iOS device.

Desktop

The desktop app can be launched by using the demo.desktop configuration in Android Studio. Or run ./gradlew :shared:run in the terminal.

Browser

To run the sample in the browser, simply use the demo.browser configuration in Android Studio. Or run ./gradlew :shared:jsBrowserRun in the terminal.

WebAssembly (Wasm)

To run the wasm sample use the demo.wasm configuration in Android Studio or run ./gradlew :shared:wasmJsBrowserRun in the terminal.

License

Copyright 2023 Valentin Ilk

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.