Shimmer for Jetpack Compose

A library which offers a shimmering effect for Android’s Jetpack Compose.

It was developed in need for a shimmer effect that traverses across the whole screen, bringing only a certain subset of child views to shine.

Download

repositories {
  mavenCentral()
}

dependencies {
  implementation 'com.valentinilk.shimmer:compose-shimmer:1.0.0'
}

Usage

The library provides a simple shimmer() modifier which can be applied like any other modifier in Compose as well.

As usual, the order of the modifiers matters. Every visual defined after the modifier will be affected by the shimmer. This includes child views and other modifiers as for example background():

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

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. So overwriting the shimmer’s default theme for the whole application is possible as usual:

val yourShimmerTheme = defaultShimmerTheme.copy(...)
CompositionLocalProvider(
  LocalShimmerTheme provides yourShimmerTheme
) {
  ...
}

The theme offers a few simple configurations like the shimmer’s rotation or width. Additionally 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 effect that will traverse over an area that is equal to the view’s position and size. While this looks great for most cases, it just doesn’t look as satisfiying for cases where the shimmer has to be applied to multiple views:

Due to the differences in sizes, all three shimmers have a different velocitiy, which doesn’t look as calm as it should be. Even having in fact three different and independent shimmerings doesn’t look as clean as it could.

That’s why the library offers different options for the effect’s boundaries:

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

ShimmerBounds.Window

One option is to use the boundaries of the current window. This will create a shimmer that travels over the whole window, while affecting only the views (and child views) which have the shimmer modifier attached.

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

By using the Window option one imperfection remains, which can be a problem if the content is for example scrollable. Attaching the effect to the window means its position is fixed. So while the shimmer is traversing over the content, the content itself might be moved relative to the shimmer. Depending on the theme this effect might be more or less visible.

That’s where the ShimmerBounds.Custom option comes into play. By using it the shimmer and its content will not be drawn until the bounds are set manually by using the updateBounds method on the Shimmer.

// 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))
}
“>

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))
}

Updating the bounds will not trigger a recomposition.

License

Copyright 2021 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.

GitHub

https://github.com/valentinilk/compose-shimmer