Android ClipDrawable
I needed to create a custom progress bar for an Android app. Its indicator was colored with a green-to-yellow gradient, such that for low values the bar would appear mostly green, but for higher values it would gradually show yellow.
I could easily define each element of the bar in alayer-list
XML:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Border -->
<item>
<shape android:shape="rectangle">
<stroke
android:width="@dimen/progressBarBorderWidth"
android:color="@color/progressBarBackground" />
<corners android:radius="@dimen/progressCornerRadius" />
</shape>
</item>
<!-- Indicator Background -->
<item
android:bottom="@dimen/progressIndicatorPadding"
android:left="@dimen/progressIndicatorPadding"
android:right="@dimen/progressIndicatorPadding"
android:top="@dimen/progressIndicatorPadding">
<shape android:shape="rectangle">
<solid android:color="@color/progressBarBackground" />
</shape>
</item>
<!-- Indicator -->
<item
android:bottom="@dimen/progressIndicatorPadding"
android:left="@dimen/progressIndicatorPadding"
android:right="@dimen/progressIndicatorPadding"
android:top="@dimen/progressIndicatorPadding">
<shape android:shape="rectangle">
<gradient
android:angle="0"
android:endColor="@color/progressEndColor"
android:startColor="@color/progressStartColor" />
</shape>
</item>
</layer-list>
The question was: how do I get my code to only show a percentage of the indicator? I couldn’t just set its width, because then the gradient would always fill from green to yellow. I basically wanted the indicator to be clipped based on different percentage values. I was wondering if I would have to do something complicated like override onDraw
and do the clipping in there.
But then I found out about ClipDrawable.
ClipDrawable basically wraps another Drawable and clips it, either from the top, bottom, left or right. You can specify it in XML as follows:
<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:clipOrientation="horizontal"
android:drawable="@drawable/progress_limit_indicator"
android:gravity="start" />
A clipOrientation
of “horizontal” means you want to clip the Drawable from either the left or the right; “vertical” means from top or bottom. Then you use the gravity
attribute to specify from which side. If you want to clip the right (end) side of the Drawable, then you specify horizontal
/left
(or better yet, horizontal
/start
to support right-to-left layouts). If you want to clip the top of the Drawable, then specify vertical
/bottom
. And so on.
This visual shows the different ways to clip an image with ClipDrawable.
You control the amount of clipping by setting the ClipDrawable’s level
attribute. Its value ranges from 0 to 10,000, where 10,000 means the image is fully revealed (unclipped) and 0 means it is fully hidden (fully clipped). To set the level, you obtain a reference to the ClipDrawable in your code, and then set the level
attribute appropriately. For example:
val clipDrawable: ClipDrawable...some code in here assigns clipDrawable...clipDrawable.level = 3000
Note that calling .level
(setLevel()
) will automatically invalidate the Drawable and force it to redraw. Also, there appears to be no need to call mutate()
on the Drawable either.
<clip>
can also be included within a layer-list Drawable. This means that for my progress bar, I can replace the <shape>
in the <!-- Indicator -->
section with a <clip>
, so that my progress bar XML now looks like the following:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Border -->
<item>
<shape android:shape="rectangle">
<stroke
android:width="@dimen/progressBarBorderWidth"
android:color="@color/progressBarBackground" />
<corners android:radius="@dimen/progressCornerRadius" />
</shape>
</item>
<!-- Indicator Background -->
<item
android:bottom="@dimen/progressIndicatorPadding"
android:left="@dimen/progressIndicatorPadding"
android:right="@dimen/progressIndicatorPadding"
android:top="@dimen/progressIndicatorPadding">
<shape android:shape="rectangle">
<solid android:color="@color/progressBarBackground" />
</shape>
</item>
<!-- Indicator -->
<item
android:id="@+id/indicator"
android:bottom="@dimen/progressIndicatorPadding"
android:left="@dimen/progressIndicatorPadding"
android:right="@dimen/progressIndicatorPadding"
android:top="@dimen/progressIndicatorPadding">
<clip
android:clipOrientation="horizontal"
android:drawable="@drawable/progress_limit_indicator"
android:gravity="start" />
</item>
</layer-list>
Note that an id
was added so that we could reference the ClipDrawable in our code (which I will demonstrate soon). And the progress indicator shape was moved to its own file so it could be properly referenced by the <clip>
object:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="0"
android:endColor="@color/progressEndColor"
android:startColor="@color/progressStartColor" />
</shape>
Now suppose we include our progress bar as an ImageView in our layout:
<ImageView
android:id="@+id/progress"
android:layout_width="@dimen/progressBarWidth"
android:layout_height="@dimen/progressBarHeight"
android:src="@drawable/progress_bar"
/>
If binding
is our View Binding, then we can set the progress indicator as follows:
val layerDrawable = binding.progress.drawable as LayerDrawable
val clipDrawable = layerDrawable.findDrawableByLayerId(R.id.indicator) as ClipDrawable
clipDrawable.level = 3000
The above code renders the progress bar as follows:
Pretty simple, eh?
We can get even simpler than that. Instead of drilling down to reference the ClipDrawable directly, we can just set the level of the entire ImageView’s Drawable like so:
binding.progress.setImageLevel(3000)
This approach is definitely simpler, and you would not need to specify an id
for the <item>
in the <layer-list>
. But one reason you may want to stick with the prior approach is if you had multiple ClipDrawables in your <layer-list>
, and you wanted to set the levels for each independently.
ClipDrawable is a pretty easy and effective way to perform simple clips of your Drawables. By combining this with other Drawable XML elements, you can create some pretty nifty images. For more information, check out the ClipDrawable guide on the Android Developers website.