DEV Community

Cover image for Composing palettes
Thomas Künneth
Thomas Künneth

Posted on

Composing palettes

In this article I will show you a barely known Jetpack library, Palette It can extract significant colors from an android.graphics.Bitmap. We will use the data created by Palette in a Jetpack Compose app. Here's how the app looks like right after the start:

The app PaletteDemo right after the start

After clicking the FAB the user can select an image. Then, the app looks like this:

PaletteDemo showing the selected image and significant colors

Not too bad, right? Let's dive into the source code. You can find it on GitHub.

Loading a bitmap and getting a palette

To use Jetpack Palette, we need to add it to the implementation dependencies.

implementation 'androidx.palette:palette-ktx:1.0.0'
Enter fullscreen mode Exit fullscreen mode

Next, let's look at the activity.

class PaletteDemoActivity : ComponentActivity() {

  private lateinit var viewModel: PaletteDemoViewModel

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    viewModel = ViewModelProvider(this).get(PaletteDemoViewModel::class.java)
    setContent {
      PaletteDemoTheme {
        Surface(color = MaterialTheme.colors.background) {
          PaletteDemo(
            onClick = { showGallery() }
          )
        }
      }
    }
  }
  
Enter fullscreen mode Exit fullscreen mode

To get a nice architecture, we use ViewModel. You will see shortly where viewModel is used in the activity. But let's look at PaletteViewModel first.

class PaletteDemoViewModel : ViewModel() {

  private val _bitmap: MutableLiveData<Bitmap> =
    MutableLiveData<Bitmap>()

  val bitmap: LiveData<Bitmap>
    get() = _bitmap

  fun setBitmap(bitmap: Bitmap) {
    _bitmap.value = bitmap
  }

  private val _palette: MutableLiveData<Palette> =
    MutableLiveData<Palette>()

  val palette: LiveData<Palette>
    get() = _palette

  fun setPalette(palette: Palette) {
    _palette.value = palette
  }
}
Enter fullscreen mode Exit fullscreen mode

So, we have two properties, a bitmap and a palette. Both will be set from inside the activity and consumed inside composable functions. PaletteDemo() is the root of my composable hierarchy. It receives a lambda expression that invokes showGallery(). Here's what this function does:

private fun showGallery() {
  val intent = Intent(Intent.ACTION_PICK)
  intent.type = "image/*"
  val mimeTypes =
    arrayOf("image/jpeg", "image/png")
  intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
  startActivityForResult(intent, REQUEST_GALLERY)
}
Enter fullscreen mode Exit fullscreen mode

Please note that we should replace startActivityForResult() because it is deprecated in ComponentActivity, but let's save this for a future article. 😎 Next comes the interesting part. What happens when the user has picked an image?

override fun onActivityResult(requestCode: Int, 
                              resultCode: Int, 
                              intent: Intent?) {
  super.onActivityResult(requestCode, resultCode, intent)
  when (requestCode) {
    REQUEST_GALLERY -> {
      if (resultCode == RESULT_OK) {
        intent?.let {
          it.data?.let { uri ->
            val source = ImageDecoder.createSource(
              contentResolver,
              uri
            )
            val bitmap = ImageDecoder.decodeBitmap(source).asShared()
            viewModel.setBitmap(bitmap)
            lifecycleScope.launch {
              viewModel.setPalette(
                Palette.Builder(bitmap).generate()
              )
            }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

To get a bitmap, we first create a source using ImageDecoder.createSource(). The source is then passed to ImageDecoder.decodeBitmap(). Have you spotted asShared()? Inside Jetpack Palette, getPixels() is invoked. This method may fail with IllegalStateException: unable to getPixels(), pixel access is not supported on Config#HARDWARE bitmaps. asShared() prevents this. The docs say:

Return an immutable bitmap backed by shared memory which
can be efficiently passed between processes via Parcelable.

If this bitmap already meets these criteria it will return
itself.

The method was introduced with API level 31, so to support older platforms you should replace it by something similar.

Back to the code. How do we obtain significant colors from the bitmap? First we create a Palette.Builder instance, passing the bitmap. Then we invoke generate() on this object. Besides this synchronous version, there is also a variant based on AsyncTask. As you can see, I chose to use a coroutine instead.

Now, let's turn to the composable function PaletteDemo().

Composing the palette

Most Compose apps will have a Scaffold() as its root, which may include a TopAppBar() and, like my example, a FloatingActionButton(). My content area consists of a vertically scrollable Column(), which remains empty until an image has been selected. Then it contains an Image() and several Box() elements which represent the significant colors.

@Composable
fun PaletteDemo(
  viewModel: PaletteDemoViewModel = viewModel(),
  onClick: () -> Unit
) {
  val bitmap = viewModel.bitmap.observeAsState()
  val palette = viewModel.palette.observeAsState()
  Scaffold(topBar = {
    TopAppBar(title = { Text(stringResource(id =
                                 R.string.app_name)) })
  },
    floatingActionButton = {
      FloatingActionButton(onClick = onClick) {
        Icon(
          Icons.Default.Search,
          contentDescription =
              stringResource(id = R.string.select)
        )
      }
    }
  ) {
    Column(
      modifier = Modifier
        .verticalScroll(rememberScrollState())
        .padding(16.dp),
      horizontalAlignment = Alignment.CenterHorizontally
    ) {
      bitmap.value?.run {
        Image(
          bitmap = asImageBitmap(),
          contentDescription = null,
          alignment = Alignment.Center
        )
      }
      palette.value?.run {
        swatches.forEach {
          Box(
            modifier = Modifier
              .padding(top = 8.dp)
              .fillMaxWidth()
              .height(32.dp)
              .clip(RectangleShape)
              .background(Color(it.rgb))
          )
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

To get informed about changes, we need to invoke observeAsState() on both bitmap and palette. Have you noticed asImageBitmap()? It converts the bitmap to ImageBitmap.

There a quite a few methods you can invoke on Palette instances, for example getVibrantColor() or getDarkVibrantColor(). My code just iterates over a list of swatches. Please refer to the documentation for details.

Conclusion

Using Jetpack Palette inside Composable apps is easy and fun. It will be interesting to see if the library receives updates in the wake of Material You. I hope you liked this post. Please share your thoughts in the comments.

Discussion (1)

Collapse
eagskunst profile image
Emmanuel Guerra

Good article!
I was wondering about lifecycleScope.launch, AFAIK it launches on the main thread right? Any particular reason?