How to Test ImageView Color Filters in Robolectric

Brian Terczynski
4 min readDec 24, 2022
Photo by Stephen Kraakmo on Unsplash

This article describes one way you can test color filters on Android ImageViews in Robolectric. This is when you want to verify that the current color filter on an ImageView, set with ImageView.setColorFilter(int color), is correct.

Extending on the hokey stepper control I had in one of my previous articles, I now want to limit the values to be between 0 and 10. The buttons are colored purple in most cases. If the value is at 0, then the decrement button will be grayed out indicating that the value cannot be decremented anymore. If the value is at 10, then the increment button will be grayed out.

Whenever a button is pressed, after the specified delay occurs we call the below method to update the stepper control’s value and set the colors of the buttons:

val ACCEPTED_RANGE = 0..10

private fun performStep(view: View, incrementValue: Int): Boolean {
if (view.isPressed) {
val newValue = (binding.value.text.toString().toInt() + incrementValue).coerceIn(ACCEPTED_RANGE)
binding.value.text = newValue.toString()
updateColor()
return true
}
return false
}

The updateColor() method is defined as so:

private fun updateColor() {
binding.decrement.setColorFilter(
ContextCompat.getColor(
requireContext(),
if (binding.value.text.toString().toInt() > ACCEPTED_RANGE.first()) {
R.color.purple_700
} else {
R.color.gray
}
)
)
binding.increment.setColorFilter(
ContextCompat.getColor(
requireContext(),
if (binding.value.text.toString().toInt() < ACCEPTED_RANGE.last()) {
R.color.purple_700
} else {
R.color.gray
}
)
)
}

We want to verify the following in our Robolectric test:

  • When the stepper’s value is at 0, the decrement button is gray but the increment button is purple. And we can’t decrement any further.
  • When the stepper’s value is at 10, the increment button is gray but the decrement button is purple. And we can’t increment any further.
  • In all other cases, the buttons are purple.

It is simple to verify the color of an ImageView’s color filter in Robolectric:

Shadows.shadowOf(imageView.colorFilter as PorterDuffColorFilter).color

We are assuming that the color filter being used is the PorterDuffColorFilter. The documentation for ImageView.setColorFilter(int color) does not explicitly say this is the filter that is used, but it does say that it applies the PorterDuff.Mode = SRC_ATOP filtering mode which would imply that’s the filter being used. And if you look at the source code for ImageView.setColorFilter(int color, PorterDuff.Mode mode) you will see that indeed the filter used is PorterDuffColorFilter. Now, if we were calling setColorFilter(ColorFilter colorFilter), then of course we could not assume Porter Duff was used. Also, at any time the implementation of ImageView.setColorFilter(int color) could change which would cause our test code to fail. But, in order for us to actually obtain the color, we need a subclass of the generic ColorFilter. And since PorterDuffColorFilter is what is actually used and has a Shadow defined for it in Robolectric, this code works (for now at least).

With all that, we can now write our test as follows:

@Test
fun `test stepper color`() {
val activityController = Robolectric.buildActivity(MainActivity::class.java)
.setup()
val activity = activityController.get()
// Assuming we just have one fragment.
val fragment = (activity.supportFragmentManager.fragments[0] as NavHostFragment).childFragmentManager
.fragments[0] as FirstFragment
// Initial value
var initialValue = "8"
fragment.binding.value.text = initialValue
// Touch our increment control
fragment.binding.increment.dispatchTouchEvent(
MotionEvent.obtain(0L, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 0f, 0f, 0)
)
// Value updated right away after touch-down
assertEquals((initialValue.toInt() + 1).toString(), fragment.binding.value.text)
verifyColor(fragment, R.color.purple_700, R.color.purple_700)
// Move the clock forward enough so the value changes to "10"
ShadowLooper.idleMainLooper(
(ViewConfiguration.getLongPressTimeout() + fragment.INITIAL_PRESS_DELAY_OFFSET).toLong(),
TimeUnit.MILLISECONDS
)
// Verify the value is updated and increment button is now grayed out
assertEquals((initialValue.toInt() + 2).toString(), fragment.binding.value.text)
verifyColor(fragment, R.color.purple_700, R.color.gray)
// Move the clock again
ShadowLooper.idleMainLooper(
ViewConfiguration.getLongPressTimeout().toLong(),
TimeUnit.MILLISECONDS
)
// Verify the value is not updated
assertEquals((initialValue.toInt() + 2).toString(), fragment.binding.value.text)
verifyColor(fragment, R.color.purple_700, R.color.gray)
// Stop pressing
fragment.binding.increment.dispatchTouchEvent(
MotionEvent.obtain(0L, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 0f, 0f, 0)
)

initialValue = "2"
fragment.binding.value.text = initialValue
// Touch our decrement control
fragment.binding.decrement.dispatchTouchEvent(
MotionEvent.obtain(0L, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 0f, 0f, 0)
)
// Value updated right away after touch-down
assertEquals((initialValue.toInt() - 1).toString(), fragment.binding.value.text)
verifyColor(fragment, R.color.purple_700, R.color.purple_700)
// Move the clock forward enough so the value changes to "0"
ShadowLooper.idleMainLooper(
(ViewConfiguration.getLongPressTimeout() + fragment.INITIAL_PRESS_DELAY_OFFSET).toLong(),
TimeUnit.MILLISECONDS
)
// Verify the value is updated and decrement button is grayed out
assertEquals((initialValue.toInt() - 2).toString(), fragment.binding.value.text)
verifyColor(fragment, R.color.gray, R.color.purple_700)
// Move the clock again
ShadowLooper.idleMainLooper(
ViewConfiguration.getLongPressTimeout().toLong(),
TimeUnit.MILLISECONDS
)
// Verify the value is not updated
assertEquals((initialValue.toInt() - 2).toString(), fragment.binding.value.text)
verifyColor(fragment, R.color.gray, R.color.purple_700)
// Stop pressing
fragment.binding.decrement.dispatchTouchEvent(
MotionEvent.obtain(0L, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 0f, 0f, 0)
)
}

private fun verifyColor(fragment: FirstFragment, @ColorRes decrementColor: Int, @ColorRes incrementColor: Int) {
assertEquals(
ContextCompat.getColor(fragment.requireContext(), decrementColor),
Shadows.shadowOf(fragment.binding.decrement.colorFilter as PorterDuffColorFilter).color
)
assertEquals(
ContextCompat.getColor(fragment.requireContext(), incrementColor),
Shadows.shadowOf(fragment.binding.increment.colorFilter as PorterDuffColorFilter).color
)
}

We run this, and the test passes.

So to summarize, to verify color filters on ImageViews in Robolectric:

Shadows.shadowOf(imageVie.colorFilter as PorterDuffColorFilter).color

--

--

Brian Terczynski

Documenting my learnings on my journey as a software engineer.