Elevation & Shadows: Material Design in Android XML with Kotlin

Material Design, introduced by Google, emphasizes visual depth and realism in user interfaces. One of the core elements that achieves this effect is the use of shadows created by adjusting the elevation of UI elements. This blog post provides a comprehensive guide to adding elevation to create Material Design shadows in XML layouts using Kotlin in Android development.

What is Elevation in Material Design?

Elevation, in the context of Material Design, refers to the z-coordinate of a UI element. Elements with higher elevation cast larger, softer shadows on the elements beneath them, creating a sense of depth and hierarchy. This visual cue helps users understand the interactive nature and relative importance of different parts of the UI.

Why Use Elevation for Shadows?

  • Visual Hierarchy: Helps in creating a clear visual distinction between UI elements.
  • User Experience: Provides intuitive feedback on interactive elements, making the UI more engaging.
  • Aesthetic Appeal: Enhances the overall look and feel of the application by adhering to Material Design principles.

How to Add Elevation in XML Using Kotlin

To implement elevation in your Android application using XML, you’ll primarily work with the elevation attribute in your layout files. Below is a detailed guide with practical examples.

Step 1: Set Up Your Project

Ensure you have Android Studio installed and a basic Kotlin project set up. You can create a new project or use an existing one to follow along.

Step 2: Add Elevation in XML Layout File

Open your layout XML file (e.g., activity_main.xml) and add the elevation attribute to the view you want to elevate. For instance, you can add elevation to a CardView or a Button.

<androidx.cardview.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="16dp"
    app:cardElevation="8dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:text="Card with Elevation"
        android:textSize="18sp"/>

</androidx.cardview.widget.CardView>

Here:

  • app:cardElevation="8dp" sets the elevation of the CardView to 8dp (density-independent pixels). The higher the value, the more prominent the shadow.

Step 3: Apply Elevation to Other Views

You can apply elevation to other views like Button and ImageView as well. Here’s how to add elevation to a Button:

<Button
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Elevated Button"
    android:elevation="4dp"/>

In this example, android:elevation="4dp" is used to elevate the button. Note that CardView uses app:cardElevation, while other views use the standard android:elevation.

Step 4: Create Ripple Effect on Click

To enhance the user experience further, consider adding a ripple effect to interactive elements when they are clicked. This complements the shadow effect and provides clear feedback to the user.

<Button
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Elevated Button with Ripple"
    android:elevation="4dp"
    android:background="?android:attr/selectableItemBackground"/>

Here, android:background="?android:attr/selectableItemBackground" adds the default ripple effect on the button when it’s pressed.

Step 5: Using Kotlin to Programmatically Control Elevation

While you can set elevation in XML, Kotlin allows you to programmatically control and modify the elevation. Here’s how:

import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val elevatedButton: Button = findViewById(R.id.elevatedButton)
        elevatedButton.elevation = 8f // Set elevation programmatically
    }
}

In your XML layout (activity_main.xml):

<Button
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/elevatedButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Elevated Button"
    android:elevation="0dp"/> <!-- Initial elevation can be set to 0 -->

The elevatedButton.elevation = 8f line in Kotlin sets the elevation of the button programmatically. Setting the initial elevation in XML to 0dp allows you to control the shadow entirely from your Kotlin code.

Handling Clipping and Outline Providers

Sometimes, shadows might not appear correctly if the view’s outline is not properly defined, or if clipping is enabled. Here are a few points to consider:

Set Outline Provider

You might need to set an outline provider if the default shape doesn’t work well with the shadow. Here’s an example for a custom view:

import android.content.Context
import android.graphics.Outline
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import androidx.appcompat.widget.AppCompatButton

class RoundedButton(context: Context, attrs: AttributeSet?) : AppCompatButton(context, attrs) {

    init {
        outlineProvider = object : ViewOutlineProvider() {
            override fun getOutline(view: View, outline: Outline) {
                outline.setRoundRect(0, 0, view.width, view.height, 20f)
            }
        }
        clipToOutline = true
    }
}

And in your XML layout:

<your.package.RoundedButton
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Rounded Button"
    android:elevation="8dp"/>

Clipping to Outline

Make sure the view is not clipping its content, which can interfere with the shadow. Ensure android:clipChildren and android:clipToPadding are set appropriately on parent views.

Best Practices for Using Elevation

  • Consistency: Maintain consistent elevation levels throughout your app for a uniform look.
  • Subtlety: Use shadows subtly. Overusing elevation can clutter the UI and make it visually noisy.
  • Interaction: Use elevation changes to indicate interaction, providing visual feedback to the user.
  • Testing: Test your layout on different devices to ensure shadows render correctly across various screen sizes and resolutions.

Conclusion

Adding elevation to create Material Design shadows in XML layouts in Kotlin is a straightforward yet effective way to enhance the visual appeal and user experience of your Android applications. By thoughtfully using elevation, ripple effects, and handling outline providers, you can create a modern, intuitive, and engaging user interface that aligns with Material Design principles. Whether you’re creating simple UI elements or complex layouts, understanding and utilizing elevation is a key skill for any Android developer.