Android fundamentals 05.1: Drawables, styles, and themes

1. Welcome

Introduction

In this chapter you learn how to apply common styles to your views, use drawable resources, and apply themes to your app. These practices reduce your code and make your code easier to read and maintain.

What you should already know

You should be able to:

  • Create and run apps in Android Studio.
  • Create and edit UI elements using the layout editor.
  • Edit XML layout code, and access elements from your Java code.
  • Extract hardcoded strings into string resources.
  • Extract hardcoded dimensions into dimension resources.
  • Add menu items and icons to the options menu in the app bar.
  • Create a click handler for a Button click.
  • Display a Toast message.
  • Pass data from one Activity to another using an Intent.

What you'll learn

  • How to define a style resource.
  • How to apply a style to a View.
  • How to apply a theme to an Activity or app in XML and programmatically.
  • How to use Drawable resources.

What you'll do

  • Create a new app and add Button and TextView elements to the layout.
  • Create Drawable resources in XML and use them as backgrounds for your Button elements.
  • Apply styles to UI elements.
  • Add a menu item that changes the theme of the app to a low contrast "night mode."

2. App overview

The Scorekeeper app consists of two sets of Button elements and two TextView elements, which are used to keep track of the score for any point-based game with two players.

The Scorekeeper app in Day Mode

The Scorekeeper app in Night Mode showing the Day Mode choice in the menu

3. Task 1: Create the Scorekeeper app

In this section, you create your Android Studio project, modify the layout, and add the android:onClick attribute to its Button elements.

1.1 Create the project

  1. Start Android Studio and create a new Android Studio project with the name Scorekeeper.
  2. Accept the default minimum SDK and choose the Empty Activity template.
  3. Accept the default Activity name (MainActivity), and make sure the Generate Layout file and Backwards Compatibility (AppCompat) options are checked.
  4. Click Finish.

1.2 Create the layout for MainActivity

The first step is to change the layout from ConstraintLayout to LinearLayout:

  1. Open activity_main.xml and click the Text tab to see the XML code. At the top, or root, of the View hierarchy is the ConstraintLayout ViewGroup:
android.support.constraint.ConstraintLayout
  1. Change this ViewGroup to LinearLayout. The second line of code now looks something like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  1. Delete the following line of XML code, which is related to ConstraintLayout:
xmlns:app="http://schemas.android.com/apk/res-auto"

The block of XML code at the top should now look like this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.android.scorekeeper.MainActivity">
  1. Add the following attributes (without removing the existing attributes):

Attribute

Value

android:orientation

"vertical"

android:padding

"16dp"

1.3 Create the score containers

The layout for this app includes score containers defined by two RelativeLayout view group elements—one for each team. The following diagram shows the layout in its simplest form.

Box diagram of Scorekeeper layout

  1. Inside the LinearLayout, add two RelativeLayout elements with the following attributes:

RelativeLayout attribute

Value

android:layout_width

"match_parent"

android:layout_height

"0dp"

android:layout_weight

"1"

You may be surprised to see that the layout_height attribute is set to 0dp. This is because we are using the layout_weight attribute to determine how much space these RelativeLayout elements take up in the parent LinearLayout.

  1. Add two ImageButton elements to each RelativeLayout: one for decreasing the score, and one for increasing the score. Use these attributes:

ImageButton attribute

Value

android:id

"@+id/decreaseTeam1"

android:layout_width

"wrap_content"

android:layout_height

"wrap_content"

android:layout_alignParentLeft

"true"

android:layout_alignParentStart

"true"

android:layout_centerVertical

"true"

Use increaseTeam1 as the android:id for the second ImageButton that increases the score. Use decreaseTeam2 and increaseTeam2 for the third and fourth ImageButton elements. Use alignParentRight and alignParentEnd instead of alignParentLeft and alignParentStart for the second and fourth ImageButton elements.

  1. Add a TextView to each RelativeLayout between the ImageButton elements for displaying the score. Use these attributes:

TextView attribute

Value

android:id

"@+id/score_1"

android:layout_width

"wrap_content"

android:layout_height

"wrap_content"

android:layout_centerHorizontal

"true"

android:layout_centerVertical

"true"

android:text

"0"

Use score_2 as the android:id for the second TextView between the ImageButton elements.

  1. Add another TextView to each RelativeLayout above the score to represent the Team Names. Use these attributes:

TextView attribute

Value

android:layout_width

"wrap_content"

android:layout_height

"wrap_content"

android:layout_alignParentTop

"true"

android:layout_centerHorizontal

"true"

android:text

"Team 1"

Use "Team 2" as the android:text for the second TextView.

1.4 Add vector assets

You will use material icons in the Vector Asset Studio for the scoring ImageButton elements.

  1. Select File > New > Vector Asset to open the Vector Asset Studio.
  2. Click the icon to open a list of material icon files. Select the Content category.
  3. Choose the add icon and click OK.
  4. Rename the resource file ic_plus and check the Override checkbox next to size options.
  5. Change the size of the icon to 40dp x 40dp.
  6. Click Next and then Finish.
  7. Repeat this process to add the remove icon and name the file ic_minus.

You can now add the icons and content descriptions for the ImageButton elements. The content description provides a useful and descriptive label that explains the meaning and purpose of the ImageButton to users who may require Android accessibility features such as the Google TalkBack screen reader.

  1. Add the following attributes to the ImageButton elements on the left side of the layout:
android:src="@drawable/ic_minus"
android:contentDescription="Minus Button"
  1. Add the following attributes to the ImageButton elements on the right side of the layout:
android:src="@drawable/ic_plus"
android:contentDescription="Plus Button"
  1. Extract all of your string resources. This process removes all of your strings from the Java code and puts them in the strings.xml file. This allows for your app to be easily localized into different languages.

Solution code for the layout

The following shows the layout for the Scorekeeper app, and the XML code for the layout.

Scorekeeper layout

XML code:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context="com.example.android.scorekeeper.MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            android:text="@string/team_1" />

        <ImageButton
            android:id="@+id/decreaseTeam1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true"
            android:src="@drawable/ic_minus"
            android:contentDescription=
                             "@string/minus_button_description" />

        <TextView
            android:id="@+id/score_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="@string/initial_count" />

        <ImageButton
            android:id="@+id/increaseTeam1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:src="@drawable/ic_plus"
            android:contentDescription=
                              "@string/plus_button_description" />
    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            android:text="@string/team_2" />

        <ImageButton
            android:id="@+id/decreaseTeam2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true"
            android:src="@drawable/ic_minus"
            android:contentDescription=
                               "@string/minus_button_description" />

        <TextView
            android:id="@+id/score_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="@string/initial_count" />

        <ImageButton
            android:id="@+id/increaseTeam2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:src="@drawable/ic_plus"
            android:contentDescription=
                                "@string/plus_button_description" />
    </RelativeLayout>
</LinearLayout>

1.5 Initialize the TextView elements and score count variables

To keep track of the scores, your app needs two things:

  • Integer variables to keep track of the scores.
  • A reference to each score TextView element in MainActivity so that you can update the scores.

Follow these steps:

  1. Create two integer member variables representing the score of each team.
// Member variables for holding the score.
private int mScore1;
private int mScore2;
  1. Create two TextView member variables to hold references to the TextView elements.
// Member variables for holding the score.
private TextView mScoreText1;
private TextView mScoreText2;
  1. In the onCreate() method of MainActivity, find your score TextView elements by id and assign them to the member variables.
@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  // Find the TextViews by ID.
  mScoreText1 = (TextView)findViewById(R.id.score_1);
  mScoreText2 = (TextView)findViewById(R.id.score_2);
}

1.6 Implement the click handlers for the ImageButton elements

You will add the android:onClick attribute to each ImageButton, and create two click handler methods in MainActivity. The left ImageButton elements should decrement the score TextView, while the right ones should increment it.

  1. Open activity_main.xml if it is not already open, and add the following android:onClick attribute to the first ImageButton on the left side of the layout:
android:onClick="decreaseScore"
  1. The decreaseScore method name is underlined in red. Click the red bulb icon in the left margin, or click the method name and press Option-Return, and choose Create ‘decreaseScore(view)' in ‘MainActivity'. Android Studio creates the decreaseScore() method stub in MainActivity.
  2. Add the above android:onClick attribute to the second ImageButton on the left side of the layout. This time the method name is valid, because the stub has already been created.
  3. Add the following android:onClick attribute to each ImageButton on the right side of the layout:
android:onClick="increaseScore"
  1. The increaseScore method name is underlined in red. Click the red bulb icon in the left margin, or click the method name and press Option-Return, and choose Create ‘increaseScore(view)' in ‘MainActivity'. Android Studio creates the increaseScore() method stub in MainActivity.
  2. Add code to the stub methods to decrease and increase the score as shown below. Both methods use view.getID() to get the ID of the team's ImageButton that was clicked, so that your code can update the proper team.

Solution code for increaseScore() and decreaseScore()

/**
* Method that handles the onClick of both the decrement buttons
* @param view The button view that was clicked
*/
public void decreaseScore(View view) {
  // Get the ID of the button that was clicked.
  int viewID = view.getId();
  switch (viewID) {
    // If it was on Team 1
    case R.id.decreaseTeam1:
      //Decrement the score and update the TextView
      mScore1--;
      mScoreText1.setText(String.valueOf(mScore1));
      break;
    // If it was Team 2
    case R.id.decreaseTeam2:
      // Decrement the score and update the TextView
      mScore2--;
      mScoreText2.setText(String.valueOf(mScore2));
  }
}

/**
* Method that handles the onClick of both the increment buttons
* @param view The button view that was clicked
*/
public void increaseScore(View view) {
  // Get the ID of the button that was clicked
  int viewID = view.getId();
  switch (viewID) {
    // If it was on Team 1
    case R.id.increaseTeam1:
      // Increment the score and update the TextView
      mScore1++;
      mScoreText1.setText(String.valueOf(mScore1));
      break;
    // If it was Team 2
    case R.id.increaseTeam2:
      // Increment the score and update the TextView
      mScore2++;
      mScoreText2.setText(String.valueOf(mScore2));
  }
}

4. Task 2: Create a drawable resource

You now have a functioning Scorekeeper app! However, the layout is dull and does not communicate the function of the ImageButton elements. In order to make it clearer, the standard grey background of the buttons can be changed.

In Android, graphics are often handled by a resource called a Drawable. In the following step you learn how to create a certain type of Drawable called a ShapeDrawable, and apply it to your ImageButton elements as a background.

For more information on Drawables, see Drawable resources.

2.1 Create a ShapeDrawable

A ShapeDrawable is a primitive geometric shape defined in an XML file by a number of attributes including color, shape, padding and more. It defines a vector graphic, which can scale up and down without losing any definition.

  1. In the Project > Android pane, right-click (or Control-click) on the drawable folder in the res directory.
  2. Choose New > Drawable resource file.
  3. Name the file button_background and click OK. (Don't change the Source set or Directory name, and don't add qualifiers). Android Studio creates the file button_background.xml in the drawable folder.
  4. Open button_background.xml, click the Text tab to edit the XML code, and remove all of the code except:
<?xml version="1.0" encoding="utf-8"?>
  1. Add the following code which creates an oval shape with an outline:
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
        <stroke 
            android:width="2dp"
            android:color="@color/colorPrimary"/>
</shape>

2.2 Apply the ShapeDrawable as a background

  1. Open activity_main.xml.
  2. Add the Drawable as the background for all four ImageButton elements:
android:background="@drawable/button_background"
  1. Click the Preview tab in the layout editor to see that the background automatically scales to fit the size of the ImageButton.
  2. To render each ImageButton properly on all devices, you change the android:layout_height and android:layout_width attributes for each ImageButton to 70dp, which is a good size on most devices. Change the first attribute for the first ImageButton as follows:
android:layout_width="70dp"
  1. Click once on "70dp", press Alt-Enter in Windows or Option-Enter in macOS, and choose Extract dimension resource from the popup menu.
  2. Enter button_size for the Resource name.
  3. Click OK. This creates a dimension resource in the dimens.xml file (in the values folder), and the dimension in your code is replaced with a reference to the resource:

@dimen/button_size

  1. Change the android:layout_height and android:layout_width attributes for each ImageButton element using the new dimension resource:
android:layout_width="@dimen/button_size"
android:layout_height="@dimen/button_size"
  1. Click the Preview tab in the layout editor to see the layout. The ShapeDrawable is now used for the ImageButton elements.

Scorekeeper layout with the ShapeDrawable

5. Task 3: Style your View elements

As you continue to add View elements and attributes to your layout, your code will start to become large and repetitive, especially when you apply the same attributes to many similar elements. A style can specify common properties such as padding, font color, font size, and background color. Attributes that are layout-oriented such as height, width and relative location should remain in the layout resource file.

In the following step, you learn how to create styles and apply them to multiple View elements and layouts, allowing common attributes to be updated simultaneously from one location.

3.1 Create Button styles

In Android, styles can inherit properties from other styles. You can declare a parent for your style using an optional parent parameter and has the following properties:

  • Any style attributes defined by the parent style are automatically included in the child style.
  • A style attribute defined in both the parent and child style uses the child style's definition (the child overrides the parent).
  • A child style can include additional attributes that the parent does not define.

For example, all four ImageButton elements in the Scorekeeper app share a common background Drawable but with different icons for plus (increase the score) and minus (decrease the score). Furthermore, the two plus ImageButton elements share the same icon, as do the two minus ImageButton elements. You can therefore create three styles:

  1. A style for all of the ImageButton elements, which includes the default properties of an ImageButton and also the Drawable background.
  2. A style for the minus ImageButton elements. This style inherits the attributes of the ImageButton style and includes the minus icon.
  3. A style for the plus ImageButton elements. This style inherits from the ImageButton style and includes the plus icon.

These styles are represented in the figure below.

Diagram of three styles

Do the following:

  1. Expand res > values in the Project > Android pane, and open the styles.xml file.

This is where all of your style code will be located. The "AppTheme" style is always automatically added, and you can see that it extends from Theme.AppCompat.Light.DarkActionBar.

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

Note the parent attribute, which is how you specify your parent style using XML.

The name attribute, in this case AppTheme, defines the name of the style. The parent attribute, in this case Theme.AppCompat.Light.DarkActionBar, declares the parent style attributes that AppTheme inherits. In this case it is the Android default theme, with a light background and a dark action bar.

A theme is a style that's applied to an entire Activity or app instead of a single View. Using a theme creates a consistent style throughout an entire Activity or app—for example, a consistent look and feel for the app bar in every part of your app.

  1. In between the <resources> tags, add a new style with the following attributes to create a common style for all buttons:
<style name="ScoreButtons" parent="Widget.AppCompat.Button">
    <item name="android:background">@drawable/button_background</item>
</style>

The above snippet sets the parent style to Widget.AppCompat.Button to retain the default attributes of a Button. It also adds an attribute that changes the background of the Drawable to the one you created in the previous task.

  1. Create the style for the plus buttons by extending the ScoreButtons style:
<style name="PlusButtons" parent="ScoreButtons">
    <item name="android:src">@drawable/ic_plus</item>
    <item name=
        "android:contentDescription">@string/plus_button_description</item>
</style>

The contentDescription attribute is for visually impaired users. It acts as a label that certain accessibility devices use to read out loud to provide some context about the meaning of the UI element.

  1. Create the style for the minus buttons:
<style name="MinusButtons" parent="ScoreButtons">
    <item name="android:src">@drawable/ic_minus</item>
    <item name=
        "android:contentDescription">@string/minus_button_description</item>
</style>
  1. You can now use these styles to replace specific style attributes of the ImageButton elements. Open the activity_main.xml layout file for MainActivity, and replace the following attributes for both of the minus ImageButton elements:

Delete these attributes

Add this attribute

android:src

style="@style/MinusButtons

android:contentDescription

android:background

  1. Replace the following attributes for both of the plus ImageButton elements:

Delete these attributes

Add this attribute

android:src

style="@style/PlusButtons

android:contentDescription

android:background

3.2 Create TextView styles

The team name and score display TextView elements can also be styled since they have common colors and fonts. Do the following:

  1. Add the following attribute to all TextView elements:
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
  1. Right-click (or Control-click) anywhere in the first score TextView attributes and choose Refactor > Extract > Style...
  2. Name the style ScoreText and check the textAppearance box (the attribute you just added) as well as the Launch ‘Use Styles Where Possible' refactoring after the style is extracted checkbox. This will scan the layout file for views with the same attributes and apply the style for you. Do not extract the attributes that are related to the layout—click the other checkboxes to turn them off.
  3. Choose OK.
  4. Make sure the scope is set to the activity_main.xml layout file and click OK.
  5. A pane at the bottom of Android Studio will open if the same style is found in other views. Select Do Refactor to apply the new style to the views with the same attributes.
  6. Run your app. There should be no change except that all of your styling code is now in your resources file and your layout file is shorter.

Scorekeeper app with styled buttons and text

Layout and style solution code

The following are code snippets for the layout and styles.

styles.xml:

<resources>
   <!-- Base application theme. -->
   <style name="AppTheme" 
                      parent="Theme.AppCompat.Light.DarkActionBar">
       <!-- Customize your theme here. -->
       <item name="colorPrimary">@color/colorPrimary</item>
       <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
       <item name="colorAccent">@color/colorAccent</item>
   </style>

   <style name="ScoreButtons" parent="AppTheme">
       <item 
         name="android:background">@drawable/button_background</item>
   </style>

   <style name="PlusButtons" parent="ScoreButtons">
       <item name="android:src">@drawable/ic_plus</item>
       <item name=
         "android:contentDescription">@string/plus_button_description</item>
   </style>

   <style name="MinusButtons" parent="ScoreButtons">
       <item name="android:src">@drawable/ic_minus</item>
       <item name=
        "android:contentDescription">@string/minus_button_description</item>
   </style>

   <style name="ScoreText">
       <item name= "android:textAppearance">@style/TextAppearance.AppCompat.Headline</item>
   </style>
</resources>

activity_main.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context="com.example.android.scorekeeper.MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            android:text="@string/team_1"
            style="@style/ScoreText" />

        <ImageButton
            android:id="@+id/decreaseTeam1"
            android:layout_width="@dimen/button_size"
            android:layout_height="@dimen/button_size"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true"
            style="@style/MinusButtons"
            android:onClick="decreaseScore"/>

        <TextView
            android:id="@+id/score_1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="@string/initial_count"
            style="@style/ScoreText" />

        <ImageButton
            android:id="@+id/increaseTeam1"
            android:layout_width="@dimen/button_size"
            android:layout_height="@dimen/button_size"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            style="@style/PlusButtons"
            android:onClick="increaseScore"/>

    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            android:text="@string/team_2"
            style="@style/ScoreText" />

        <ImageButton
            android:id="@+id/decreaseTeam2"
            android:layout_width="@dimen/button_size"
            android:layout_height="@dimen/button_size"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true"
            style="@style/MinusButtons"
            android:onClick="decreaseScore"/>

        <TextView
            android:id="@+id/score_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="@string/initial_count"
            style="@style/ScoreText" />

        <ImageButton
            android:id="@+id/increaseTeam2"
            android:layout_width="@dimen/button_size"
            android:layout_height="@dimen/button_size"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            style="@style/PlusButtons"
            android:onClick="increaseScore"/>
    </RelativeLayout>
</LinearLayout>

3.3 Updating the styles

The power of using styles becomes apparent when you want to make changes to several elements of the same style. You can make the text bigger, bolder and brighter, and change the icons to the color of the button backgrounds.

  1. Open the styles.xml file, and add or modify the following attributes for the styles:

Style

Item

ScoreButtons


@color/colorPrimary

ScoreText


@style/TextAppearance.AppCompat.Display3

The colorPrimary value is automatically generated by Android Studio when you create the project. You can find it in the Project > Android pane in the colors.xml file inside the values folder. The TextAppearance.AppCompat.Display3 attribute is a predefined text style supplied by Android.

  1. Create a new style called TeamText with the textAppearance attribute set as follows:
<style name="TeamText">
   <item name=
    "android:textAppearance">@style/TextAppearance.AppCompat.Display1
   </item>
</style>
  1. In activity_main.xml, change the style attribute of the TextView team name elements to the newly created TeamText style.
style="@style/TeamText" 
  1. Run your app. With only these adjustments to the style.xml file, all of the views updated to reflect the changes.

Scorekeeper app with team names in a display text style

6. Task 4: Themes and final touches

You've seen that View elements with similar characteristics can be styled together in the styles.xml file. But what if you want to define styles for an entire activity, or the entire app? It's possible to accomplish this by using themes. To set a theme for each activity, you need to modify the AndroidManifest.xml file.

In this task, you add the "night mode" theme to your app, which will allow the users to use a low contrast version of your app that is easier on the eyes at night time, as well as make a few polishing touches to the UI.

4.1 Explore themes

  1. Open the AndroidManifest.xml file, find the <application> tag, and change the android:theme attribute to:
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
     This is a predefined theme that removes the action bar from your activity.
  1. Run your app. The toolbar disappears!
  2. Change the theme of the app back to AppTheme, which is a child of the Theme.Appcompat.Light.DarkActionBar theme as can be seen in styles.xml.

To apply a theme to an activity instead of the entire application, place the theme attribute in the <Activity> tag instead of the <application> tag. For more information on themes and styles, see the Style and Theme Guide.

4.2 Add theme button to the menu

One use for setting a theme for your app is to provide an alternate visual experience for browsing at night. In such conditions, it is often better to have a low contrast, dark layout. The Android framework provides a theme that is designed exactly for this: The DayNight theme.

This theme has built-in options that allow you to control the colors in your app programmatically: either by setting it to change automatically by time, or by user command.

In this step you add an options menu item that will toggle the app between the regular theme and a "night-mode" theme.

  1. Open strings.xml and create two string resources for this options menu item:
<string name="night_mode">Night Mode</string>
<string name="day_mode">Day Mode</string>
  1. Right-click (or Control-click) on the res directory in the Project > Android pane, and choose New > Android resource file.
  2. Name the file main_menu, change the Resource Type to Menu, and click OK. The layout editor appears for the main_menu.xml file.
  3. Click the Text tab of the layout editor to show the XML code.
  4. Add a menu item with the following attributes:
<item 
    android:id="@+id/night_mode"
    android:title="@string/night_mode"/>
  1. Open MainActivity, press Ctrl-O to open the Override Method menu, and select the onCreateOptionsMenu(menu:Menu):boolean method located under the android.app.Activity category.
  2. Click OK. Android Studio creates the onCreateOptionsMenu() method stub with return super.onCreateOptionsMenu(menu) as its only statement.
  3. In onCreateOptionsMenu() right before the return.super statement, add code to inflate the menu:
getMenuInflater().inflate(R.menu.main_menu, menu);

4.3 Change the theme from the menu

The DayNight theme uses the AppCompatDelegate class to set the night mode options in your Activity. To learn more about this theme, visit this blog post.

  1. In your styles.xml file, modify the parent of AppTheme to "Theme.AppCompat.DayNight.DarkActionBar".
  2. Override the onOptionsItemSelected() method in MainActivity, and add code to check which menu item was clicked:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
  // Check if the correct item was clicked
  if (item.getItemId() == R.id.night_mode) {
    // TODO: Get the night mode state of the app.
  }
  return true;
}
  1. Replace the TODO: comment in the above snippet with code that checks if the night mode is enabled. If enabled, the code changes night mode to the disabled state; otherwise, the code enables night mode:
if (item.getItemId() == R.id.night_mode) {
  // Get the night mode state of the app.
  int nightMode = AppCompatDelegate.getDefaultNightMode();
  //Set the theme mode for the restarted activity.
  if (nightMode == AppCompatDelegate.MODE_NIGHT_YES) {
    AppCompatDelegate.setDefaultNightMode
                          (AppCompatDelegate.MODE_NIGHT_NO);
  } else {
    AppCompatDelegate.setDefaultNightMode
                         (AppCompatDelegate.MODE_NIGHT_YES);
  }
  // Recreate the activity for the theme change to take effect.
  recreate();
}

In response to a click on the menu item, the code verifies the current night mode setting by calling AppCompatDelegate.getDefaultNightMode().

The theme can only change while the activity is being created, so the code calls recreate() for the theme change to take effect.

  1. Run your app. The Night Mode menu item should now toggle the theme of your Activity.
  2. You may notice that the label for your menu item always reads Night Mode, which may be confusing to your user if the app is already in the dark theme.
  3. Replace the return super.onCreateOptionsMenu(menu) statement in the onCreateOptionsMenu() method with the following code:
// Change the label of the menu based on the state of the app.
int nightMode = AppCompatDelegate.getDefaultNightMode();
if (nightMode == AppCompatDelegate.MODE_NIGHT_YES) {
  menu.findItem(R.id.night_mode).setTitle(R.string.day_mode);
} else {
  menu.findItem(R.id.night_mode).setTitle(R.string.night_mode);
}
return true;
  1. Run your app. The menu item label Night Mode, after the user taps it, now changes to Day Mode (along with the theme).

Scorekeeper app set to Night Mode with the Day Mode option

4.4 SaveInstanceState

You learned in previous lessons that you must be prepared for your Activity to be destroyed and recreated at unexpected times, for example when your screen is rotated. In this app, the TextView elements containing the scores are reset to the initial value of 0 when the device is rotated. To fix this, do the following:

  1. Open MainActivity and add tags under the member variables, which will be used as the keys in onSaveInstanceState():
static final String STATE_SCORE_1 = "Team 1 Score";
static final String STATE_SCORE_2 = "Team 2 Score";
  1. At the end of MainActivity, override the onSaveInstanceState() method to preserve the values of the two score TextView elements:
@Override
protected void onSaveInstanceState(Bundle outState) {
  // Save the scores.
  outState.putInt(STATE_SCORE_1, mScore1);
  outState.putInt(STATE_SCORE_2, mScore2);
  super.onSaveInstanceState(outState);
}
  1. At the end of the onCreate() method, add code to check if there is a savedInstanceState. If there is, restore the scores to the TextView elements:
if (savedInstanceState != null) {
   mScore1 = savedInstanceState.getInt(STATE_SCORE_1);
   mScore2 = savedInstanceState.getInt(STATE_SCORE_2);

   // Set the score text views.
   mScoreText1.setText(String.valueOf(mScore1));
   mScoreText2.setText(String.valueOf(mScore2));
}
  1. Run the app, and tap the plus ImageButton to increase the score. Switch the device or emulator to horizontal orientation to see that the score is retained.

Scorekeeper app switched to horizontal orientation still retains the score value.

That's it! Congratulations, you now have a styled Scorekeeper app that continues to work if the user changes the device to horizontal or vertical orientation.

7. Solution code

Android Studio project: Scorekeeper

8. Coding challenge

Challenge: Right now, your buttons do not behave intuitively because they do not change their appearance when they are pressed. Android has another type of Drawable called StateListDrawable which allows for a different graphic to be used depending on the state of the object.

For this challenge problem, create a Drawable resource that changes the background of the ImageButton to the same color as the border when the state of the ImageButton is "pressed". You should also set the color of the text inside the ImageButton elements to a selector that makes it white when the button is "pressed".

9. Summary

  • Drawable elements enhance the look of an app's UI.
  • A ShapeDrawable is a primitive geometric shape defined in an XML file. The attributes that define a ShapeDrawable include color, shape, padding, and more.
  • The Android platform supplies a large collection of styles and themes.
  • Using styles can reduce the amount of code needed for your UI components.
  • A style can specify common properties such as height, padding, font color, font size, and background color.
  • A style should not include layout-related information.
  • A style can be applied to a View, Activity, or the entire app. A style applied to an Activity or the entire app must be defined in the AndroidManifest.xml file.
  • To inherit a style, a new style identifies a parent attribute in the XML.
  • When you apply a style to a collection of View elements in an activity or in your entire app, that is known as a theme.
  • To apply a theme, you use the android:theme attribute.

10. Related concepts

The related concept documentation is in 5.1: Drawables, styles, and themes.

11. Learn more

Android Studio documentation:

Android developer documentation:

Material Design:

Android Developers Blog: Android Design Support Library

Other:

12. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

Create an app that displays an ImageView and plus and minus buttons, as shown below. The ImageView contains a level list drawable that is a battery level indicator. Tapping the plus or minus button changes the level of the indicator.

Use the battery icons from the Vector Asset Studio to represent 7 different values for the battery level.

The app should have the following properties:

  • The plus button increments the level, causing the battery indicator to appear more full.
  • The minus button decrements the level, causing the indicator to empty one level.

Example battery indicator app

Answer these questions

Question 1

Which type of Drawable do you use to create a Button with a background that stretches properly to accommodate the text or image inside the Button so that it looks correct for different screen sizes and orientations? Choose one:

  • LevelListDrawable
  • TransitionDrawable
  • StateListDrawable
  • NinePatchDrawable

Question 2

Which type of Drawable do you use to create a Button that shows one background when it is pressed and a different background when it is hovered over? Choose one:

  • LevelListDrawable
  • TransitionDrawable
  • StateListDrawable
  • NinePatchDrawable

Question 3

Suppose you want to create an app that has a white background, dark text, and a dark action bar. Which base style does your application style inherit from? Choose one:

  • Theme.AppCompat.Light
  • Theme.AppCompat.Dark.NoActionBar
  • Theme.AppCompat.Light.DarkActionBar
  • Theme.AppCompat.NoActionBar
  • Theme.NoActionBar

Submit your app for grading

Guidance for graders

Check that the app has the following features:

  • The buttons increment a count variable. The count variable sets the level on the ImageView, using the setImageLevel() method.
  • The levels in the LevelListDrawable go from 0 to 6.
  • Before incrementing or decrementing the image level, the onClick() methods check to see whether the count variable is within the range of the LevelListDrawable (0 to 6). This way, the user can't set a level that doesn't exist.

13. Next codelab

To find the next practical codelab in the Android Developer Fundamentals (V2) course, see Codelabs for Android Developer Fundamentals (V2).

For an overview of the course, including links to the concept chapters, apps, and slides, see Android Developer Fundamentals (Version 2).