1. Before you begin
As you've learned in earlier codelabs, Material is a design system created by Google with guidelines, components, and tools that support the best practices of user interface design. In this codelab, you will update the tip calculator app (from previous codelabs) to have a more polished user experience, as seen in the final screenshot below. You'll also test the app in some additional scenarios to ensure the user experience is as smooth as possible.
Prerequisites
- Familiar with common UI widgets such as
TextView
,ImageView
,Button
,EditText
,RadioButton
,RadioGroup
, andSwitch
- Familiar with
ConstraintLayout
and positioning child views by setting constraints - Comfortable with modifying XML layouts
- Aware of the difference between bitmap images and vector drawables
- Can set theme attributes in a theme
- Able to turn on Dark theme on a device
- Have previously modified the app's
build.gradle
file for project dependencies
What you'll learn
- How to use Material Design Components in your app
- How to use Material icons in your app by importing them from Image Asset Studio
- How to create and apply new styles
- How to set other theme attributes aside from color
What you'll build
- A polished tip calculator app that follows recommended UI best practices
What you need
2. Starter app overview
Through prior codelabs, you built up the Tip Time app which is a tip calculator app with options to customize the tip. Your app UI currently looks like the below screenshot. The functionality works, but it looks more like a prototype. The fields aren't quite lined up visually. There's definitely room for improvement in terms of more consistent styling and spacing, as well as the use of Material Design Components.
3. Material Components
Material Components are common UI widgets that make it easier to implement Material styling in your app. The documentation provides information for how to use and customize the Material Design Components. There are general Material Design guidelines for each component, and Android platform-specific guidance for the components that are available on Android. The labeled diagrams give you enough information to recreate a component if it happens to not exist on your chosen platform.
By using Material Components, your app will operate in a more consistent way alongside other apps on the user's device. That way the UI patterns learned in one app can be carried over to the next one. Hence users will be able to learn how to use your app much faster. It's recommended to use Material Components whenever possible (as opposed to the non-Material widgets). They are also more flexible and customizable, as you will learn in this next task.
The Material Design Components (MDC) library needs to be included as a dependency in your project. This line should already be present in your project by default. In your app's build.gradle
file, make sure this dependency is included with the latest version of the library. For more details, see the Get started page on the Material site.
app/build.gradle
dependencies {
...
implementation 'com.google.android.material:material:<version>'
}
Text Fields
In your tip calculator app, at the top of the layout, you currently have an EditText
field for the cost of service. This EditText
field works, but it doesn't follow the recent Material Design guidelines on how text fields should look and behave.
For any new component that you want to use, start by learning about it on the Material site. From the guide on Text Fields, there are two types of text fields:
Filled text field
Outlined text field
To create a text field as shown above, use a TextInputLayout
with an enclosed TextInputEditText
from the MDC library. The Material text field can be easily customized to:
- Display input text or a label that's always visible
- Display an icon in the text field
- Display helper or error messages
In the first task of this codelab, you'll be replacing the cost of service EditText
with a Material text field (which is composed of a TextInputLayout
and TextInputEditText
).
- With the Tip Time app open in Android Studio, go to the
activity_main.xml
layout file. It should contain aConstraintLayout
with the tip calculator layout. - To see an example of what the XML looks like for a Material text field, go back to the Android guidance for Text fields. You should see snippets like this one:
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
- After seeing this example, insert a Material text field as the first child of the
ConstraintLayout
(before theEditText
field). You'll get rid of theEditText
field in a later step.
You can type this into Android Studio and use autocomplete to make it easier. Or you can copy the example XML from the documentation page and paste it into your layout like this. Notice how the TextInputLayout
has a child view, the TextInputEditText
. Remember that ellipsis (...) are used to abbreviate snippets, so that you can focus on the lines of XML that have actually changed.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/cost_of_service" ... />
...
You are expected to see errors on the TextInputLayout
element. You haven't properly constrained this view yet in the parent ConstraintLayout
. Also the string resource isn't recognized. You'll fix these errors in the coming steps.
- Add vertical and horizontal constraints onto the text field, to properly position it within the parent
ConstraintLayout
. Since you haven't deleted theEditText
yet, cut and paste the following attributes from theEditText
and place them onto theTextInputLayout
: the constraints, resource IDcost_of_service
, layout width of160dp
, layout height ofwrap_content
, and the hint text@string/cost_of_service
.
...
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:hint="@string/cost_of_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>
...
You may see an error that the cost_of_service
ID is the same as the resource ID of the EditText
, but you can ignore this error for now. (EditText
will be removed in a couple steps).
- Next make sure the
TextInputEditText
element has all the appropriate attributes. Cut and paste over the input type from theEditText
onto theTextInputEditText
. Change theTextInputEditText
element resource ID tocost_of_service_edit_text
.
<com.google.android.material.textfield.TextInputLayout ... >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/cost_of_service_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
Width of match_parent
and height of wrap_content
is fine as-is. When setting a width of match_parent
, the TextInputEditText
will have the same width as its parent TextInputLayout
which is 160dp
.
- Now that you have copied over all relevant information from the
EditText
, go ahead and delete theEditText
from the layout. - In the Design view of your layout, you should see this preview. The cost of service field now looks like a Material text field.
- You can't run the app yet because there's an error in your
MainActivity.kt
file in thecalculateTip()
method. Recall from an earlier codelab that with view binding enabled for your project, Android creates properties in a binding object based on the resource ID name. The field we retrieved the cost of service from has changed in the XML layout, so the Kotlin code needs to be updated accordingly.
You will now be retrieving the user input from the TextInputEditText
element with resource ID cost_of_service_edit_text
. In the MainActivity
, use binding.costOfServiceEditText
to access the text string stored within it. The rest of the calculateTip()
method can stay the same.
private fun calculateTip() {
// Get the decimal value from the cost of service text field
val stringInTextField = binding.costOfServiceEditText.text.toString()
val cost = stringInTextField.toDoubleOrNull()
...
}
- Great work! Now run the app and test that it still works. Notice how the "Cost of Service" label now appears above your input as you type. The tip should still calculate as expected.
Switches
In the Material Design guidelines, there's also guidance on switches. A switch is a widget where you can toggle a setting on or off.
- Check out the Android guidance for Material switches. You will learn about the
SwitchMaterial
widget (from the MDC library), which provides Material styling for switches. If you keep scrolling through the guide, you will see some examples of XML. - To use
SwitchMaterial
, you must explicitly specifySwitchMaterial
in your layout and use the fully qualified path name.
In the activity_main.xml
layout, change the XML tag from Switch
to com.google.android.material.switchmaterial.SwitchMaterial
.
...
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
android:layout_width="0dp"
android:layout_height="wrap_content" ... />
...
- Run the app to verify it still compiles. There happens to be no visible change in the app. However, an advantage to using
SwitchMaterial
from the MDC library (instead ofSwitch
from the Android platform), is that when the library's implementation forSwitchMaterial
gets updated (e.g. the Material Design guidelines change), then you will get the updated widget for free without any changes required on your part. This helps future-proof your app.
At this point, you've seen two examples of how your UI can benefit from using out-of-the-box Material Design Components and how that brings your app closer in line with Material guidelines. Remember that you can always explore other Material Design Components provided on Android at this site.
4. Icons
Icons are symbols that can help users understand a user interface by visually communicating the intended function. They often take inspiration from objects in the physical world that a user is expected to have experienced. Icon design often reduces the level of detail to the minimum required to be familiar to a user. For example, a pencil in the physical world is used for writing so its icon counterpart usually indicates creating, adding, or editing an item.
|
Sometimes icons are linked to obsolete physical world objects as is the case with the floppy disk icon. This icon is the ubiquitous indication of saving a file or database record; however, while floppy disks were popularized in the 1970s, they ceased to be common after 2000. But its continual use today speaks to how a strong visual can transcend the lifetime of its physical form.
|
Representing icons in your app
For icons in your app, instead of providing different versions of a bitmap image for different screen densities, the recommended practice is to use vector drawables. Vector drawables are represented as XML files that store the instructions on how to create an image rather than saving the actual pixels that make up that image. Vector drawables can be scaled up or down without any loss of visual quality or increase in file size.
Provided Icons
Material Design provides a number of icons arranged in common categories for most of your needs. See list of icons.
These icons can also be drawn using one of five themes (Filled, Outlined, Rounded, Two-Tone, and Sharp) and can be tinted with colors.
Filled | Outlined | Rounded | Two-Tone | Sharp |
Adding Icons
In this task, you'll add three vector drawable icons to the app:
- Icon next to the cost of service text field
- Icon next to the service question
- Icon next to the round up tip prompt
Below is a screenshot of the final version of the app. After you add the icons, you'll adjust the layout to accommodate the placement of these icons. Notice how the fields and calculate button get shifted over to the right, with the addition of the icons.
Add vector drawable assets
You can create these icons as vector drawables directly from Asset Studio in Android Studio.
- Open the Resource Manager tab located on the left of the application window.
- Click the + icon and select Vector Asset.
- For the Asset Type, make sure the radio button labeled Clip Art is selected.
- Click the button next to Clip Art: to select a different clip art image. In the prompt that appears, type "call made" into the window that appears. You'll be using this arrow icon for the round up tip option. Select it and click OK.
- Rename the icon to ic_round_up. (It's recommended to use the prefix ic_ when naming your icon files.) You can leave the Size as 24 dp x 24 dp and Color as black 000000.
- Click Next.
- Accept the default directory location and click Finish.
- Repeat steps 2 - 7 for the other two icons:
- Service question icon: Search for "room service" icon, save it as
ic_service
. - Cost of service icon: Search for "store" icon, save it as
ic_store
.
- Once you're done, the Resource Manager will look like the below screenshot. You will also have these three vector drawables (
ic_round_up
,ic_service
, andic_store
) listed in yourres/drawable
folder.
Support older Android versions
You just added vector drawables to your app, but it's important to note that support for vector drawables on the Android platform wasn't added until Android 5.0 (API level 21).
Based on how you set up the project, the minimum SDK version for the Tip Time app is API 19. That means the app can run on Android devices that are running Android platform version 19 or higher.
To make your app work on these older versions of Android (known as backwards compatibility), add the vectorDrawables
element to your app's build.gradle
file. This enables you to use vector drawables on versions of the platform less than API 21, versus converting to PNGs when the project is built. See more details here.
app/build.gradle
android {
defaultConfig {
...
vectorDrawables.useSupportLibrary = true
}
...
}
With your project configured properly, you can now move onto adding the icons into the layout.
Insert icons and position elements
You'll be using ImageViews
to display icons in the app. This is how your final UI will appear.
- Open the
activity_main.xml
layout. - First position the store icon next to the cost of service text field. Insert a new
ImageView
as the first child of theConstraintLayout
, before theTextInputLayout
.
<androidx.constraintlayout.widget.ConstraintLayout
...>
<ImageView
android:layout_width=""
android:layout_height=""
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
- Set up the appropriate attributes on the
ImageView
to hold theic_store
icon. Set the ID toicon_cost_of_service
. Set theapp:srcCompat
attribute to the drawable resource@drawable/ic_store
, and you'll see a preview of the icon next to that line of XML.
Also set android:importantForAccessibility="no"
since this image is used for decorative purposes only.
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store" />
It is expected that there will be an error on the ImageView
because the view is not constrained yet. You'll fix that next.
- Position the
icon_cost_of_service
in two steps. First add constraints onto theImageView
(this step), and then update constraints on theTextInputLayout
next to it (step 5). This diagram shows how the constraints should be set up.
On the ImageView
, you want its starting edge to be constrained to the starting edge of the parent view (app:layout_constraintStart_toStartOf="parent"
).
The icon appears centered vertically compared to the text field beside it, so constrain the top of this ImageView
(layout_constraintTop_toTopOf
) to the top of the text field. Constrain the bottom of this ImageView
(layout_constraintBottom_toBottomOf
) to the bottom of the text field. To refer to the text field, use the resource ID @id/cost_of_service
. The default behavior is that when two constraints are applied to a widget in the same dimension (such as a top and bottom constraint), the constraints are applied equally. The result is that the icon gets vertically centered, in relation to the cost of service field.
<ImageView
android:id="@+id/icon_cost_of_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_store"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/cost_of_service"
app:layout_constraintBottom_toBottomOf="@id/cost_of_service" />
The icon and text field are still overlapping in the Design view. That will be fixed in the next step.
- Before the addition of the icon, the text field was positioned at the start of parent. Now it needs to be shifted over to the right. Update the constraints on the
cost_of_service
text field in relation toicon_cost_of_service
.
The starting edge of the TextInputLayout
should be constrained to the ending edge of the ImageView
(@id/icon_cost_of_service
). To add some spacing between the two views, add a start margin of 16dp
on the TextInputLayout
.
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/cost_of_service"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_cost_of_service">
<com.google.android.material.textfield.TextInputEditText ... />
</com.google.android.material.textfield.TextInputLayout>
After all these changes, the icon should be positioned correctly next to the text field.
- Next insert the service bell icon next to the "How was the service?"
TextView
. While you could declare theImageView
anywhere within theConstraintLayout
, your XML layout will be easier to read if you insert the newImageView
in the XML layout after theTextInputLayout
, but before theservice_question
TextView
.
For the new ImageView
, assign it a resource ID of @+id/icon_service_question
. Set the appropriate constraints on the ImageView
and the service question TextView
.
Also add a 16dp
top margin to the service_question TextView
so there's more vertical space between the service question and the cost of service text field above it.
...
<ImageView
android:id="@+id/icon_service_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_service"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/service_question"
app:layout_constraintBottom_toBottomOf="@id/service_question" />
<TextView
android:id="@+id/service_question"
...
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@id/cost_of_service"
app:layout_constraintTop_toBottomOf="@id/cost_of_service"/>
...
- At this point the Design view should look like this. The cost of service field and service question (and their respective icons) look great, but the radio buttons now look out of place. They aren't vertically aligned with the content above it.
- Improve the positioning of the radio buttons by shifting them to the right, underneath the service question. That means updating a
RadioGroup
constraint. Constrain the starting edge of theRadioGroup
to the starting edge of theservice_question
TextView
. All other attributes on theRadioGroup
can remain the same.
...
<RadioGroup
android:id="@+id/tip_options"
...
app:layout_constraintStart_toStartOf="@id/service_question">
...
- Then proceed with adding the
ic_round_up
icon to the layout next to the "Round up tip?" switch. Try doing this on your own and if you get stuck, you can consult the XML below. You can assign the newImageView
a resource ID oficon_round_up
. - In the layout XML, insert a new
ImageView
after theRadioGroup
but before theSwitchMaterial
widget. - Assign the
ImageView
a resource ID oficon_round_up
and set thesrcCompat
to the drawable of the icon@drawable/ic_round_up
. Constrain the start of theImageView
to the start of the parent, and also vertically center the icon relative to theSwitchMaterial
. - Update
SwitchMaterial
to be next to the icon and have a16dp
start margin. This is what the resulting XML should look like foricon_round_up
andround_up_switch
.
...
<ImageView
android:id="@+id/icon_round_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_round_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/round_up_switch"
app:layout_constraintBottom_toBottomOf="@id/round_up_switch" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/round_up_switch"
...
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/icon_round_up" />
...
- The Design view should look like this. All three icons are correctly positioned.
- If you compare this with the final app screenshot, you'll notice the calculate button is also shifted over to align vertically with the cost of service field, service question, radio button options, and round up tip question. Achieve this look by constraining the start of the calculate button to the start of the
round_up_switch
. Also add8dp
of vertical margin between the calculate button and the switch above it.
...
<Button
android:id="@+id/calculate_button"
...
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="@id/round_up_switch" />
...
- Last but not least, position
tip_result
by adding8dp
of top margin to theTextView
.
...
<TextView
android:id="@+id/tip_result"
...
android:layout_marginTop="8dp" />
...
- That was a lot of steps! Great job on working through them step-by-step. It requires a lot of attention to detail to get elements aligned correctly in the layout, but it makes the end result look much better! Run the app and it should look like the below screenshot. By vertically aligning and increasing spacing between elements, they are not as crowded together.
You're not done yet! You may have noticed that the font size and color of the service question and tip amount look different than the text in the radio buttons and switch. Let's make these consistent in the next task by using styles and themes.
5. Styles and Themes
A style is a collection of view attributes values for a single type of widget. For example, a TextView
style can specify font color, font size, and background color, to name a few. By extracting these attributes into a style, you can easily apply the style to multiple views in the layout and maintain it in a single place.
In this task, you will first create styles for the text view, radio button, and switch widgets.
Create Styles
- Create a new file named
styles.xml
in the res > values directory if one doesn't already exist. Create it by right-clicking on the values directory and selecting New > Values Resource File. Call itstyles.xml
. The new file will have the following contents.
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
- Create a new
TextView
style so that text appears consistent throughout the app. Define the style once instyles.xml
and then you can apply it to all theTextViews
in the layout. While you could define a style from scratch, you can extend from an existingTextView
style from the MDC library.
When styling a component, you should generally extend from a parent style of the widget type you are using. This is important for two reasons. First, it makes sure all important default values are set on your component, and secondly, your style will continue to inherit any future changes to that parent style.
You can name your style anything you'd like, but there is a recommended convention. If you inherit from a parent Material style, then name your style in a parallel way by substituting MaterialComponents
with your app's name (TipTime
). This moves your changes into its own namespace which eliminates the possibility for future conflicts when Material Components introduces new styles. Example:
Your style name: Widget.TipTime.TextView
inherits from parent style: Widget.MaterialComponents.TextView
.
Add this to your styles.xml
file in between the resources
opening and closing tags.
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
</style>
- Set up your
TextView
style so that it overrides the following attributes:android:minHeight,android:gravity,
andandroid:textAppearance.
android:minHeight
sets a minimum height of 48dp on the TextView
. The smallest height for any row should be 48dp according to the Material Design guidelines.
You can center the text in the TextView
vertically by setting the android:gravity
attribute. (See screenshot below.) Gravity controls how the content within a view will position itself. Since the actual text content doesn't take up the full 48dp in height, the value center_vertical
centers the text within the TextView
vertically (but does not change its horizontal position). Other possible gravity values include center
, center_horizontal
, top
, and bottom
. Feel free to try out the other gravity values to see the effect on the text.
Set the text appearance attribute value to ?attr/textAppearanceBody1
. TextAppearance is a set of pre-made styles around text size, fonts, and other properties of text. For other possible text appearances that are provided by Material, see this list of type scales.
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
- Apply the
Widget.TipTime.TextView
style to theservice_question
TextView
by adding a style attribute on eachTextView
inactivity_main.xml
.
<TextView
android:id="@+id/service_question"
style="@style/Widget.TipTime.TextView"
... />
Before the style, the TextView
looked like this with small font size and gray font color:
After adding the style, the TextView
looks like this. Now this TextView
looks more consistent with the rest of the layout.
- Apply that same
Widget.TipTime.TextView
style to thetip_result
TextView
.
<TextView
android:id="@+id/tip_result"
style="@style/Widget.TipTime.TextView"
... />
- The same text style should be applied to the text label in the switch. However, you can't set a
TextView
style onto aSwitchMaterial
widget.TextView
styles can only be applied onTextViews
. Hence create a new style for the switch. The attributes are the same in terms ofminHeight
,gravity
, andtextAppearance
. What's different here is the style name and parent because you're now inheriting theSwitch
style from the MDC library. Your name for the style should also mirror the name of the parent style.
Your style name: Widget.TipTime.CompoundButton.Switch
inherits from parent style: Widget.MaterialComponents.CompoundButton.Switch.
<style name="Widget.TipTime.CompoundButton.Switch" parent="Widget.MaterialComponents.CompoundButton.Switch">
<item name="android:minHeight">48dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
You could also specify additional attributes specific to switches in this style, but in your app, there's no need to.
- The radio button text is the last place you want to make sure the text appears visually consistent. You can't apply a
TextView
style orSwitch
style onto aRadioButton
widget. Instead, you must create a new style for radio buttons. You can extend from the MDC library'sRadioButton
style.
While you are creating this style, also add some padding between the radio button text and the circle visual. paddingStart
is a new attribute you haven't used yet. Padding is the amount of space between the contents of a view and the bounds of the view. The paddingStart
attribute sets the padding only at the start of the component. See the difference between 0dp and 8dp of paddingStart
on a radio button.
<style name="Widget.TipTime.CompoundButton.RadioButton"
parent="Widget.MaterialComponents.CompoundButton.RadioButton">
<item name="android:paddingStart">8dp</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
- (Optional) Create a
dimens.xml
file to improve manageability of frequently-used values. You can create the file in the same manner you did for thestyles.xml
file above. Select the values directory, right click and select New > Values Resource File.
In this small app, you've repeated the minimum height setting twice. That's certainly manageable for now, but would quickly get out of control if we had 4, 6, 10 or more components sharing that value. Remembering to change all of them individually is tedious and error-prone. You can create another helpful resource file in res > values called dimens.xml
that holds common dimensions that you can name. By standardizing common values as named dimensions, we make it easier to manage our app. TipTime is small so we won't be using it outside this optional step. However, with more complex apps in a production environment where you might work with a design team, dimens.xml
will easily allow you to change these values more often.
dimens.xml
<resources>
<dimen name="min_text_height">48dp</dimen>
</resources>
You would update styles.xml
file to use @dimen/min_text_height
instead of 48dp
directly.
...
<style name="Widget.TipTime.TextView" parent="Widget.MaterialComponents.TextView">
<item name="android:minHeight">@dimen/min_text_height</item>
<item name="android:gravity">center_vertical</item>
<item name="android:textAppearance">?attr/textAppearanceBody1</item>
</style>
...
Add these styles to your themes
You may have noticed that you haven't applied the new RadioButton
and Switch
styles onto the respective widgets yet. The reason is because you will be using theme attributes to set the radioButtonStyle
and switchStyle
in the app theme. Let's revisit what a theme is.
A theme is a collection of named resources (called theme attributes) that can be referenced later in styles, layouts, etc. You can specify a theme for an entire app, activity, or view hierarchy—not just an individual View
. Previously you modified the app's theme in themes.xml
by setting theme attributes like colorPrimary
and colorSecondary
, which gets used throughout the app and its components.
radioButtonStyle
and switchStyle
are other theme attributes you can set. The style resources that you provide for these theme attributes will be applied to every radio button and every switch in the view hierarchy that the theme applies to.
There's also a theme attribute for textInputStyle
where the specified style resource will be applied to all text input fields within the app. To make a TextInputLayout
appear like an outlined text field (as shown in the Material Design guidelines), there is an OutlinedBox
style defined in the MDC library as Widget.MaterialComponents.TextInputLayout.OutlinedBox
. This is the style you'll use.
- Modify the
themes.xml
file so that the theme refers to the desired styles. Setting a theme attribute is done the same way you declared thecolorPrimary
andcolorSecondary
theme attributes in an earlier codelab. This time however, the relevant theme attributes aretextInputStyle
,radioButtonStyle
, andswitchStyle
. You'll be using the styles you've created previously for theRadioButton
andSwitch
along with the style for the MaterialOutlinedBox
text field.
Copy the following into res/values/themes.xml
into the style tag for your app theme.
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
- This is what your
res/values/themes.xml
file should look like. You can add comments in the XML if you want (indicated by<!-
- and-->
).
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- Radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- Switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- Be sure to make the same changes to the dark theme in themes.xml (night). Your
res/values-night/themes.xml
file should look like this.
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Application theme for dark theme. -->
<style name="Theme.TipTime" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Text input fields -->
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox</item>
<!-- For radio buttons -->
<item name="radioButtonStyle">@style/Widget.TipTime.CompoundButton.RadioButton</item>
<!-- For switches -->
<item name="switchStyle">@style/Widget.TipTime.CompoundButton.Switch</item>
</style>
</resources>
- Run the app and see the changes. The
OutlinedBox
style looks much better for the text field, and all the text now looks consistent!
6. Enhance the user experience
As you near completion of your app, you should test your app not just with the expected workflow but in other user scenarios as well. You may find that some small code changes can improve the user experience in a large way.
Rotating the device
- Rotate your device to landscape mode. You may need to enable Auto-rotate setting first. (This is located under the device's Quick Settings or under Settings > Display > Advanced > Auto-rotate screen option.)
In the emulator, you can then use the emulator options (located on the upper right side adjacent to the device) to rotate the screen to the right or left.
- You'll notice some of the UI components including the Calculate button will get truncated. This clearly prevents you from using the app!
- To solve this bug, add a
ScrollView
around theConstraintLayout
. Your XML will look something like this.
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent"
android:layout_width="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:context=".MainActivity">
...
</ConstraintLayout>
</ScrollView>
- Run and test the app again. When you rotate the device to landscape mode, you should be able to scroll the UI to access the calculate button and see the tip result. This fix is not only useful for landscape mode, but also for other Android devices that may have different dimensions. Now regardless of the device screen size, the user can scroll the layout.
Hide keyboard on Enter key
You may have noticed that after you enter in a cost of service, the keyboard still stays up. It's a bit cumbersome to manually hide the keyboard each time to better access the calculate button. Instead, make the keyboard automatically hide itself when the Enter key is pressed.
For the text field, you can define a key listener to respond to events when certain keys are tapped. Every possible entry option on a keyboard has a key code associated with it, including the Enter
key. Note that an onscreen keyboard is also known as a soft keyboard, as opposed to a physical keyboard.
In this task, set up a key listener on the text field to listen for when the Enter
key is pressed. When that event is detected, hide the keyboard.
- Copy and paste this helper method into your
MainActivity
class. You can insert it right before the closing brace of theMainActivity
class. ThehandleKeyEvent()
is a private helper function that hides the onscreen keyboard if thekeyCode
input parameter is equal toKeyEvent.
KEYCODE_ENTER
. The InputMethodManager controls if a soft keyboard is shown, hidden, and allows the user to choose which soft keyboard is displayed. The method returns true if the key event was handled, and returns false otherwise.
MainActivity.kt
private fun handleKeyEvent(view: View, keyCode: Int): Boolean {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
// Hide the keyboard
val inputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
return true
}
return false
}
- Now attach a key listener on the
TextInputEditText
widget. Remember that you can access theTextInputEditText
widget through the binding object asbinding.costOfServiceEditText
.
Call the setOnKeyListener()
method on the costOfServiceEditText
and pass in an OnKeyListener
. This is similar to how you set a click listener on the calculate button in the app with binding.calculateButton.setOnClickListener { calculateTip() }
.
The code for setting a key listener on a view is a little more complex, but the general idea is that OnKeyListener
has an onKey()
method that gets triggered when a key press happens. The onKey()
method takes in 3 input arguments: the view, the code for the key that was pressed, and a key event (which you won't use, so you can call it "_
"). When the onKey()
method is called, you should call your handleKeyEvent()
method and pass along the view and key code arguments. The syntax for writing this out is: view, keyCode, _ -> handleKeyEvent(view, keyCode)
. This is actually called a lambda expression, but you will learn more about lambdas in a later unit.
Add the code for setting up the key listener on the text field within the activity's onCreate()
method. This is because you want your key listener to be attached as soon as the layout is created and before the user starts interacting with the activity.
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
setContentView(binding.root)
binding.calculateButton.setOnClickListener { calculateTip() }
binding.costOfServiceEditText.setOnKeyListener { view, keyCode, _ -> handleKeyEvent(view, keyCode)
}
}
- Test that your new changes work. Run the app and enter in a cost of service. Hit the Enter key on the keyboard and the soft keyboard should get hidden.
Test your app with Talkback enabled
As you've been learning throughout this course, you want to build apps that are accessible to as many users as possible. Some users may use Talkback to access and navigate your app. TalkBack is the Google screen reader included on Android devices. TalkBack gives you spoken feedback so that you can use your device without looking at the screen.
With Talkback enabled, ensure that a user can complete the use case of calculating tip within your app.
- Enable Talkback on your device by following these instructions.
- Return to the Tip Time app.
- Explore your app with Talkback using these instructions. Swipe right to navigate through screen elements in sequence, and swipe left to go in the opposite direction. Double-tap anywhere to select. Verify that you can reach all elements of your app with swipe gestures.
- Ensure that a Talkback user is able to navigate to each item on the screen, enter in a cost of service, change the tip options, calculate the tip, and hear the tip announced. Remember that no spoken feedback is provided for the icons since you marked those as
importantForAccessibility="no"
.
For more information on how to make your app more accessible, check out these principles and this learning pathway.
(Optional) Adjust the tint of the vector drawables
In this optional task, you will tint the icons based on the primary color of the theme, so that the icons appear differently in light vs. dark themes (as seen below). This change is a nice addition to your UI to make the icons appear more cohesive with the app theme.
As we mentioned before, one of the advantages of VectorDrawables
versus bitmap images is the ability to scale and tint them. Below we have the XML representing the bell icon. There are two specific color attributes to take notice of: android:tint
and android:fillColor
.
ic_service.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2,17h20v2L2,19zM13.84,7.79c0.1,-0.24 0.16,-0.51 0.16,-0.79 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2c0,0.28 0.06,0.55 0.16,0.79C6.25,8.6 3.27,11.93 3,16h18c-0.27,-4.07 -3.25,-7.4 -7.16,-8.21z"/>
</vector>
If there is a tint present, it will override any fillColor
directives for the drawable. In this case, the white color is overridden with the colorControlNormal
theme attribute. colorControlNormal
is the color of the "normal" (unselected/unactivated state) of a widget. Currently that's a gray color.
One visual enhancement we can make to the app is to tint the drawable based on the primary color of the app theme. For light theme, the icon will appear as @color/green
, whereas in dark theme, the icon will appear as @color/green_light
, which is the ?attr/colorPrimary
. Tinting the drawable based on the primary color of the app theme can make the elements in the layout appear more unified and cohesive. This also saves us from having to duplicate the set of icons for light theme and dark theme. There's only 1 set of vector drawables, and the tint will change based on the colorPrimary
theme attribute.
- Change the value of the
android:tint
attribute inic_service.xml
android:tint="?attr/colorPrimary"
In Android Studio, that icon now has the proper tint.
The value that the colorPrimary
theme attribute points to will differ depending on light vs. dark theme.
- Repeat the same for changing the tint on the other vector drawables.
ic_store.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
ic_round_up.xml
<vector ...
android:tint="?attr/colorPrimary">
...
</vector>
- Run the app. Verify that the icons appear differently in light vs. dark themes.
- As a final cleanup step, remember to reformat all XML and Kotlin code files in your app.
Congratulations, you have finally completed the tip calculator app! You should be very proud of what you've built. Hopefully this is the stepping stone for you to build even more beautiful and functional apps!
7. Solution code
The solution code for this codelab is in the GitHub repository listed below.
To get the code for this codelab and open it in Android Studio, do the following.
Get the code
- Click on the provided URL. This opens the GitHub page for the project in a browser.
- Check and confirm the branch name matches with the branch name specified in the codelab. For example, in the following screenshot the branch name is main.
- On the GitHub page for the project, click the Code button, which brings up a popup.
- In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open.
Note: If Android Studio is already open, instead, select the File > Open menu option.
- In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
8. Summary
- Use Material Design Components where possible to adhere to Material Design guidelines and allow for more customization.
- Add icons to give users visual cues about how parts of your app will function.
- Use
ConstraintLayout
to position elements in your layout. - Test your app for edges cases (e.g. rotating your app in landscape mode) and make improvements where applicable.
- Comment your code to help other people who are reading your code understand what your approach was.
- Reformat your code and clean up your code to make it as concise as possible.
9. Learn more
10. Practice on your own
- As a continuation from earlier codelabs, update your unit converter cooking app to more closely follow the Material guidelines by using the best practices you learned here (such as using Material Design Components).