Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

14. Style & Theme
Written by Subhrajyoti Sen

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

A polished user interface makes a good first impression. It can even be one of the reasons users like using your app. A key feature of a polished user interface is consistency in components across different sections of the app. These components include color schemes, shapes, typography and more. These days, another important feature of the user interface is having a dark theme.

Android lets you use styles and themes to achieve these goals and much more.

In this chapter, you’ll learn about:

  • Styles, themes and their differences.
  • The order of different modes of styling.
  • Using theme overlays to tweak specific attributes.
  • Adding styling support to custom views.
  • Adding dark theme support to your app.

As first step open, as usual, the starter project in the material for this chapter.

Defining Styles and Themes

Usually, you define styles and themes in res/styles.xml, which contains a collection of attributes and their values. These can be specific to a certain view or they can apply to a collection of views.

Structure of a Style

A typical style looks like this:

<style name="LargeText">
  <item name="android:textSize">@dimen/large_text</item>
</style>
<style name="LargeRedText">
  <item name="android:textSize">@dimen/large_text</item>
  <item name="android:textColor">@android:color/red</item>
</style>

Structure of a Theme

The structure of a theme is identical to that of a style:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  <item name="colorAccent">@color/colorAccent</item>
</style>

Style Hierarchy

Android provides a wide variety of ways to set attributes in your app. For example, you can set view attributes in XML layouts, apply a style to the view and apply a theme to your activity or even the entire app.

Theme Overlay

Sometimes, you want to modify the appearance of a View or ViewGroup but the attribute(s) you want to change derive from a theme. Take the example of MaterialButton.

<com.google.android.material.button.MaterialButton
  android:id="@+id/red_button"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:background="#DA2222"
  android:layout_marginTop="@dimen/default_margin"
  android:text="Red Button"
  app:layout_constraintBottom_toBottomOf="parent"
  android:layout_marginBottom="@dimen/default_margin"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@id/adopt_button" />
Figure 14.1 — A Blue Button
Zozaca 75.1 — O Qwou Laytec

<style name="ThemeOverlay.PetSave.RedButton" parent="">
  <item name="colorPrimary">#DA2222</item>
</style>
<com.google.android.material.button.MaterialButton
  android:id="@+id/red_button"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:theme="@style/ThemeOverlay.PetSave.RedButton"
  android:layout_marginTop="@dimen/default_margin"
  android:text="Red Button"
  app:layout_constraintBottom_toBottomOf="parent"
  android:layout_marginBottom="@dimen/default_margin"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@id/adopt_button" />
Figure 14.2 — A Red Button
Figibe 57.7 — A Paf Xivjin

TextAppearance

textAppearance lets you define text-specific styling for a TextView and decouple it from the rest of the styling. One benefit of textAppearance is you can programmatically set a view to use it at any time, whereas you can only specify a style when the view inflates.

<style name="PetLabelTextAppearance" parent="TextAppearance.MaterialComponents.Headline3" >
  <item name="android:textSize">@dimen/large_text</item>
  <item name="android:textStyle">bold</item>
</style>
<TextView
  android:id="@+id/special_needs_label"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/default_margin"
  android:layout_marginTop="@dimen/default_margin"
  android:text="@string/special_needs"
  android:textAppearance="@style/PetLabelTextAppearance"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@id/divider" />
Figure 14.3 — Using TextAppearance
Dovupu 06.0 — Ocavb BohcUgjuamirdo

Setting Up Dark Themes

Dark themes have dark background colors and light foreground colors, and the Material dark theme system helps you make dark options for your app. Some of the benefits of providing one are:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
  <!-- Customize your theme here. -->
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
</style>

Understanding Material Color Attributes

Before creating a dark theme, you need to understand the color system in Material Design components.

Adding a Theme Toggle

To let the user switch between themes, you’ll add a toggle, which will have three options:

<?xml version="1.0" encoding="utf-8"?>
<menu
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:android="http://schemas.android.com/apk/res/android">
  <item
    android:id="@+id/light_theme"
    android:title="Light theme"
    app:showAsAction="never" />
  <item
    android:id="@+id/dark_theme"
    android:title="Dark Theme"
    app:showAsAction="never" />
  <item
    android:id="@+id/follow_system"
    android:title="Follow System"
    app:showAsAction="never" />
</menu>
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  // ...
  override fun onCreateOptionsMenu(menu: Menu): Boolean {
    val inflater = menuInflater
    inflater.inflate(R.menu.theme_options, menu) // HERE
    return true
  }
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  // ...
  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    val themeMode = when (item.itemId) {
      R.id.light_theme -> {
        AppCompatDelegate.MODE_NIGHT_NO
      }
      R.id.dark_theme -> {
        AppCompatDelegate.MODE_NIGHT_YES
      }
      else -> {
        AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
      }
    }
    AppCompatDelegate.setDefaultNightMode(themeMode)
    return true
  }
}
Figure 14.4 — Theme Options
Munuyi 39.1 — Bkepo Ecmiedp

Resolving Dark Theme Inconsistencies

Use the toggle to switch to the dark theme and explore the app. Observe the same screens in both light and dark themes. Some of the inconsistencies you’ll notice are:

Using Theme Attributes

As Android developers, one of the first things you learn is not to hard code color values, but to use color resources instead. So instead of using #FFFFFF, you might define colorWhite and use this color resource throughout your app.

<com.google.android.material.floatingactionbutton.FloatingActionButton
  android:id="@+id/call"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_margin="@dimen/default_margin"
  android:contentDescription="@string/contact"
  android:src="@drawable/ic_call_24dp"
  android:visibility="gone"
  app:backgroundTint="@color/colorPrimary"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
  app:tint="@android:color/white"
  tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
  //...
  app:tint="?attr/colorOnPrimary"
  tools:visibility="visible" />
Figure 14.5 — Floating Action Button Color
Qeniwi 58.2 — Zlaopijx Uvheuv Kitdin Povex

Fixing Other Hard-coded Colors

Similarly, open fragment_search.xml and look at AppBarLayout. You’ll notice that background is a static color:

<com.google.android.material.appbar.AppBarLayout
  android:id="@+id/collapsible_search_params_container"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@android:color/white"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.AppBarLayout
  android:id="@+id/collapsible_search_params_container"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="?attr/colorPrimarySurface"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent">
Figure 14.6 — Search Bar in Dark Mode
Situpa 44.1 — Heughv Ser oc Bayq Seta

Using Night Colors

You might have noticed that you haven’t specified any separate color values for the dark theme, yet switching to dark theme displays different colors in many places. That’s because Material Components themes have default values for dark themes. If you want to tweak these values, you can do so by defining night color resources.

Figure 14.7 — New Resource Directory
Wiciro 86.5 — Veg Diwuosnu Hohuqtehj

Figure 14.8 — New Resource Directory
Mezota 32.5 — Xal Woqaalxa Hisarfiwj

Figure 14.9 — Colors for Night Mode
Giluda 85.4 — Guzadm nuj Puwqb Tiyi

Figure 14.10 — Qualified Color Resources
Qerelu 62.63 — Toirixaes Nexuc Qecoojhok

<color name="colorPrimary">#BA86FC</color>
<color name="colorPrimaryDark">#000000</color>
<color name="colorBackground">#000000</color>
<color name="colorBackground">#FFFFFF</color>
<item name="android:colorBackground">@color/colorBackground</item>
Figure 14.11 — Dark Theme in Action
Cuhise 00.56 — Xewg Sregu ev Iyyaoj

Styling Custom Views

Most of the views Android provides have good styling support out of the box. To give developers a good experience, it’s also important to provide styling support in your custom views. In this section, you’ll make ProgressButton styleable.

Adding Styleable Attributes

First, you need to modify your view so it can read attribute values from a style. To do this, you need to remove any hard-coded colors from the view.

private val textPaint = Paint().apply {
  isAntiAlias = true
  style = Paint.Style.FILL
  textSize = context.dpToPx(16f)
}

private val backgroundPaint = Paint().apply {
  isAntiAlias = true
  style = Paint.Style.FILL
}

private val progressPaint = Paint().apply {
  isAntiAlias = true
  style = Paint.Style.STROKE
  strokeWidth = context.dpToPx(2f)
}
<attr name="progressButton_backgroundColor" format="color" />
<attr name="progressButton_textColor" format="color" />
<attr name="progressButton_progressColor" format="color" />
val typedValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.colorPrimary, typedValue, true)
val defaultBackgroundColor = typedValue.data
val defaultTextColor = Color.WHITE
val defaultProgressColor = Color.WHITE
val backgroundColor = typedArray.getColor(R.styleable.ProgressButton_progressButton_backgroundColor, defaultBackgroundColor)
backgroundPaint.color = backgroundColor

val textColor = typedArray.getColor(R.styleable.ProgressButton_progressButton_textColor, defaultTextColor)
textPaint.color = textColor

val progressColor = typedArray.getColor(R.styleable.ProgressButton_progressButton_progressColor, defaultProgressColor)
progressPaint.color = progressColor

Default Styles

In the last chapter, you learned about the View constructor, which looked like this:

class ProgressButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr)
class ProgressButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressButton, defStyleAttr, defStyleRes)
<attr name="progressButtonStyle" format="reference"/>

Defining Your Custom View’s Style

The next step is to define the style for your custom view. Open styles.xml and add the following code:

<style name="ProgressButtonStyle">
  <item name="progressButton_backgroundColor">?attr/colorPrimary</item>
  <item name="progressButton_textColor">?attr/colorOnPrimary</item>
  <item name="progressButton_progressColor">?attr/colorOnPrimary</item>
</style>
<item name="progressButtonStyle">@style/ProgressButtonStyle</item>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
  <!-- Customize your theme here. -->
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  <item name="android:colorBackground">@color/colorBackground</item>
  <item name="progressButtonStyle">@style/ProgressButtonStyle</item>
</style>

Setting defStyleAttr and defStyleRes

Your final step is to set the values of defStyleAttr and defStyleRes. Open ProgressButton.xml and change the default values of the constructor arguments as follows:

class ProgressButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = R.attr.progressButtonStyle,
    defStyleRes: Int = R.style.ProgressButtonStyle
) : View(context, attrs, defStyleAttr, defStyleRes)
Figure 14.12 — Styled Custom View
Nekora 07.44 — Hmbbil Wiqheg Teah

Key Points

  • Use styles and themes for consistent UI elements throughout the app.
  • Styles apply to a specific view but themes apply to a view hierarchy.
  • Different styling modes have a different order of precedence.
  • Make your custom views styleable and provide a default style.
  • Use textAppearance to group character level styling attributes.
  • Extend a DayNight variant of an AppCompat or Material Components theme when adding a dark theme.
  • Use theme attributes as often as possible.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now