Android Material Components: Exploring MaterialShapeDrawable
Material Components is the (not so) new library made by the Material team to replace the old support design library. It provides components to apply Material Design in your application with ease. Among these components, you can find the famous FloatingActionButton
, the CardView
or the BottomSheet
. But there are also some less known, nonetheless powerful, components. And one of them is the MaterialShapeDrawable
.
Note: This article was written using the version 1.1.0-alpha07> of the Material Components. MaterialShapeDrawable
existed in the 1.0.0 stable but the API changed quite a lot in the version 1.1.0-alpha01.
What is MaterialShapeDrawable
?
According to the documentation:
Base drawable class for Material Shapes that handles shadows, elevation, scale and color for a generated path.
And by Shape
, they are referring to the Material Guidelines shapes: Rectangular shapes with curved or angled edges and/or corners...
In the build.gradle
of your app, add the new dependency (or update it if you already use Material Components)
implementation 'com.google.android.material:material:1.1.0-alpha07'
We will start by creating a new MaterialShapeDrawable
and setting it as the background of a TextView
.
<TextView
android:id="@+id/helloTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="8dp"
android:textColor="@android:color/white"
android:text="Hello world!" />
val shapeDrawable = MaterialShapeDrawable()
helloTextView.background = shapeDrawable
The drawable supports a couple of customizations like:
- Fill color
shapeDrawable.fillColor = ColorStateList.valueOf(colorAccent)
- Strokes
shapeDrawable.strokeWidth = resources.getDimension(R.dimen.stroke_width)
shapeDrawable.strokeColor = ColorStateList.valueOf(colorPrimary)
If your view has elevation, a shadow will be cast on API above 21 like any other drawable. But this drawable supports a compatibility version of shadows for devices running API lower than 21. In this case, you need to specify the elevation on the drawable itself:
shapeDrawable.elevation = resources.getDimension(R.dimen.elevation)
For now, this provides nothing more that a simple ShapeDrawable couldn’t do, apart from the compatibility shadows. So what’s the point?
Introducing ShapeAppearanceModel
A ShapeAppearanceModel
is a model of edges and corners used by a MaterialShapeDrawable
to render itself. Every MaterialShapeDrawable
has a ShapeAppearanceModel
.
The default ShapeAppearanceModel
has 4 rounded corners with a 0px radius and 4 flat edges. And of course, you can customize it in order to provide your own model:
val shapeAppearanceModel = ShapeAppearanceModel()
shapeDrawable.shapeAppearanceModel = shapeAppearanceModel
// or
val shapeDrawable = MaterialShapeDrawable(shapeAppearanceModel)
Corners
ShapeAppearanceModel
has two out-of-the-box types of corners: rounded and cut.
You can change all 4 corners at once:
val cornerSize = resources.getDimensionPixelSize(R.dimen.corner_size)
shapeAppearanceModel.setAllCorners(
CornerFamily.CUT, //or CornerFamily.ROUNDED
cornerSize
)
Or you can change each corner individually:
shapeAppearanceModel.setTopLeftCorner(CornerFamily.CUT, cornerSize)
shapeAppearanceModel.setBottomRightCorner(CornerFamily.ROUNDED, cornerSize)
different corners with strokes
You can also create your own corners. ShapeAppearanceModel
uses a CornerTreatment
object for each corner. The previously shown ways of customizing the corners were just shortcuts to build instances of CutCornerTreatment
and RoundedCornerTreatment
.
You can also write your own implementation of CornerTreatment
if cut or rounded corners are not enough for you. For example, if you want an inside cut corner, you could use this implementation of CornerTreatment
:
class InnerCutCornerTreatment(cornerSize: Float): CornerTreatment(cornerSize) {
override fun getCornerPath(angle: Float, interpolation: Float, shapePath: ShapePath) {
val radius = cornerSize * interpolation
shapePath.reset(0f, radius, 180f, 180 - angle)
shapePath.lineTo(radius, radius)
shapePath.lineTo(radius, 0f)
}
}
The interpolation
parameter is used for animations. Its value ranges from 0 to 1 and it's used to compute the current radius of the corner. More example are shown later in this article.
And apply it to your ShapeAppearanceModel
:
shapeAppearanceModel.setAllCorners(InnerCutCornerTreatment(cornerSize))
Edges
Edges are handled the same way as corners, by giving to the model instances of EdgeTreatment
objects. There is only one built-in edge treatment: the TriangleEdgeTreatment
. It draws a triangle facing in or out of the shape in the middle of an edge.
shapeAppearanceModel.topEdge = TriangleEdgeTreatment(edgeSize, false)
shapeAppearanceModel.bottomEdge = TriangleEdgeTreatment(edgeSize, true)
This can be useful to add nice looking tooltips into your app.
Like for corners, you can build custom edge treatments. You can find a very good example in the code of the BottomAppBar
of the material components.
The BottomAppBar is using a MaterialShapeDrawable and the cradle for the FAB is a custom EdgeTreatment.
Animations
MaterialShapeDrawable
supports animation of its treatments, whether it’s edge or corner, via the interpolation
parameter. This attribute is ranging from 0 to 1. At 0, the treatments are not rendered at all (this results in square corners and flat edges). At 1, they are fully rendered.
When building custom treatments, be aware of the interpolation parameter of getCornerPath
method as this is the current interpolation of the drawable and you should take it into account when building the ShapePath
of your treatment.
seekBar.progress = seekBar.max
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
shapeDrawable.interpolation = progress.toFloat() / seekBar.max.toFloat()
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
animateButton.setOnClickListener {
val seekbarRatio = seekBar.progress.toFloat() / seekBar.max.toFloat()
ObjectAnimator.ofFloat(shapeDrawable, "interpolation", seekbarRatio, 1f).apply {
duration = 1000
addUpdateListener {
seekBar.progress = ((it.animatedValue as Float) * seekBar.max).toInt()
}
}.start()
}
In this example, the interpolation property is either animated with an ObjectAnimator
, or modified by the tracking of a SeekBar
.
XML usage with Material Theming
The shapeAppearance
attribute
If you dug a bit in the source code of MaterialShapeDrawable
, you probably noticed that this component supports XML inflation, but in a special way.
You can leverage it by using Material components widgets that are using MaterialShapeDrawable
internally:
BottomSheet
Chip
MaterialButton
(and subclasses likeExtandedFloatingActionButton
)FloatingActionButton
MaterialCardView
TextInputLayout
On all theses widgets, you can use the
app:shapeAppearance
attribute and pass it a reference to a style containing the attributes of your custom appearance.
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MaterialButton"
app:shapeAppearance="@style/CustomShapeAppearance" />
<style name="CustomShapeAppearance">
<!-- Attributes for the custom appearance-->
</style>
Here is a list of all supported attributes to customize the appearance :
cornerFamily
(values: rounded or cut)cornerFamilyTopLeft
cornerFamilyTopRight
cornerFamilyBottomRight
cornerFamilyBottomLeft
cornerSize
(dimension)cornerSizeTopLeft
cornerSizeTopRight
cornerSizeBottomRight
cornerSizeBottomLeft
If you want to implements the shape of the Shrine Material Studies, you can simply write:
<style name="CustomShapeAppearance">
<item name="cornerFamily">cut</item>
<item name="cornerSize">4dp</item>
</style>
You can change the shape of any supported widgets without writing code.
But it can be tedious to remember to always specify the shapeAppearance
if you want all your widgets to have the same shape.
Fortunately, with the new Material Themes, you can control the default shape of all the Material widgets.
There are 3 new theme attributes to control the default shapes:
shapeAppearanceSmallComponent
used by:
Chip
MaterialButton
(andExtendedFloatingActionButton
)FloatingActionButton
TextInputLayout
the items of
NavigationView
shapeAppearanceMediumComponent
used by:
MaterialCardView
shapeAppearanceLargeComponent
used by:
BottomSheet
By settings those attributes in your theme to a custom ShapeAppearance
Style, every widgets impacted will be automatically properly shaped.
How does it work internally? The default style of these widgets all include a shapeAppearance
attribute referencing one of the 3 theme attributes. You can see an example for MaterialButton
in the source code.
If you want to learn more about how all the theming, styling and default attributes work, you can read Android Styles & Themes for developers.
Here is how you would customise all the shapes of you app using Material Theming:
<resources>
<!-- Application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Other Theme Attributes. -->
<item name="shapeAppearanceSmallComponent">@style/CustomSmallShapeAppearance</item>
<item name="shapeAppearanceMediumComponent">@style/CustomMediumShapeAppearance</item>
<item name="shapeAppearanceLargeComponent">@style/CustomLargeShapeAppearance</item>
</style>
<style name="CustomSmallShapeAppearance">
<item name="cornerFamily">cut</item>
<item name="cornerSize">4dp</item>
</style>
<style name="CustomMediumShapeAppearance">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
</style>
</resources>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:clipChildren="false"
app:cardBackgroundColor="#FFFFFF"
app:cardElevation="8dp"
app:contentPadding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="MaterialCardView" />
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="MaterialButton" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="TextInputLayout">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextInputEditText" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.chip.ChipGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.chip.Chip
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Chip1" />
<com.google.android.material.chip.Chip
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Chip2" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="16dp"
app:srcCompat="@drawable/ic_android_black_24dp" />
</LinearLayout>
We can observe that some widgets like the Chip or the FloatingActionButton override the cornerSize of the theme. For the FAB, the cornerSize will be computed to always be 50% of the size of the fab.
Wrap-up
MaterialShapeDrawable
is a very powerful tool used a lot across the Material Components library. You have learned how to use it directly in code to build very specific shapes and you also learned how to use it in your layout files and to customise your app theme to globally change all the shapes of your application.
Material Components are open-source and you can find the code on Github.
If you want to go further with Material Theming (colors, typography etc…), I highly recommend Styles, Themes, Material Theming, Oh My! by Anita Singh.
I hope you will have fun experiencing with it in your own apps!