Advanced Android Practicals PDF
Advanced Android Practicals PDF
Advanced Android Practicals PDF
of Contents
Introduction 1.1
Unit 1: Expand the user experience 1.2
Lesson 1: Fragments 1.2.1
1.1: Creating a Fragment with a UI 1.2.1.1
1.2: Communicating with a Fragment 1.2.1.2
Lesson 2: App widgets 1.2.2
2.1: Building app widgets 1.2.2.1
Lesson 3: Sensors 1.2.3
3.1: Working with sensor data 1.2.3.1
3.2: Working with sensor-based orientation 1.2.3.2
Unit 2: Make your apps fast and small 1.3
Lesson 4: Performance 1.3.1
4.1A: Using the Profile GPU Rendering tool 1.3.1.1
4.1B: Using the Debug GPU Overdraw and Layout Inspector tools 1.3.1.2
4.1C: Using the Systrace and dumpsys tools 1.3.1.3
4.2: Using the Memory Profiler tool 1.3.1.4
4.3: Optimizing network, battery, and image use 1.3.1.5
Unit 3: Make your apps accessible 1.4
Lesson 5: Localization 1.4.1
5.1: Using resources for languages 1.4.1.1
5.2: Using the locale to format information 1.4.1.2
Lesson 6: Accessibility 1.4.2
6.1: Exploring accessibility in Android 1.4.2.1
6.2: Creating accessible apps 1.4.2.2
Unit 4: Add geo features to your apps 1.5
Lesson 7: Location 1.5.1
7.1: Using the device location 1.5.1.1
Lesson 8: Places 1.5.2
8.1: Using the Places API 1.5.2.1
Lesson 9: Mapping 1.5.3
1
9.1: Adding a Google Map to your app 1.5.3.1
Unit 5: Advanced graphics and views 1.6
Lesson 10: Custom views 1.6.1
10.1A: Creating a custom view from a View subclass 1.6.1.1
10.1B: Creating a custom view from scratch 1.6.1.2
Lesson 11: Canvas 1.6.2
11.1A: Creating a simple Canvas object 1.6.2.1
11.1B: Drawing on a Canvas object 1.6.2.2
11.1C: Applying clipping to a Canvas object 1.6.2.3
11.2: Creating a SurfaceView object 1.6.2.4
Lesson 12: Animations 1.6.3
12.1: Creating property animations 1.6.3.1
Lesson 13: Media 1.6.4
13.1: Playing video with VideoView 1.6.4.1
Unit 6: Working with Architecture Components 1.7
Lesson 14: Room, LiveData, ViewModel 1.7.1
14.1A: Room, LiveData, ViewModel 1.7.1.1
14.1B: Deleting and updating data with Room 1.7.1.2
Appendix 1.8
Appendix: Setup 1.8.1
Appendix: Homework 1.8.2
2
Introduction
This course is intended to be taught in a classroom, but all the materials are available online,
so if you like to learn by yourself, go ahead!
Prerequisites
The Advanced Android Development course is intended for experienced developers who
have Java programming experience and know the fundamentals of how to build an Android
app using the Java programming language. This course assumes you have mastered the
topics in Units 1 to 4 of the Android Developer Fundamentals course.
Course materials
The course materials include:
This practical workbook, which guides you through creating Android apps to practice
and perfect the skills you're learning
A concept reference: Advanced Android Development—Concepts
Slide decks for optional use by instructors
Source code in GitHub for apps that you create during the practical exercises
3
Introduction
The practicals in this book assume that you are using the latest version of Android Studio.
Some of the practicals require at least Android Studio 3.0.
Create a fragment that has its own UI, and enable your activities to communicate with
fragments.
Lesson 3: Sensors
Learn how to work with sensor data and build an app that detects and responds to device
orientation.
Learn how to detect and analyze your app's performance using tools like these:
Use resources for different languages, and use the device locale to format information.
Lesson 6: Accessibility
Explore accessibility in Android, and learn how to make your app more usable for users who
use Google TalkBack.
4
Introduction
Enable your app to detect and update the device's current location.
Lesson 8: Places
Use the Places API to detect the user's location and offer a "picker" showing local places.
Lesson 9: Mapping
Create a custom view based on an existing View class, then create a custom view from
scratch.
Create a simple Canvas object, then draw on the canvas and apply clipping regions to it.
Use a SurfaceView object to draw to the screen outside of the main UI thread.
Learn how use property animations to define an animation to change an object property.
5
Introduction
This work is licensed under a Creative Commons Attribution 4.0 International License
6
Introduction
A Fragment is a self-contained component with its own user interface (UI) and lifecycle that
can be reused in different parts of an app's UI. (A Fragment can also be used without a UI,
in order to retain values across configuration changes, but this lesson does not cover that
usage.)
A Fragment can be a static part of the UI of an Activity , which means that the Fragment
remains on the screen during the entire lifecycle of the Activity . However, the UI of an
Activity may be more effective if it adds or removes the Fragment dynamically while the
Activity is running.
on top of its Activity window when a user taps a button or an action occurs. The user can
7
Introduction
This practical introduces the Fragment class and shows you how to include a Fragment as
a static part of a UI, as well as how to use Fragment transactions to add, replace, or remove
a Fragment dynamically.
8
Introduction
App overview
The FragmentExample1 app shows an image and the title and text of a magazine article. It
also shows a Fragment that enables users to provide feedback for the article. In this case
the feedback is very simple: just "Yes" or "No" to the question "Like the article?" Depending
on whether the user gives positive or negative feedback, the app displays an appropriate
response.
The Fragment is skeletal, but it demonstrates how to create a Fragment to use in multiple
places in your app's UI.
In the first task, you add the Fragment statically to the Activity layout so that it is displayed
for the entire duration of the Activity lifecycle. The user can interact with the radio buttons
in the Fragment to choose either "Yes" or "No," as shown in the figure above.
If the user chooses "Yes," the text in the Fragment changes to "Article: Like."
9
Introduction
If the user chooses "No," the text in the Fragment changes to "Article: Thanks."
In the second task, in which you create the FragmentExample2 app, you add the Fragment
dynamically — your code adds, replaces, and removes the Fragment while the Activity is
running. You will change the Activity code and layout to do this.
As shown below, the user can tap the Open button to show the Fragment at the top of the
screen. The user can then interact with the UI elements in the Fragment . The user taps
Close to close the Fragment .
Activity . This is a useful technique for consolidating a set of UI elements (such as radio
buttons and text) and user interaction behavior that you can reuse in layouts for other
activities.
10
Introduction
11
Introduction
4. Uncheck the Include fragment factory methods and Include interface callbacks
options. Click Finish to create the Fragment .
public SimpleFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_simple,
container, false);
The Fragment class uses callback methods that are similar to Activity callback
methods. For example, onCreateView() provides a LayoutInflater to inflate the
Fragment UI from the layout resource fragment_simple .
12
Introduction
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:textAppearance "@style/Base.TextAppearance.AppCompat.Medium"
android:padding "4dp"
android:text "@string/question_article"
3. Change the FrameLayout root element to LinearLayout , and change the following
attributes for the LinearLayout :
android:background "@color/my_fragment_color"
android:orientation "horizontal"
4. Within the LinearLayout , add the following RadioGroup element after the TextView
element:
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/radio_button_yes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:text="@string/yes" />
<RadioButton
android:id="@+id/radio_button_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:text="@string/no" />
</RadioGroup>
13
Introduction
The @string/yes and @string/no resources are defined in the strings.xml file in the
starter app as "Yes" and "No" . In addition, the following string resources are also
defined in the strings.xml file:
The layout preview for fragment_simple.xml should look like the following:
14
Introduction
3. Add the following constants to the top of SimpleFragment to represent the two states of
the radio button choice: 0 = yes and 1 = no:
4. Replace the TODO comment inside the onCreateView() method with the following code
to set the radio group listener and change the textView (the fragment_header in the
layout) depending on the radio button choice:
radioGroup.setOnCheckedChangeListener(new
RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
View radioButton = radioGroup.findViewById(checkedId);
int index = radioGroup.indexOfChild(radioButton);
TextView textView =
rootView.findViewById(R.id.fragment_header);
switch (index) {
case YES: // User chose "Yes."
textView.setText(R.string.yes_message);
break;
case NO: // User chose "No."
textView.setText(R.string.no_message);
break;
default: // No choice made.
// Do nothing.
break;
}
}
});
buttons is 0 ("Yes") or 1 ("No"), which display either the yes_message text or the
no_message text.
15
Introduction
<fragment
android:id="@+id/fragment"
android:name="com.example.android.fragmentexample.SimpleFragment"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout="@layout/fragment_simple" />
The figure below shows the layout preview, with SimpleFragment statically included in
the activity layout:
A render error may appear below the preview, because a <fragment> element can
dynamically include different layouts when the app runs, and the layout editor doesn't
know what layout to use for a preview. To see the Fragment in the preview, click the
@layout/fragment_simple link in the error message, and choose SimpleFragment.
3. Run the app. The Fragment is now included in the MainActivity layout, as shown in
the figures below.
16
Introduction
4. Tap a radio button. The "LIKE THE ARTICLE?" text is replaced by either the
yes_message text ("ARTICLE: Like") or the no_message text ("ARTICLE: Thanks"),
5. After tapping a radio button, change the orientation of your device or emulator from
portrait to landscape. Note that the "Yes" or "No" choice is still selected.
Switching the device orientation after choosing "No" demonstrates that a Fragment can
retain an instance of its data after a configuration change (such as changing the orientation).
This feature makes a Fragment useful as a UI component, as compared to using separate
Views . While an Activity is destroyed and recreated when a device's configuration
17
Introduction
Coding challenge
Note: All coding challenges are optional.
Challenge: Expand the Fragment to include another question ("Like the song?")
underneath the first question, so that it appears as shown in the figure below. Add a
RatingBar so that the user can set a rating for the song, and a Toast message that shows
Hint: Use the OnRatingBarChangeListener in the Fragment , and be sure to include the
android:isIndicator attribute, set to false , for the RatingBar in the Fragment layout.
This challenge demonstrates how a Fragment can handle different kinds of user input.
18
Introduction
19
Introduction
tapping a button.
You will change the FragmentExample1 app to manage the Fragment using
FragmentManager and FragmentTransaction statements that can add, remove, and replace a
Fragment .
1. Copy the FragmentExample1 project, and open the copy in Android Studio. Refactor
and rename the project to FragmentExample2 . (For help with copying projects and
refactoring and renaming, see Copy and rename a project.)
2. Open the activity_main.xml layout, and switch the layout editor to Text (XML) view.
<FrameLayout
android:id="@+id/fragment_container"
20
Introduction
1. Open activity_main.xml , click the Design tab if it is not already selected, and add a
Button under the imageView element.
2. Constrain the Button to the bottom of imageView and to the left side of the parent.
3. Open the Attributes pane, and add the ID open_button and the text " @string/open ".
The @string/open and @string/close resources are defined in the strings.xml file in
the starter app as "OPEN" and "CLOSE" .
Note that since you have changed <fragment> to <FrameLayout> , the Fragment (now
called fragment_container ) no longer appears in the design preview—but don't worry,
4. Open MainActivity , and declare and initialize the Button . Also add a private
boolean to determine whether the Fragment is displayed:
mButton = findViewById(R.id.open_button);
21
Introduction
The best practice for instantiating the Fragment in the A ctivity is to provide a
newinstance() factory method in the Fragment . Follow these steps to add the
You will also add the displayFragment() and closeFragment() methods, and use Fragment
transactions:
1. Open SimpleFragment , and add the following method for instantiating and returning the
Fragment to the Activity :
3. Replace the TODO: Get the FragmentManager... comment in the above code with the
following:
22
Introduction
4. Replace the TODO: Add the SimpleFragment comment in the above code with the
following:
This code adds a new Fragment using the add() transaction method. The first
argument passed to add() is the layout resource ( fragment_container ) for the
ViewGroup in which the Fragment should be placed. The second parameter is the
The code then calls commit() for the transaction to take effect.
The code also changes the text of the Button to "CLOSE" and sets the Boolean
isFragmentDisplayed to true so that you can track the state of the Fragment .
23
Introduction
a series of transactions, and acquires a reference to the Fragment using the layout
resource ( fragment_container ). It then uses the remove() transaction to remove the
Fragment .
However, before creating this transaction, the code checks to see if the Fragment is
displayed (not null ). If the Fragment is not displayed, there's nothing to remove.
The code also changes the text of the Button to "OPEN" and sets the Boolean
isFragmentDisplayed to false so that you can track the state of the Fragment .
You will also add code to save the value of isFragmentDisplayed and use it if the
configuration changes, such as if the user switches from portrait or landscape orientation.
1. Open MainActivity , and add the following to the onCreate() method to set the click
listener for the Button:
24
Introduction
2. To save the boolean value representing the Fragment display state, define a key for the
Fragment state to use in the savedInstanceState Bundle . Add this member variable to
MainActivity :
3. Add the following method to MainActivity to save the state of the Fragment if the
configuration changes:
4. Go back to the onCreate() method and add the following code. It checks to see if the
instance state of the Activity was saved for some reason, such as a configuration
change (the user switching from vertical to horizontal). If the saved instance was not
saved, it would be null . If the saved instance is not null , the code retrieves the
Fragment state from the saved instance, and sets the Button text:
if (savedInstanceState != null) {
isFragmentDisplayed =
savedInstanceState.getBoolean(STATE_FRAGMENT);
if (isFragmentDisplayed) {
// If the fragment is displayed, change button to "close".
mButton.setText(R.string.close);
}
}
5. Run the app. Tapping Open adds the Fragment and shows the Close text in the button.
Tapping Close removes the Fragment and shows the Open text in the button. You can
switch your device or emulator from vertical to horizontal orientation to see that the
buttons and Fragment work.
25
Introduction
Summary
To create a blank Fragment , expand app > java in Project: Android view, select the
folder containing the Java code for your app, and choose File > New > Fragment >
Fragment (Blank).
The Fragment class uses callback methods that are similar to Activity callbacks,
such as onCreateView() , which provides a LayoutInflater object to inflate the
Fragment UI from the layout resource fragment_simple .
26
Introduction
or remove it, specify a ViewGroup inside the layout file for the Activity such as a
FrameLayout .
When adding a Fragment dynamically to an Activity , the best practice for creating
the fragment is to create the instance with a newinstance() method in the Fragment
itself. Call the newinstance() method from the Activity to create a new instance.
To get an instance of FragmentManager , use getSupportFragmentManager() in order to
instantiate the Fragment class using the Support Library so your app remains
compatible with devices running system versions as low as Android 1.6.
To start a series of Fragment transactions, call beginTransaction()
ona FragmentTransaction .
With FragmentManager your code can perform the following Fragment transactions
while the app runs, using FragmentTransaction methods:
Related concept
The related concept documentation is Fragments.
Learn more
Android developer documentation:
Fragment
Fragments
FragmentManager
FragmentTransaction
Creating a Fragment
Building a Flexible UI
Building a Dynamic UI with Fragments
Handling Configuration Changes
Supporting Tablets and Handsets
Videos:
27
Introduction
28
Introduction
An Activity hosting a Fragment can send data to and receive data from the Fragment . A
Fragment can't communicate directly with another Fragment , even within the same
29
Introduction
Apps overview
In FragmentExample2, the app from the lesson on using a Fragment , a user can tap the
Open button to show the Fragment , tap a radio button for a "Yes" or "No" choice, and tap
Close to close the Fragment . If the user opens the Fragment again, the previous choice is
not retained.
30
Introduction
In this lesson you modify the code in the FragmentExample2 app to send the user's choice
back to the host Activity . When the Activity opens a new Fragment , the Activity can
send the user's previous choice to the new Fragment . As a result, when the user taps Open
again, the app shows the previously selected choice.
On a mobile phone screen, the SongDetail app looks like the following figure:
31
Introduction
Define a listener interface in the Fragment with a callback method to get the user's
choice.
Implement the interface and callback in the host Activity to retrieve the user's choice.
Use the newInstance() factory method to provide the user's choice back to the
Fragment when creating the next Fragment instance.
32
Introduction
represent the third state of the radio button choice, which is 2 if the user has not yet
made a choice:
interface OnFragmentInteractionListener {
void onRadioButtonChoice(int choice);
}
5. Define a variable for the listener at the top of the SimpleFragment class. You will use
this variable in onAttach() in the next step:
OnFragmentInteractionListener mListener;
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnFragmentInteractionListener) {
mListener = (OnFragmentInteractionListener) context;
} else {
throw new ClassCastException(context.toString()
+ getResources().getString(R.string.exception_message));
}
}
The onAttach() method is called as soon as the Fragment is associated with the
Activity . The code makes sure that the host Activity has implemented the callback
The string resource exception_message is included in the starter app for the text "must
implement OnFragmentInteractionListener".
33
Introduction
2. In Android Studio, the above code is underlined in red, and a red bulb appears in the left
margin. Click the bulb and choose Implement methods. Choose
onRadioButtonChoice(choice:int):void, and click OK. An empty
onRadioButtonChoice() method appears in MainActivity .
3. Add a member variable in MainActivity for the choice the user makes with the radio
buttons, and set it to the default value:
4. Add code to the new onRadioButtonChoice() method to assign the user's radio button
choice from the Fragment to mRadioButtonChoice . Add a Toast to show that the
Activity has received the data from the Fragment :
@Override
public void onRadioButtonChoice(int choice) {
// Keep the radio button choice to pass it back to the fragment.
mRadioButtonChoice = choice;
Toast.makeText(this, "Choice is " + Integer.toString(choice),
Toast.LENGTH_SHORT).show();
}
34
Introduction
5. Run the app, tap Open, and make a choice. The Activity shows the Toast message
with the choice as an integer (0 is "Yes" and 1 is "No").
35
Introduction
36
Introduction
Fragment .
2. Open SimpleFragment and add the following constant, which is the key to finding the
information in the Bundle :
the Fragment :
4. Now that a Bundle of arguments is available in the SimpleFragment , you can add code
to the onCreateView() method in SimpleFragment to get the choice from the Bundle .
Right before the statement that sets the radioGroup onCheckedChanged listener, add the
following code to retrieve the radio button choice (if a choice was made), and pre-select
the radio button.
37
Introduction
if (getArguments().containsKey(CHOICE)) {
// A choice was made, so get the choice.
mRadioButtonChoice = getArguments().getInt(CHOICE);
// Check the radio button choice.
if (mRadioButtonChoice != NONE) {
radioGroup.check
(radioGroup.getChildAt(mRadioButtonChoice).getId());
}
}
5. Run the app. At first, the app doesn't show the Fragment (see the left side of the
following figure).
6. Tap Open and make a choice such as "Yes" (see the center of the figure below). The
choice you made appears in a Toast .
7. Tap Close to close the Fragment .
8. Tap Open to reopen the Fragment . The Fragment appears with the choice already
made (see the right side of the figure). Your app has communicated the choice from the
Fragment to the Activity , and then back to the Fragment .
38
Introduction
This task demonstrates how you can use a Fragment to implement a two-pane master/detail
layout for a horizontal tablet display. It also shows how to take code from an Activity and
encapsulate it within a Fragment , thereby simplifying the Activity .
In this task you use a starter app called SongDetail_start that displays song titles that the
user can tap to see song details.
On a tablet, the app doesn't take advantage of the full screen size, as shown in the following
figure:
39
Introduction
When set to a horizontal orientation, a tablet device is wide enough to show information in a
master/detail layout. You will modify the app to show a master/detail layout if the device is
wide enough, with the song list as the master, and the Fragment as the detail, as shown in
the following figure.
The following diagram shows the difference in the code for the SongDetail starter app (1),
and the final version of the app for both mobile phone and wide tablets (2-3).
40
Introduction
1. Phone or tablet: The SongDetail_start app displays the song details in a vertical layout
in SongDetailActivity , which is called from MainActivity .
2. Phone or small screen: The final version of SongDetail displays the song details in
SongDetailFragment . MainActivity calls SongDetailActivity , which then hosts the
3. Tablet or larger screen in horizontal orientation: If the screen is wide enough for the
master/detail layout, the final version of SongDetail displays the song details in
SongDetailFragment . MainActivity hosts the Fragment directly.
1. Open the SongDetail_start app in Android Studio, and rename and refactor the project
to SongDetail (for help with copying projects and refactoring and renaming, see "Copy
and rename a project").
2. Run the app on a tablet or a tablet emulator in horizontal orientation. For instructions on
using the emulator, see Run Apps on the Android Emulator. The starter app uses the
same layout for tablets and mobile phones—it doesn't take advantage of a wide screen.
3. Examine the layouts. Although you don't need to change them, you will reference the
android:id values in your code.
song_list.xml (w900dp) for devices with screens that have a width of 900dp or larger. It
41
Introduction
The following layouts are also provided, which you don't have to change:
song_list.xml .
// ...
// This activity displays the detail. In a real-world scenario,
// get the data from a content repository.
mSong = SongUtils.SONG_ITEMS.get
(getIntent().getIntExtra(SongUtils.SONG_ID_KEY, 0));
In the next step you will add a new Fragment , and copy the if (mSong != null) block with
setText() to the new Fragment , so that the Fragment controls how the song detail is
displayed.
The SongUtils.java class in the content folder creates an array of fixed entries for the
song title and song detail information. You can modify this class to refer to different types of
data. However, in a real-world production app, you would most likely get data from a
repository or server, rather than hardcoding it in the app.
1. Select the app package name within java in the Project: Android view, add a new
42
Introduction
3. Open SongDetailFragment , and Edit > Paste the above declaration at the top of the
class.
4. In SongDetailFragment , remove all code in the onCreateView() method and change it to
inflate the song_detail.xm l layout:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView =
inflater.inflate(R.layout.song_detail, container, false);
// TODO: Show the detail information in a TextView.
return rootView;
}
5. To use the song detail in the Fragment , replace the TODO comment with the if (mSong
!= null) block from SongDetailActivity , which includes the setText() method to
show the detail information in the song_detail TextView . You need to add rootView
to use the findViewById() method; otherwise, the block is the same as the one
formerly used in SongDetailActivity :
if (mSong != null) {
((TextView) rootView.findViewById(R.id.song_detail))
.setText(mSong.details);
}
If the display is wide enough for a two-pane layout, MainActivity will host the
Fragment , and send the position of the selected song in the list directly to the
Fragment .
43
Introduction
If the screen is not wide enough for a two-pane layout, MainActivity will use an intent
with extra data—the position of the selected song—to start SongDetailActivity .
SongDetailActivity will then host the Fragment , and send the position of the selected
In other words, the Fragment will take over the job of displaying the song detail. Therefore,
your code needs to host the Fragment in MainActivity if the screen is wide enough for a
two-pane display, or in SongDetailActivity if the screen is not wide enough.
1. To serve as a check for the size of the screen, add a private boolean to the
MainActivity class called mTwoPane :
if (findViewById(R.id.song_detail_container) != null) {
mTwoPane = true;
}
The above code checks for the screen size and orientation. The song_detail_container
view for MainActivity will be present only if the screen's width is 900dp or larger,
because it is defined only in the song_list.xml (w900dp) layout, not in the default
song_list.xml layout for smaller screen sizes. If this view is present, then the
If a tablet is set to portrait orientation, its width will most likely be lower than 900dp, and so it
will not show a two-pane layout. If the tablet is set to horizontal orientation and its width is
900dp or larger, it will show a two-pane layout.
In the newInstance() method you can set a Bundle and use the
Fragment. setArguments(Bundle) method to supply the construction arguments for the
Fragment . In a following step, you will use the Fragment. getArguments() method in the
44
Introduction
The above method receives the selectedSong (the integer position of the song title in
the list), and creates the arguments Bundle with SONG_ID_KEY and selectedSong . It
then uses setArguments(arguments) to set the arguments for the Fragment , and returns
the Fragment .
holder.mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Context context = v.getContext();
Intent intent = new Intent(context, SongDetailActivity.class);
intent.putExtra(SongUtils.SONG_ID_KEY,
holder.getAdapterPosition());
context.startActivity(intent);
}
});
detail for the tapped song title. You will have to send this data to the new Fragment .
3. Change the code within the onClick() method to create a new instance of the
Fragment for two-pane display, or to use the intent (as before) to launch the second
45
Introduction
if (mTwoPane) {
int selectedSong = holder.getAdapterPosition();
SongDetailFragment fragment =
SongDetailFragment.newInstance(selectedSong);
getSupportFragmentManager().beginTransaction()
.replace(R.id.song_detail_container, fragment)
.addToBackStack(null)
.commit();
} else {
Context context = v.getContext();
Intent intent = new Intent(context, SongDetailActivity.class);
intent.putExtra(SongUtils.SONG_ID_KEY,
holder.getAdapterPosition());
context.startActivity(intent);
}
If mTwoPane is true, the code gets the selected song position ( selectedSong ) in the
song title list, and passes it to the new instance of SongDetailFragment using the
newInstance() method in the Fragment . It then uses getSupportFragmentManager() with
The transaction code for managing a Fragment should be familiar, as you performed
such operations in a previous lesson. By replacing the Fragment , you can refresh with
new data a Fragment that is already running.
If mTwoPane is false, the code does exactly the same thing it did in the starter app: it
starts SongDetailActivity with an intent and SONG_ID_KEY and
holder.getAdapterPosition() as extra data.
4. Open SongDetailActivity , and find the code in the onCreate() method that no longer
works due to the removal of the mSong declaration. In the next step you will replace it.
Previously you cut the if (mSong != null) block that followed the above code and
pasted it into the Fragment , so that the Fragment could display the song detail. You
can now replace the above code in the next step so that SongDetailActivity will use
the Fragment to display the song detail.
46
Introduction
5. Replace the above code in onCreate() with the following code. It first checks if
savedInstanceState is null , which means the Activity started but its state was not
saved—such as when the screen is rotated. In such cases, you don't need to add the
Fragment .)
if (savedInstanceState == null) {
int selectedSong =
getIntent().getIntExtra(SongUtils.SONG_ID_KEY, 0);
SongDetailFragment fragment =
SongDetailFragment.newInstance(selectedSong);
getSupportFragmentManager().beginTransaction()
.add(R.id.song_detail_container, fragment)
.commit();
}
The code first gets the selected song title position from the intent extra data. It then
creates an instance of the Fragment and adds it to the Activity using a Fragment
transaction. SongDetailActivity will now use the SongDetailFragment to display the
detail.
6. To set up the data in the Fragment , open SongDetailFragment and add the entire
onCreate() method before the onCreateView() method. The getArguments() method
in the onCreate() method gets the arguments supplied to the Fragment using
setArguments(Bundle) .
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments().containsKey(SongUtils.SONG_ID_KEY)) {
// Load the content specified by the fragment arguments.
mSong = SongUtils.SONG_ITEMS.get(getArguments()
.getInt(SongUtils.SONG_ID_KEY));
}
}
7. Run the app on a mobile phone or phone emulator. It should look the same as it did
before. (Refer to the figure at the beginning of this task.)
8. Run the app on a tablet or tablet emulator in horizontal orientation. It should display the
master/detail layout as shown in the following figure.
47
Introduction
Summary
Adding a Fragment dynamically:
Fragment lifecycle:
The system calls onAttach() when a Fragment is first associated with an Activity .
Use onAttach() to initialize essential components of the Fragment , such as a listener.
The system calls onCreate() when creating a Fragment . Use onCreate() to initialize
components of the Fragment that you want to retain when the Fragment is paused or
48
Introduction
The host Activity can call methods in a Fragment by acquiring a reference to the
Fragment from FragmentManager , using findFragmentById() .
Save the Fragment state during the onSaveInstanceState() callback and restore it
during either onCreate() , onCreateView() , or onActivityCreated() .
To communicate from the host Activity to a Fragment , use a Bundle and the following:
The interface in the Fragment defines a callback method to communicate to its host
Activity .
Related concept
The related concept documentation is Fragment lifecycle and communications.
Learn more
Android developer documentation:
Fragment
Fragments
FragmentManager
FragmentTransaction
Creating a Fragment
Communicating with Other Fragments
49
Introduction
Videos:
50
Introduction
An app widget is a miniature app view that appears on the Android home screen and can be
updated periodically with new data. App widgets display small amounts of information or
perform simple functions such as showing the time, summarizing the day's calendar events,
51
Introduction
App widgets are add-ons for an existing app and are made available to users when the app
is installed on a device. Your app can have multiple widgets. You can't create a stand-alone
app widget without an associated app.
Users can place widgets on any home screen panel from the widget picker. To access the
widget picker, touch & hold any blank space on the home screen, and then choose Widgets.
Touch & hold any widget to place it on the home screen. You can also touch & hold an
already-placed widget to move or resize it, if it is resizeable.
Note: As of Android 5.0, widgets can only be placed on the Android home screen. Previous
versions of Android (4.2/API 17 to 4.4/API 19) also allowed widgets to appear on the lock
screen (keyguard). Although the app widget tools and APIs still occasionally mention lock
screen widgets, that functionality is deprecated. This chapter discusses only the home
screen widgets.
App widgets that display data can be updated periodically to refresh that data, either by the
system or by the widget's associated app, through a broadcast intent. An app widget is a
broadcast receiver that accepts those intents.
App widgets can also perform actions when tapped, such as launching their associated app.
You can create click handlers to perform actions for the widget as a whole, or for any view of
the widget layout such as a button.
52
Introduction
In this practical, you build a simple widget that displays data, updates itself periodically, and
includes a button to refresh the data on request.
Note: The term "widget" in Android also commonly refers to the user interface elements
(views) you use to build an app, such as buttons and checkboxes. In this chapter all
instances of the word widget refers to app widgets.
Identify app widgets, and understand the key parts of an app widget.
Add an app widget to your project with Android Studio.
Understand the mechanics of app widget updates, and how to receive and handle
update intents for your app widget.
Implement app widget actions when an element of an app widget is tapped.
App overview
The AppWidgetSample app demonstrates a simple app widget. Because the app
demonstrates app widgets, the app's main activity is minimal, with one explanatory text view:
53
Introduction
54
Introduction
The actual app widget has two panels for information and a button:
The app widget ID. The user can place multiple widget instances on their home screen.
An internal ID identifies each widget.
How many times the widget has been updated, and the last update time.
An Update now button, to request an immediate update.
55
Introduction
Android Studio generates all the template files you need for adding an app widget to
your app. You explore those files in the next section.
The new widget appears on the home screen two cells wide and one cell high, the
default widget size you defined when you created the widget. This sample widget
doesn't do anything other than display the word EXAMPLE on a blue background.
56
Introduction
9. Add another widget. You can add multiple widgets from the same app on your home
screen. This functionality is mostly useful for widgets that can be configured to display
customized information. For example, different weather widgets could display the
weather in different locations.
10. Touch & hold either of the EXAMPLE widgets. Resize handles appear on the edges of
the widget. You can now move the widget or change the size of the widget to take up
more than the allotted cells on the home screen.
11. To remove an EXAMPLE widget from the device, touch & hold the widget and drag it to
the word Remove at the top of the screen. Removing a widget only removes that
particular widget instance. Neither the second widget nor the app are removed.
1. Open res/xml/new_app_widget_info.xml .
This XML configuration file is usually called the provider-info file . The provider-info file
defines several properties of your app widget, including the widget's layout file, default
size, configuration activity (if any), preview image, and periodic update frequency (how
often the widget updates itself, in milliseconds).
2. Open res/layout/new_app_widget.xml .
This file defines the layout of your app widget. App widget layouts are based on
RemoteViews elements, rather than the normal View hierarchy, although you define
them in XML the same way. Remote views provide a separate view hierarchy that can
be displayed outside an app. Remote views include a limited subset of the available
Android layouts and views.
4. Open java/values/NewAppWidget.java .
This file is the widget provider , the Java file that defines the behavior for your widget.
The key task for a widget provider is to handle widget update intents. App widgets
extend the AppWidgetProvider class, which in turn extends BroadcastReceiver .
57
Introduction
5. Open res/values/dimens.xml .
This default dimensions file includes a value for the widget padding of 8 dp. App widgets
look best with a little extra space around the edges so that the widgets do not display
edge-to-edge on the user's home screen. Before Android 4.0 (API 14), your layout
needed to include this padding. After API 14 the system adds the margin for you.
This dimensions file is used for Android API versions 14 and higher. The value of
widget-margin in this file is 0 dp, because the system adds the margin for you.
7. Open manifests/AndroidManifest.xml .
to app widget update broadcast intents. Note the android:resource attribute of the
meta-data tag, which specifies the widget provider-info file ( new_app_widget_info ).
1. Open res/xml/new_app_widget_info.xml .
2. Note the android:initialLayout attribute.
This attribute defines the layout resource that your app widget will use, in this case the
@layout/new_app_widget layout file.
These attributes define the minimum initial size of the widget, in dp. When you defined
your widget in Android Studio to be 2 cells wide by 2 high, Android Studio fills in these
values in the provider-info file. When your widget is added to a user's home screen, it is
stretched both horizontally and vertically to occupy as many grid cells as satisfy the
minWidth and minHeight values.
58
Introduction
The rule for how many dp fit into a grid cell is based on the equation 70 × grid_size −
30, where grid_size is the number of cells you want your widget to take up. Generally
speaking, you can use this table to determine what your minWidth and minHeight
should be:
1 40 dp
2 110 dp
3 180 dp
4 250 dp
Attribute Value
android:id "@+id/section_id"
android:layout_width "match_parent"
android:layout_height "wrap_content"
android:layout_alignParentLeft "true"
android:layout_alignParentStart "true"
android:layout_alignParentTop "true"
android:orientation "horizontal"
style "@style/AppWidgetSection"
This linear layout provides the appearance of a light-colored panel on top of a grey-blue
background. The AppWidgetSection style does not yet exist and appears in red in
Android Studio. (You add it later.)
59
Introduction
Attribute Value
android:id "@+id/appwidget_id_label"
android:layout_width "0dp"
android:layout_height "wrap_content"
android:layout_weight "2"
style "@style/AppWidgetLabel"
Extract the string for the text. This text view is the label for the widget ID. The
AppWidgetLabel style does not yet exist.
5. Add a second TextView below the first one, and give it these attributes:
Attribute Value
android:id "@+id/appwidget_id"
android:layout_width "0dp"
android:layout_height "wrap_content"
android:layout_weight "1"
android:text "XX"
style "@style/AppWidgetText"
You do not need to extract the string for the text in this text view, because the string is
replaced with the actual ID when the app widget runs. As with the previous views, the
AppWidgetText style is not yet defined.
6. Open res/values/styles.xml . Add the following code below the AppTheme styles to
define AppWidgetSection , AppWidgetLabel , and AppWidgetText :
60
Introduction
7. Return to the app widget's layout file. Click the Design tab to examine the layout in the
design editor. By default, Android Studio assumes that you are designing a layout for a
regular activity, and it displays a default activity "skin" around your layout. Android
Studio does not provide a preview for app widget designs.
TIP: To simulate a simple widget design, choose Android Wear Square from the device
menu and resize the layout to be approximately 110 dp wide and 110 dp tall (the values of
minWidth and minHeight in the provider-info file).
runs and again each time the widget receives an update request (a broadcast intent).
Retrieve any new data that the app widget needs to display.
Build a RemoteViews object from the app's context and the app widget's layout file.
Update any views within the app widget's layout with new data.
Tell the app widget manager to redisplay the widget with the new remote views.
Unlike activities, where you only inflate the layout once and then modify it in place as new
data appears, the entire app widget layout must be reconstructed and redisplayed each time
the widget receives an update intent.
1. Open java/values/NewAppWidget.java .
2. Scroll down to the onUpdate() method, and examine the method parameters.
61
Introduction
The onUpdate() method is called with several arguments including the context, the app
widget manager, and an array of integers that contains all the available app widget IDs.
Every app widget that the user adds to the home screen gets a unique internal ID that
identifies that app widget. Each time you get an update request in your provider class,
you must update all app widget instances by iterating over that array of IDs.
The template code that Android Studio defines for your widget provider's onUpdate()
method iterates over that array of app widget IDs and calls the updateAppWidget()
helper method.
@Override
public void onUpdate(Context context,
AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
When you use this template code for an app widget, you do not need to modify the
actual onUpdate() method. Use the updateAppWidget() helper method to update each
individual widget.
3. In the updateAppWidget() method, delete the line that gets the app widget text:
CharSequence widgetText =
context.getString(R.string.appwidget_text);
The template code for the app widget includes this string. You won't use it for this app
widget.
views.setTextViewText(R.id.appwidget_id, String.valueOf(appWidgetId));
You would use onEnabled() to perform initial setup for a widget (such as opening a
new database) when the first instance is initially added to the user's home screen. Even
if the user adds multiple widgets, this method is only called once. Use onDisabled() ,
62
Introduction
correspondingly, to clean up any resources that were created in onEnabled() once the
last instance of that widget is removed. You won't use either of these methods for this
app, so you can delete them.
6. Build and run the app. When the app launches, go to the device's Home screen.
Delete the existing EXAMPLE widget from the home screen, and add a new widget. The
preview for your app widget in the widget picker still uses an image that represents the
EXAMPLE widget. When you place the new widget on a home screen, the widget
should show the new layout with the internal widget ID. Note that the current height of
the widget leaves a lot of space below the panel for the ID. You'll add more panels soon.
Note: Because app widgets are updated independently from their associated app,
sometimes when you make changes to an app widget in Android Studio those changes
do not show up in existing apps. When testing widgets make sure to remove all existing
widgets before adding new ones.
7. Add a second copy of the app widget to the home screen. Note that each widget has its
own widget ID.
The widget can update itself at regular intervals. You can define the interval in the
widget's provider-info file.
The widget's associated app can request a widget update explicitly.
In both these cases the app widget manager sends a broadcast intent with the action
ACTION_APPWIDGET_UPDATE . Your app widget-provider class receives that intent, and calls the
onUpdate() method.
63
Introduction
In this task, you add a second panel to the app widget that indicates how many times the
widget has been updated, and the last update time.
1. In the widget layout file, add a second LinearLayout element just after the first, and
give it these attributes:
Attribute Value
android:id "@+id/section_update"
android:layout_width "match_parent"
android:layout_height "wrap_content"
android:layout_alignParentLeft "true"
android:layout_alignParentStart "true"
android:layout_below "@+id/section_id"
android:orientation "vertical"
style "@style/AppWidgetSection"
2. Inside the new LinearLayout , add a TextView with these attributes, and extract the
string:
Attribute Value
android:id "@+id/appwidget_update_label"
android:layout_width "match_parent"
android:layout_height "wrap_content"
android:layout_marginBottom "2dp"
style "@style/AppWidgetLabel"
3. Add a second TextView after the first one and give it these attributes:
Attribute Value
android:id "@+id/appwidget_update"
android:layout_width "match_parent"
android:layout_height "wrap_content"
android:layout_weight "1"
style "@style/AppWidgetText"
Extract the string in android:text and give it the name date_count_format . The odd
characters in this text string are placeholder formatting code. Parts of this string will be
replaced in the Java code for your app, with the formatting codes filled in with numeric
values. In this case the formatting code has four parts:
64
Introduction
$d : The format for the first placeholder value. In this case, a decimal number.
The parts of the string that are not placeholders (here, just the @ sign) are passed
through to the new string. You can find out more about placeholders and formatting
codes in the Formatter documentation.
TIP: To see the new widget in Android Studio's Design tab, you may need to enlarge
the layout by dragging the lower-right corner downward.
4. In the app widget provider-info file ( res/xml/new_app_widget_info.xml ), change the
android:minHeight attribute to 180dp .
android:minHeight="180dp"
When you add more content to the layout, the default size of the widget in cells on the
home screen also needs to change. This iteration of the app widget is 3 cells high by 2
wide, which means minHeight is now 180 dp and minWidth remains 110 dp.
The android:updatePeriodMillis attribute defines how often the app widget is updated.
The default update interval is 86,400,000 milliseconds (24 hours). 1,800,000
milliseconds is a 30 minute interval. If you set updatePeriodMillis to less than
1,800,000 milliseconds, the app widget manager only sends update requests every 30
minutes. Because updates use system resources, even if the associated app is not
running, you should generally avoid frequent app widget updates.
6. In the app widget provider ( NewAppWidget.java ), add static variables to the top of the
class for shared preferences. You'll use shared preferences to keep track of the current
update count for the widget.
7. At the top of the updateAppWidget() method, get the value of the update count from the
shared preferences, and increment that value.
65
Introduction
The key to get the current count out of the shared preferences includes both the static
key COUNT_KEY and the current app widget ID. Each app widget may have a different
update count so each app widget needs its own entry in the shared preferences.
String dateString =
DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date());
You will need to import both the DateFormat ( java.text.DateFormat ) and Date
( java.util.Date ) classes.
9. After updating the text view for appwidget_id , add a line to update the
appwidget_update text view. This code gets the date format string from the resources
and substitutes the formatting codes in the string with the actual values for the number
of updates ( count ) and the current update time ( dateString ):
views.setTextViewText(R.id.appwidget_update,
context.getResources().getString(
R.string.date_count_format, count, dateString));
10. After constructing the RemoteViews object and before requesting the update from the
app widget manager, put the current update count back into shared preferences:
As with the earlier lines where you retrieved the count from the shared preferences,
here you store the count with a key that includes COUNT_KEY and the app widget ID, to
differentiate between different counts.
11. Compile and run the app. As before, make sure you remove any existing widgets.
66
Introduction
12. Add two widgets to the home screen, at least one minute apart. The widgets will have
the same update count (1) but different update times.
At each half an hour interval after you add an app widget to the home screen, the
Android app widget manager sends an update broadcast intent. Your widget provider
accepts that intent, increments the count and updates the time for each widget. You
could wait half an hour to see the widgets update itself, but in the next section you add a
button that manually triggers an update.
The automatic update interval is timed from the first instance of the widget you placed
on the home screen. Once that first widget receives an update request, then all the
widget instances are updated at the same time.
1. In the widget layout file, add a Button element just below the second LinearLayout ,
and give it these attributes:
Attribute Value
android:id "@+id/button_update"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:layout_below "@+id/section_update"
android:layout_centerHorizontal "true"
style "@style/AppWidgetButton"
67
Introduction
Again, you may need to enlarge the widget in the Design tab.
The new intent is an explicit intent with the widget-provider class ( NewAppWidget.class )
as the target component.
4. After the lines to create the intent, create an array of integers with only one element: the
current app widget ID.
5. Add an intent extra with the key AppWidgetManager.EXTRA_APPWIDGET_IDS , and the array
you just created.
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray);
The intent needs an array of app widget IDs to update. In this case there's only the
current widget ID, but that ID still needs to be wrapped in an array.
You use a pending intent here because the app widget manager sends the broadcast
intent on your behalf.
68
Introduction
7. Set the onClick listener for the button to send the pending intent. Specifically for this
action, the RemoteViews class provides a shortcut method called
setOnClickPendingIntent() .
views.setOnClickPendingIntent(R.id.button_update, pendingUpdate);
TIP: In this step, a single view (the button) sends a pending intent. To have the entire
widget send a pending intent, give an ID to the top-level widget layout view. Specify that
ID as the first argument in the setOnClickPendingIntent() method.
8. Compile and run the app. Remove all existing app widgets from the home screen, and
add a new widget. When you tap the Update now button, both the update count and
9. Add a second widget. Confirm that tapping the Update now button in one widget only
updates that particular widget and not the other widget.
Solution code
Android Studio project: AppWidgetSample
Coding challenge
Note: All coding challenges are optional.
Challenge: Update the widget preview image with a screenshot of the actual app widget.
TIP: The Android emulator includes an app called "Widget Preview" that helps create a
preview image.
Summary
An app widget is a miniature app view that appears on the Android home screen. App
69
Introduction
widgets can be updated periodically with new data. To add app widgets to your app in
Android Studio, use File > New > Widget > AppWidget. Android Studio generates all
the template files you need for your app widget.
The app widget provider-info file is in res/xml/ . This file defines several properties of
your app widget, including its layout file, default size, configuration activity (if any),
preview image, and periodic update frequency.
The app widget provider is a Java class that extends AppWidgetProvider , and
implements the behavior for your widget—primarily handling managing widget update
requests. The AppWidgetProvider class in turn inherits from BroadcastReceiver .
Because widgets are broadcast receivers, the widget provider is defined as a broadcast
receiver in the AndroidManifest.xml file with the <receiver> tag.
App widget layouts are based on remote views , which are view hierarchies that can be
displayed outside an app. Remote views provide a limited subset of the available
Android layouts and views.
When placed on the home screen, app widgets take up a certain number of cells on a
grid. The cells correspond to a specific minimum width and height defined in the widget
provider file. The rule for how many dp fit into a grid cell is based on the equation 70 ×
grid_size − 30, where grid_size is the number of cells you want your widget to take
1 40 dp
2 110 dp
3 180 dp
4 250 dp
App widgets can receive periodic requests to be updated through a broadcast intent. An
app widget provider is a broadcast receiver which accepts those intents. To update an
app widget, implement the onUpdate() method in your widget provider.
The user may have multiple instances of your widget installed. The onUpdate() method
should update all the available widgets by iterating over an array of widget IDs.
The app widget layout is updated for new data in the onUpdate() method by rebuilding
the widget's layout views ( RemoteViews ), and passing that RemoteViews object to the
app widget manager.
App widget actions are pending intents. Use the onClickPendingIntent() method to
attach an app widget action to a view.
Widget updates can be requested with a pending intent whose intent has the action
AppWidgetManager.ACTION_APPWIDGET_UPDATE . An extra,
70
Introduction
Related concept
The related concept documentation is in App Widgets.
Learn more
App Widgets
App widget design guidelines
Determining a size for your widget
AppWidgetProvider class
AppWidgetProviderInfo class
AppWidgetManager class
BroadcastReceiver class
RemoteViews class
71
Introduction
Introduction
What you should already KNOW
What you will LEARN
What you will DO
App overview
Task 1. List the available sensors
Task 2. Get sensor data
Solution code
Coding challenge
Summary
Related concept
Learn more
Many Android-powered devices include built-in sensors that measure motion, orientation,
and environmental conditions such as ambient light or temperature. These sensors can
provide data to your app with high precision and accuracy. Sensors can be used to monitor
three-dimensional device movement or positioning, or to monitor changes in the
environment near a device, such as changes to temperature or humidity. For example, a
game might track readings from a device's accelerometer sensor to infer complex user
gestures and motions, such as tilt, shake, or rotation.
In this practical you learn about the Android sensor framework, which is used to find the
available sensors on a device and retrieve data from those sensors.
The device camera, fingerprint sensor, microphone, and GPS (location) sensor all have their
own APIs and are not considered part of the Android sensor framework.
72
Introduction
Query the sensor manager for available sensors, and retrieve information about specific
sensors.
Register listeners for sensor data.
React to incoming sensor data.
App overview
You will build two apps in this practical. The first app lists the available sensors on the device
or emulator. The list of sensors is scrollable, if it is too big to fit the screen.
73
Introduction
74
Introduction
The second app, modified from the first, gets data from the ambient light and proximity
sensors, and displays that data. Light and proximity sensors are some of the most common
Android device sensors.
75
Introduction
76
Introduction
android:layout_margin="16dp"
Attribute Value
android:layout_width "match_parent"
android:layout_height "match_parent"
app:layout_constraintBottom_toBottomOf "parent"
app:layout_constraintTop_toTopOf "parent"
app:layout_constraintLeft_toLeftOf "parent"
app:layout_constraintRight_toRightOf "parent"
The ScrollView is here to allow the list of sensors to scroll if it is longer than the
screen.
6. Add a TextView element inside the ScrollView and give it these attributes:
Attribute Value
android:id "@+id/sensor_list"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:text "(placeholder)"
This TextView holds the list of sensors. The placeholder text is replaced at runtime by
the actual sensor list. The layout for your app should look like this screenshot:
77
Introduction
78
Introduction
7. Open MainActivity and add a variable at the top of the class to hold an instance of
SensorManager :
The sensor manager is a system service that lets you access the device sensors.
8. In the onCreate() method, below the setContentView() method, get an instance of the
sensor manager from system services, and assign it to the mSensorManager variable:
mSensorManager =
(SensorManager) getSystemService(Context.SENSOR_SERVICE);
9. Get the list of all sensors from the sensor manager. Store the list in a List object
whose values are of type Sensor :
List<Sensor> sensorList =
mSensorManager.getSensorList(Sensor.TYPE_ALL);
The Sensor class represents an individual sensor and defines constants for the
available sensor types. The Sensor.TYPE_ALL constant indicates all the available
sensors.
10. Iterate over the list of sensors. For each sensor, get that sensor's official name with the
getName() method, and append that name to the sensorText string. Each line of the
sensor list is separated by the value of the line.separator property, typically a newline
character:
11. Get a reference to the TextView for the sensor list, and update the text of that view with
the string containing the list of sensors:
79
Introduction
Different Android devices have different sensors available, which means the SensorSurvey
app shows different results for each device. In addition, the Android emulator includes a
small set of simulated sensors.
1. Run the app on a physical device. The output of the app looks something like this
screenshot:
80
Introduction
81
Introduction
In this list, lines that begin with a letter/number code represent physical hardware in the
device. The letters and numbers indicate sensor manufacturers and model numbers. In
most devices the accelerometer, gyroscope, and magnetometer are physical sensors.
Lines without letter/number codes are virtual or composite sensors, that is, sensors that
are simulated in software. These sensors use the data from one or more physical
sensors. So, for example, the gravity sensor may use data from the accelerometer,
gyroscope, and magnetometer to provide the direction and magnitude of gravity in the
device's coordinate system.
2. Run the app in an emulator. The output of the app looks something like this screenshot:
Because the Android emulator is a simulated device, all the available sensors are virtual
sensors. "Goldfish" is the name of the emulator's Linux kernel.
3. Click the More button (three horizontal dots) on the emulator's control panel. The
Extended Controls window appears.
82
Introduction
This window shows the settings and current values for the emulator's virtual sensors.
Drag the image of the device to simulate motion and acceleration with the
accelerometer. Dragging the device image may also rotate the main emulator window.
83
Introduction
This tab shows the other available virtual sensors for the emulator, including the light,
temperature, and proximity sensors. You use more of these sensors in the next task.
The light sensor measures ambient light in lux, a standard unit of illumination. The light
sensor typically is used to automatically adjust screen brightness.
The proximity sensor measures when the device is close to another object. The
proximity sensor is often used to turn off touch events on a phone's screen when you
answer a phone call, so that touching your phone to your face does not accidentally
launch apps or otherwise interfere with the device's operation.
Attribute Value
android:id "@+id/label_light"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
app:layout_constraintLeft_toLeftOf "parent"
app:layout_constraintTop_toBottomOf "parent"
The "%1$.2f" part of the text string is a placeholder code. This code will be replaced in
the Java code for your app with the placeholder filled in with an actual numeric value. In
this case the placeholder code has three parts:
%1 : The first placeholder. You could include multiple placeholders in the same
84
Introduction
4. Copy and paste the TextView element. Change the attributes in the following table.
Extract the string into a resource called "label_proximity" . This text view will print
values from the proximity sensor.
Attribute Value
android:id "@+id/label_proximity"
app:layout_constraintTop_toBottomOf "@+id/label_light"
The layout for your app should look like this screenshot:
85
Introduction
86
Introduction
You'll use this message in the next task when you test if a sensor is available.
1. Open MainActivity and add private member variables at the top of the class to hold
Sensor objects for the light and proximity sensors. Also add private member variables
2. In the onCreate( ) method, delete all the existing code after the line to get the sensor
manager.
3. Add code to onCreate() to get the two TextView views and assign them to their
respective variables:
4. Get instances of the default light and proximity sensors. These will be instances of the
Sensor class. Assign them to their respective variables:
mSensorProximity =
mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
mSensorLight = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
The getDefaultSensor( ) method is used to query the sensor manager for sensors of a
given type. The sensor types are defined by the Sensor class. If there is no sensor
available for the given type, the getDefaultSensor() method returns null .
5. Get the error string you defined earlier from the strings.xml resource:
87
Introduction
6. Test that there is an available light sensor. If the sensor is not available (that is, if
getDefaultSensor() returned null ), set the display text for the light sensor's
if (mSensorLight == null) {
mTextSensorLight.setText(sensor_error);
}
Different devices have different sensors, so it is important that your app check that a
sensor exists before using the sensor. If a sensor is not available, your app should turn
off features that use that sensor and provide helpful information to the user. If your app's
functionality relies on a sensor that is not available, your app should provide a message
and gracefully quit. Do not assume that any device will have any given sensor.
if (mSensorProximity == null) {
mTextSensorProximity.setText(sensor_error);
}
handle the new sensor data in an onSensorChanged() callback. All of these tasks are part of
the SensorEventListener interface.
In this task, you register listeners for changes to the light and proximity sensors. You
process new data from those sensors and display that data in the app layout.
1. At the top of the class, modify the class signature to implement the
SensorEventListener interface.
2. Click the red light bulb icon, select "implement methods," and select all methods.
The SensorEventListener interface includes two callback methods that enable your app
to handle sensor events:
88
Introduction
onSensorChanged() : Called when new sensor data is available. You will use this
react to that change. Most sensors, including the light and proximity sensors, do not
report accuracy changes. In this app, you leave onAccuracyChanged() empty.
3. Override the onStart() activity lifecycle method to register your sensor listeners.
Listening to incoming sensor data uses device power and consumes battery life. Don't
register your listeners in onCreate() , as that would cause the sensors to be on and
sending data (using device power) even when your app was not in the foreground. Use
the onStart() and onStop() methods to register and unregister your sensor listeners.
@Override
protected void onStart() {
super.onStart();
if (mSensorProximity != null) {
mSensorManager.registerListener(this, mSensorProximity,
SensorManager.SENSOR_DELAY_NORMAL);
}
if (mSensorLight != null) {
mSensorManager.registerListener(this, mSensorLight,
SensorManager.SENSOR_DELAY_NORMAL);
}
}
Note: The onStart() and onStop() methods are preferred over onResume() and
onPause() to register and unregister listeners. As of Android 7.0 (API 24), apps can run
An app or activity Context . You can use the current activity ( this ) as the context.
The Sensor object to listen to.
A delay constant from the SensorManager class. The delay constant indicates how
quickly new data is reported from the sensor. Sensors can report a lot of data very
quickly, but more reported data means that the device consumes more power.
Make sure that your listener is registered with the minimum amount of new data it
needs. In this example you use the slowest value
( SensorManager.SENSOR_DELAY_NORMAL ). For more data-intensive apps such as
89
Introduction
4. Implement the onStop() lifecycle method to unregister your sensor listeners when the
app pauses:
@Override
protected void onStop() {
super.onStop();
mSensorManager.unregisterListener(this);
}
The sensor event stores the new data from the sensor in the values array. Depending
on the sensor type, this array may contain a single piece of data or a multidimensional
array full of data. For example, the accelerometer reports data for the x -axis, y -axis,
and z -axis for every change in the values[0] , values[1] , and values[2] positions.
Both the light and proximity sensors only report one value, in values[0] .
7. Add a switch statement for the sensorType variable. Add a case for
Sensor.TYPE_LIGHT to indicate that the event was triggered by the light sensor.
90
Introduction
switch (sensorType) {
// Event came from the light sensor.
case Sensor.TYPE_LIGHT:
// Handle light sensor
break;
default:
// do nothing
}
8. Inside the light sensor case , get the template string from the resources, and update
the value in the light sensor's TextView .
mTextSensorLight.setText(getResources().getString(
R.string.label_light, currentValue));
When you defined this TextView in the layout, the original string resource included a
placeholder code, like this:
When you call getString() to get the string from the resources, you include values to
substitute into the string where the placeholder codes are. The part of the string that is
not made up of placeholders ( "Light Sensor: " ) is passed through to the new string.
case Sensor.TYPE_PROXIMITY:
mTextSensorProximity.setText(getResources().getString(
R.string.label_proximity, currentValue));
break;
91
Introduction
92
Introduction
2. Move the device towards a light source, or shine a flashlight on it. Move the device
away from the light or cover the device with your hand. Note how the light sensor
reports changes in the light level.
TIP: The light sensor is often placed on the top right of the device's screen.
The light sensor's value is generally measured in lux, a standard unit of illumination.
However, the lux value that a sensor reports may differ across different devices, and the
maximum may vary as well. If your app requires a specific range of values for the light
sensor, you must translate the raw sensor data into something your app can use.
3. Move your hand toward the device, and then move it away again. Note how the
proximity sensor reports values indicating "near" and "far." Depending on how the
proximity sensor is implemented, you may get a range of values, or you may get just
two values (for example, 0 and 5) to represent near and far.
TIP: The proximity sensor is often a virtual sensor that gets its data from the light
sensor. For that reason, covering the light sensor may produce changes to the proximity
value.
As with the light sensor, the sensor data for the proximity sensor can vary from device to
device. Proximity values may be a range between a minimum and a maximum. More
often there are only two proximity values, one to indicate "near," and one to indicate
"far." All these values may vary across devices.
4. Run the app in an emulator, and click the More button (three horizontal dots) on the
emulator's control panel to bring up the Extended controls window.
93
Introduction
5. Click Virtual sensors, and then click the Additional sensors tab.
The sliders in this window enable you to simulate changes to sensor data that would
normally come from the hardware sensors. Changes in this window generate sensor
events in the emulator that your app can respond to.
6. Move the sliders for the light and proximity sensors and observe that the values in the
app change as well.
Solution code
Android Studio projects:
SensorSurvey
SensorListeners
Coding challenge
Note: All coding challenges are optional.
Challenge: Modify the SensorListeners app such that:
The background color of the app changes in response to the light level.
94
Introduction
Place an ImageView or Drawable in the layout. Make the image larger or smaller based
on the value that the app receives from the proximity sensor.
Summary
The Android sensor framework provides access to data coming from a set of device
sensors. These sensors include accelerometers, gyroscopes, magnetometers,
barometers, humidity sensors, light sensors, proximity sensors, and so on.
The SensorManager service lets your app access and list sensors and listen for sensor
events ( SensorEvent ). The sensor manager is a system service you can request with
getSystemService() .
The Sensor class represents a specific sensor and contains methods to indicate the
properties and capabilities of a given sensor. It also provides constants for sensor types,
which define how the sensors behave and what data they provide.
Use getSensorList(Sensor.TYPE_ALL) to get a list of all the available sensors.
Use getDefaultSensor() with a sensor type to gain access to a particular sensor as a
Sensor object.
Sensors provide data through a series of sensor events. A SensorEvent object includes
information about the sensor that generated it, the time, and new data. The data a
sensor provides depends on the sensor type. Simple sensors such as light and
proximity sensors report only one data value, whereas motion sensors such as the
accelerometer provide multidimensional arrays of data for each event.
Your app uses sensor listeners to receive sensor data. Implement the
SensorEventListener interface to listen for sensor events.
Use the onSensorChanged() method to handle individual sensor events. From the
SensorEvent object passed into that method, you can get the sensor that generated the
Related concept
The related concept documentation is in Sensor Basics.
95
Introduction
Learn more
Android developer documentation:
Sensors Overview
Sensor
SensorEvent
SensorManager
SensorEventListener
96
Introduction
Introduction
What you should already KNOW
What you will LEARN
What you will DO
App overview
Task 1. Build the TiltSpot app
Task 2. Add the spots
Task 3. Handle activity rotation
Solution code
Coding challenge
Summary
Related concept
Learn more
The Android platform provides several sensors that enable your app to monitor the motion or
position of a device, in addition to other sensors such as the light sensor.
Motion sensors such as the accelerometer or gyroscope are useful for monitoring device
movement such as tilt, shake, rotation, or swing. Position sensors are useful for determining
a device's physical position in relation to the Earth. For example, you can use a device's
geomagnetic field sensor to determine its position relative to the magnetic north pole.
A common use of motion and position sensors, especially for games, is to determine the
orientation of the device, that is, the device's bearing (north/south/east/west) and tilt. For
example, a driving game could allow the user to control acceleration with a forward tilt or
backward tilt, and control steering with a left tilt or right tilt.
97
Introduction
App overview
The TiltSpot app displays the device orientation angles as numbers and as colored spots
along the four edges of the device screen. There are three components to device
orientation:
98
Introduction
When you tilt the device, the spots along the edges that are tilted up become darker.
The initial layout for the TiltSpot app includes several text views to display the device
orientation angles (azimuth, pitch, and roll)—you learn more about how these angles
work later in the practical. All those textviews are nested inside their own constraint
99
Introduction
layout to center them both horizontally and vertically within in the activity. You need the
nested constraint layout because later in the practical you add the spots around the
edges of the screen and around this inner text view.
3. Open MainActivity .
MainActivity in this starter app contains much of the skeleton code for managing sensors
This method gets an instance of the SensorManager service, and then uses the
getDefaultSensor() method to retrieve specific sensors. In this app those sensors are
The accelerometer measures acceleration forces on the device; that is, it measures how
fast the device is accelerating, and in which direction. Acceleration force includes the
force of gravity. The accelerometer is sensitive, so even when you think you're holding
the device still or leaving it motionless on a table, the accelerometer is recording minute
forces, either from gravity or from the environment. This makes the data generated by
the accelerometer very "noisy."
The magnetometer , also known as the geomagnetic field sensor , measures the
strength of magnetic fields around the device, including Earth's magnetic field. You can
use the magnetometer to find the device's position with respect to the external world.
However, magnetic fields can also be generated by other devices in the vicinity, by
external factors such as your location on Earth (because the magnetic field is weaker
toward the equator), or even by solar winds.
Neither the accelerometer nor the magnetometer alone can determine device tilt or
orientation. However, the Android sensor framework can combine data from both
sensors to get a fairly accurate device orientation—accurate enough for the purposes of
this app, at least.
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
This line locks the activity in portrait mode, to prevent the app from automatically
rotating the activity as you tilt the device. Activity rotation and sensor data can interact in
unexpected ways. Later in the practical, you explicitly handle sensor data changes in
your app in response to activity rotation, and remove this line.
100
Introduction
3. Examine the onStart() and onStop() methods. The onStart() method registers the
listeners for the accelerometer and magnetometer, and the onStop() method
unregisters them.
1. Open MainActivity .
2. Add member variables to hold copies of the accelerometer and magnetometer data.
When a sensor event occurs, both the accelerometer and the magnetometer produce
arrays of floating-point values representing points on the x -axis, y -axis, and z -axis of
the device's coordinate system. You will combine the data from both these sensors, and
over several calls to onSensorChanged() , so you need to retain a copy of this data each
time it changes.
3. Scroll down to the onSensorChanged() method. Add a line to get the sensor type from
the sensor event object:
4. Add tests for the accelerometer and magnetometer sensor types, and clone the event
data into the appropriate member variables:
101
Introduction
switch (sensorType) {
case Sensor.TYPE_ACCELEROMETER:
mAccelerometerData = sensorEvent.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
mMagnetometerData = sensorEvent.values.clone();
break;
default:
return;
}
You use the clone() method to explicitly make a copy of the data in the values array.
The SensorEvent object (and the array of values it contains) is reused across calls to
onSensorChanged() . Cloning those values prevents the data you're currently interested
in from being changed by more recent data before you're done with it.
A rotation matrix is a linear algebra term that translates the sensor data from one
coordinate system to another—in this case, from the device's coordinate system to the
Earth's coordinate system. That matrix is an array of nine float values, because each
point (on all three axes) is expressed as a 3D vector.
The x -axis is horizontal and points to the right edge of the device.
The y -axis is vertical and points to the top edge of the device.
The z -axis extends up from the surface of the screen. Negative z values are
102
Introduction
The y -axis points to magnetic north along the surface of the Earth.
The x -axis is 90 degrees from y , pointing approximately east.
The z -axis extends up into space. Negative z extends down into the ground.
A reference to the array for the rotation matrix is passed into the getRotationMatrix()
method and modified in place. The second argument to getRotationMatrix() is an
inclination matrix, which you don't need for this app. You can use null for this argument.
6. Call the SensorManager.getOrientation() method to get the orientation angles from the
rotation matrix. As with getRotationMatrix() , the array of float values containing
those angles is supplied to the getOrientation() method and modified in place.
103
Introduction
The angles returned by the getOrientation() method describe how far the device is
oriented or tilted with respect to the Earth's coordinate system. There are three
components to orientation:
7. Create variables for azimuth, pitch, and roll, to contain each component of the
orientationValues array. You adjust this data later in the practical, which is why it is
8. Get the placeholder strings, from the resources, fill the placeholder strings with the
orientation angles and update all the text views.
mTextSensorAzimuth.setText(getResources().getString(
R.string.value_format, azimuth));
mTextSensorPitch.setText(getResources().getString(
R.string.value_format, pitch));
mTextSensorRoll.setText(getResources().getString(
R.string.value_format, roll));
<string name="value_format">%1$.2f</string>
104
Introduction
1. Run the app. Place your device flat on the table. The output of the app looks something
like this:
Even a motionless device shows fluctuating values for the azimuth, pitch, and roll. Note
also that even though the device is flat, the values for pitch and roll may not be 0. This
is because the device sensors are extremely sensitive and pick up even tiny changes to
the environment, both changes in motion and changes in ambient magnetic fields.
2. Turn the device on the table from left to right, leaving it flat on the table.
Note how the value of the azimuth changes. An azimuth value of 0 indicates that the
device is pointing (roughly) north.
105
Introduction
Note that even if the value of the azimuth is 0, the device may not be pointing exactly
north. The device magnetometer measures the strength of any magnetic fields, not just
that of the Earth. If you are in the presence of other magnetic fields (most electronics
emit magnetic fields, including the device itself), the accuracy of the magnetometer may
not be exact.
Note: If the azimuth on your device seems very far off from actual north, you can
calibrate the magnetometer by waving the device a few times in the air in a figure-eight
motion.
3. Lift the bottom edge of the device so the screen is tilted away from you. Note the
change to the pitch value. Pitch indicates the top-to-bottom angle of tilt around the
106
Introduction
4. Lift the left side of the device so that it is tilted to the right. Note the change to the roll
value. Roll indicates the left-to-right tilt along the device's vertical axis.
5. Pick up the device and tilt it in various directions. Note the changes to the pitch and roll
values as the device's tilt changes. What is the maximum value you can find for any tilt
direction, and in what device position does that maximum occur?
The color changes in the spots rely on dynamically changing the alpha value of a shape
drawable in response to new sensor data. The alpha determines the opacity of that
drawable, so that smaller alpha values produce a lighter shape, and larger values produce a
darker shape.
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/black"/>
</shape>
<dimen name="spot_size">84dp</dimen>
107
Introduction
Attribute Value
android:id "@+id/spot_top"
android:layout_width "@dimen/spot_size"
android:layout_height "@dimen/spot_size"
android:layout_margin "@dimen/base_margin"
app:layout_constraintLeft_toLeftOf "parent"
app:layout_constraintRight_toRightOf "parent"
app:layout_constraintTop_toTopOf "parent"
app:srcCompat "@drawable/spot"
tools:ignore "ContentDescription"
This view places a spot drawable the size of the spot_size dimension at the top edge
of the screen. Use the app:srcCompat attribute for a vector drawable in an ImageView
(versus android:src for an actual image.) The app:srcCompat attribute is available in
the Android Support Library and provides the greatest compatibility for vector
drawables.
5. Add the following code below that first ImageView . This code adds the other three spots
along the remaining edges of the screen.
108
Introduction
<ImageView
android:id="@+id/spot_bottom"
android:layout_width="@dimen/spot_size"
android:layout_height="@dimen/spot_size"
android:layout_marginBottom="@dimen/base_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:srcCompat="@drawable/spot"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/spot_right"
android:layout_width="@dimen/spot_size"
android:layout_height="@dimen/spot_size"
android:layout_marginEnd="@dimen/base_margin"
android:layout_marginRight="@dimen/base_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/spot"
tools:ignore="ContentDescription"/>
<ImageView
android:id="@+id/spot_left"
android:layout_width="@dimen/spot_size"
android:layout_height="@dimen/spot_size"
android:layout_marginLeft="@dimen/base_margin"
android:layout_marginStart="@dimen/base_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/spot"
tools:ignore="ContentDescription" />
109
Introduction
6. Add the android:alpha attribute to all four ImageView elements, and set the value to
"0.05" . The alpha is the opacity of the shape. Smaller values are less opaque (less
visible). Setting the value to 0.05 makes the shape very nearly invisible, but you can still
see them in the layout view.
1. In MainActivity , add member variables at the top of the class for each of the spot
ImageView objects:
2. In onCreate() , just after initializing the text views for the sensor data, initialize the spot
views:
110
Introduction
3. In onSensorChanged() , right after the lines that initialize the azimuth, pitch, and roll
variables, reset the pitch or roll values that are close to 0 (less than the value of the
VALUE_DRIFT constant) to be 0:
When you initially ran the TiltSpot app, the sensors reported very small non-zero values
for the pitch and roll even when the device was flat and stationary. Those small values
can cause the app to flash very light-colored spots on all the edges of the screen. In this
code if the values are close to 0 (in either the positive or negative direction), you reset
them to 0.
4. Scroll down to the end of onSensorChanged() , and add these lines to set the alpha of all
the spots to 0. This resets all the spots to be invisible each time onSensorChanged() is
called. This is necessary because sometimes if you tilt the device too quickly, the old
values for the spots stick around and retain their darker color. Resetting them each time
prevents these artifacts.
mSpotTop.setAlpha(0f);
mSpotBottom.setAlpha(0f);
mSpotLeft.setAlpha(0f);
mSpotRight.setAlpha(0f);
5. Update the alpha value for the appropriate spot with the values for pitch and roll.
if (pitch > 0) {
mSpotBottom.setAlpha(pitch);
} else {
mSpotTop.setAlpha(Math.abs(pitch));
}
if (roll > 0) {
mSpotLeft.setAlpha(roll);
} else {
mSpotRight.setAlpha(Math.abs(roll));
}
Note that the pitch and roll values you calculated in the previous task are in radians, and
their values range from -π to +π. Alpha values, on the other hand, range only from 0.0
to 1.0. You could do the math to convert radian units to alpha values, but you may have
111
Introduction
noted earlier that the higher pitch and roll values only occur when the device is tilted
vertical or even upside down. For the TiltSpot app you're only interested in displaying
dots in response to some device tilt, not the full range. This means that you can
conveniently use the radian units directly as input to the alpha.
You should now be able to tilt the device and have the edge facing "up" display a dot
which becomes darker the further up you tilt the device.
You may assume that with TiltSpot, if you rotate the device from landscape to portrait, the
sensors will report the correct data for the new device orientation, and the spots will continue
to appear on the correct edges. That's not the case. When the activity rotates, the activity
drawing coordinate system rotates with it, but the sensor coordinate system remains the
same. The sensor coordinate system never changes position, regardless of the orientation
of the device.
The second tricky point for handling activity rotation is that the default or natural orientation
for your device may not be portrait. The default orientation for many tablet devices is
landscape. The sensor's coordinate system is always based on the natural orientation of a
device.
The TiltSpot starter app included a line in onCreate( ) to lock the orientation to portrait
mode:
setRequestedOrientation (ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
Locking the screen to portrait mode in this way solves one problem—it prevents the
coordinate systems from getting out of sync on portrait-default devices. But on landscape-
default devices, the technique forces an activity rotation, which causes the device and
sensor-coordinate systems to get out of sync.
112
Introduction
Here's the right way to handle device and activity rotation in sensor-based drawing: First,
use the Display.getRotation() method to query the current device orientation. Then use the
SensorManager.remapCoordinateSystem() method to remap the rotation matrix from the sensor
data onto the correct axes. This is the technique you use in the TiltSpot app in this task.
The getRotation() method returns one of four integer constants, defined by the Surface
class:
ROTATION_90 : The "sideways" orientation of the device (landscape for phones). Different
Note that many devices do not have ROTATION_180 at all or return ROTATION_90 or
ROTATION_270 regardless of which direction the device was rotated (clockwise or
counterclockwise). It is best to handle all possible rotations rather than to make assumptions
for any particular device.
//setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
3. At the end of onCreate() , get a reference to the window manager, and then get the
default display. You use the display to get the rotation in onSensorChanged() .
5. Get the current device rotation from the display and add a switch statement for that
113
Introduction
value. Use the rotation constants from the Surface class for each case in the switch.
For ROTATION_0 , the default orientation, you don't need to remap the coordinates. You
can just clone the data in the existing rotation matrix:
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0:
rotationMatrixAdjusted = rotationMatrix.clone();
break;
}
6. Add additional cases for the other rotations, and call the
SensorManager.remapCoordinateSystem() method for each of these cases.
This method takes as arguments the original rotation matrix, the two new axes on which
you want to remap the existing x -axis and y -axis, and an array to populate with the
new data. Use the axis constants from the SensorManager class to represent the
coordinate system axes.
case Surface.ROTATION_90:
SensorManager.remapCoordinateSystem(rotationMatrix,
SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X,
rotationMatrixAdjusted);
break;
case Surface.ROTATION_180:
SensorManager.remapCoordinateSystem(rotationMatrix,
SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y,
rotationMatrixAdjusted);
break;
case Surface.ROTATION_270:
SensorManager.remapCoordinateSystem(rotationMatrix,
SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X,
rotationMatrixAdjusted);
break;
7. Modify the call to getOrientation() to use the new adjusted rotation matrix instead of
the original matrix.
SensorManager.getOrientation(rotationMatrixAdjusted,
orientationValues);
8. Build and run the app again. The colors of the spots should now change on the correct
edges of the device, regardless of how the device is rotated.
Solution code
114
Introduction
Coding challenge
Note: All coding challenges are optional.
Challenge: A general rule is to avoid doing a lot of work in the onSensorChanged() method,
because the method runs on the main thread and may be called many times per second. In
particular, the changes to the colors of the spot can look jerky if you're trying to do too much
work in onSensorChanged() . Rewrite onSensorChanged() to use an AsyncTask object for all
the calculations and updates to views.
Summary
Motion sensors such as the accelerometer measure device movement such as tilt,
shake, rotation, or swing.
Position sensors such as the geomagnetic field sensor (magnetometer) can determine
the device's position relative to the Earth.
The accelerometer measures device acceleration, that is, how much the device is
accelerating and in which direction. Acceleration forces on the device include the force
of gravity.
The magnetometer measures the strength of magnetic fields around the device. This
includes Earth's magnetic field, although other fields nearby may affect sensor readings.
You can use combined data from motion and position sensors to determine the device's
orientation (its position in space) more accurately than with individual sensors.
The 3-axis device-coordinate system that most sensors use is relative to the device
itself in its default orientation. The y -axis is vertical and points toward the top edge of
the device, the x -axis is horizontal and points to the right edge of the device, and the z -
axis extends up from the surface of the screen.
The Earth's coordinate system is relative to the surface of the Earth, with the y -axis
pointing to magnetic north, the x -axis 90 degrees from y and pointing east, and the z -
axis extending up into space.
Orientation angles describe how far the device is oriented or tilted with respect to the
Earth's coordinate system. There are three components to orientation:
Azimuth : The direction (north/south/east/west) the device is pointing. 0 is magnetic
north.
Pitch : The top-to-bottom tilt of the device. 0 is flat.
Roll : The left-to-right tilt of the device. 0 is flat.
To determine the orientation of the device:
Use the SensorManager.getRotationMatrix() method. The method combines data from
115
Introduction
the accelerometer and magnetometer and translates the data into the Earth's
coordinate system.
Use the SensorManager.getOrientation() method with a rotation matrix to get the
orientation angles of the device.
The alpha value determines the opacity of a drawable or view. Lower alpha values
indicate more transparency. Use the setAlpha() method to programmatically change
the alpha value for a view.
When Android automatically rotates the activity in response to device orientation, the
activity coordinate system also rotates. However, the device-coordinate system that the
sensors use remains fixed.
The default device-coordinate system sensors use is also based on the natural
orientation of the device, which may not be "portrait" or "landscape."
Query the current device orientation with the Display.getRotation() method.
Use the current device orientation to remap the coordinate system in the right
orientation with the SensorManager.remapCoordinateSystem() method.
Related concept
The related concept documentation is in Motion and position sensors.
Learn more
Android developer documentation:
Sensors Overview
Motion Sensors
Position Sensors
Sensor
SensorEvent
SensorManager
SensorEventListener
Surface
Display
Other:
Accelerometer Basics
Sensor fusion and motion prediction (written for VR, but many of the basic concepts
116
Introduction
117
Introduction
Every app is different and could have different performance issues. While there are best
practices, you need to analyze the unique configuration of your app to find and fix
performance issues.
To discover what causes your app's specific performance problems, use tools to collect data
about your app's execution behavior. Understand and analyze what you see, then improve
your code. Android Studio and your device provide profiling tools to record and visualize the
rendering, compute, memory, and battery performance of your app.
In this practical, you use the Profile GPU Rendering tool on your device to visualize how
long it takes an app to draw frames to the screen.
Important: To run the Profile GPU Rendering tool and complete this practical exercise, you
need a physical or virtual device running at least Android 4.1 (API level 16) with Developer
Options turned on.
Your app must consistently draw the screen fast enough for your users to see smooth, fluid
motions, transitions, and responses. To this end, typically, modern devices strive to display
60 frames per second (a frame rate of 60 FPS), making 16 milliseconds available to each
frame. Therefore, your app has to do its work in 16 milliseconds or less for every screen
refresh.
Important: For real-world performance tuning, run your app and the tools on a physical
device, not an emulator, as the data you get from running on an emulator may not be
accurate. Always test on the lowest-end device that your app is targeting. If you do not have
a physical device, you can use an emulator to get an idea of how your app is performing,
even though some performance data from the emulator may not be accurate.
118
Introduction
As a refresher, watch the Invalidation, Layouts, and Performance video for an overview of
the Android rendering pipeline. Read the Rendering and Layout concept chapter as well as
How Android Draws Views.
Use the Profile GPU Rendering tool to visualize Android drawing the screen.
App overview
119
Introduction
For the first part of this practical, use the RecyclerView app.
For the second part of this practical, you build an app that loads images, LargeImages,
to see how image size affects performance. In a later practical, you learn more about
using images correctly.
The following screenshot shows one way of implementing the LargeImages app.
120
Introduction
121
Introduction
The CPU computes display lists. Each display list is a series of graphics commands that
define an output image.
The GPU (graphics processing unit) renders images to the display.
The memory stores your app's images and data, among other things.
The battery provides electrical power for all the hardware on the device.
Each of these pieces of hardware has constraints. Stressing or exceeding those constraints
can cause your app to be slow, have bad display performance, or exhaust the battery.
The Profile GPU Rendering tool gives you a quick visual representation of how much time it
takes to render the frames of a UI window relative to the 16-ms-per-frame benchmark. The
tool shows a colored bar for each frame. Each bar has colored segments indicating the
relative time that each stage of the rendering pipeline takes, as shown in the screenshot
below.
122
Introduction
123
Introduction
1. Open the Settings or Preferences dialog: On Windows or Linux, select File > Settings
from the menu bar. On Mac OSX, select Android Studio > Preferences from the menu
bar.
2. Navigate to Build, Execution, Deployment > Instant Run.
3. Deselect the Restart activity on code changes checkbox.
RecyclerView is an efficient way of displaying information, and on most devices and recent
versions of Android, the app performs smoothly. What users perceive and how they
experience the app are your ultimate benchmarks for performance.
124
Introduction
Go to Settings > Developer options and follow the steps as illustrated in the screenshot
below.
This is not an absolute requirement. For example, when you first load an image, the bar
may go above the green line, but users may not notice a problem, because they may
expect to wait a moment for an image to load. Having some tall bars does not mean
your app has a performance problem. However, if your app does have a performance
problem, tall bars can give you a hint as to where to start looking.
125
Introduction
The colors in each bar represent the different stages in rendering the frame. The
colored sections visualize the stages and their relative times. The table below gives an
explanation.
The image below shows the bars and color legend for a device that is running Android 6.0 or
higher. The bars for older versions use different coloring. See the Profile GPU Rendering
Walkthrough for the bar legend for older versions.
The following table shows the component bars in Android 6.0 and higher.
126
Introduction
Rendering
Color Description
stage
Represents the time the CPU is waiting for the GPU to finish its
Swap
work. If this part of the bar is tall, the app is doing too much
buffers
work on the GPU.
Represents the time spent by Android's 2-D renderer as it
issues commands to OpenGL to draw and redraw display lists.
Command
The height of this part of the bar is directly proportional to the
issue
sum of the time it takes for all the display lists to execute—
more display lists equals a taller red segment of the bar.
Represents the time it take to upload bitmap information to the
Sync and
GPU. If this part of the bar is tall, the app is taking considerable
upload
time loading large amounts of graphics.
Represents the time used to create and update the view's
Draw display lists. If this part of the bar is tall, there may be a lot of
custom view drawing, or a lot of work in onDraw methods.
Represents the amount of time spent on onLayout and
Measure onMeasure callbacks in the view hierarchy. A large segment
and layout indicates that the view hierarchy is taking a long time to
process.
Represents the time spent evaluating animators. If this part of
the bar is tall, your app could be using a custom animator that's
Animation
not performing well, or unintended work is happening as a
result of properties being updated.
Represents the time that the app spent executing code inside
Input of an input event callback. If this part of the bar is tall, the app is
handling spending too much time processing user input. Consider
offloading such processing to a different thread.
Represents the time that the app spends executing operations
Misc. time between consecutive frames. It might indicate too much
/ Vsync processing happening in the UI thread that could be offloaded
delay to a different thread. (Vsync is a display option found in some
3-D apps.)
For example, if the green Input handling portion of the bar is tall, your app spends a lot of
time handling input events, executing code called as a result of input event callbacks. To
improve this, consider when and how your app requests user input, and whether you can
handle it more efficiently.
Note that RecyclerView scrolling can show up in the Input handling portion of the bar.
RecyclerView scrolls immediately when it processes the touch event. For this reason,
127
Introduction
If you spend time using the Profile GPU Rendering tool on your app, it will help you identify
which parts of the UI interaction are slower than expected, and then you can take action to
improve the speed of your app's UI.
Important: As the Android system and tools improve, recommendations may change. Your
best and most up-to-date source of information on performance is the Android developer
documentation.
128
Introduction
129
Introduction
One way to implement the LargeImages app is to show simple instructions in a TextView
and the large background image that is provided with the solution code.
Do the following:
5. Create a layout with a LinearLayout with a TextView . Set the height and width of the
TextView to match_parent .
9. Run the app with Profile GPU Rendering turned on and tap to swap pictures. When you
load the small image, you should see relatively short bars. When you load the large
130
Introduction
image, there should be relatively tall bars, similar to the ones shown below.
Profile GPU Rendering bars for the large image app are shown in the previous screenshots.
The detail on the left shows the faint short bars for the small image (1) staying below the
green line (2). The screenshot on the right show the bars as they appear on your device
emphasizing the bars that cross the green line (3,4).
1. The narrow, faint bar for the small image is below the green line, so there is no
performance issue.
2. The green horizontal line indicates the 16 millisecond rendering time. Ideally, most bars
should be close to or below this line.
3. After the small image is replaced by the large image, the red bar segment is huge. This
Command issue segment represents the time spent by Android's 2D renderer issuing
commands to draw and redraw display lists.
4. The large image also has a huge orange segment. This Swap buffers segment
represents the time the CPU is waiting for the GPU to finish its work.
The GPU works in parallel with the CPU. The Android system issues draw commands
to the GPU, and then moves on to its next task. The GPU reads those draw commands
from a queue. When the CPU issues commands faster than the GPU consumes them,
the queue between the processors becomes full, and the CPU blocks. The CPU waits
until there is space in the queue to place the next command.
131
Introduction
To mitigate this problem, reduce the complexity of work occurring on the GPU, similar to
what you would do for the "Command issue" phase.
See Analyzing with Profile GPU Rendering for details on each stage.
To fix the app, you would use a different or smaller image. Finding problems can be as
straightforward as this, or it can take additional analysis to pinpoint what is happening in
your app's code.
Troubleshooting
If your app crashes with the images provided for the sample app, you may need to use
smaller images for your device.
If your app crashes on the emulator, edit the emulator configuration in the AVD
Manager. Use your computer's graphics card instead of simulating the device's graphic
software by selecting "Graphics: Hardware - GLES" on the configuration screen.
If you get a failed to find style 'textviewstyle' in current theme or other rendering
error in the Design view in Android Studio, click the refresh link provided with the error
message.
1. Add code to the click handler of the LargeImages app to let your app sleep for two
screen refreshes before switching the background to the smaller image. This means
that instead of refreshing the screen every 16 ms, your app now refreshes every 48 ms
with new content. This will be reflected in the bars displayed by the Profile GPU
Rendering tool.
132
Introduction
2. Run your app. The previously short bars for the small image should now be much taller.
Wait a while before clicking the TextView to get the next image; otherwise, the Android
system may display an ANR.
If your app is only swapping backgrounds, the impact from this change may not be
significant for the user. However, if you are running animated content, skipping two out of
three frames will result in a stuttering and jagged animation.
While Profile GPU Rendering cannot tell you exactly what to fix in your code, it can hint at
where you should start looking. And let's admit it, the on-screen bars are pretty cool!
Solution code
Android Studio project: LargeImages.
Summary
You can use the Profile GPU Rendering tool on your device to visualize the relative
times it takes for your app to render frames on screen.
Because it can be turned on quickly and its results are immediate and graphical, this
tool is a good first step when analyzing potential app performance issues related to
rendering.
The sections of the colored bars can indicate where in your app you might look for
performance problems.
Common reasons for slow drawing are an app taking too long to process user input on
the UI thread, and backgrounds and images that are unnecessary or too large.
Related concepts
The related concept documentation is in Rendering and layout.
Learn more
Profile GPU Rendering Walkthrough
Analyzing with Profile GPU Rendering
How Android Draws Views
Video: Invalidation, Layouts, and Performance
133
Introduction
134
Introduction
You can make your apps interesting and visually compelling by following Material Design
guidelines, creating nested hierarchies of layouts, and using drawables as background
elements for your views. However, with increasing complexity come performance
challenges, and layouts draw faster and use less power and battery if you spend time
designing them in the most efficient way. For example:
Overlapping views result in overdraw , where the app wastes time drawing the same
pixel multiple times, and only the final rendition is visible to the user. Size and organize
your views so that every pixel is only drawn once or twice.
Deeply nested layouts force the Android system to perform more passes to lay out all
the views than if your view hierarchy is flat.
To simplify your view hierarchy, combine, flatten, or eliminate views.
Create apps with Android Studio and run them on a mobile device.
Work with the Developer options settings on a mobile device.
135
Introduction
Run the Debug GPU Overdraw tool to visualize Android drawing to the screen.
Use the Layout Inspector tool to inspect the view hierarchy of an app.
App overview
The StackedViews app arranges three overlapping TextView objects inside a
ConstraintLayout to demonstrate overdraw. While this is a contrived and simplistic
example, overlapping views can easily happen during development as you add more
features and complexity to your app. The screenshot below shows an arrangement of three
overlapping views. View 1 fills the screen. View 2 fills the bottom two thirds of the screen.
View 3 fills the bottom third of the screen to create progressively more overlap.
136
Introduction
137
Introduction
Learn more about overdraw in the Rendering and layout concept chapter.
7. Run the app on your device. It should look similar to the screenshot below, with three
progressively more overlapping views.
138
Introduction
139
Introduction
<android.support.constraint.ConstraintLayout
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_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.android.stackedviews.MainActivity"
android:background="@android:color/background_light">
<TextView
android:id="@+id/view1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/very_light_gray"
android:text="@string/view1"
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/view2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="156dp"
android:background="@color/light_gray"
android:text="@string/view2"
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/view3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="300dp"
android:background="@color/medium_gray"
android:text="@string/view3"
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
140
Introduction
The following key shows which colors indicate how much overdraw.
An app with too much overdraw will have a lot of pink and red coloring, as shown in the
screenshot below.
141
Introduction
A properly laid out app has little overdraw and primarily show true and purple coloring, as
shown in the screenshot below. Some overdraw is unavoidable, for example, when drawing
1. Run the StackedViews app. The screen should look similar to the one below, with
green, pink, and red colors that indicate significant areas of overdraw where the
TextView objects overlap.
142
Introduction
143
Introduction
Important: If it's hard for you to see the difference between the colors that the tool shows,
try adjusting the tool for deuteranomaly:
Compare the colors of the views in this app with the color key shown above, and you see
that View 3 and View 2 are colored for 3x and 4x overdraw. This is because as laid out in
the XML file, you stacked the views on top of each other. A ConstraintLayout with a
background is the bottom layer, and View 1 is drawn on top of that, so View 1 is overdrawn
twice, and so on.
In a real-world app, you would not use a layout like this, but remember that this is a simple
example to demonstrate the tool. Common, realistic examples that produce overdraw
include:
144
Introduction
Fixing overdraw can be as basic as removing invisible views, and as complex as re-
architecting your view hierarchy. To fix overdraw, you usually need to take one or more of the
following actions.
Eliminate unnecessary backgrounds, for example, when a View displays an image that
fills it, the background is completely covered, so it can be removed.
Remove invisible views. That is, remove views that are completely covered by other
views.
In custom views, clip generously. (See Rendering and Layout. You learn about custom
views in another chapter.)
Reduce the use of transparency. For example, instead of using transparency to create a
color, find a color. Instead of using transparency to create a shine-through effect for two
overlapping images, preprocess the two images into one.
Flatten and reorganize the view hierarchy to reduce the number of views. See the
Rendering and Layout concept chapter for examples and best practices.
Resize or rearrange views so they do not overlap.
To reduce overdraw for the StackedViews app, rearrange the views in the app so that they
do not overlap. In the XML code:
145
Introduction
<TextView
android:id="@+id/view1"
...
app:layout_constraintBottom_toTopOf="@+id/view2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/view2"
...
app:layout_constraintBottom_toTopOf="@+id/view3"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view1" />
<TextView
android:id="@+id/view3"
...
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/view2" />
Your final layout should produce output from Debug GPU Overdraw similar to one of the
images below. (The second image shows overdraw for deuteranomaly.) You draw once for
the background, and on top of that, the text.
146
Introduction
147
Introduction
148
Introduction
149
Introduction
In a realistic setting without custom views, overdraw may not be a huge performance
problem for your app since the Android framework optimizes drawing the screen. However,
since overdraw is usually straightforward to find and fix, you should always minimize it so it
does not pollute your other performance data.
Eliminate from your code the views that are completely covered, never displayed, or outside
the screen. This seems obvious, but during development, views that later become
unnecessary can accumulate.
Android Layouts allow you to nest UI objects in the view hierarchy. This nesting can impose
a cost. When your app processes an object for layout, the app performs the same process
on all children of the layout as well.
Keep your view hierarchy flat and efficient by using ConstraintLayout wherever possible.
If your UI has many simple views, you may be able to combine some of them without
diminishing the user experience.
Combining views may affect how you present information to the user and will include
design trade-offs. Opt for simplicity wherever you can.
Reduce the number of views by combining them into fewer views. For example, you
may be able to combine TextView objects if you reduce the number of fonts and styles.
Some layout containers, such a RelativeLayout , require two layout passes in order to
finalize the positions of their child views. As a result, their children also require two layout
passes. When you nest these types of layout containers, the number of layout passes
150
Introduction
increases exponentially with each level of the hierarchy. See the Optimizing View
Hierarchies documentation and the Double Layout Taxation video.
RelativeLayout
Using any of the above view groups as the root of a complex view hierarchy, the parent of a
deep subtree, or using many of them in your layout, can hurt performance. Consider whether
you can achieve the same layout using a view group configuration that does not result in
these exponential numbers of layout passes, such as a ConstraintLayout .
Design view shows you the static layout of an activity as defined in your XML files, but it
does not include any views that you might create, destroy, or move at runtime.
Inspecting the view hierarchy can show you where you might simplify your view hierarchy. In
addition, the Layout Inspector can also help you identify overlapping views. If Debug GPU
Overdraw shows that your app has overdraw, you may need to run Layout Inspector to
identify the exact views involved, or to help you determine which views you might be able to
rearrange.
With Layout Inspector you take a snapshot of the view hierarchy of a running app and
display it for inspection.
1. In your code, change the size of View 2 to completely cover View 3. In the layout for
@id/view3 , set android:layout_marginTop="156dp"
151
Introduction
In the preview, you can only see outlines around View 1 and View 3, which are on top.
Layout Inspector does not connect to your running app. It creates a static capture based on
your code.
You can find the captures Android Studio made in the captures folder of your Android Studio
project. The files use the package name and the date, and have a .li extension (for
example, com.example.android.largimages_2017.4.12_12.01.li ). In Project view, expand the
captures folder and double-click any capture file to show its contents.
Inspecting the view hierarchy, the view arrangement and boundaries, and the properties of
each view can help you determine how to optimize your app's view hierarchy.
Solution code
Android Studio projects:
152
Introduction
StackedViews_fixed – final version of the StackedViews app after the overdraw issues
have been fixed.
Summary
Use the Debug GPU Overdraw tool to identify overlapping views.
Reduce overdraw by removing unused backgrounds and invisible views; use clipping to
reduce overlapping of views.
Use the Layout Inspector to investigate your view hierarchy.
Simplify your view hierarchy by flattening the hierarchy, combining views, eliminating
invisible views, or rearranging your layout.
Related concept
The related concept documentation is in Rendering and layout.
Learn more
Debug GPU Overdraw Walkthrough
Layout Inspector
Reducing Overdraw
Optimizing Layout Hierarchies
Performance and View Hierarchies
Optimizing Your UI
153
Introduction
Systrace (Android System Trace) captures and displays execution times of your app's
processes and other Android system processes, which helps you analyze the performance
of your app. The tool combines data from the Android kernel such as the CPU scheduler,
disk activity, and app threads. The tool uses this data to generate an HTML report that
shows an overall picture of an Android-powered device's system processes for a given
period of time.
Systrace is particularly useful in diagnosing display problems when an app is slow to draw or
stutters while displaying motion or animation.
dumpsys is an Android tool that runs on the device and dumps interesting information about
Both of these powerful tools let you take a detailed look at what is happening when your app
runs. They produce a huge amount of detailed information about the system and apps. This
information helps you create, debug, and improve the performance of your apps.
This practical will help you get started, but fully analyzing Systrace and dumpsys output
takes a lot of experience and is beyond the scope of this practical.
154
Introduction
Create apps with Android Studio and run them on a mobile device.
Work with Developer Options on a mobile device.
Use Systrace to collect data about your app and view its reports.
Run dumpsys to get a snapshot of information about app performance.
App overview
You will not build a new app in this practical. Instead you will use apps from other practicals.
You need the WordListSQL app. Use the version of the WordListSQL app that you built
in the Android Fundamentals Course, or download the app from the
WordListSql_finished folder from GitHub.
You also need the LargeImages app from a previous practical, or you can download the
course version of the app from GitHub.
155
Introduction
To analyze your app with Systrace, you first collect a trace log of your app and the system
activity. The generated trace allows you to view highly detailed, interactive reports showing
everything happening in the system for the traced duration.
You can run the Systrace tool from one of the Android SDK's graphical user interface tools,
or from the command line. The following sections describe how to run the tool using either of
these methods.
1.1 Prerequisites
To run Systrace, you need:
Android SDK Tools 20.0.0 or higher, which you already have if you've followed this
course.
A mobile device running at least Android 4.1 with USB Debugging enabled in Developer
Options. You already have this, if you've followed this course.
You can run on an emulator for practice, but your trace data may not be accurate.
Some devices do not have Systrace enabled on the kernel they are running, and
you will get an error while generating the trace. In this case, you will need to use an
emulator. See this Stack Overflow post if you want to explore deeper.
Python language installed and included in your development computer's execution path.
Check your Python installation from a command-line window:
If you do not have Python installed, follow the instructions for your platform at python.org.
You can also find instructions in this Check for Python documentation.
1. If you don't already have the WordListSQL app, download the WordListSQL app from
the WordListSql_finished folder on GitHub. Open the app in Android Studio and run it
on your device.
2. From Android Studio, choose Tools > Android > Android Device Monitor. If you have
instant run enabled, you may be asked to Disable ADB Integration. Click Yes.
156
Introduction
Important: If you run your app on an emulator, you must restart adb as root in order to
get a trace. Type adb root at the command line.
Follow the steps as illustrated in the screenshot below:
2. Select your app by package name in the Devices tab. (2) in the screenshot below.
3. Click the Systrace button to initiate the trace. (3) in the screenshot below.
4. In the Systrace (Android System Trace) pop-up, choose your settings for the trace.
Destination File: Where the trace is stored as an HTML file. Default is in your
home directory with the filename trace.html .
Trace duration: Default is 5 seconds, and 15-30 seconds is a good time to choose.
Trace Buffer Size: Start with the default.
Enable Application Traces from: Make sure your app is visible and selected.
Now select tags to enable. Choose all the Commonly Used Tags. In the
Advanced Options, choose Power Management and Database. Use fewer tags
to limit the size and complexity of the trace output.
157
Introduction
6. Interact with the WordListSQL app on your device. Create, edit, and delete an item.
7. When time is up, the pop-up closes, and your trace is done.
The following instructions are for Android 4.3 and higher. If you are running on an older
version of Android, follow the instructions in the systrace user guide.
158
Introduction
1. In Android Studio, open the Terminal pane, for example, by selecting View > Tool
Windows > Terminal.
2. In the terminal, change directory to where Systrace is located in the Android SDK: cd
< android-sdk >/platform-tools/systrace
If you need to find the directory where the Android SDK is installed: In Android Studio,
choose Android Studio > Preferences and search for "sdk". Alternatively, open Tools
> Android > SDK Manager. The top of the pane has a text field that shows the Android
SDK location.
This runs systrace for 10 seconds, with default category tags, and saves the output
HTML file to the systrace directory, which is the current directory.
Systrace is written in the Python language.
The time argument is the time to run, in seconds.
See the Systrace documentation for versions and options.
If you get an error about serial not being installed, install serial using this
command: sudo pip install pyserial
4. Interact with the WordListSQL app on your device until the trace stops, which happens
when time is up. Create, edit, and delete an item. If you need more time to perform
these actions, run the command with --time=30 , as you did with the Android Device
Monitor example above.
$ cd android-sdk/platform-tools/systrace
159
Introduction
The file generated for WordListSQL is large, and for a more complex app will be even larger.
Practice navigating around the file to find information about your app.
1. Open one of your saved trace.html files in your Chrome browser. At this time, you
must use Chrome. Using a different browser may show a blank page. The file should
look similar to the one shown below.
The groupings are in the order Kernel, SurfaceFlinger (the Android compositor process),
followed by apps, each labeled by package name. Each app process contains all of the
tracing signals from each thread it contains, including a hierarchy of high level tracing events
based on the enabled tracing categories.
160
Introduction
The image shows part of the Systrace output for WordListSQL. The numbers have the
following meanings:
1. The page has one section for each process that was running when the trace was
recorded. Scroll through the very long file and find the section for the WordListSQL app,
which is called ple.wordlistsql (pid ... . The results will look different depending on
how you interacted with your app, what device you used, and what configuration you
used. You may need to scroll down quite a bit. If you get lost, close the tab and reopen
the file to start afresh. Tip: Instead of scrolling, you can use the search function of your
browser to search for Frames until you find the correct one.
2. Inside the ple.wordlistsql section, find Frames.
3. In the Frames line, in the graph to the right, you should see circles that are either green
or red and are labeled with the letter F for Frame. You may need to zoom in (w on the
keyboard) and pan right (d on the keyboard) to see individual, labeled circles.
Select a green circle. This frame is green because it completed rendering in the allotted
16 milliseconds per frame.
4. Select a red circle. This frame is red because it did not complete rendering within 16
milliseconds.
161
Introduction
5. In the left-hand pane, scroll down if necessary until you find Render Thread. On
devices running Android 5.0 (API level 21) or higher, this work is split between the UI
Thread and RenderThread. On prior versions, all work in creating a frame is done on
the UI thread. If you do not see Render Thread, select UI Thread instead.
6. Click on the black triangle to the right of Render Thread to expand the graph. This
expands the bars that show you how much time was spent in each system action
involved in rendering this frame.
7. Click on a system action to show more timing information in the pane below the graph.
Systrace analyzes the events in the trace and highlights many performance problems
as alerts, suggesting what to do next. Below is an example of a frame that was not
scheduled, perhaps because the frame was waiting for other work to be completed.
8. Open the Alerts tab on the right edge of the trace window. Click on an Alert type to see
a list of all alerts in the bottom pane. Click an alert to see details, a description of what
may be the problem, and links to further resources.
162
Introduction
Think of the alerts panel as a list of bugs to be fixed. Often a tiny change or
improvement in one area can eliminate an entire class of alerts from your app!
1. Open the LargeImages app in Android Studio and run it on your connected device.
2. Start a Systrace trace that is 10 seconds long either from the command line or Android
Device Monitor.
3. Flip a few times between the images of the running LargeImages app, going through at
163
Introduction
8. Notice the pattern of green and red frames. This pattern corresponds with the pattern of
tall and short bars shown by Profile GPU Rendering in the Profile GPU Rendering
practical. Turn on Profile GPU Rendering to refresh your memory, if necessary.
12. Notice decodeBitmap and above it, the name of the resource that is being decoded.
Decoding the image is taking up all the time AND this decoding is being done on the UI
Thread.
Now you know the cause of the problem. In a real app, you could address this problem
by, for example, using a smaller or lower resolution image.
Important: Your trace may look different, and you may need to explore to find the
information you need. Use the above steps as a guideline.
Important: Systrace is a powerful tool that gathers enormous amounts of systems data that
you can use to learn more about the Android system and your app's performance. See the
systrace documentation. Explore on your own!
of system services since the app started. You can use dumpsys to generate diagnostic
output for all system services running on a connected device. Passing the gfxinfo
164
Introduction
command to dumpsys provides output in Android Studio's logcat pane. The output
includes performance information that relates to frames of animation.
The purpose of this practical is to get you started with this powerful tool. The dumpsys tool
has many other options that you can explore in the dumpsys developer documentation.
For example:
2. Starting with Android 6.0, you can get even more detailed information using the
framestats command with dumpsys . For example, run the following command against
Learn more about how to use the data generated by this command in the Testing UI
Performance and dumpsys documentation.
Important: This practical has only given you the first steps in using Systrace and dumpsys .
Systrace and dumpsys are powerful debugging tools that provide you with a lot of
information. Making efficient use of this information takes practice.
Coding challenge
The tracing signals defined by the system do not have visibility into everything your app
is doing, so you may want to add your own signals. In Android 4.3 (API level 18) and
higher, you can use the methods of the Trace class to add signals to your code. This
technique can help you see what work your app's threads are doing at any given time.
Learn more at Instrument your app code.
Summary
Systrace is a powerful tool for collection runtime information about your app, for a slice
of time. Use alerts generated by the tool to find potential performance issues. Use the
165
Introduction
$cd android-sdk/platform-tools/systrace
$python systrace.py --time=10
The dumpsys tool collects comprehensive statistics about your app since its start time,
including frame rendering statistics. Use these commands:
adb shell dumpsys gfxinfo <PACKAGE_NAME> adb shell dumpsys gfxinfo <PACKAGE_NAME>
framestats
Related concept
The related concept documentation is Rendering and layout.
Learn more
Android developer documentation:
166
Introduction
All processes, services, and apps require memory to store their instructions and data. As
your app runs, it allocates memory for objects and processes in its assigned memory heap.
This heap has a limited but somewhat flexible size. The Android system manages this
limited resource for you by increasing or decreasing allocatable memory size. The system
also frees memory for reuse by removing objects that are no longer used. If your app uses
more memory than the system can make available, the system can terminate the app, or the
app may crash.
Memory allocation is the process of reserving memory for your app's objects and
processes.
Garbage collection is an automatic process where the system frees space in a
computer's memory by removing data that is no longer required, or no longer in use.
Android provides a managed memory environment. When the system determines that your
app is no longer using some objects, the garbage collector releases the unused memory
back to the heap. How Android finds unused memory is constantly being improved, but on
all Android versions, the system must at some point briefly pause your code. Most of the
time, the pauses are imperceptible. However, if your app allocates memory faster than the
system can collect unused memory, your app might be delayed while the collector frees
memory. The delay could cause your app to skip frames and look slow.
Even if your app doesn't seem slow, if your app leaks memory, it can retain that memory
even while in the background. This behavior can slow the rest of the system's memory
performance by forcing unnecessary garbage-collection events. Eventually, the system is
167
Introduction
forced to stop your app process to reclaim the memory. When the user returns to your app,
the app must restart completely.
To help prevent these problems, use the Memory Profiler tool to do the following:
Look for undesirable memory-allocation patterns in the timeline. These patterns might
be causing performance problems.
Dump the Java heap to see which objects are using memory at any given time.
Dumping the heap several times over an extended period can help you identify memory
leaks.
Record memory allocations during normal and extreme user interactions. Use this
information to identify where your code is allocating too many or large objects in a short
time, or where your code is not freeing the allocating objects and causing a memory
leak.
For information about programming practices that can reduce your app's memory use, read
Manage Your App's Memory.
Create apps with Android Studio and run them on a mobile device.
App overview
You will run the LargeImages and RecyclerView apps from previous practicals, using
168
Introduction
169
Introduction
In this practical, you learn the basics of using Memory Profiler to track down performance
problems and crashes related to your app's memory usage.
If you did any of the previous performance tools practicals, your environment is already set
up for debugging with the Android Profiler. Otherwise, see the Prerequisites.
1.1 Start the Memory Profiler tool with the LargeImages app
1. Open the LargeImages app in Android Studio.
2. Run LargeImages on a device with Developer options and USB Debugging enabled.
If you connect a device over USB but don't see the device listed, ensure that you have
enabled USB debugging on the device.
1. To open the Android Profiler, at the bottom of Android Studio click the Android Profiler
tab (shown as 1 in the screenshot).
2. Select your device and app, if they are not automatically selected (2 in the screenshot).
The Memory graph starts to display. The graph shows real-time memory use (3). The x -
axis shows time elapsed, and the y -axis shows the amount of memory used by your
app (4).
170
Introduction
3. In the app, swap the images to see the Memory graph change.
4. Click in the Memory graph, and the graph expands and separates into memory types.
Each memory type (such as Java, Native, and Graphics) is indicated with a different
color in a stacked graph. Check the key along the top of the graph to match colors and
memory types.
171
Introduction
The Memory Profiler panes and features that you use in this practical are shown in the
screenshot, as follows:
(1) Force garbage collection. Small Trash icons in the graph indicate garbage-collection
events that you or the system triggered.
(2) Capture a heap dump and display its contents.
(3) Record memory allocations and display the recorded data.
(4) The highlighted portion of the graph shows allocations that have been recorded. The
purple dots above the graph indicate user actions.
(5) Allocation-recording and heap-dump results appear in a pane below the timeline.
This example shows the memory allocation results during the time indicated in the
timeline.
(5 and 6) When you view either a heap dump or memory allocations, you can select a
class name from this list (5) to view the list of instances on the right (6).
(7) Click an instance to open an additional pane. When you are viewing the allocation
record, the additional pane shows the stack trace for where that memory was allocated.
When you are viewing the heap dump, the additional pane shows the remaining
references to that object.
See the Memory Profiler documentation for a full list of controls and features.
172
Introduction
the worst case, eventually make the app crash. Finding and fixing memory leaks is a lot
easier if you have a tool that shows you what's happening with the memory that your app is
using.
To demonstrate a memory leak, the MemoryOverload app creates and loads hundreds of
TextView objects at the tap of a button. When you run the app and monitor it with Memory
Profiler, you see a graph that shows more and more memory being allocated. Eventually, the
app runs out of memory and crashes.
Note: You may notice a delay between tapping the plus button and the next batch of
views being added. To see just how long it takes, run the Profile GPU Rendering tool,
which you enable from within your device's Developer options settings.
6. Keep adding views until the app crashes and shows an Application Not Responding
(ANR) dialog. Logcat displays a message like this one:
SIGABRT is the signal to initiate abort() and is usually called by library functions that
173
Introduction
7. After overloading and crashing your device, it is a good idea to remove the app from
your device and restart your device.
The MemoryOverload app is a made-up example to show a pattern, and it does not follow
best practices! However, allocating and not releasing views is a common cause of memory
problems. See the Memory and Threading video for more on this topic.
One fix for the MemoryOverload app would be to not create views that are not visible on the
screen. A second solution would be to combine views. Instead of creating a view for each
rectangle in a row, you could create a patterned background of rectangles and show multiple
rectangles in one view.
5. In Android Studio, click the Dump Java Heap button to capture the app heap into a
file and open the list of classes. This can take a long time. The Heap Dump pane will
174
Introduction
1. In the Heap Dump pane (1), find and click the TextView class. This opens an Instance
View pane.
2. In the Instance View pane (2), click one of the TextView instances. This opens a
References pane (3). Your screen should now look similar to the screenshot below.
The Heap Dump pane (1) shows all the classes related to your app, as they are represented
on the heap. The columns give you size information for all the objects of this class. Click a
column header to sort by that metric.
175
Introduction
Retained Size: Size of memory that all instances of this class are dominating.
After you click the floating action button (+) in the MemoryOverload app, you see a large
number of TextView instances on the screen. Corresponding allocations are recorded on
the graph and in the allocation count for the TextView class.
The Instance View pane (2) lists all the instances of the selected TextView class that are
on the heap. The columns are as follows.
Depth: Shortest number of hops from any garbage-collection root to the selected
instance.
Shallow Size: Size of this instance, in bytes.
Retained Size: Total size of memory being retained due to all instances of this class, in
bytes.
The References pane (3) shows all the references to the selected instance. For example, in
the MemoryOverload app, all the views are created and added to the view hierarchy. When
you are debugging an app, look for classes and instances that should not be there, and then
check their references.
For example, if you are offloading work to another thread, it is possible that references to
views or activities remain after an Activity has been restarted, leaking memory on every
configuration change. Look for long-lived references to Activity , Context , View ,
Drawable , and other objects that might hold a reference to the Activity or Context
container.
Right-click a class or instance and select Jump to Source. This opens the source code, and
you can inspect it for potential issues.
Click the Export button at the top of the Heap Dump pane to export your snapshot of
the Java heap to an Android-specific Heap/CPU Profiling file in HPROF format. HPROF is a
binary heap-dump format originally supported by J2SE. See HPROF Viewer and Analyzer if
you want to dig deeper.
See Memory Profiler, Processes and Threads, and Manage Your App's Memory.
176
Introduction
5. On your device with the MemoryOverload app running, tap the floating action button (+)
to add a set of views.
6. Wait only a few seconds, then click the Record Memory Allocations button again. The
button is now a black square indicating that clicking it will pause the recording.
Don't wait too long to pause the recording, because the recorded files can get large.
1. The Memory graph indicates which portion was recorded (1). Select the recorded
portion, if necessary. You can record more than one section of the graph and switch
between them.
2. As in the previous task, the Heap Dump pane (2) shows the recorded data.
3. Click a class name to see instances allocated during this period of time (3). As in the
previous task, there should be many instances of the TextView class.
4. Click an instance to see its Call Stack (4).
5. At the top of the Call Stack (which is actually the bottom...) is the method call in your
code that initiated creation of this instance. In this case, the method is
addRowOfTextViews() .
177
Introduction
To export the recordings to an hprof file (for heaps) or an alloc file (for allocations), click
the Export button in the top-left corner of the Heap Dump or Allocations pane. Load
the file into Android Studio later for exploration.
Solution code
Android Studio project: MemoryOverload
Summary
Use Memory Profiler to observe how your app uses memory over time. Look for
patterns that indicate memory leaks.
Use Java heap dumps to identify which classes allocate large amounts of memory.
Record allocations over time to observe how apps allocate memory and where in your
code the allocation is happening.
Related concept
178
Introduction
Learn more
Android developer documentation:
The Android Performance Patterns video series show older tools but the principles all apply:
Memory Monitor
Do Not Leak Views
Memory and Threading
179
Introduction
Table of Contents:
What you should already KNOW
What you will LEARN
What you will DO
App overview
Task 1. Run the Network Profiler tool
Task 2. Run battery statistics and visualization tools
Task 3. Convert images to WebP format
Summary
Related concepts
Learn more
Networking is one of the biggest users of battery. Optimizing your networking by following
best practices will reduce battery drain, as well as reducing the size and frequency of data
transfers.
In this practical, you learn how to use tools to measure and analyze network and battery
performance. You also learn how to compress images to reduce the size of your data
stream.
Optimizing network and battery performance is a huge topic, and as devices change, so do
some of the details and recommendations. The Android team is constantly improving the
framework and APIs to make it easier for you to write apps that perform well.
See the Best Practices: Network, Battery, Compression concept for an essential overview.
Make use of the extensive linked resources to dive deeper and get the most up-to-date
recommendations.
Create apps with Android Studio and run them on a mobile device.
Work with Developer options on a mobile device.
Start the Android Profiler.
180
Introduction
App overview
You'll use the WhoWroteIt app, or any app of your choice that makes network calls.
You'll create a simple demo app with a large image and an image converted to WebP
format.
In this task you run Network Profiler with the WhoWroteIt app.
Make multiple calls for each search. This will save you some tedious on-screen
typing.
Call the sleep() method between requests to ensure that each network call is
181
Introduction
made separately—that is, to ensure that the system does not batch the calls for
you.
Obviously, you should not do either of these things in a production app.
// If the network is active and the search field is not empty, start a FetchBook A
syncTask.
if (networkInfo != null && networkInfo.isConnected() && queryString.length()!=0) {
new FetchBook(mTitleText, mAuthorText, mBookInput).execute(queryString);
}
// If the network is active and the search field is not empty, start a FetchBook A
syncTask.
if (networkInfo != null && networkInfo.isConnected() && queryString.length()!=0) {
new FetchBook(mTitleText, mAuthorText, mBookInput).execute(queryString);
SystemClock.sleep(3000);
new FetchBook(mTitleText, mAuthorText, mBookInput).execute(queryString);
SystemClock.sleep(3000);
new FetchBook(mTitleText, mAuthorText, mBookInput).execute(queryString);
SystemClock.sleep(3000);
new FetchBook(mTitleText, mAuthorText, mBookInput).execute(queryString);
SystemClock.sleep(3000);
new FetchBook(mTitleText, mAuthorText, mBookInput).execute(queryString);
}
3. Run your app and perform one search. The app's user interface has not changed, and
you should see one search result for your query.
Orange spikes indicate data sent, and blue spikes data received. The WhoWroteIt
app does not send a lot of data, so the orange spikes are short, while the blue
spikes are much taller.
182
Introduction
The horizontal axis moves in time, and the vertical axis shows data transfer rate in
kilobytes per second.
Every time your app makes a network request, a vertical spike on the Network
Profiler indicates the activity.
The width of the base of the spike is how long the request took. The height of the
spikes is the amount of data sent or received.
5. Click the Network Profiler section of the Android Profiler to expand the pane and see
more details, as shown in the annotated screenshot below. To keep the graph from
moving, click the Live button in the top-right corner of the profiler, or just scroll back.
Doing this does not stop the recording.
6. The horizontal colored bar at the top indicates whether the request was made on Wi-Fi
(gray) or the mobile radio (dark or light blue). The power state of the mobile radio is
indicated by dark blue for high (using more battery power) and light blue for low (using
less battery power).
7. Legend for type of radio used and power state of mobile radio.
8. The orange dotted line indicates the number of active connections over time, as shown
on the y -axis on the right.
9. The x -axis shows the time that has passed.
10. Orange spikes mark data sent. The width indicates how long it took, and the height how
much data was sent. The WhoWroteIt app requests are small, so it does not take long
to send this small about of data.
11. Blue spikes mark data received. The width indicates how long data wa received and the
height how much data was received. Depending on how many books match the
183
Introduction
request, the size of the received data, and how long it takes to receive it, can vary.
12. The y -axis on the left shows numeric values for the amounts of data in KB per second.
13. Experiment with different queries to see whether it slightly changes the network request
patterns.
Note the purple dot at the top of the pane (1), which indicates a user action. In this case,
the action was pressing the Search Books button.
Next to the user action, you see the payload for your request, which in this case
consists of the word "jungle" (2).
A pane opens below the graph listing all the requests and responses with additional
information in the selected portion of the graph (4).
184
Introduction
3. Click on a request, and a new pane with three tabs opens (5). By default, you see the
contents of the first tab, displaying the contents of the response (6).
4. In the screenshot above, notice that the blue bar above the selected portion of the
graph remains dark, indicating that the radio is in a high-power state throughout the
requests. This is because the requests follow each other closely enough to keep the
radio on.
5. In the WhoWroteIt app, change the sleep time for one of the requests until the radio
powers off between requests. How long the mobile radio stays in the high state can vary
greatly between devices.
In the screenshot below, after a request, the radio is in the high-power state (1). The
radio stays in the high-power state for almost 30 seconds before powering down (2).
Then the radio powers up again for the next request (3).
6. Click the Call Stack tab to see where the request originated in your code. When your
app makes many different types of network requests, the Call Stack tab helps you
pinpoint where in your code you may want to start optimizing.
185
Introduction
When sending requests and receiving responses, try to optimize for power consumption of
the mobile radio by minimizing the time the mobile radio is in a high-power state. This can be
challenging, and a full explanation is beyond the scope of this practical. However, here are
some tips to get you started.
Limit the size of your requests and responses. Formulate your queries to only request
the data you absolutely need.
Batch your requests so that you can send several of them together without delay to take
advantage of the high-power state. Space the sending of requests to maximize for the
mobile radio staying in lower power states.
For more best practices, see Optimizing Battery Life and the Best Practices: Network,
Battery, Compression concept.
information about the status of system services as you use the device. The dumpsys
command retrieves this information from the device so that you can use it to debug and
optimize your app.
Batterystats collects battery data from your device, and the Battery Historian tool converts
that data into an HTML visualization that you can view in your browser. Batterystats is part
of the Android framework, and the Battery Historian tool is open-sourced and available on
GitHub. Batterystats shows you where and how processes are drawing current from the
battery, and it helps you identify tasks in your app that could be deferred or even removed to
improve battery life.
186
Introduction
You can use the Battery Historian tool on the output of the dumpsys command to generate a
visualization of power-related events from the system logs, as shown in the screenshot
below. This information makes it easier for you to understand and diagnose battery-related
issues. Battery Historian is not part of the Android framework, and you will need to download
and install it from the internet.
In this practical, you create and work with a Battery Historian chart.
WARNING: Installing the Docker and Battery Historian tools requires downloading about
500 MB of data to your local computer, which may take some time. Make sure that you have
sufficient time, bandwidth, and disk space before you begin.
IMPORTANT: Use the Chrome browser to work with Battery Historian files.
There are different ways to install the tool. Using Docker is by far the easiest. Docker is an
open platform for developers and system administrators to build, ship, and run distributed
apps. See the README.md file with the Battery Historian source code on GitHub for more
installation options and documentation.
187
Introduction
3. Once Docker is ready, open a terminal window in Android Studio or on your desktop.
4. Enter the following commands in the terminal to verify that Docker is installed, running,
and working properly:
docker --version
docker ps
docker run hello-world
1234 is the port number for Battery Historian on your localhost. You can replace 1234
with the port number of your choice. (This might pull the Battery Historian, which might
take a while depending on your internet speed.)
Important: Check the README file for Battery Historian in GitHub for the latest version
number, because the tool gets updated and improved.
You will see a series of messages in the terminal, similar to the following:
188
Introduction
Do not close the terminal or terminate the terminal window, because this will stop Battery
Historian.
Linux and Mac OS X:
That's it. Open Battery Historian, which will be available in your browser at
http://localhost:1234/
Windows:
You may have to enable virtualization. If you are able to run the emulator with Android
Studio, then virtualization is already enabled.
Once you start Docker, it should tell you the IP address of the machine it is using. If, for
example, the IP address is 123.456.78.90 , Battery Historian will be available at
http://123.456.78.90:1234. (If you used a different port, substitute it in the URL.)
189
Introduction
adb kill-server
7. Restart adb and check for connected devices. The command lists any connected
devices.
$ adb devices
List of devices attached
emulator-5554 device
LGH918ce000b4b device
8. Reset battery data gathering. Resetting erases old battery collection data; otherwise,
the output from the dump command will be huge.
9. Disconnect your mobile device from your computer so that you are only drawing power
from the device's battery. On the emulator, you will not get accurate data for this
exercise.
10. Play with the WhoWroteIt app for a short time. Search for different books by using
different search text.
11. Reconnect your mobile device.
12. On your mobile device, turn Wi-Fi on.
13. Make sure that adb recognizes your mobile device.
adb devices
14. Dump all battery data and redirect the output to save it into a text file. This can take a
while.
15. To create a report from raw data on a device running Android 7.0 and higher:
190
Introduction
bugreport could take several minutes to complete. Do not cancel, close the terminal
16. On the Battery Historian starting page in your Chrome browser (see previous
screenshot), browse for the bugreport.txt or bugreport.zip file. Click Submit to
upload and display the chart. If you used Terminal pane in Android Studio, the
bugreport file will be located in the WhoWroteIt app's root directory.
Each row shows a colored bar segment when a system component is active and drawing
current from the battery. The chart does not show how much battery was used by the
component, only that the app was active.
1. Hover over the bars to get details about each graph. You will work more with this in a
minute.
2. Hover over the "i" information icon on the left to get a color legend, as shown in the
figure below.
3. Using the chart screenshot below and explanations below, explore your chart. Your
chart looks different than the screenshot because you are on a different device, and you
likely performed different actions.
Chart guidelines
191
Introduction
The following chart shows three minutes of information for an LG V20 mobile device running
the modified WhoWroteIt app.
The numbers in the following list reference the numbered callouts in the image below,
illustrating some of the powerful information you can gather and analyze.
(1) The Mobile radio active line shows the activity of the mobile radio. The mobile radio
is one of the biggest battery users. When working on battery efficiency, you always want
to look at and optimize the mobile radio's activity.
(2) Movable timeline that displays information about battery charge state. The upper box
shows information about the current battery state. The bottom bars show that the device
was not plugged in at this moment, and it was not charging. This information is useful
when you want to verify that your code only performs certain battery intensive activities,
such as syncs, when the device is plugged in and on Wi-Fi.
(3) Mobile radio activity. The selected bar in the Mobile radio active line, and those
nearby, are likely the WhoWroteIt app's network calls to fetch books. The information
box for the bar underneath the timeline marker shows additional information such as the
duration and the number of occurrences.
192
Introduction
Most download traffic consists of images fetched from a server. The smaller you can make
your downloadable images, the better the network experience your app can provide for
users. Downloading smaller images means faster downloads, less time on the radio, and
potential savings of battery power. Using WebP images with your app sources reduces APK
size, which in turn means faster app downloads for your users.
WebP is an image file format from Google. WebP provides lossy compression (as JPG
does) and transparency (as PNG does), but WebP can provide better compression than
either JPG or PNG. A WebP file is often smaller in size than its PNG and JPG counterparts,
with at least the same image quality. Even using lossy settings, WebP can produce a nearly
identical image to the original. Android has included lossy WebP support since Android 4.0
(API 14) and support for lossless, transparent WebP since Android 4.3 (API 18).
Serve WebP files over the network to reduce image load times and save network bandwidth.
In general, use the following algorithm to decide which image format to use:
3.1 Create an app and compare image sizes and quality for
different formats
Android has included lossy WebP support since Android 4.0 (API 14) and support for
lossless, transparent WebP since Android 4.2 (API 18).
Support for lossless and transparent WebP images is only available in Android 4.2 and
higher. That means your project must declare a minSdkVersion of 18 or higher to create
lossless or transparent WebP images using Android Studio.
Note: You can create a new app for this task, or you can make a copy of the LargeImages
app and modify it in accordance with the steps below.
1. Create an app with a minSdkVersion of 18 using the Basic Template.
2. Add the same large image that you used in the LargeImages app as the background for
the text view.
3. Make a backup copy of the image in res/drawable. You need the backup because
conversion occurs in place, replacing your original image.
4. In your res/drawable folder, right-click the image and choose Convert to WebP at the
bottom of the menu. The Convert Images to WebP dialog opens.
193
Introduction
This conversion happens "in-place"; that is, your original image is changed into the
compressed image.
5. Choose Lossy encoding, move the slider to 21%, and click OK to preview the files. For
this particular image, these settings will give you about 10% of the size for the
converted image. This saves you over 350 K!
6. Click Finish to save the converted small image and rename it to mnt_diablo_webp.webp .
7. Add an onClick() handler to the text view that swaps the background between the
backed up original and the WebP image.
8. Run your app.
9. Click to swap between the two images. Do you notice any difference in quality? In most
cases the difference should be unnoticeable to casual user.
194
Introduction
Summary
Network Monitor gives you a quick live look at how your app is making network
requests.
The dumpsys batterystats command gets battery-related information from your mobile
device.
The Battery Historian tool displays the batterystats information as an interactive time
chart. You can use the chart to correlate battery usage with activities taken by your app.
Related concepts
The related concept documentation is in Best Practices: Networking, Battery, Compression
with extensive references to more documentation.
Learn more
Performance is a huge and important topic, and there are many resources for you to dive
deeper.
Network:
Android Profiler
Inspect Network Traffic with Network Profiler
Connectivity for billions
Managing Network Usage
Battery Drain and Networking (video)
Transferring Data Without Draining the Battery: in-depth docs
Battery:
WebP:
195
Introduction
WebP website
Image Compression for Web Developers (good, comprehensive intro)
Reducing Image Download Sizes
Reduce APK Size
Reduced data cost for billions
196
Introduction
Users run Android devices in many different languages. To reach the most users, your app
should handle text and layouts in ways appropriate for those languages.
An Android user can change the language for a device in the Settings app. As a developer,
you should localize your app to support different languages in the locales in which your app
is released.
In this chapter, you learn how to provide support for different languages using string
resources and the Translations Editor in Android Studio.
197
Introduction
Apps overview
In this lesson you work from a starter app called LocaleText_start, which is a simple app
created using the Basic Activity template. Its UI uses the default ConstraintLayout provided
by the template. It has text, an image, and a floating action button that can make a phone
call. The options menu has a single menu item, Help, that launches a second activity with
help information.
198
Introduction
This lesson demonstrates how you can localize an app for a locale, and provide translated
text within your app for the other languages.
199
Introduction
After adding an RTL language, you will use XML layout attributes to take advantage of the
layout mirroring feature added to Android 4.2. Layout mirroring redraws the layout for a right-
to-left orientation for RTL languages. As a result, the image, the TextView elements, the
options menu, and the floating action button are all automatically moved to the opposite side
of the screen as shown below. The app demonstrates the attributes you use for layout
mirroring.
200
Introduction
A floating action button with a phone icon in the lower right side.
The options menu in the app bar with one choice: Help.
A TextView for the title, constrained to the left margin of the parent (the screen).
A floating action button with a phone icon on the left side.
Another TextView with body text, constrained to the right side of the floating action
button and to the title TextView .
The Up button and Activity name in the app bar.
Open content_main.xml to see the MainActivity layout in the Design tab. You should see
an ImageView and two TextView elements in the layout (shown in the figure below).
The product_image ImageView is constrained to the top and left side of the screen.
The heading TextView is constrained to the top and right side of the product_image
ImageView , and to the right side of the parent.
201
Introduction
To save time, the LocaleText_start starter app has been prepared with all text strings in
strings.xml . Open strings.xml —known as the default strings.xml file because it is
used for the default language—to see the string resources in the app. It includes every piece
of text in the app including the action_help resource for the options menu item (Help). The
file include comments that provide the context for the translator to translate your strings—
such as the use of grammar structures and product terms.
<resources>
<!-- All comments are notes to translators. -->
<string name="app_name">LocaleText</string>
<!-- The Help option in the options menu, no more than 30 chars. -->
<string name="action_help">Help</string>
...
</resources>
202
Introduction
Include comments in the strings.xml file to describe the context in which each string is
used. If you use a translator to translate your strings, the translator will understand the
context from the comments you provide, which will result in a better quality translation.
1. Open the strings.xml file, and click the Open editor link in the top right corner.
The Translations Editor appears with a row for each resource key (such as
action_help , app_name , and nougat ). The Default Value column is the default value
203
Introduction
2. Click the globe button in the top left corner of the Translations Editor pane ("1" in the
following figure), and select French in the dropdown menu:
After you choose a language, a new column with blank entries appears in the
Translations Editor for that language, and the keys that have not yet been translated
appear in red.
To show only the keys that require translation, check the Show only keys needing
translation checkbox option at the top of the Translations Editor pane, next to the
globe.
3. Select a key (such as description ). The Key, Default Value, and Translation fields
appear at the bottom of the Translations Editor pane.
4. Add the French translation for the key by selecting the key's cell in the column for the
language (French), and entering the translation in the Translation field at the bottom of
the pane as shown in the following figure.
Note: For an app to be released to the public, you would use a translation service if you
don't know the language. You can click the Order a translation link at the top of the
Translations Editor pane to start the process with the service. The service should provide a
translated strings.xml file, which you can copy into your project folder as described in the
concept chapter, and the translations appear in the Translations Editor. For this example, if
you don't know French use Google Translate.
204
Introduction
1. Enter the translations in the French column for each key, as shown in the following
figure:
Since there is no translation for the support_phone and dial_log_message keys, the
default values will be used in the layout no matter what language and locale the user
chooses in the Settings app.
If you don't want to translate the app name, you can click the Untranslatable checkbox
for it; however, you can also modify the app name for each language, as shown in the
above figure. The choice of translating the app name is up to you. Many apps add
translated names so that the users in other languages understand the app's purpose
and can find the app in Google Play in their own languages.
3. To see a preview of the layout in the new language, open content_main.xml in the
Design tab, and choose the language you added from the Language menu at the top
of the Layout Editor. The layout reappears with a preview of what it looks like in that
language.
205
Introduction
Changing the language can affect the layout. In this case, the headline in the previous
figure, "Pacquet de bonbons de nougat," takes up two lines. To prepare for different
languages, you need to create layouts that have enough room in their text fields to
expand at least 30 percent, because a translated version of the text may take more
room.
4. Switch to Project: Project Files view, and expand the res directory. You see that the
Translations Editor created a new directory for the French language: values-fr . The
values directory is the default directory for colors, dimensions, strings, and styles,
206
Introduction
while for now, the values-fr directory has only string resources.
An app can include multiple values directories, each customized for a specific language
and locale combination. When a user runs the app, Android automatically selects and loads
the values directories that best match the user's chosen language and locale. For
example, if the user chooses French as the language for the device in the Settings app, the
French strings.xml file in the values-fr directory is used rather than the strings.xml file
in the default values directory.
207
Introduction
2. To switch the preferred language in your device or emulator, open the Settings app. If
your Android device is in another language, look for the gear icon:
3. Find the Languages & input settings in the Settings app, and choose Languages.
Be sure to remember the globe icon for the Languages & input choice, so that you can
find it again if you switch to a language you do not understand. Languages is the first
4. For devices and emulators running a version of Android previous to Android 7, choose
Language on the Languages & input screen, select Français (France), and skip the
following steps.
(In versions of Android previous to Android 7, users can choose only one language. In
Android 7 and newer versions, users can choose multiple languages and arrange them
by preference. The primary language is numbered 1, as shown in the following figure,
followed by lower-preference languages.)
5. For devices and emulators running Android 7 or newer, follow these steps:
208
Introduction
7. To add a language not on the list, click Add a language, select Français, and then
select France for the locale.
8. Use the move icon on the right side of the Language preferences screen to drag
Français (France) to the top of the list.
209
Introduction
If the user has chosen a language that your app does not support, the app displays the
strings that are in the default strings.xml file. For example, if the user has chosen Spanish
(or any other language which your app does not support), the app displays strings in
English, which is the language used in the default strings.xml file for this app.
Note: Remember to include all the strings you need for the app in the default strings.xml
file. If the default file is absent, or if it is missing a string that the app needs, the app may
stop.
So far, the app uses two LTR languages: English (for the default) and French. In this task
you add Hebrew, an RTL language.
210
Introduction
Android provides the layout mirroring feature, which redraws the layout for RTL languages
so that the image view, the text view, the options menu, and the floating action button are all
automatically moved to the opposite side of the layout.
Open AndroidManifest.xml to check that the following attribute is part of the <application>
element. This enables RTL layout mirroring:
android:supportsRtl="true"
Android Studio adds the above attribute to new projects. If you change true to false ,
RTL layout mirroring is not supported, and the views and other elements that use the RTL
language appear as they would for an LTR language.
1. Open the LocaleText app from the previous task in Android Studio.
2. Add Hebrew and translate the strings using the Translations Editor (see previous task
for details). The translations appear in the new Hebrew (iw) column for the key.
3. To see a preview of the layout in the new language, open content_main.xml , click the
Design tab if it is not already selected, and choose Hebrew (iw) from the Language
menu at the top of the Layout Editor. The layout reappears with a preview of what it will
look like in Hebrew.
211
Introduction
As shown in the figure, adding an RTL language can affect the layout, because Android
Studio automatically supplies a declaration in the <application> element in the
AndroidManifest.xml file that automatically turns on RTL layout support:
android:supportsRtl="true" . The RTL language characters are properly set from right to
However, the UI elements—with the exception of the floating action button—are still in the
same place in the layout, as if they were showing an LTR language. The floating action
button moves to the opposite (left) side of the screen, as it should for an RTL language. But
the ImageView is still constrained to the left margin, and the two TextView elements are
constrained to the right side of the ImageView.
The ImageView and TextView elements should move to the opposite side of the screen,
where a Hebrew reader would expect them to be. In the next step you will fix the layout to
fully support an RTL language.
212
Introduction
To adjust a layout so that it fully supports an RTL language, you need to add a "start" and
"end" attribute for every "left" and "right" attribute in each layout. For example, if you use
android:layout_marginLeft , add android:layout_marginStart :
Android Studio highlights "right" and "left" attributes (such as layout_marginLeft in the
figure) and provides suggestions about including "start" and "end" attributes to better
support right-to-left languages.
2. Add a "start" attribute for every "left" attribute, and an "end" attribute for every "right"
attribute. For example, add the android:layout_marginStart attribute to all uses of the
layout_marginLeft attribute:
android:layout_marginLeft="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_margin"
The "start" attribute defines the start of the view, which is the same as the "left" attribute
for an LTR language; however, in RTL languages, the start side is the right side.
The "end" attribute defines the end of the view, which is the same as the "right" attribute
for an LTR language; however, in RTL languages, the end side is the left side.
Be sure to add "start" and "end" to the constraint attributes. For example:
213
Introduction
app:layout_constraintLeft_toRightOf="@+id/product_image"
app:layout_constraintStart_toEndOf="@+id/product_image"
3. To see a preview of the layout in the new language, click the Design tab, and choose
Hebrew (iw) in the Language menu at the top of the Layout Editor. The layout
reappears with a preview of what it will look like, now that you have added the "start"
and "end" attributes.
Compare this layout preview with the layout preview in the previous task. As a result of
adding the "start" and "end" attributes for margins and constraints, the layout now shows the
image on the right side of the screen (the "start" side), and the TextView elements are
constrained to its left ("end") side.
The following snippet is the revised XML code for the two TextView elements and the
ImageView in content_main.xml :
214
Introduction
<TextView
android:id="@+id/heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_margin"
android:text="@string/nougat"
android:textAppearance=
"@style/TextAppearance.AppCompat.Headline"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toRightOf="@+id/product_image"
app:layout_constraintStart_toEndOf="@+id/product_image"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/product_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/nougat"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginLeft="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_margin"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/description"
android:layout_marginTop="0dp"
app:layout_constraintTop_toBottomOf="@id/heading"
app:layout_constraintLeft_toRightOf="@id/product_image"
app:layout_constraintStart_toEndOf="@id/product_image"
android:layout_marginLeft="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginRight="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" >
</TextView>
215
Introduction
Modify the activity_help.xml layout for the RTL language: Add a "start" attribute for every
"left" attribute, and an "end" attribute for every "right" attribute, as done previously for
content.xml , for the two TextView elements and the floating action button. The following
<TextView
android:id="@+id/help_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_margin"
android:text="@string/action_help"
android:textAppearance=
"@style/TextAppearance.AppCompat.Headline"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
app:fabSize="mini"
app:srcCompat="@android:drawable/ic_menu_call"
android:layout_marginLeft="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_margin"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="@dimen/standard_margin"
app:layout_constraintTop_toBottomOf="@+id/help_title" />
<TextView
android:id="@+id/help_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="@dimen/standard_margin"
android:text="@string/help_text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:layout_marginLeft="@dimen/standard_margin"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginRight="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/help_title"
app:layout_constraintLeft_toRightOf="@+id/fab"
app:layout_constraintStart_toEndOf="@+id/fab"
app:layout_constraintHorizontal_bias="1.0" >
</TextView>
216
Introduction
To see the changes, run the app on the device or emulator, and then change the language to
Hebrew and run the app again.
The image now appears on the right of the screen, at the start of the text in the main activity.
The help activity's layout is also mirrored for the RTL language.
Note also that the app name, the Help title for HelpActivity , and the Up button for
HelpActivity (shown on the right side of the figure) are also moved to the opposite side of
217
Introduction
The Language menu option will make it easier for the user to open the language list without
having to look for and launch the Settings app. The user can also immediately return to the
LocaleText app and see the app in the new language by tapping the Back button.
Add the Language item to menu_main.xml , using action_language as the id . Set the
android:orderInCategory attribute value to a higher number than action_help so that it
<item
android:id="@+id/action_language"
android:orderInCategory="110"
android:title="Language"
app:showAsAction="never" >
</item>
218
Introduction
Extract the string resource for the option's android:title attribute ("Language"). Use the
Translations Editor to provide that string resource for French and Hebrew.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle options menu item clicks here.
switch (item.getItemId()) {
case R.id.action_help:
showHelp();
return true;
case R.id.action_language:
Intent languageIntent = new
Intent(Settings.ACTION_LOCALE_SETTINGS);
startActivity(languageIntent);
return true;
default:
// Do nothing.
}
return super.onOptionsItemSelected(item);
}
Coding challenge
Note: All coding challenges are optional and not prerequisite for the material in the later
chapters.
Challenge: Localize the UI elements of another app.
Developers often work in teams and are asked to update existing apps to localize them. In
such cases you may not know everything about the code in the app, but you can apply
localization best practices to update the code.
219
Introduction
For this challenge, download the RecyclerView_start starter app, rename it to RecyclerView ,
and run the app. It shows a list of "Word" items. The floating action button adds a "Word"
item each time you click it. Clicking on an item displays "Clicked!" with "Word" (as shown in
the figure).
220
Introduction
221
Introduction
1. Add string resources to the project with translations for French (or any LTR language)
and Hebrew (or any RTL language).
The challenge requires you to support a RTL language. The number that appears after
"Word" for an LTR language ("Word 20") should appear before "Word" ("20 מָלה
ִ ") for an
RTL language. To control placement, use a string format with an argument for the
number, as described in the Formatting and Styling section of String Resources.
2. Open MainActivity , find the code In the onCreate() method that puts the initial data
into the word list (shown in the comment below), and replace it with code to use the
word string resource:
3. Within the onCreate() method, find the code for the floating action button in onClick()
that adds a new word to the word list (shown in the comment below), and replace it to
use the word string resource:
4. Open WordListAdapter , and use the getString() method to replace the hardcoded
"Clicked!" with R.string.clicked . In the WordViewHolder class within WordListAdapter,
change the onClick() method to the following:
5. Run the app and click the floating action button twice to add two more entries: "+ Word
20" and "+ Word 21." Click the Word 16 item to ensure that it displays "Clicked! Word
222
Introduction
16" as shown in the previous figure. Then change your device or emulator to another
language, run the app, and perform the same steps.
Summary
To prepare an app to support different languages, extract and save all strings as
resources in strings.xml , including menu items, tabs, and any other navigation
elements that use text.
To use the Translations Editor, open the strings.xml file, and click the Open editor
link in the top right corner.
To add a language, click the globe button in the top left corner of the Translations Editor.
To see a preview of the layout in the new language, open the layout XML file, click the
Design tab, and choose the language in the Language menu at the top of the layout
editor.
223
Introduction
To support right-to-left (RTL) languages with RTL layout mirroring, Android Studio
automatically includes the android:supportsRtl attribute, set to true , as part of the
<application> element in AndroidManifest.xml for each project.
To support RTL languages in a layout, add the "start" and "end" XML attributes for all
"left" and "right" attributes.
Switch the preferred language in your device or emulator by choosing Languages &
input > Languages in Settings. In Android 7 and newer you can add another language
by clicking Add a language. Drag the preferred language to the top of the list.
Use the default strings.xml file to define each and every string in the app, so that if a
language or dialect is not available, the app shows the default language.
Related concept
The related concept documentation is in Languages and layouts.
Learn more
Android developer documentation:
Other:
224
Introduction
To provide the best experience for Android users in different regions, your app should handle
not only text but also numbers, dates, times, and currencies in ways appropriate to those
regions.
When users choose a language, they also choose a locale for that language, such as
English (United States) or English (United Kingdom). Your app should change to show the
formats for that locale: for dates, times, numbers, currencies, and similar information. Dates
can appear in different formats (such as dd / mm / yyyy or yyyy - mm - dd ), depending on
the locale. Numbers appear with different punctuation, and currency formatting also varies.
225
Introduction
Use the current locale to set the format for the date and for numbers.
Parse a number from a locale-formatted string.
Show different currencies based on the locale.
App overview
The LocaleText app (from the lesson about using language resources) offers localized
language resources, but there are other aspects of localization that need to be addressed.
Dates, numbers, and currencies are the most important aspects.
You will use LocaleText3_start as the starter code. It already contains extra UI elements and
strings translated into French and Hebrew, and the RTL layout adjustments ("start" and
"end" attributes) that are described in the previous lesson. You will do the following:
Convert a product expiration date to the format for the user's chosen locale.
Format the user's quantity for the user's chosen locale, so that the quantity properly
displays thousands and higher numbers with the right separators.
Set the currency format for the language and locale, and use it to display the price.
226
Introduction
localize the views by translating a string and adding RTL attributes to the layout.
Open content_main.xml to see the layout and become familiar with the UI elements:
227
Introduction
Open the Translations Editor to see French and Hebrew translations for the new string
resources. (For instructions on adding a language, see the lesson about using language
resources.) You will not be translating the date, so the Untranslatable checkbox for the
date key is already selected.
Since there is no translation for the date key, the default value will be used in the layout no
matter what language and locale the user chooses. You will add code to format the date so
that it appears in the locale's date format.
Note: Throughout this lesson, places where you will add code are marked by TODO
comments. After adding the code, you can delete or edit the TODO comments.
1. Open MainActivity , and find the the code at the end of the onCreate() method after
the fab.setOnClickListener() section:
228
Introduction
This code adds five days to the current date to get the expiration date.
The DateFormat.getDateInstance() method gets the default formatting style for the
user's selected language and locale. The DateFormat format() method formats a date
string.
229
Introduction
3. Run the app, and switch languages. The date is formatted in each specific language as
shown in the figure.
Use the Java class NumberFormat to format numbers and to parse formatted strings to
retrieve numbers. You will use the quantity , which is provided for entering an integer
quantity amount.
the user taps the Done key (the checkmark icon in a green circle):
You will add code to parse the quantity, convert it to a number, and then format the number
according to the Locale .
230
Introduction
@Override
public boolean onEditorAction(TextView v, int actionId,
KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
// Close the keyboard.
InputMethodManager imm = (InputMethodManager)
v.getContext().getSystemService
(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
// TODO: Parse string in view v to a number.
//...
The listener defines callbacks to be invoked when an action is performed on the editor. In
this case, when the user taps the Done key, it triggers the EditorInfo.IME_ACTION_DONE
action, and the callback closes the keyboard.
Note: The EditorInfo.IME_ACTION_DONE constant may not work for some smartphones that
implement a proprietary soft keyboard that ignores imeOptions (as described in Stack
Overflow tip, "How do I handle ImeOptions' done button click?"). For more information about
setting input method actions, see Specify the Input Method Action, and for details about the
EditorInfo class, see EditorInfo .
231
Introduction
After you enter NumberFormat , the expression appears in red. Click it and press Option-
Return on a Mac, or Alt-Return on Windows, to choose java.text.NumberFormat .
2. In the listener code in MainActivity , change the quantity to a number (if the view is
not empty) by using the NumberFormat.parse() method with intValue() to return an
integer.
3. Android Studio displays a red bulb in the left margin of the numberFormat.parse
statement because the statement requires an exception handler. Although the keyboard
is restricted to a numeric keypad, the code still needs to handle an exception when
parsing the string to convert it to a number. Click the bulb and choose Surround with
try/catch to create a simple try and catch block to handle exceptions. Android
Studio automatically imports java.text.ParseException .
4. The exception handling is not yet finished. The best practice is to display a message to
the user. The TextEdit setError() method provides a popup warning if, for some
reason, a number was not entered. Change the code in the previous step to the
following, using the string resource enter_number (provided in the strings.xml file and
previously translated).
232
Introduction
If the user runs the app and taps the Done key without entering a number, the
enter_number message appears. Since this is a string resource, the translated version
appears in French or Hebrew if the user chooses French or Hebrew for the device's
language.
In the listener code in MainActivity , add the following to convert the number to a string
using the format for the current locale, and show the string:
1. Enter a quantity and tap the Done key to close the keyboard.
2. Switch the language. First choose English (United States), run the app again, and
enter a quantity again. The thousands separator is a comma.
3. Switch the language to Français (France), run the app again, and enter a quantity
again. The thousands separator is a space.
4. Switch the language to Español (España), run the app again, and enter a quantity
again. The thousands separator is a period. Note that even though you have no
Spanish string resources (which is why the text appears in English), the date and the
number are both formatted for the Spain locale.
233
Introduction
To demonstrate how to show amounts in a currency format for the user's chosen locale, you
will add code that will show the price in the locale's currency. Given the complexities of
multiple currencies and daily fluctuations in exchange rates, you may want to limit the
currencies in an app to specific locales. To keep this example simple, you will use a fixed
exchange rate for just the France and Israel locales, based on the U.S. dollar. The starter
app already includes fixed (fake) exchange rates.
The starter app already includes variables for the France and Israel currency exchange
rates. You will use this to calculate and show the price. Pricing information would likely come
from a database or a web service, but to keep this app simple and focused on localization, a
fixed price in U.S. dollars has already been added to the starter app.
234
Introduction
1. Open MainActivity and find the fixed price and exchange rates at the top of the class:
235
Introduction
The Locale.getDefault() method gets the current value of the Locale , and the
Locale.getCountry() method returns the country/region code for this Locale . Used
together, they provide the country code that you need to check. The code FR is for
France, and IL is for Israel. The code tests only for those locales, because all other
locales, including the U.S., use the default currency (U.S. dollars).
The code then uses the currency format to create the string myFormattedPrice .
2. After the above code, add code to show the price string:
3. Run the app. The "Price per package" appears in U.S. dollars (left side of the figure
below) because the device or emulator is set to English (United States).
4. Change the language and locale to Français (France), and navigate back to the app.
The price appears in euros (center of the figure below).
5. Change the language to Français (Canada), and the price appears in U.S. dollars (right
side of the figure below) because the app doesn't support Canadian currency.
When the user chooses the Français (Canada) locale, the language changes to French
because French language resource strings are provided. The locale, however, is Canada.
Since Canadian currency is not supported, the code uses the default locale's currency,
which is the United States dollar. This demonstrates how the language and the locale can be
treated differently.
Solution code
Android Studio project: LocaleText3
236
Introduction
Coding challenge
Note: All coding challenges are optional and are not prerequisites for later lessons.
Challenge: This challenge demonstrates how to change colors and text styles based on the
locale. Download the Scorekeeper_start app (shown below), and rename and refactor the
project to ScorekeepLocale .
237
Introduction
238
Introduction
The Locales concept chapter explains how to change colors and styles for different locales.
In the Scorekeeper app, the text size is controlled by a style in styles.xml in the values
directory, and the color for a Button background is set in an XML file in the drawable
directory. An app can include multiple resource directories, each customized for a different
language and locale. Android picks the appropriate resource directory depending on the
user's choice for language and locale. For example, if the user chooses French, and French
has been added to the app as a language using the Translations Editor (which creates the
values-fr directory to hold it), then:
The strings.xml file in values-fr is used rather than the strings.xml file in the
default values directory, as you would expect.
The colors.xml and dimens.xml files in values-fr (if they have been added to the
directory) are used rather than the versions in the default values directory.
The drawables in drawable-fr (if this directory has been added to the app) are used
rather than the versions in the default drawable directory.
Hints:
The size is controlled in the TeamText style in styles.xml in the values directory. Add
a new styles.xml file to the values-iw directory and edit it.
The color for the Button background (the circle around the minus sign) is set in
minus_button_background.xml in the res>drawable directory. To create a different
language version, copy the drawable subdirectory and rename the copy drawable-af-
rZA. You can then change the minus_button_background.xml file in the copied directory
to change the color.
The minus sign color is set in the MinusButtons style in styles.xml .
Use "@android:color/holo_orange_light" for the new color.
239
Introduction
Summary
To format a date for current locale, use DateFormat .
The DateFormat getDateInstance() method gets the default formatting style for the
user's selected language and locale.
The DateFormat format() method formats a date string.
Use the NumberFormat class to format numbers and to parse formatted strings to retrieve
numbers.
240
Introduction
string.
The NumberFormat getCurrencyInstance() method returns the currency format for the
user-selected language and locale.
The NumberFormat format() method applies the format to create a string.
Resources are defined within specially named directories inside the project's res
directory. The path is project_name /app/src/main/res/ .
Add resource directories using the following general format:
<language code> : The language code, such as en for English or fr for French.
<country code> : Optional: The country code, such as US for the U.S. or FR for
France.
Related concept
The related concept documentation is Locales.
Learn more
Android developer documentation:
241
Introduction
Other:
242
Introduction
Common disabilities that can affect a person's use of an Android device include blindness,
low vision, color blindness, deafness or hearing loss, and restricted motor skills. When you
develop your apps with accessibility in mind, you make the user experience better not only
for users with these disabilities, but also for all of your other users.
In this lesson, you explore the features Android provides on the device to enable
accessibility, including Google TalkBack (Android's screen reader) and other options in the
Android framework.
Note: This practical describes the accessibility features in Android 7 (Nougat). Your device
or version of Android may not have all these options, or the options may have different
names.
243
Introduction
Turn on TalkBack and navigate the Google user interface with TalkBack enabled.
Enable other accessibility settings to customize your device.
App overview
For this lesson you use the built-in Android settings and apps. You do not build an app.
In this task, you enable TalkBack to understand how screen readers work and how to
navigate apps.
Note: By default, TalkBack is unavailable in the Android emulator and in some older devices.
The source for the TalkBack feature is available on GitHub. To test TalkBack on the emulator
or on a device without TalkBack, download and build the source.
If this is the first time you've run TalkBack, a tutorial launches. (The tutorial may not be
available on older devices.) Use the tutorial to learn about:
Explore by touch: TalkBack identifies every item you touch on the screen. You can
touch items individually or move your finger over the screen. Swipe left or right to
explore the items in tab (focus) order. The currently selected item has a green
border around the view. To activate the selected item (the last item heard), double-
tap it.
Scrolling. Lists can be scrolled with a two finger scroll, or you can jump forward or
244
Introduction
1. Tap the Home button, then double-tap the button to activate it and return to the Home
screen.
2. Tap the Camera icon, then double-tap to activate it.
3. Explore the various buttons and options in the Camera app. TalkBack identifies each
button and control by its function (for example, "Shutter Button" or "Zoom Out" button),
not by its appearance ("plus button").
4. Tap the Home button, then double-tap the button to activate it and return to the Home
screen.
5. Navigate to the Apps screen and activate the Calculator app.
6. Swipe right with one finger to navigate the views on the screen. Navigation in the app is
not strictly left-to-right and top-to-bottom. All the numbers in the leftmost panel are
identified before all the operations in the middle panel.
7. Keep swiping right to navigate to the advanced options screen, which is a panel in
green that moves out from the right. Swipe left and double-tap to activate the main view
and close the advanced operations. Note how these panel views do not have visible
labels, but are identified by purpose in TalkBack.
8. Perform the calculation 3 times 5. TalkBack reads the numbers, the operation as the
numbers are multiplied, and the result.
9. Navigate back to Settings > Accessibility > TalkBack, and turn off TalkBack. TIP:
</strong> To scroll in TalkBack, use a two-finger swipe gesture.
1. Navigate to Settings > Accessibility > Select to Speak. If you don't see Select to
Speak, go to Google Play to download the latest version of TalkBack.
245
Introduction
A speech button appears in the lower right corner of the screen. Select to Speak works
similarly to TalkBack, but Select to Speak works only on user-selected parts of the
screen, and only by request.
5. Tap the speech button, then tap to select a portion of the text on the screen. Android
reads that text to you.
6. Tap the Back button to return to the main Accessibility settings page.
7. Tap the speech button, then select a portion of the screen. Android reads all the text in
the selection.
8. Tap Select to Speak. Turn the toggle button off.
9. Tap the Back button, then scroll down and tap Accessibility shortcut.
This option lets you use the device buttons to enable TalkBack by holding down the
power button and then touching and holding two fingers on the screen. To disable
TalkBack again, you must go to Settings > Accessibility and turn it off manually.
10. Tap the Back button, then scroll down to Text-to-Speech output. Tap that item.
The Google text-to-speech engine is used for TalkBack and other text-to-speech
functions in apps that support speech-to-text. On this screen you can configure the
settings for speech rate and pitch. You can also configure the speech language, change
the language, and install new language packs. (To see all the options, tap the gear icon
on the Text-to-Speech output screen.)
246
Introduction
To use the magnification gesture, tap three times on the screen. Android zooms into the
spot where you tapped, providing a magnified view. Tap again three times to exit
magnification mode.
4. Tap Font size. Move the slider all the way to the right to increase the font size to the
largest setting. The preview at the top of the screen shows text at the chosen size.
A larger font size can make apps easier to read. A larger font size may cause app
elements to also be larger or to wrap in unexpected ways.
Note: These font-size changes only affect apps that use the Android font system. In
particular, the enlarged font size does not apply to the Chrome web browser. To enlarge
the font for web pages, use Chrome > Options menu > Settings > Accessibility >
Text scaling.
5. Return to the Home screen and examine the effect of the larger font in several apps. Try
messages, email, calendar, or any of the apps you've created in this course. Note that
apps that use their own internal fonts, such as some games, may not enlarge with this
setting.
6. Navigate back to Settings > Accessibility > Font size and return the font size to the
default.
7. Tap the Back button, select Display size, and move the slider all the way to the right.
Enlarging the display size makes all elements on the screen larger, including the text.
Fewer elements fit on the screen. Enlarging the display size can make apps easier to
read, and it also makes buttons and other interactive elements easier to tap.
8. Return the slider in the Display size settings to the default position.
High contrast text fixes the display text to be either black or white, depending on the
background, and outlines the text to emphasize it. High contrast text can make text
easier to read.
Note: High contrast text is an experimental feature and may not work in all apps.
2. Return to the Home screen. See how text appears in messages, email, calendar, or any
of the apps you've created in this course.
3. Return to Settings > Accessibility and turn High contrast text off again.
4. Scroll down to Color inversion and tap to turn it on.
247
Introduction
Color inversion reverses the colors on your screen. For example, black text on a white
screen becomes white text on a black screen. Color inversion can make some text
easier to read and reduce eye strain.
5. Return to the Home screen and try some apps with inverted colors.
6. Swipe from the top of the screen to open the navigation drawer. Click the down arrow to
open the quick settings.
7. Tap Invert colors to disable color inversion.
8. Navigate to Settings > Accessibility > Color Correction.
Color correction changes the system colors to help users with various forms of color
blindness distinguish screen elements.
10. Navigate to the Home screen and note the differences in system colors.
11. Return to the Color Correction setting and tap to turn it off.
High contrast text, Color inversion, and Color correction are experimental features in
Android. The features may not work in all apps and may affect app performance.
In the next practical, you learn how to support accessibility features in your own apps.
Solution code
No project for this practical.
Coding challenge
Note: All coding challenges are optional.
Challenge: Download and build the RecyclerView app, if you have not already done so. Run
the RecyclerView app with TalkBack enabled, and note where you could improve the label
text and feedback.
Summary
An accessible app works well for all users, including users with low vision, blindness,
deafness or hearing loss, cognitive impairments, or motor impairments.
On an Android device, all accessibility features are available under Settings >
Accessibility.
248
Introduction
With TalkBack enabled, users can interact with their Android device without seeing the
screen, because Android describes various screen elements aloud. Users with visual
impairments may rely on TalkBack when they use your app.
The TalkBack "explore by touch" feature identifies the element under a user's finger.
In TalkBack, swiping left or right identifies the next element to the user. Which element
is next depends on the focus order, which is generally from left to right and top to
bottom. If you don't want the default, you can define focus order for elements.
Select To Speak works similarly to TalkBack, but only by request, and only for specific
parts of the screen. To identify those parts of the screen, click the Speech button. Then
tap or drag a selection on the screen.
The Accessibility shortcut enables the user to turn on TalkBack quickly, without needing
to navigate to the settings pages.
The Text-to-speech output settings provide options for the Google text-to-speech
engine, which TalkBack uses.
Magnification gestures enable the user to magnify portions of the screen by tapping
three times.
Font and display-size settings can be used to enlarge the default system text or all app
elements.
The high-contrast-text setting fixes the display text to be either black or white,
depending on the background, and outlines the text to emphasize it.
The color-inversion setting reverses the colors on your screen.
The color-correction setting changes the system colors to help users with various forms
of color blindness distinguish screen elements.
Related concept
The related concept documentation is in Accessibility.
Learn more
Android support documentation:
249
Introduction
Text-to-speech output
Color inversion
Color correction
Other:
250
Introduction
Accessibility is a set of design, implementation, and testing techniques that enable your app
to be usable by everyone, including people with disabilities.
Common disabilities that can affect a person's use of an Android device include blindness,
low vision, color blindness, deafness or hearing loss, and restricted motor skills. When you
develop your apps with accessibility in mind, you make the user experience better not only
for users with these disabilities, but also for all of your other users.
Accessibility usually does not require a full overhaul of your app's design or code.
Accessibility does require you to pay attention to details and test your app under the same
conditions your users may encounter.
Android includes several accessibility-related features, to help you optimize your app's user
interface (UI) for accessibility. In this lesson, you learn how to test your app and add features
that enhance its accessibility.
251
Introduction
App overview
The SimpleAccessibility app demonstrates how to add accessible features to your app's UI.
252
Introduction
None of the views have click handlers, so the app does not have any actual functionality.
The focus for this app (and this lesson) is in the layout XML code and in the attributes that
enable accessibility.
253
Introduction
Note: By default, TalkBack is unavailable in the Android emulator and in some older devices.
The source for the TalkBack feature is available on GitHub. To test TalkBack on the emulator
or on a device without TalkBack, download and build the source.
To test the app with TalkBack enabled, use these steps:
Tap a UI element to hear the description for that element. Double-tap to select.
Swipe right or left to navigate through the elements in sequence. Double-tap
anywhere to select.
Drag your finger over the screen to hear what's under your finger. Double-tap
anywhere to select.
4. Build and run your app in Android Studio.
When your app starts with TalkBack enabled, no item is initially focused. TalkBack reads
the title of the app.
5. Tap each element or swipe to hear the descriptions for the elements in sequence. Note
the following:
The spoken feedback for the Compose button is the button title itself.
The feedback for the image button is "unlabelled button."
The feedback for a checkbox is the checkbox label and the current state of the
checkbox (selected or cleared.)
No feedback is available for the partly cloudy image. The image is not even a
focusable element.
6. Swipe from left to right to navigate between focusable elements on the screen. Note
that the focus moves from top to bottom. The partly cloudy image is not focusable.
254
Introduction
Android Studio highlights places in your XML layout code that have potential accessibility
problems, and makes suggestions for fixing those problems. For accessibility, Android
Studio checks two things:
To inspect and fix your code for accessibility, use these steps:
1. In Android Studio, open the res/layout/activity_main.xml file, if it's not already open.
Switch to the Text tab.
2. Note that the ImageButton and ImageView elements have yellow highlights on them.
When you hover over the elements, the message reads, "Missing contentDescription
attribute on image."
3. Add an android:contentDescription attribute to the ImageButton view, with the value
"Discard." Extract this string into a resource.
4. Add an android:contentDescription attribute to the ImageView , with the value "Partly
Cloudy." Extract this string into a resource.
5. Build and run your app.
6. Navigate your app with TalkBack turned on. The trash-can button now has a reasonable
description, but you still can't focus on the partly cloudy image.
Most views are focusable by default, and the focus moves from view to view in your layout.
The Android platform tries to figure out the most logical focus order based on each view's
closest neighbor. Generally, views are focussed from left to right and top to bottom. In some
cases, you want to make your views explicitly focusable or change the focus order to reflect
how the user uses your app.
In the case of the SimpleAccessibility app, the partly cloudy image is not focusable, even
with a content description added. Assuming that the partly cloudy image is there to show the
current weather, you must add a focusable attribute to that image so that TalkBack can read
the content description.
255
Introduction
android:focusable="true"
3. Build and run the app. With TalkBack enabled, navigate to the partly cloudy image.
TalkBack reads the image's content description.
Note: In addition to making an item focusable, you can add focus-related attributes to
the views in your app. The attributes include nextFocusUp , nextFocusDown ,
nextFocusLeft , nextFocusRight , and nextFocusForward . See Supporting Keyboard
For text views, TalkBack reads the text that's in the android:text attribute. Here, the
text is "Message." You do not need content descriptions for text views.
For EditText views, TalkBack reads the text that's in the android:hint attribute. Here,
the text is "Enter your message." If android:hint does not exist, TalkBack reads the
text that's in android:text instead.
In EditText views, it's better to use android:hint rather than android:text for the
default text.
When the app starts, the first EditText has the initial focus.
256
Introduction
TIP: In TalkBack, you can move the cursor in an EditText view with the device's volume up
and volume down buttons.
Explicit labels are only available in API 17 or higher, and Android Studio can highlight
missing labels for EditText views as part of code inspection. The SimpleAccessibility app
uses the default minimum SDK of 15. In this task, you change the API level to enable explicit
labels. Then you add the appropriate label attribute.
1. Open the build.gradle (Module: app) file and change minSdkVersion from 15 to 17 .
2. Click Sync Now to rebuild the project.
3. In the activity_main.xml layout file, delete the android:hint attribute from the
EditText .
The EditText element is now highlighted in yellow. When you hover over the element,
the message reads, "No label views point to this text field with an
android:labelFor="@+id/edittext_message" attribute."
4. Add the android:labelFor attribute to the TextView that serves as a label for this
EditText :
android:labelFor="@+id/edittext_message"
5. Build and run the app. TalkBack now reads the contents of the label to identify the
EditText .
Solution code
Android Studio project: SimpleAccessibility
Coding challenge
257
Introduction
Modify the SimpleAccessibility app to include a click handler for the Discard (trash can)
button. When the button is clicked, toggle between two images: the default trash can
and a lock icon. (You can use @android:drawable/ic_lock_lock for the lock icon.)
Use the setContentDescription() method to change the content description when the
button image changes.
Test the app in TalkBack. Verify that the correct content description appears for each
image.
Hints:
if (drawable1.getConstantState() == drawable2.getConstantState()) {
...
}
Summary
Adding accessibility to your app does not require significant code changes. You can add
many accessibility features to an existing app through attributes in the XML layout. Use
Android's TalkBack feature to test your app for users with low vision.
Android Studio can highlight missing accessibility attributes (content descriptions and
labels) in your app's layout.
To provide readable descriptions of buttons, add android:contentDescription attributes
to ImageView and ImageButton elements.
To provide the ability to navigate your app's UI, use focus and focus order.
Add android:focusable attributes to ImageView views that are not by default focusable.
You don't need to add content descriptions or focus to decorative images.
For EditText views, use android:hint instead of android:contentDescription .
258
Introduction
If your app supports API 17 or higher, use the android:labelFor attribute to indicate
that a view is a label for some other view.
Related concept
The related concept documentation is in Accessibility.
Learn more
Android support documentation:
Accessibility Overview
Making Apps More Accessible
Accessibility Developer Checklist
Building Accessible Custom Views
Developing Accessible Applications
Designing Effective Navigation
Testing Your App's Accessibility
Developing an Accessibility Service
Videos:
Other:
259
Introduction
Introduction
What you should already KNOW
What you will LEARN
What you will DO
App overview
Task 1. Set up location services
Task 2. Get the last known location
Task 3. Get the location as an address
Task 4. Receive location updates
Solution code
Coding challenge
Summary
Related concept
Learn more
Users are constantly moving around in the world, usually with their phones in their pockets.
Advances in GPS and network technologies have made it possible to create Android apps
with precise location awareness, using the location services APIs included in Google Play
services.
When an app requests the device location, it impacts network and battery consumption,
which impacts device performance. To improve the performance of your app, keep the
frequency of location requests as low as possible.
In this practical, you learn how to access the device's last known location. You also learn
how to convert a set of geographic coordinates (longitude and latitude) into a street address,
and how to perform periodic location updates.
260
Introduction
App overview
The WalkMyAndroid app prompts the user to change the device location settings, if
necessary, to allow the app to access precise location data. The app shows the device
location as latitude and longitude coordinates, then shows the location as a physical
address. The completed app does periodic location updates and shows an animation that
gives the user a visual cue that the app is tracking the device's location.
In tasks 1 and 2, you implement a button that gets the most recent location for the
device and displays the coordinates and timestamp of the location in a TextView .
In task 3, you turn the coordinates into a physical address, through a process called
reverse geocoding .
In task 4, you learn how to trigger periodic updates of the location.
261
Introduction
To find the device location efficiently without worrying about which provider or network type
to use, use the FusedLocationProviderClient interface. Before you can use
FusedLocationProviderClient , you need to set up Google Play services. In this task, you
The UI has a Get Location button, but tapping it doesn't do anything yet.
262
Introduction
Now you can include Google Play services packages in your app.
To add Google Play services to your project, add the following line of code to the
dependencies section in your app-level build.gradle (Module: app) file:
compile 'com.google.android.gms:play-services-location:XX.X.X'
Replace XX.X.X with the latest version number for Google Play services, for example
11.0.2 . Android Studio will let you know if you need to update it. For more information and
the latest version number, see Add Google Play Services to Your Project.
Note: If your app references more than 65K methods, the app may fail to compile. To
mitigate this problem, compile your app using only the Google Play services APIs that your
app uses. To learn how to selectively compile APIs into your executable, see Set Up Google
Play Services in the developer documentation. To learn how to enable an app configuration
known as multidex , see Configure Apps with Over 64K Methods.
Now that Google Play services is installed, you're ready to connect to the LocationServices
API.
ACCESS_COARSE_LOCATION
ACCESS_FINE_LOCATION
The permission you choose determines the accuracy of the location returned by the API. For
this lesson, use the ACCESS_FINE_LOCATION permission, because you want the most accurate
location information possible.
263
Introduction
1. Add the following element to your manifest file, above the <application> element:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Dangerous permissions cover areas where the app wants data or resources that involve the
user's private information, or could potentially affect the user's stored data or the operation
of other apps.
To request location permission at runtime:
2. Create a method stub called getLocation() that takes no arguments and doesn't return
anything. Invoke the getLocation() method from the button's onClick() method.
3. In the getLocation() method, check for the ACCESS_FINE_LOCATION permission.
264
Introduction
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_LOCATION_PERMISSION:
// If the permission is granted, get the location,
// otherwise, show a Toast
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
getLocation();
} else {
Toast.makeText(this,
R.string.location_permission_denied,
Toast.LENGTH_SHORT).show();
}
break;
}
}
6. Run the app. Clicking the button requests permission from the user. If permission is
granted, you see a log statement in the console.
After you grant permission, subsequent clicks on the Get Location button have no
effect. Because you already granted permission, the app doesn't need to ask for
permission again (unless the user manually revokes it in the Settings app), even if you
close and restart the app.
If no location has been obtained since the device was restarted, the getLastLocation()
method may return null . Usually, the getLastLocation() method returns a Location
object that contains a timestamp of when this location was obtained.
Note: If you aren't familiar with string replacement and formatting, see Formatting and
Styling and the Formatter documentation.
265
Introduction
2. In your MainActivity class, create a member variable of the Location type called
mLastLocation .
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
The getLastLocation() method returns a Task that results in a Location object (after
the Task's onSuccess() callback method is called, signifying that the Task was
successful).
Retrieve the latitude and longitude coordinates of a geographic location from the
resulting Location object:
6. Replace the log statement in the getLocation() method with the following code
snippet. The code obtains the device's most recent location and assigns it to
mLastLocation .
If the returned location is not null , set the TextView to show the coordinates and
time stamp of the Location object.
If the location returned is null , the fused location provider has not obtained a
location since the device was restarted. Display a message in the TextView that
says that the location is not available.
mFusedLocationClient.getLastLocation().addOnSuccessListener(
new OnSuccessListener<Location>() {
@Override
public void onSuccess(Location location) {
if (location != null) {
mLastLocation = location;
mLocationTextView.setText(
getString(R.string.location_text,
mLastLocation.getLatitude(),
mLastLocation.getLongitude(),
mLastLocation.getTime()));
} else {
mLocationTextView.setText(R.string.no_location);
}
}
7. Run the app. You now see the latest location that is stored in the fused location
provider.
266
Introduction
1. In Android Studio, create a new virtual device and select hardware for it.
2. In the System Image dialog, choose an image that says "Google APIs" or "Google
Play" in the Target column.
1. The emulator appears on your screen with a vertical menu to the right of the virtual
device. To access emulator options, click the ... icon at the bottom of this vertical menu.
2. Click Location.
3. Enter or change the coordinates in the Longitude and Latitude fields.
4. Click Send to update the fused location provider.
Clicking Send does not affect the location returned by getLastLocation() , because
getLastLocation() uses a local cache that the emulator tools do not update.
When you test the app on an emulator, the getLastLocation() method might return null ,
because the fused location provider doesn't update the location cache after the device is
restarted. If getLastLocation() returns null unexpectedly:
1. Start the Google Maps app and accept the terms and conditions, if you haven't already.
2. Use the steps above to update the fused location provider. Google Maps will force the
local cache to update.
3. Go back to your app and click the Get Location button. The app updates with the new
location.
Later in this lesson, you learn how to force the fused location to update the cache using
periodic updates.
267
Introduction
In this task, you subclass an AsyncTask to perform reverse geocoding off the main thread.
When the AsyncTask completes its process, it updates the UI in the onPostExecute()
method.
Using an AsyncTask means that when your main Activity is destroyed when the device
orientation changes, you will not longer be able to update the UI. To handle this, you make
the location tracking state persistent in Task 4.5.
Use the Params type to pass parameters into the doInBackground() method. For this
app, the passed-in parameter is the Location object.
Use the Progress type to mark progress in the onProgressUpdate() method. For this
app, you are not interested in the Progress type, because reverse geocoding is
typically quick.
Use the Results type to publish results in the onPostExecute() method. For this app,
the published result is the returned address String.
To create a subclass of AsyncTask that you can use for reverse geocoding:
2. In Android Studio, this class declaration is underlined in red, because you have not
implemented the required doInBackground() method. Press Alt + Enter (Option +
Enter on a Mac) on the highlighted line and select Implement methods. (Or select
Code > Implement methods.)
268
Introduction
Notice that the method signature for doInBackground() includes a parameter of the
Location type, and returns a String ; this comes from parameterized types in the
class declaration.
3. Override the onPostExecute() method by going to the menu and selecting Code >
Override Methods and selecting onPostExecute() . Again notice that the passed-in
parameter is automatically typed as a String, because this what you put in the
FetchAddressTask class declaration.
4. Create a constructor for the AsyncTask that takes a Context as a parameter and
assigns it to a member variable.
FetchAddressTask(Context applicationContext) {
mContext = applicationContext;
}
@Override
protected String doInBackground(Location... locations) {
return null;
}
@Override
protected void onPostExecute(String address) {
super.onPostExecute(address);
}
}
message.
1. Create a Geocoder object. This class handles both geocoding (converting from an
address into coordinates) and reverse geocoding:
2. Obtain a Location object. The passed-in parameter is a Java varargs argument that
can contain any number of objects. In this case we only pass in one Location object,
269
Introduction
3. Create an empty List of Address objects, which will be filled with the address
obtained from the Geocoder . Create an empty String to hold the final result, which will
be either the address or an error:
4. You are now ready to start the geocoding process. Open up a try block and use the
following code to attempt to obtain a list of addresses from the Location object. The
third parameter specifies the maximum number of addresses that you want to read. In
this case you only want a single address:
try {
addresses = geocoder.getFromLocation(
location.getLatitude(),
location.getLongitude(),
// In this sample, get just a single address
1);
}
5. Open a catch block to catch IOException exceptions that are thrown if there is a
network error or a problem with the Geocoder service. In this catch block, set the
resultMessage to an error message that says "Service not available." Log the error and
result message:
270
Introduction
7. You need to catch the case where Geocoder is not able to find the address for the given
coordinates. In the try block, check the address list and the resultMessage string. If
the address list is empty or null and the resultMessage string is empty, then set the
resultMessage to "No address found" and log the error:
8. If the address list is not empty or null , the reverse geocode was successful.
The next step is to read the first address into a string, line by line:
else {
// If an address is found, read it into resultMessage
Address address = addresses.get(0);
ArrayList<String> addressParts = new ArrayList<>();
271
Introduction
interface.
interface OnTaskCompleted {
void onTaskCompleted(String result);
}
@Override
protected void onPostExecute(String address) {
mListener.onTaskCompleted(address);
super.onPostExecute(address);
}
onTaskCompleted() method.
6. In this method, updated the TextView with the resulting address and the current time:
272
Introduction
@Override
public void onTaskCompleted(String result) {
// Update the UI
mLocationTextView.setText(getString(R.string.address_text,
result, System.currentTimeMillis()));
}
7. In the getLocation() method, inside the onSuccess() callback, replace the lines that
assigns the passed-in location to mLastLocation and sets the TextView with the
following line of code. This code creates a new FetchAddressTask and executes it,
passing in the Location object. You can also remove the now unused mLastLocation
member variable.
8. At the end of the getLocation() method, show loading text while the FetchAddressTask
runs:
mLocationTextView.setText(getString(R.string.address_text,
getString(R.string.loading),
System.currentTimeMillis()));
9. Run the app. After briefly loading, the app displays the location address in the
TextView .
273
Introduction
If your app relies heavily on device location, using the getLastLocation() method may not
be sufficient, because getLastLocation() relies on a location request from a different app
and only returns the last value stored in the provider.
Create a LocationRequest object that contains the requirements for your location
requests. The requirements include update frequency, accuracy, and so on. You do this
step in 4.2 Create the LocationRequest object, below.
Create a LocationCallback object and override its onLocationResult() method. The
onLocationResult() method is where your app receives location updates. You do this
updates.
The user has no way of knowing that the app is making location requests, except for a tiny
icon in the status bar. In this step, you use an animation (included in the starter code) to add
a more obvious visual cue that the device's location is being tracked. You also change the
button text to show the user whether location tracking is on or off.
assign it to mRotateAnim . Finally set the Android ImageView as the target for the
animation:
mRotateAnim.setTarget(mAndroidImageView);
Change the button text to "Start Tracking Location." Do this for for both the portrait
and the landscape layouts.
Change the TextView text to "Press the button to start tracking your location."
4. Refactor and rename the getLocation() method to startTrackingLocation() .
274
Introduction
@Override
public void onClick(View v) {
if (!mTrackingLocation) {
startTrackingLocation();
} else {
stopTrackingLocation();
}
}
/**
* Method that stops tracking the device. It removes the location
* updates, stops the animation and reset the UI.
*/
private void stopTrackingLocation() {
if (mTrackingLocation) {
mTrackingLocation = false;
mLocationButton.setText(R.string.start_tracking_location);
mLocationTextView.setText(R.string.textview_hint);
mRotateAnim.end();
}
}
275
Introduction
efficient by batching requests from different apps. This means that you may receive
updates faster than what you set in setInterval() , which can cause problems if your
UI is not ready for updates. To limit the rate of location updates, use the
setFastestInterval() method. In this app, use 5 seconds (5000 milliseconds)
Priority: Use this parameter with one of the priority constants to specify a balance
between power consumption and accuracy. (Greater accuracy requires greater power
consumption.) For this app, use the PRIORITY_HIGH_ACCURACY constant to prioritize
accuracy.
276
Introduction
You now have the required LocationRequest and LocationCallback objects to request
periodic location updates. When your app receives the LocationResult objects in
onLocationResult() , use the FetchAddressTask to reverse geocode the Location object
into an address:
mFusedLocationClient.requestLocationUpdates
(getLocationRequest(), mLocationCallback,
null /* Looper */);
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
@Override
public void onLocationResult(LocationResult locationResult) {
// If tracking is turned on, reverse geocode into an address
if (mTrackingLocation) {
new FetchAddressTask(MainActivity.this, MainActivity.this)
.execute(locationResult.getLastLocation());
}
Testing the location-update functionality on an emulator can be tough: the UI will say
"Loading" until you send a new location, and seeing the timing of the set interval is
impossible. You can use a GPX file to simulate different locations over time. For testing, you
can use the places_gps_data.gpx GPX file, which contains several locations:
1. Download the places_gps_data.gpx file.
2. Open your emulator, click the ... icon at the bottom of this vertical settings menu, and
select the Location tab.
3. Click Load GPX/KML and select the downloaded file.
277
Introduction
4. Change the duration of each item to 10 seconds, and click the play button. If you start
tracking when the GPX file is playing, you see a changing address displayed in the UI.
Right now, the app continues to request location updates until the user clicks the button, or
until the Activity is destroyed. To conserve power, stop location updates when your
Activity is not in focus (in the paused state) and resume location updates when the
false when the Activity is recreated. This means the UI defaults to the initial state.
In this step, you use the saved instance state to make mTrackingLocation persistent so that
the app continues to track location when there is a configuration change.
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putBoolean(TRACKING_LOCATION_KEY, mTrackingLocation);
super.onSaveInstanceState(outState);
}
278
Introduction
if (savedInstanceState != null) {
mTrackingLocation = savedInstanceState.getBoolean(
TRACKING_LOCATION_KEY);
}
5. Run the app and start location tracking. Rotate the device. A new FetchAddressTask is
triggered, and the device continues to track the location.
Solution code
WalkMyAndroid-Solution
Coding challenge
Note: All coding challenges are optional.
Challenge: Extend the location TextView to include the distance traveled from the first
location obtained. (See the distanceTo () method.)
Summary
Location information is available through the FusedLocationProviderClient .
Using location services requires location permissions.
Location permissions are categorized as "dangerous permissions," so you must include
them in the manifest and request them at runtime.
Use the getLastLocation() method to obtain the device's last known location from the
FusedLocationProviderClient .
The process of converting a set of coordinates (longitude and latitude) into a physical
address is called reverse geocoding . Reverse geocoding is available through the
Geocoder class' getFromLocation() method.
279
Introduction
Related concept
The related concept documentation is in 7.1 C: Location services.
Learn more
Android developer documentation:
Geocoder
FusedLocationProviderClient
280
Introduction
Introduction
What you should already KNOW
What you will LEARN
What you will DO
App overview
Task 1. Sign up and obtain API keys
Task 2. Get details on the current place
Task 3. Add the place-picker UI
Coding challenge
Solution code
Summary
Related concept
Learn more
The Location APIs can provide timely and accurate location information, but these APIs only
return a set of geographic coordinates. In 7.1 Using the device location, you learned how to
make geographic coordinates more useful by reverse geocoding them into physical
addresses.
But what if you want to know more about a location, like the type of place it is? In this
practical, you use the Google Places API for Android to obtain details about the device's
current location. You also learn about the place picker and the place-autocomplete APIs.
The place picker and autocomplete let users search for places rather than having your app
detect the device's current place.
App overview
The app for this practical extends the WalkMyAndroid app from the previous practical in two
ways:
1. Get details about the current location of the device, including the place type and place
name. Display the name in the label TextView and change the Android robot image to
reflect the place type.
282
Introduction
283
Introduction
2. Add a Pick a Place button that launches the place-picker UI, allowing the user to select
a place.
284
Introduction
285
Introduction
A debug certificate , which is the certificate you use for this practical. The Android SDK
tools generate a debug certificate when you do a debug build. Don't attempt to publish
an app that's signed with a debug certificate. The debug certificate is described in more
detail in Sign your debug build.
A release certificate , which you don't need for this practical. The Android SDK tools
generate a release certificate when you do a release build. You can also generate a
release certificate using the keytool utility. Use the release certificate when you're
ready to release your app to the world. For information about release certificates, see
Signup and API Keys.
For this practical, make sure that you use the debug certificate.
1. Locate your debug keystore file, which is named debug.keystore . By default, the file
is stored in the same directory as your Android Virtual Device (AVD) files:
286
Introduction
For Linux or macOS, open a terminal window and enter the following:
The line that begins with SHA1 contains the certificate's SHA-1 fingerprint. The
fingerprint is the sequence of 20 two-digit hexadecimal numbers separated by
colons.
3. Copy this value to your clipboard. You need it in the next set of steps.
Note: To protect your keystore file and key, don't enter the storepass or keypass
arguments on the command line unless you're confident of your computer's security. For
example, on a public computer, someone could look at your terminal window history or list of
running processes, get the password, and have write-access to your signing certificate. That
person could modify your app or replace your app with their own.
287
Introduction
For example:
BB:0D:AC:74:D3:21:E1:43:67:71:9B:62:91:AF:A1:66:6E:44:5D:75
com.example.android.walkmyandroidplaces
On the Credentials page, your new Android-restricted API key appears in the list of API
keys for your project. An API key is a string of characters, something like this:
AIzaSyBdVl-cTCSwYZrZ95SuvNw0dbMuDt1KG0
5. Copy the API key to your clipboard. You'll paste the key into your app manifest in the
next step.
288
Introduction
<application>
...
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY"/>
...
289
Introduction
290
Introduction
1. Add the following statement to your app-level build.gradle file. Replace XX.X.X with
the appropriate support library version. For the latest version number, see Add Google
Play Services to Your Project.
compile 'com.google.android.gms:play-services-places:XX.X.X'
3. In the onClick() method for the Start Tracking Location button, add another
argument to the setText() call. Pass in the loading string so that the TextView label
shows "Loading..." for the Name and Address lines:
mLocationTextView.setText(getString(R.string.address_text,
getString(R.string.loading),// Name
getString(R.string.loading),// Address
new Date())); // Timestamp
291
Introduction
the onTaskComplete() method, obtain the current place name and update the
TextView . To get the current place name, call PlaceDetectionClient.getCurrentPlace() .
interested in all places, you can pass in null for the PlaceFilter :
Task<PlaceLikelihoodBufferResponse> placeResult =
mPlaceDetectionClient.getCurrentPlace(null);
placeResult.addOnCompleteListener
(new OnCompleteListener<PlaceLikelihoodBufferResponse>() {
@Override
public void onComplete(@NonNull
Task<PlaceLikelihoodBufferResponse> task) {
});
if (task.isSuccessful()) {
} else {
{
5. If the Task was successful, call getResult() on the task to obtain the
PlaceLikelihoodBufferResponse . Initialize an integer to hold the maximum value.
292
Introduction
if (task.isSuccessful()) {
PlaceLikelihoodBufferResponse likelyPlaces = task.getResult();
float maxLikelihood = 0;
Place currentPlace = null;
}
6. Iterate over each PlaceLikelihood object and check whether it has the highest
likelihood so far. If it does, update the maxLikelihood and currentPlace objects:
if (task.isSuccessful()) {
PlaceLikelihoodBufferResponse likelyPlaces = task.getResult();
float maxLikelihood = 0;
Place currentPlace = null;
for (PlaceLikelihood placeLikelihood : likelyPlaces) {
if (maxLikelihood < placeLikelihood.getLikelihood()) {
maxLikelihood = placeLikelihood.getLikelihood();
currentPlace = placeLikelihood.getPlace();
}
}
}
7. If the currentPlace is not null , update the TextView with the result. The following
code should all be within the if (task.isSuccessful()) loop:
if (currentPlace != null) {
mLocationTextView.setText(
getString(R.string.address_text,
currentPlace.getName(), result //This is the address from the AsyncTask,
System.currentTimeMillis()));
}
likelyPlaces.release();
9. In the else block for the case where the Task is not successful, show an error
message instead of a place name:
else {
mLocationTextView.setText(
getString(R.string.address_text,
"No Place name found!",
result, System.currentTimeMillis()));
}
10. Run the app. You see the place name along with the address in the label TextView .
293
Introduction
In this step, you change the image of the Android robot to reflect the place type of the
current location. The starter code includes a bitmap image for a "plain" Android robot, plus
images for four other Android robots, one for each these place types: school, gym,
restaurant, and library.
If you want to add support for other place types, use the Androidify tool to create a custom
Android robot image and include the image in your app.
if (drawableID < 0) {
drawableID = R.drawable.android_plain;
}
mAndroidImageView.setImageResource(drawableID);
}
Note: The setAndroidType() method is where you can add support for more place
types. All you need to do is add another case to the switch statement and select the
appropriate drawable. For a list of supported place types, see Place Types.
2. In the onComplete() callback where you obtain the current Place object, call
setAndroidType() . Pass in the currentPlace object.
294
Introduction
3. Run your app. Unless you happen to be in one of the supported place types, you don't
see any difference in the Android robot image. To get around this, run the app on an
emulator and follow the steps below to set up fake locations.
To simulate a location, use a GPX file, which provides a set of GPS coordinates over time:
1. Download the WalkMyAndroidPlaces-gpx file. The file contains five locations. The first
location doesn't correspond to any of the supported place types. The other four
locations have the place types that the WalkMyAndroidPlaces app supports: school,
gym, restaurant, library.
2. Start an emulator of your choice.
3. To navigate to your emulator settings, select the three dots at the bottom of the menu
next to the emulator, then select the Location tab.
4. In the bottom right corner, click Load GPX/KML. Select the file you downloaded.
5. Notice the Delay column. By default, the emulator changes the location every 2
seconds. Change the delay to 10 seconds for each item except the first item, which
should load immediately and have a delay of 0.
6. Run the WalkMyAndroid app on the emulator to start tracking the device location.
7. Use the play button in the bottom left corner of the emulator Location tab to deliver the
GPX file's location information to your app. The location TextView and the Android
robot image should update every 10 seconds to reflect the new locations as they are
"played" by the GPX file!
295
Introduction
The screenshot below shows the Location tab (1) for emulator location settings, the GPS
data playback button (2), and the Load GPX/KML button (3).
Having the user select a location from a map seems complicated: you need a map with
places of interest already on it, and you need a way for the user to search for a place and
select a place. Fortunately, the Place API includes the place picker, a UI that greatly
simplifies the work.
In this task, you add a button that launches the place-picker UI. The place-picker UI lets the
user select a place, and it displays the place information in the UI, as before. (It displays the
relevant Android robot image, if available, and it displays the place name, address, and
update time.)
296
Introduction
1. Add a button next to the Start Tracking Location button. Use "Pick a Place" as the
button's text in both the portrait and the landscape layout files.
2. Add a click handler that executes the following code to start the PlacePicker . Create
an arbitrary constant called REQUEST_PICK_PLACE that you'll use later to obtain the result.
(This constant should be different from your permission-check integer.)
297
Introduction
3. Run your app. When you click Pick a Place, a PlacePicker dialog opens. In the dialog,
you can search for and select any place.
The next step is to get whatever place the user selects in onActivityResult() , then update
the TextView and Android robot image .
To get the Place object that was selected from the PlacePicker , use the
PlacePicker.getPlace() method.
3. If the resultCode is RESULT_OK , get the selected place from the data Intent using the
PlacePicker.getPlace() method. Pass in the application context and the data Intent ,
If the resultCode isn't RESULT_OK , show a message that says that a place was not
selected.
if (resultCode == RESULT_OK) {
Place place = PlacePicker.getPlace(this, data);
} else {
mLocationTextView.setText(R.string.no_place);
}
4. After you get the Place object, call setAndroidType() . Pass in your obtained object.
5. Update the label TextView with the place name and address from the Place object.
298
Introduction
mLocationTextView.setText(
getString(R.string.address_text, place.getName(),
place.getAddress(), System.currentTimeMillis()));
6. Run the app. You can now use the PlacePicker to choose any place in the world,
provided that the place exists in the Places API. To test your app's functionality, search
for one of the place types for which you have an Android robot image.
Note: When the user selects a location using the PlacePicker , this data is not persisted
using the SavedInstanceState . For this reason, when you rotate the device, the app resets
to the initial state.
Coding challenge
Note: All coding challenges are optional.
Challenge: Add a place autocomplete search dialog UI element to your Activity . The
place autocomplete dialog lets the user search for a place without launching the
PlacePicker .
Solution code
WalkMyAndroidPlaces-Solution
Summary
To use the Google Places API for Android, you must create an API key that's restricted
to Android apps. You do this in the Google API Console. In your project in the API
Console, you also need to enable the Google Places API for Android.
Include the API key in a metadata tag in your AndroidManifest.xml file.
The Place object contains information about a specific geographic location, including
the place name, address, coordinates, and more.
Use the PlaceDetectionClient.getCurrentPlace() method to get information about the
device's current location.
PlaceDetectionClient.getCurrentPlace() returns a PlaceLikelihoodBuffer in a Task .
299
Introduction
The Places API includes the PlacePicker , which you use to include the place-picker UI
in your app. The place-picker UI is a dialog that lets the user search for places and
select places.
Launch the PlacePicker using startActivityForResult() . Pass in an Intent created
with PlacePicker.IntentBuilder() .
Retrieve the selected place in onActivityResult() by calling PlacePicker.getPlace() .
Pass in the activity context and the data Intent .
Related concept
The related concept documentation is in 8.1: Places API.
Learn more
Android developer documentation:
PlaceDetectionClient
300
Introduction
Introduction
What you should already KNOW
What you will LEARN
What you will DO
App overview
Task 1. Set up the project and get an API Key
Task 2. Add map types and markers
Task 3. Style your map
Task 4. Enable location tracking and Street View
Coding challenge
Solution code
Summary
Related concept
Learn more
Building apps with Google Maps allows you to add features to your app such as satellite
imagery, robust UI controls, location tracking, and location markers. You can add value to
the standard Google Maps by showing information from your own data set, such as the
locations of well-known fishing or climbing areas. You can also create games tied to the real
world, like Pokemon Go.
301
Introduction
App overview
In this practical you create the Wander app, which is a styled Google Map. The Wander app
allows you to drop markers onto locations, see your location in real time, and look at Street
View panoramas.
302
Introduction
In this practical, you use the API key for the debug certificate. The debug certificate is
insecure by design, as described in Sign your debug build. Published Android apps that use
the Google Maps API require a second API key: the key for the release certificate. For more
information about obtaining a release certificate, see Get API Key.
Android Studio includes a Google Maps Activity template, which generates helpful template
code. The template code includes a google_maps_api.xml file containing a link that simplifies
obtaining an API key.
Note: If you wish to build the Activity without using the template, follow the steps in the API
key guide to obtain the API key without using the link in the template.
303
Introduction
google_maps_api.xml
You use this configuration file to hold your API key. The template generates two
google_maps_api.xml files: one for debug and one for release. The file for the API key
for the debug certificate is located in src/debug/res/values . The file for the API key for
the release certificate is located in src/release/res/values . In this practical we only use
the debug certificate.
activity_maps.xml
This layout file contains a single fragment that fills the entire screen. The
SupportMapFragment class is a subclass of the Fragment class. You can include
additional attribute:
android:name="com.google.android.gms.maps.SupportMapFragment"
MapsActivity.java
The MapsActivity.java file instantiates the SupportMapFragment class and uses the
class's getMapAsync() method to prepare the Google Map. The activity that contains
the SupportMapFragment must implement the OnMapReadyCallback interface and that
interface's onMapReady() method. The getMapAsync() method returns a GoogleMap
object, signifying that the map is loaded.
Note: If you test the Wander app on an emulator, use a system image that includes Google
APIs and Google Play. Select an image that shows Google Play in the Target column of the
virtual-devices list.
Run the app and notice that the map fails to load. If you look in the logs, you see a message
saying that your API key is not properly set up. In the next step, you obtain the API key to
make the app display the map.
304
Introduction
The file includes a comment with a long URL. The URL's parameters include specific
information about your app.
3. Follow the prompts to create a project in the Google API Console. Because of the
parameters in the provided URL, the API Console knows to automatically enable the
Google Maps Android API
4. Create an API key and click Restrict Key to restrict the key's use to Android apps. The
generated API key should start with AIza .
5. In the google_maps_api.xml file, paste the key into the google_maps_key string where it
says YOUR_KEY_HERE .
6. Run your app. You have an embedded map in your activity, with a marker set in Sydney,
Australia. (The Sydney marker is part of the template, and you change it later.)
1. To create a new menu XML file, right-click on your res directory and select New >
Android Resource File.
2. In the dialog, name the file map_options . Choose Menu for the resource type. Click OK.
3. Replace the code in the new file with the following code to create the map options. The
"none" map type is omitted, because "none" results in the lack of any map at all.
305
Introduction
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.map_options, menu);
return true;
}
7. To change the map type, use the setMapType () method on the GoogleMap object,
passing in one of the map-type constants.
Override the onOptionsItemSelected() method. Paste the following code to change the
map type when the user selects one of the menu options:
306
Introduction
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Change the map type based on the user's selection.
switch (item.getItemId()) {
case R.id.normal_map:
mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
return true;
case R.id.hybrid_map:
mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
return true;
case R.id.satellite_map:
mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);
return true;
case R.id.terrain_map:
mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
8. Run the app. Use the menu in the app bar to change the map type. Notice how the
map's appearance changes.
1. In the onMapReady() method, remove the code that places the marker in Sydney and
moves the camera.
2. Go to www.google.com/maps in your browser and find your home.
3. Right-click on the location and select What's here?
Near the bottom of the screen, a small window pops up with location information,
including latitude and longitude.
4. Create a new LatLng object called home . In the LatLng object, use the coordinates
you found from Google Maps in the browser.
5. Create a float variable called zoom and set the variable to your desired initial zoom
level. The following list gives you an idea of what level of detail each level of zoom
shows:
1 : World
307
Introduction
5 : Landmass/continent
10 : City
15 : Streets
20 : Buildings
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(home, zoom));
7. Run the app. The map should pan to your home and zoom into the desired level.
class. The default marker uses the standard Google Maps icon:
In this step, you add a marker when the user touches and holds a location on the map. You
then add an InfoWindow that displays the coordinates of the marker when the marker is
tapped.
308
Introduction
309
Introduction
map.addMarker(new MarkerOptions().position(latLng));
Navigation buttons appear at the bottom-left side of the screen, allowing the user to use
the Google Maps app to navigate to the marked position.
1. In the MarkerOptions object, set the title field and the snippet field.
2. In onMapLongClick() , set the title field to "Dropped Pin." Set the snippet field to the
location coordinates inside the addMarker() method.
310
Introduction
map.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() {
@Override
public void onMapLongClick(LatLng latLng) {
String snippet = String.format(Locale.getDefault(),
"Lat: %1$.5f, Long: %2$.5f",
latLng.latitude,
latLng.longitude);
map.addMarker(new MarkerOptions()
.position(latLng)
.title(getString(R.string.dropped_pin))
.snippet(snippet));
}
});
3. Run the app. Touch and hold on the map to drop a location marker. Tap the marker to
show the info window.
In this step, you add a GoogleMap.OnPoiClickListener to the map. This click-listener places a
marker on the map immediately, instead of waiting for a touch & hold. The click-listener also
displays the info window that contains the POI name.
311
Introduction
312
Introduction
map.setOnPoiClickListener(new GoogleMap.OnPoiClickListener() {
@Override
public void onPoiClick(PointOfInterest poi) {
}
});
3. In the onPoiClick() method, place a marker at the POI location. Set the title to the
name of the POI. Save the result to a variable called poiMarker .
poiMarker.showInfoWindow();
You can customize a MapFragment object using the available XML attributes, as you would
customize any other fragment. However, in this step you customize the look and feel of the
content of the MapFragment , using methods on the GoogleMap object. You use the online
Styling Wizard to add a style to your map and customize your markers. You also add a
GroundOverlay to your home location that scales and rotates with the map.
313
Introduction
To create a customized style for your map, you generate a JSON file that specifies how
features in the map are displayed.You don't have to create this JSON file manually: Google
provides the Styling Wizard, which generates the JSON for you after you visually style your
map. In this practical, you style the map for "night mode," meaning that the map uses dim
colors and low contrast for use at night.
Note: Styling only applies to maps that use the normal map type.
1. Navigate to https://mapstyle.withgoogle.com/ in your browser.
2. Select Create a Style.
3. Select the Night theme.
4. Click More Options at the bottom of the menu.
5. At the bottom of the Feature type list, select Water > Fill. Change the color of the water
to a dark blue (for example, #160064).
6. Click Finish. Copy the JSON code from the resulting pop-up window.
7. In Android Studio, create a resource directory called raw in the res directory. Create
a file in res/raw called map_style.json .
8. Paste the JSON code into the new resource file.
9. To set the JSON style to the map, call setMapStyle() on the GoogleMap object. Pass in
a MapStyleOptions object, which loads the JSON file. The setMapStyle() method
returns a boolean indicating the success of the styling. If the file can't be loaded, the
method throws a Resources.NotFoundException .
Copy the following code into the onMapReady() method to style the map. You may need
to create a TAG string for your log statements:
try {
// Customize the styling of the base map using a JSON object defined
// in a raw resource file.
boolean success = googleMap.setMapStyle(
MapStyleOptions.loadRawResourceStyle(
this, R.raw.map_style));
if (!success) {
Log.e(TAG, "Style parsing failed.");
}
} catch (Resources.NotFoundException e) {
Log.e(TAG, "Can't find style. Error: ", e);
}
10. Run your app. The new styling should be visible when the map is in normal mode.
314
Introduction
315
Introduction
1. In the onMapLongClick() method, add the following line of code to the MarkerOptions()
constructor to use the default marker but change the color to blue:
.icon(BitmapDescriptorFactory.defaultMarker
(BitmapDescriptorFactory.HUE_BLUE))
2. Run the app. The markers you place are now shaded blue, which is more consistent
with the night-mode theme of the app.
Note that POI markers are still red, because you didn't add styling to the onPoiClick()
method.
Shapes: You can add polylines, polygons, and circles to the map.
TileOverlay objects: A tile overlay defines a set of images that are added on top of the
base map tiles. Tile overlays are useful when you want to add extensive imagery to the
map. A typical tile overlay covers a large geographical area.
GroundOverlay objects: A ground overlay is an image that is fixed to a map. Unlike
markers, ground overlays are oriented to the Earth's surface rather than to the screen.
Rotating, tilting, or zooming the map changes the orientation of the image. Ground
overlays are useful when you wish to fix a single image at one area on the map
In this step, you add a ground overlay in the shape of an Android to your home location.
316
Introduction
4. Set the position property for the GroundOverlayOptions object by calling the
position() method. Pass in the home LatLng object and a float for the width in
meters of the desired overlay. For this example, a width of 100 m works well:
mMap.addGroundOverlay(homeOverlay);
6. Run the app. Zoom in on your home location, and you see the Android image as an
overlay.
The location-data layer adds a My Location button to the top-right side of the map. When
the user taps the button, the map centers on the device's location. The location is shown as
a blue dot if the device is stationary, and as a blue chevron if the device is moving.
317
Introduction
318
Introduction
You can provide additional information about a location using Google Street View, which is a
navigable panorama photo of a given location.
In this task, you enable the location-data layer and Street View so that when the user taps
the info window for the POI marker, the map goes into to Street View mode.
In this step, you request location permissions and enable the location tracking.
4. Call enableMyLocation() from the onMapReady() callback to enable the location layer.
319
Introduction
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
// Check if location permissions are granted and if so enable the
// location data layer.
switch (requestCode) {
case REQUEST_LOCATION_PERMISSION:
if (grantResults.length > 0
&& grantResults[0]
== PackageManager.PERMISSION_GRANTED) {
enableMyLocation();
break;
}
}
}
6. Run the app. The top-right corner now contains the My Location button, which displays
the device's current location.
Note: When you run the app on an emulator, the location may not be available. If you
haven't used the emulator settings to set a location, the location button will be unavailable.
In this step, you enable a Street View panorama that is activated when the user taps a POI's
info window. You need to do two things:
1. Distinguish POI markers from other markers, because you want your app's functionality
to work only on POI markers. This way, you can start Street View when the user taps a
POI info window, but not when the user taps any other type of marker.
The Marker class includes a setTag() method that allows you to attach data. (The
data can be anything that extends from Object ). You will set a tag on the markers that
are created when users click POIs.
2. When the user taps a tagged info window in an OnInfoWindowClickListener , replace the
MapFragment with a StreetViewPanoramaFragment . (The code below uses
If any of the fragments change at runtime, you must add them in the containing
Activity class, and not statically in XML.
320
Introduction
poiMarker.setTag("poi");
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
4. Add the fragment to the FrameLayout using a fragment transaction with the
FragmentManager :
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, mapFragment).commit();
5. Keep the line of code that triggers the asynchronous loading of the map:
mapFragment.getMapAsync(this);
321
Introduction
map.setOnInfoWindowClickListener(
new GoogleMap.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick(Marker marker) {
}
});
8. In the onInfoWindowClick() method, check whether the marker contains the string tag
you set in the onPoiClick() method:
if (marker.getTag() == "poi") {}
StreetViewPanoramaOptions options =
new StreetViewPanoramaOptions().position(
marker.getPosition());
SupportStreetViewPanoramaFragment streetViewFragment
= SupportStreetViewPanoramaFragment
.newInstance(options);
11. Start a fragment transaction. Replace the contents of the fragment container with the
new fragment, streetViewFragment . Add the transaction to the back stack, so that
pressing back will navigate back to the SupportMapFragment and not exit the app:
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container,
streetViewFragment)
.addToBackStack(null).commit();
322
Introduction
Note: The argument for the addToBackStack() method is an optional name. The name
is used to manipulate the back stack state. In this case, the name is not used again, so
you can pass in null .
12. Call setInfoWindowClickToPanorama(mMap) in onMapReady() after the call to
setPoiClick().
13. Run the app. Zoom into a city that has Street View coverage, such as Mountain View
(home of Google HQ), and find a POI, such as a park. Tap on the POI to place a marker
and show the info window. Tap the info window to enter Street View mode for the
location of the marker. Press the back button to return to the map fragment.
323
Introduction
324
Introduction
Coding challenge
Note: All coding challenges are optional.
Challenge: If you tap the info window for a POI in a location where there is no Street View
coverage, you see a black screen.
StreetViewPanorama.OnStreetViewPanoramaChangeListener .
If Street View isn't available in a selected area, go back to the map fragment and show
an error.
Solution code
Wander solution code. (Doesn't include the Challenge solution.)
Summary
To use the Maps API, you need an API key from the Google API Console.
In Android Studio, using the Google Maps Activity template generates an Activity
with a single SupportMapFragment in the app's layout. The template also adds the
ACCESS_FINE_PERMISSION to the app manifest, implements the OnMapReadyCallback in
Normal : Typical road map. Shows roads, some features built by humans, and
important natural features like rivers. Road and feature labels are also visible.
Hybrid : Satellite photograph data with road maps added. Road and feature labels
are also visible.
Satellite : Photograph data. Road and feature labels are not visible.
Terrain : Topographic data. The map includes colors, contour lines and labels, and
perspective shading. Some roads and labels are also visible.
None : No map.
You can change the map type at runtime by using the GoogleMap.setMapType() . method.
A marker is an indicator for a specific geographic location.
When tapped, the default behavior for a marker is to display an info window with
information about the location.
325
Introduction
By default, points of interest (POIs) appear on the base map along with their
corresponding icons. POIs include parks, schools, government buildings, and more.
In addition, business POIs (shops, restaurants, hotels, and more) appear by default on
the map when the map type is normal .
You can capture clicks on POIs using the OnPoiClickListener .
You can change the visual appearance of almost all elements of a Google Map using
the Styling Wizard. The Styling Wizard generates a JSON file that you pass into the
Google Map using the setMapStyle() method.
You can customize your markers by changing the default color, or replacing the default
marker icon with a custom image.
Use a ground overlay to fix an image to a geographic location.
Use a GroundOverlayOptions object to specify the image, the image's size in meters,
and the image's position. Pass this object to the GoogleMap.addGroundOverlay() method
to set the overlay to the map.
Provided that your app has the ACCESS_FINE_LOCATION permission, you can enable
location tracking using the mMap.setMyLocationEnabled(true) method.
Google Street View provides panoramic 360-degree views from designated roads
throughout its coverage area.
Use the StreetViewPanoramaFragment.newInstance() method to create a new Street View
fragment.
To specify the options for the view, use a StreetViewPanoramaOptions object. Pass the
object into the newInstance() method.
Related concept
The related concept documentation is in 9.1 C: Google Maps API.
Learn more
Android developer documentation:
Reference documentation:
326
Introduction
GoogleMap
SupportMapFragment
SupportStreetViewPanoramaFragment
327
Introduction
Android offers a large set of View subclasses, such as Button , TextView , EditText ,
ImageView , CheckBox , or RadioButton . You can use these subclasses to construct a UI
that enables user interaction and displays information in your app. If the View subclasses
don't meet your needs, you can create a custom view that does.
After you create a custom view, you can add it to different layouts in the same way you
would add a TextView or Button . This lesson shows you how to create and use custom
views based on View subclasses.
328
Introduction
App overview
The CustomEditText app demonstrates how to extend EditText to make a custom text-
editing view. The custom view includes a clear (X) button for clearing text. After the custom
view is created, you can use multiple versions of it in layouts, applying different EditText
attributes as needed.
329
Introduction
You also change a TextView to an EditText with attributes for controlling its appearance. If
the layout direction is set to a right-to-left (RTL) language, these attributes change the
direction in which the user enters text. (For more about supporting RTL languages, see the
lesson on localization.)
1. Create an app named CustomEditText using the Empty Activity template. Make sure
that Generate Layout File is selected so that the activity_main.xml layout file is
generated.
2. Edit the build.gradle (Module: app) file. Change the minimum SDK version to 17, so
that you can support RTL languages and place drawables in either the left or right
position in EditText views:
minSdkVersion 17
3. Right-click the drawable/ folder and choose New > Vector Asset. Click the Android
icon and choose the clear (X) icon. Its name changes to ic_clear_black_24dp . Click
Next and Finish.
330
Introduction
4. Repeat step 3, choosing the clear (X) icon again, but this time drag the Opacity slider to
50% as shown below. Change the icon's name to ic_clear_opaque_24dp .
Attribute Value
android:id "@+id/my_edit_text"
android:layout_width "wrap_content"
android:layout_height "wrap_content"
android:textAppearance "@style/Base.TextAppearance.AppCompat.Display1"
android:inputType "textCapSentences"
android:layout_gravity "start"
android:textAlignment "viewStart"
app:layout_constraintBottom_toBottomOf "parent"
app:layout_constraintLeft_toLeftOf "parent"
app:layout_constraintRight_toRightOf "parent"
app:layout_constraintTop_toTopOf "parent"
331
Introduction
7. Run the app. It displays an EditText field for entering text (a last name), and uses the
textCapSentences attribute to capitalize the first letter.
332
Introduction
333
Introduction
subclass that supports compatible features on older version of the Android platform.
2. The editor opens EditTextWithClear.java . A red bulb appears a few moments after you
click the class definition because the class is not complete—it needs constructors.
3. Click the red bulb and select Create constructor matching super. Select all three
constructors in the popup menu, and click OK.
Drawable mClearButtonImage;
2. Create a private method called init() , with no parameters, that initializes the
member variable to the drawable resource ic_clear_opaque_24dp .
The code includes two TODO comments for upcoming steps of this task.
334
Introduction
To show or hide the button, use the TextWatcher interface, whose methods are called if the
text changes. Follow these steps:
button.
/**
* Shows the clear (X) button.
*/
private void showClearButton() {
setCompoundDrawablesRelativeWithIntrinsicBounds
(null, // Start of text.
null, // Above text.
mClearButtonImage, // End of text.
null); // Below text.
}
/**
* Hides the clear button.
*/
private void hideClearButton() {
setCompoundDrawablesRelativeWithIntrinsicBounds
(null, // Start of text.
null, // Above text.
null, // End of text.
null); // Below text.
}
335
Introduction
Use null for positions that should not show a drawable . In hideClearButton() , the
setCompoundDrawablesRelativeWithIntrinsicBounds() method replaces the drawable
addTextChangedListener()
addTextChangedListener(new T)
5. Choose the TextWatcher{...} suggestion that appears. Android Studio creates the
beforeTextChanged() , onTextChanged() , and afterTextChanged() methods inside the
336
Introduction
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start,
int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
6. In the onTextChanged() method, call the showClearButton() method for showing the
clear (X) button. You implement only onTextChanged() in this practical, so leave
beforeTextChanged() and afterTextChanged() alone, or just add comments to them.
Clear the text from the field if the user taps the clear (X) button.
Render the clear (X) button as opaque before the user taps it, and black while the user
is tapping it.
To detect the tap and clear the text, use the View.OnTouchListener interface. The interface's
onTouch() method is called when a touch event occurs with the button.
You should design the EditTextWithClear class to be useful in both left-to-right (LTR) and
right-to-left (RTL) language layouts. However, the button is on the right side in an LTR
layout, and on the left side of an RTL layout. The code needs to detect whether the touch
occurred on the button itself. It checks to see if the touch occurred after the start location of
337
Introduction
the button. The start location of the button is different in an RTL layout than it is in an LTR
layout, as shown in the figure.
1. The start location of the button in an LTR layout. Moving to the right, the touch must
occur after this location on the screen and before the right edge.
2. The start location of the button in an RTL layout. Moving to the left, the touch must
occur after this location on the screen and before the left edge.
Tip: To learn more about reporting finger movement events, see MotionEvent .
1. In the init() method, replace the first TODO comment ( TODO: If the clear (X) button
is tapped, clear the text ) with the following code. If the clear (X) button is visible, this
code sets a touch listener that responds to events inside the bounds of the button.
2. In the onTouch() method, replace the single return false statement with the
following:
if ((getCompoundDrawablesRelative()[2] != null)) {
float clearButtonStart; // Used for LTR languages
float clearButtonEnd; // Used for RTL languages
boolean isClearButtonClicked = false;
// TODO: Detect the touch in RTL or LTR layout direction.
// TODO: Check for actions if the button is tapped.
}
return false;
In the previous step, you set the location of the clear (X) button using
setCompoundDrawablesRelativeWithIntrinsicBounds() :
338
Introduction
if that location is not null —which means that the clear (X) button is in that location.
Otherwise, the code returns false .
For an LTR language, the clear (X) button starts on the right side of the field. Any touch
occurring after the start point is a touch on the button itself.
For an RTL language, the clear (X) button ends on the left side of the field. Any touch
occurring before the endpoint is a touch on the button itself.
1. Use getLayoutDirection() to get the current layout direction. Use the MotionEvent
getX() method to determine whether the touch occurred after the start of the button in
an LTR layout, or before the end of the button in an RTL layout. In the onTouch()
method, replace the first TODO comment ( TODO: Detect the touch in RTL or LTR layout
direction ):
339
Introduction
2. Check for actions if the clear (X) button is tapped. On ACTION_DOWN , you want to show
the black version of the button as a highlight. On ACTION_UP , you want to switch back to
the default version of the button, clear the text, and hide the button. In the onTouch()
method, replace the second TODO comment ( TODO: Check for actions if the button is
tapped ):
340
Introduction
The first touch event is ACTION_DOWN . Use it to check if the clear (X) button is touched
( ACTION_DOWN ). If it is, switch the clear button to the black version.
The second touch event, ACTION_UP occurs when the gesture is finished. Your code
can then clear the text, hide the clear (X) button, and return true . Otherwise the code
returns false .
The EditTextWithClear class inherits the attributes defined for the original EditText ,
so there is no need to change any of them for this step.
If you see the message "classes missing" in the preview, click the link to rebuild the
project.
341
Introduction
2. Run the app. Enter text, and then tap the clear (X) button to clear the text.
1. Open the strings.xml file, and click the Open editor link in the top right corner to open
the Translations Editor.
2. Click the globe button in the top left corner of the Translations Editor pane, and select
Hebrew (iw) in Israel (IL) in the dropdown menu.
After you choose a language, a new column with blank entries appears in the
Translations Editor for that language, and the keys that have not yet been translated
appear in red.
3. Enter the Hebrew translation of "Last name" for the last_name key by selecting the
key's cell in the column for the language (Hebrew), and entering the translation in the
Translation field at the bottom of the pane. (For instructions on using the Translations
Editor, see the chapter on localization.) When finished, close the Translations Editor.
4. On your device or emulator, find the Languages & input settings in the Settings app.
For devices using Android Oreo (8) or newer, the Languages & input choice is under
System.
342
Introduction
Be sure to remember the globe icon for the Languages & input choice, so that you can
6. For devices and emulators running Android 6 or older, select ִעבִריתfor Hebrew. For
devices and emulators running Android 7 or newer, click Add a language, select ִעבִרית,
select ( עברית )ישראלfor the locale, and then use the move icon on the right side of the
Language preferences screen to drag the language to the top of the list.
7. Run the app. The EditTextWithClear element should be reversed for an RTL language,
with the clear (X) button on the left side, as shown below.
8. Put a finger on the clear (X) button, or if you're using a mouse, click and hold on the
clear button. Then drag away from the clear button. The button changes from gray to
black, indicating that it is still touched.
343
Introduction
344
Introduction
9. To change back from Hebrew to English, repeat Steps 4-6 above with the selection
English for language and United States for locale.
Solution code
Android Studio project: CustomEditText
Summary
To create a custom view that inherits the look and behavior of a View subclass such as
EditText , add a new class that extends the subclass (such as EditText ), and make
Tip: View the different methods of the View subclasses, such as TextView , Button , and
ImageView , to see how you can modify a View subclass by overriding these methods. For
Related concept
The related concept documentation is Custom views.
Learn more
Android developer documentation:
Input Events
onDraw()
Canvas
drawCircle()
345
Introduction
drawText()
Paint
Video:
346
Introduction
By extending View directly, you can create an interactive UI element of any size and shape
by overriding the onDraw() method for the View to draw it. After you create a custom view,
you can add it to different layouts in the same way you would add any other View . This
lesson shows you how to create a custom view from scratch by extending View directly.
347
Introduction
App overview
The CustomFanController app demonstrates how to create a custom view subclass from
scratch by extending the View class. The app displays a circular UI element that resembles
a physical fan control, with settings for off (0), low (1), medium (2), and high (3). You can
modify the subclass to change the number of settings, and use standard XML attributes to
define its appearance.
348
Introduction
All the code to draw the custom view is provided in this task. (You learn more about
onDraw() and drawing on a Canvas object with a Paint object in another lesson.)
3. Add or change the following TextView attributes, leaving the other layout attributes
(such as layout_constraintTop_toTopOf ) the same:
android:textAppearance "@style/Base.TextAppearance.AppCompat.Display1"
android:padding "16dp"
android:layout_marginLeft "8dp"
android:layout_marginStart "8dp"
android:layout_marginEnd "8dp"
android:layout_marginRight "8dp"
android:layout_marginTop "24dp"
349
Introduction
android:layout_width "200dp"
android:layout_height "200dp"
android:background "@android:color/darker_gray"
app:layout_constraintTop_toBottomOf "@+id/customViewLabel"
app:layout_constraintLeft_toLeftOf "parent"
app:layout_constraintRight_toRightOf "parent"
android:layout_marginLeft "8dp"
android:layout_marginRight "8dp"
android:layout_marginTop "8dp"
3. ImageView attributes
350
Introduction
The SELECTION_COUNT defines the total number of selections for this custom view. The
code is designed so that you can change this value to create a control with more or
fewer selections.
The mTempLabel and mTempResult member variables provide temporary storage for the
result of calculations, and are used to reduce the memory allocations while drawing.
4. As in the previous app, use a separate method to initialize the view. This init() helper
initializes the above instance variables:
Paint styles for rendering the custom view are created in the init() method rather
351
Introduction
6. Because a custom view extends View , you can override View methods such as
onSizeChanged() to control its behavior. In this case you want to determine the drawing
bounds for the custom view's dial by setting its width and height, and calculating its
radius, when the view size changes, which includes the first time it is drawn. Add the
following to DialView :
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// Calculate the radius from the width and height.
mWidth = w;
mHeight = h;
mRadius = (float) (Math.min(mWidth, mHeight) / 2 * 0.8);
}
The onSizeChanged() method is called when the layout is inflated and when the view
has changed. Its parameters are the current width and height of the view, and the "old"
(previous) width and height.
The code for drawing this view is provided without explanation because the focus of this
lesson is creating and using a custom view. The code uses the Canvas methods
drawCircle() and drawText() .
352
Introduction
The pos parameter is a position index (starting at 0). The radius parameter is for the
outer circle.
You will use the computeXYForPosition() method in the onDraw() method. It returns a
two-element array for the position, in which element 0 is the X coordinate, and element
1 is the Y coordinate.
2. To render the view on the screen, use the following code to override the onDraw()
method for the view. It uses drawCircle() to draw a circle for the dial, and to draw the
indicator mark. It uses drawText() to place text for labels, using a StringBuffer for the
label text.
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the dial.
canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius, mDialPaint);
// Draw the text labels.
final float labelRadius = mRadius + 20;
StringBuffer label = mTempLabel;
for (int i = 0; i < SELECTION_COUNT; i++) {
float[] xyData = computeXYForPosition(i, labelRadius);
float x = xyData[0];
float y = xyData[1];
label.setLength(0);
label.append(i);
canvas.drawText(label, 0, label.length(), x, y, mTextPaint);
}
// Draw the indicator mark.
final float markerRadius = mRadius - 35;
float[] xyData = computeXYForPosition(mActiveSelection,
markerRadius);
float x = xyData[0];
float y = xyData[1];
canvas.drawCircle(x, y, 20, mTextPaint);
}
attribute.
353
Introduction
The DialView class inherits the attributes defined for the original ImageView , so there
is no need to change the other attributes.
354
Introduction
355
Introduction
1. Add the following after the TODO comment in the init() method:
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Rotate selection to the next valid choice.
mActiveSelection = (mActiveSelection + 1) % SELECTION_COUNT;
// Set dial background color to green if selection is >= 1.
if (mActiveSelection >= 1) {
mDialPaint.setColor(Color.GREEN);
} else {
mDialPaint.setColor(Color.GRAY);
}
// Redraw the view.
invalidate();
}
});
The invalidate() method of View invalidates the entire view, forcing a call to
onDraw() to redraw the view. If something in your custom view changes and the
2. Run the app. Tap the DialView element to move the indicator from 0 to 1. The dial
should turn green. With each tap, the indicator should move to the next position. When
the indicator reaches 0, the dial should turn gray.
356
Introduction
Coding challenge 1
Note: All coding challenges are optional.
Challenge: Define two custom attributes for the DialView custom view dial colors:
fanOnColor for the color when the fan is set to the "on" position, and fanOffColor for the
Hints
For this challenge you need to do the following:
Create the attrs.xml file in the values folder to define the custom attributes:
357
Introduction
<resources>
<declare-styleable name="DialView">
<attr name="fanOnColor" format="reference|color" />
<attr name="fanOffColor" format="reference|color" />
</declare-styleable>
</resources>
<resources>
<color name="red1">#FF2222</color>
<color name="green1">#22FF22</color>
<color name="blue1">#2222FF</color>
<color name="cyan1">#22FFFF</color>
<color name="gray1">#8888AA</color>
<color name="yellow1">#ffff22</color>
</resources>
Specify the fanOnColor and fanOffColor custom attributes with DialView in the
layout:
<com.example.customfancontroller.DialView
android:id="@+id/dialView"
android:layout_width="@dimen/dial_width"
android:layout_height="@dimen/dial_height"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginRight="@dimen/standard_margin"
android:layout_marginLeft="@dimen/standard_margin"
app:fanOffColor="@color/gray1"
app:fanOnColor="@color/cyan1"
app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
Create three constructors for DialView , which call the init() method. Include in the
init() method the default color settings, and paint the initial "off" state of the
Use the following code in the init() method to supply the attributes to the custom
view. The code uses a typed array for the attributes:
358
Introduction
In the init() method, change the onClick() method for the DialView to use
mFanOnColor and mFanOffColor to set the colors when the dial is clicked:
Run the app. The dial's color for the "off" position should be gray (as before), and the color
for any of the "on" positions should be cyan—defined as the default colors for mFanOnColor
and mFanOffColor .
359
Introduction
app:fanOffColor="@color/blue1"
app:fanOnColor="@color/red1"
360
Introduction
Run the app again. The dial's color for the "off" position should be blue, and the color for any
of the "on" positions should be red. Try other combinations of colors that you defined in
colors.xml .
You have successfully created custom attributes for DialView that you can change in your
layout to suit the color choices for the overall UI that will include the DialView .
Coding challenge 2
Note: All coding challenges are optional.
361
Introduction
Challenge: Enable the app user to change the number of selections on the circular dial in
the DialView , as shown in the figure below.
For four selections (0, 1, 2, and 3) or fewer, the selections should still appear as before
along the top half of the circular dial. For more than four selections, the selections should be
symmetrical around the dial.
To enable the user to change the number of selections, use the options menu in
MainActivity . Note that because you are adding the number of selections as a custom
attribute, you can set the initial number of selections in the XML layout file (as you did with
colors).
Preliminary steps
Add an options menu to the app. Because this involves also changing the styles.xml file
and the code for showing the app bar, you may find it easier to do the following:
1. Start a new app using the Basic Activity template, which provides a MainActivity with
an options menu and a floating action button. Remove the floating action button.
2. Add a new Activity using the Empty Activity template. Copy the DialView custom
view code from the CustomFanController app or CustomFanChallenge app and paste it
into the new Activity .
3. Add the elements of the activity_main.xml layout from the CustomFanController app
or CustomFanChallenge app to content_main.xml in the new app.
Hints
The DialView custom view is hardcoded to have 4 selections (0, 1, 2, and 3), which are
defined by the integer constant SELECTION_COUNT . However, if you change the code to use
an integer variable ( mSelectionCount ) and expose a method to set the number of selections,
362
Introduction
then the user can customize the number of selections for the dial. The following code
elements are all you need:
In DialView.java :
mSelectionCount =
typedArray.getInt(R.styleable.DialView_selectionIndicators,
mSelectionCount);
363
Introduction
private float[] computeXYForPosition(final int pos, final float radius , boolean isLab
el) {
float[] result = mTempResult;
Double startAngle;
Double angle;
if (mSelectionCount > 4) {
startAngle = Math.PI * (3 / 2d);
angle= startAngle + (pos * (Math.PI / mSelectionCount));
result[0] = (float) (radius * Math.cos(angle * 2))
+ (mWidth / 2);
result[1] = (float) (radius * Math.sin(angle * 2))
+ (mHeight / 2);
if((angle > Math.toRadians(360)) && isLabel) {
result[1] += 20;
}
} else {
startAngle = Math.PI * (9 / 8d);
angle= startAngle + (pos * (Math.PI / mSelectionCount));
result[0] = (float) (radius * Math.cos(angle))
+ (mWidth / 2);
result[1] = (float) (radius * Math.sin(angle))
+ (mHeight / 2);
}
return result;
}
Change the onDraw() code that calls computeXYForPosition() to include the isLabel
argument:
Add a "setter" method that sets the selection count, and resets the active selection to
zero and the color to the "off" color:
In the menu_main.xml file, add menu items for the options menu:
Provide the text for a menu item for each dial selection from 3 through 9.
364
Introduction
Use the android:orderInCategory attribute to order the menu items from 3 through 9.
For example, for "Selections: 4," which corresponds to the string resource
dial_settings4 :
<item
android:orderInCategory="4"
android:title="@string/dial_settings4"
app:showAsAction="never" />
In MainActivity.java :
DialView mCustomView;
After setting the content view in onCreate() , assign the dialView resource in the
layout to mCustomView :
mCustomView = findViewById(R.id.dialView);
int n = item.getOrder();
mCustomView.setSelectionCount(n);
return super.onOptionsItemSelected(item);
Run the app. You can now change the number of selections on the dial using the options
menu, as shown in the previous figure.
Summary
To create a custom view of any size and shape, add a new class that extends View .
Override View methods such as onDraw() to define the view's shape and basic
365
Introduction
appearance.
Use invalidate() to force a draw or redraw of the view.
To optimize performance, assign any required values for drawing and painting before
using them in onDraw() , such as in the constructor or the init() helper method.
Add listeners such as View.OnClickListener to the custom view to define the view's
interactive behavior.
Add the custom view to an XML layout file with attributes to define its appearance, as
you would with other UI elements.
Create the attrs.xml file in the values folder to define custom attributes. You can
then use the custom attributes for the custom view in the XML layout file.
Related concept
The related concept documentation is Custom views.
Learn more
Android developer documentation:
Video:
366
Introduction
When you want to create your own custom 2D drawings for Android, you can do so in the
following ways.
1. Draw your graphics or animations on a View object in your layout. By using this option,
the system's rendering pipeline handles your graphics—it's your responsibility to define
the graphics inside the view.
2. Draw your graphics in a Canvas object. To use this option, you pass your Canvas to
the appropriate class' onDraw(Canvas) method. You can also use the drawing methods
in Canvas . This option also puts you in control of any animation.
Drawing to a view is a good choice when you want to draw simple graphics that don't need
to change dynamically, and when your graphics aren't part of a performance-intensive app
such as a game. For example, you should draw your graphics into a view when you want to
display a static graphic or predefined animation, within an otherwise static app. For more
information, read Drawables .
Drawing to a canvas is better when your app needs to regularly redraw itself. Apps, such as
video games, should draw to the canvas on their own. This practical shows you how to
create a canvas, associate it with a bitmap, and associate the bitmap with an ImageView for
display.
When you want to draw shapes or text into a view on Android, you need:
367
Introduction
The figure below shows all the pieces required to draw to a canvas.
You do not need a custom view to draw, as you learn in this practical. Typically you draw by
overriding the onDraw() method of a View , as shown in the next practicals.
See the Graphics Architecture series of articles for an in-depth explanation of how the
Android framework draws to the screen.
Create apps with Android Studio and run them on a physical or virtual mobile device.
Add a click event handler to a View .
Create and display a custom View .
Create a Canvas object, associate it with a Bitmap object, and display the bitmap in an
ImageView .
368
Introduction
App overview
As you build the SimpleCanvas app, you learn how to create a canvas, associate it with a
bitmap, and associate the bitmap with an ImageView for display.
When the user clicks in the app, a rectangle appears. As the user continues to click, the app
draws increasingly smaller rectangles onto the canvas.
When you start the app, you see a white surface, the default background for the ImageView .
Tap the screen, and it fills with orange color, and the underlined text "Keep tapping" is
drawn. For the next four taps, four differently colored inset rectangles are drawn. On the final
tap, a circle with centered text tells you that you are "Done!", as shown in the screenshot
below.
If the device is rotated, the drawing is reset, because the app does not save state. In this
case, this behavior is "by design," to give you a quick way of clearing the canvas.
369
Introduction
370
Introduction
Note: The benefit of doing an example without a custom view is that you can focus on
drawing on the canvas. For a real-world application, you are likely to need a custom view.
<ImageView
android:id="@+id/myimageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="drawSomething"/>
</android.support.constraint.ConstraintLayout>
<color name="colorRectangle">#455A64</color>
<color name="colorBackground">#FFFFD600</color>
371
Introduction
The Canvas object stores information on what to draw onto its associated bitmap. For
example, lines, circles, text, and custom paths.
2. Create a Paint member variable mPaint and initialize it with default values.
The Paint objects store how to draw. For example, what color, style, line thickness, or
text size. Paint offers a rich set of coloring, drawing, and styling options. You
customize them below.
3. Create a Paint object for underlined text. Paint offers a full complement of
typographical styling methods. You can supply these styling flags when you initialize the
object or set them later.
The Bitmap represents the pixels that are shown on the display.
A view, in this example an ImageView , is the container for the bitmap. Layout on the
screen and all user interaction is through the view.
6. Create two Rect variables, mRect and mBounds and initialize them to rectangles.
372
Introduction
7. Create a constant OFFSET initialized to 120, and initialize a member variable mOffset
with the constant. This offset is the distance of a rectangle you draw from the edge of
the canvas.
8. Create a MULTIPLIER constant initialized to 100. You will need this constant later, for
generating random colors.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
2. In onCreate() , get color resources and assign them to the color member variables.
mColorBackground = ResourcesCompat.getColor(getResources(),
R.color.colorBackground, null);
mColorRectangle = ResourcesCompat.getColor(getResources(),
R.color.colorRectangle, null);
mColorAccent = ResourcesCompat.getColor(getResources(),
R.color.colorAccent, null);
mPaint.setColor(mColorBackground);
4. In onCreate() , set the color for mPaintText to the theme color colorPrimaryDark , and
set the text size to 70. Depending on the screen size of your device, you may need to
373
Introduction
mPaintText.setColor(
ResourcesCompat.getColor(getResources(),
R.color.colorPrimaryDark, null)
);
mPaintText.setTextSize(70);
Important: You cannot create the Canvas in onCreate() , because the views have not
been laid out, so their final size is not available. When you create a custom view in a
later lesson, you learn different ways to initialize your drawing surface.
The drawSomething() click handler responds to user taps by drawing an increasingly smaller
rectangle until it runs out of room. Then it draws a circle with the text "Done!" to demonstrate
basics of drawing on canvas.
1. Create Bitmap .
2. Associate Bitmap with View .
3. Create Canvas with Bitmap .
4. Draw on Canvas .
5. Call invalidate() on the View to force redraw.
2. Get the width and height of the view and create convenience variables for half the width
and height. You must do this step every time the method is called, because the size of
the view can change (for example, when the device is rotated).
374
Introduction
4. mOffset == OFFSET . The app is only in this state the first time the user taps. Create the
Bitmap , associate it with the View , create the Canvas , fill the background, and draw
5. mOffset != OFFSET and the offset is smaller than half the screen width and height. Draw
if (mOffset == OFFSET) {
} else {
if (mOffset < halfWidth && mOffset < halfHeight) {
} else {
}
}
8. Supply the width and height for the bitmap, which are going to be the same as the width
and height of the view.
mImageView.setImageBitmap(mBitmap);
11. Create a Canvas and associate it with mBitmap , so that drawing on the canvas draws
on the bitmap.
375
Introduction
mCanvas.drawColor(mColorBackground);
13. Draw the "Keep tapping" text onto the canvas. You need to supply a string, x and y
positions, and a Paint object for styling.
mOffset += OFFSET;
15. At the end of the drawSomething() method, invalidate() the view so that the system
redraws the view every time drawSomething() is executed.
When a view is invalidated, the system does not draw the view with the values it already
has. Instead, the system recalculates the view with the new values that you supply. The
screen refreshes 60 times a second, so the view is drawn 60 times per second. To save
work and time, the system can reuse the existing view until it is told that the view has
changed, the existing view is invalid, and the system thus has to recalculate an updated
version of the view.
view.invalidate();
Note: If you run the app at this point, it should start with a blank screen, and when you
tap, the screen fills and the text appears.
17. Set the color of mPaint . This code generates the next color by subtracting the current
offset times a multiplier from the original color. A color is represented by a single
number, so you can manipulate it in this way for some fun effects.
18. Change the size of the mRect rectangle to the width of the view, minus the current
offset.
19. Draw the rectangle with mPaint styling.
20. Increase the offset.
376
Introduction
1. In the else statement, when the offset is too large to draw another rectangle:
else {
mPaint.setColor(mColorAccent);
mCanvas.drawCircle(halfWidth, halfHeight, halfWidth / 3, mPaint);
String text = getString(R.string.done);
// Get bounding box for text to calculate where to draw it.
mPaintText.getTextBounds(text, 0, text.length(), mBounds);
// Calculate x and y for text so it's centered.
int x = halfWidth - mBounds.centerX();
int y = halfHeight - mBounds.centerY();
mCanvas.drawText(text, x, y, mPaintText);
}
5. Run your app and tap multiple times to draw. Rotate the screen to reset the app.
Solution code
Android Studio project: SimpleCanvas.
Summary
To draw on the display of a mobile device with Android you need a View , a Canvas , a
Paint , and a Bitmap object.
The Bitmap is the physical drawing surface. The Canvas provides an API to draw on
the bitmap, the Paint is for styling what you draw, and the View displays the Bitmap .
377
Introduction
You create a Bitmap , associate it with a View , create a Canvas with a Paint object
for the Bitmap , and then you can draw.
You must invalidate() the view when your are done drawing, so that the Android
System redraws the display.
All drawing happens on the UI thread, so performance matters.
Related concepts
The related concept documentation is in The Canvas class.
Learn more
Android developer documentation:
Canvas class
Bitmap class
View class
Paint class
Bitmap.config configurations
378
Introduction
A more common pattern for using the Canvas class is to subclass one of the View classes,
override its onDraw() and onSizeChanged() methods to draw, and override the
onTouchEvent() method to handle user touches.
Create apps with Android Studio and run them on a physical or virtual mobile device.
Add event handlers to views.
Create a custom View .
Create a bitmap and associate it with a view. Create a canvas for a bitmap. Create and
customize a Paint object for styling. Draw on the Canvas and refresh the display.
Create a custom View, capture the user's motion event, and interpret it to draw lines
onto the canvas.
379
Introduction
App overview
The CanvasExample uses a custom view to display a line in response to user touches, as
shown in the screenshot below.
380
Introduction
381
Introduction
<color name="opaque_orange">#FFFF5500</color>
<color name="opaque_yellow">#FFFFEB3B</color>
3. In styles.xml , set the parent of the default style to NoActionBar to remove the action
bar, so that you can draw fullscreen.
onDraw() method.
6. Add constructors to initialize the mPath , mPaint , and mDrawColor variables. (You only
need these two constructors of all that are available.)
Paint.Style specifies if the primitive being drawn is filled, stroked, or both (in the
same color).
382
Introduction
Paint.Join specifies how lines and curve segments join on a stroked path.
Paint.Cap specifies how the beginning and ending of stroked lines and paths.
MyCanvasView(Context context) {
this(context, null);
}
mBackgroundColor = ResourcesCompat.getColor(getResources(),
R.color.opaque_orange, null);
mDrawColor = ResourcesCompat.getColor(getResources(),
R.color.opaque_yellow, null);
383
Introduction
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyCanvasView myCanvasView;
// No XML file; just one custom view created programmatically.
myCanvasView = new MyCanvasView(this);
// Request the full available screen for layout.
myCanvasView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
setContentView(myCanvasView);
}
The onSizeChanged() method is called whenever a view changes size. Because the
view starts out with no size, the onSizeChanged() method is also called after the activity
first inflates the view. This method is thus the ideal place to create and set up the
canvas.
Create a Bitmap , create a Canvas with the Bitmap , and fill the Canvas with color.
The width and height of this Bitmap are the same as the width and height of the
screen. You will use this Bitmap to store the path that the user draws on the screen.
@Override
protected void onSizeChanged(int width, int height,
int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
// Create bitmap, create canvas with bitmap, fill canvas with color.
mExtraBitmap = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
mExtraCanvas = new Canvas(mExtraBitmap);
// Fill the Bitmap with the background color.
mExtraCanvas.drawColor(mBackgroundColor);
}
3. In MyCanvasView , override the onDraw() method. All the drawing work for
MyCanvasView happens in the onDraw() method. In this case, you draw the bitmap that
contains the path that the user has drawn. You will create and save the path in
response to user motion in the next series of steps. Notice that the canvas that is
passed to onDraw() is different than the one created in the onSizeChanged() method.
When the screen first displays, the user has not drawn anything so the screen simply
displays the colored bitmap.
384
Introduction
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the bitmap that stores the path the user has drawn.
// Initially the user has not drawn anything
// so we see only the colored bitmap.
canvas.drawBitmap(mExtraBitmap, 0, 0, null);
}
4. Run your app. The whole screen should fill with orange color.
385
Introduction
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
2. Add member variables to hold the latest x and y values, which are the starting point for
the next path.
3. Add a TOUCH_TOLERANCE float constant and set it to 4. This tolerance serves two
functions:
When the user starts to draw a new line, set the beginning of the contour (line) to x,
y and save the beginning coordinates.
Here is the code:
386
Introduction
6. Finally, add the touchUp() method. Reset the path so it doesn't get drawn again when
you draw more lines on the screen.
7. Run your app. When the app opens, use your finger to draw. (Rotate the device to clear
the screen.)
387
Introduction
As the user draws on the screen, your app constructs the path and saves it in the bitmap
mExtraBitmap . The onDraw() method displays the extra bitmap in the view's canvas. You
can do more drawing in onDraw() if you want. For example, you could draw shapes after
drawing the bitmap.
In this step you will draw a frame around the edge of the picture.
1. In MyCanvasView , add a member variable called mFrame that holds a Rect object.
2. Update onSizeChanged() to create the Rect that will be used for the frame.
@Override
protected void onSizeChanged(int width, int height,
int oldWidth, int oldHeight) {
// rest of method is here ...
3. Update onDraw() to draw a rectangle inset slightly from the edge of the frame. Draw
the frame before drawing the bitmap:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Solution code
Android Studio project: CanvasExample
388
Introduction
Coding challenge
Note: All coding challenges are optional.
Create an app that lets the user draw overlapping rectangles. First, implement it so that
tapping the screen creates the rectangles.
Add functionality where the rectangle starts at a very small size. If the user drags their
finger, let the rectangle increase in size until the user lifts the finger off the screen.
Summary
A common pattern for working with a canvas is to create a custom view and override the
onDraw() and onSizeChanged() methods.
Override the onTouchEvent() method to capture user touches and respond to them by
drawing things.
Related concepts
The related concept documentation is in The Canvas class.
Learn more
Android developer documentation:
Canvas class
Bitmap class
View class
Paint class
Bitmap.config configurations
Path class
389
Introduction
For the purpose of this practical, clipping is a method for defining regions of an image,
canvas, or bitmap that are selectively drawn or not drawn onto the screen. One purpose of
clipping is to reduce overdraw . You can also use clipping to create interesting effects in user
interface design and animation.
For example, when you draw a stack of overlapping cards as shown below, instead of fully
drawing each card, it is usually more efficient to only draw the visible portions. "Usually",
You do this by specifying a clipping region for each card. For example in the diagram below,
when a clipping rectangle is applied to an image, only the portion inside that rectangle is
displayed. The clipping region is commonly a rectangle, but it can be any shape or
combination of shapes. You can also specify whether you want the region inside the clipping
390
Introduction
region included or excluded. The screenshot below shows an example. When a clipping
rectangle is applied to an image, only the portion inside that rectangle is displayed.
Create apps with Android Studio and run them on a physical or virtual mobile device.
Add event handlers to views.
Create a custom View .
Create and draw on a Canvas .
Create a Bitmap and associate it with a View ; create a Canvas for a Bitmap ; create
and customize a Paint object for styling; draw on the canvas and refresh the display.
Create a custom View , override onDraw() and onSizeChanged() .
391
Introduction
App overview
The ClippingExample app demonstrates how you can use and combine shapes to specify
which portions of a canvas are displayed in a view.
392
Introduction
393
Introduction
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new ClippedView(this));
}
3. Create a new class for a custom view called ClippedView which extends View . The
rest of the work will all be inside ClippedView .
2. For the app to look correct on smaller screens, define dimensions for the smaller screen
in the default dimens.xml file.
394
Introduction
<dimen name="clipRectRight">90dp</dimen>
<dimen name="clipRectBottom">90dp</dimen>
<dimen name="clipRectTop">0dp</dimen>
<dimen name="clipRectLeft">0dp</dimen>
<dimen name="rectInset">8dp</dimen>
<dimen name="smallRectOffset">40dp</dimen>
<dimen name="circleRadius">30dp</dimen>
<dimen name="textOffset">20dp</dimen>
<dimen name="strokeWidth">4dp</dimen>
<dimen name="textSize">18sp</dimen>
3. Create a values-sw480dp folder and define values for the larger screens in dimens.xml
in the values-sw480dp folder. (Note: If the empty folder does not show up in Android
Studio, manually add a resource file to the ClippingExample/app/src/main/res/values-
sw480dp directory. This makes the folder show in your Project pane.)
<dimen name="clipRectRight">120dp</dimen>
<dimen name="clipRectBottom">120dp</dimen>
<dimen name="rectInset">10dp</dimen>
<dimen name="smallRectOffset">50dp</dimen>
<dimen name="circleRadius">40dp</dimen>
<dimen name="textOffset">25dp</dimen>
<dimen name="strokeWidth">6dp</dimen>
4. In ClippedView , add convenience member variables for dimensions, so that you only
have to fetch the resources once.
395
Introduction
5. In ClippedView , add convenience member variables for row and column coordinates so
that you only have to calculate them once.
6. In ClippedView , add a private final member variable for a rectangle of type RectF :
Note that the Paint.Align property specifies which side of the text to align to the origin
(not which side of the origin the text goes, or where in the region it is aligned!). Aligning
the right side of the text to the origin places it on the left of the origin.
396
Introduction
2. Run your app to make sure the code is correct. You should see the name of the app
and a white screen.
397
Introduction
398
Introduction
The algorithm used to draw the rectangles works as shown in the screenshot and
explanation below. In summary, drawing a series of rectangles by moving the origin of the
Canvas . (1) Translate Canvas . (2) Draw rectangle. (3) Restore Canvas and Origin .
399
Introduction
The app draws the rectangle below seven times, first with no clipping, then six time with
The drawClippedRectangle() method factors out the code for drawing one rectangle.
2. Apply a clipping rectangle that constraints to drawing only the square to the canvas .
canvas.clipRect(mClipRectLeft, mClipRectTop,
mClipRectRight, mClipRectBottom);
The Canvas.clipRect(left, top, right, bottom) method reduces the region of the
screen that future draw operations can write to. It sets the clipping boundaries
( clipBounds ) to be the spatial intersection of the current clipping rectangle and the
rectangle specified. There are lot of variants of the clipRect() method that accept
different forms for regions and allow different operations on the clipping rectangle.
3. Fill the canvas with white color. Because of the clipping rectangle, only the region
defined by the clipping rectangle is filled, creating a white rectangle.
canvas.drawColor(Color.WHITE);
400
Introduction
4. Draw the red line, green circle, and text, as shown in the completed method below.
5. After you paste the code, create a string resource "Clipping" to get rid of the error for
R.string.clipping in the last line.
// Set the color to blue and draw text aligned with the right edge
// of the clipping rectangle.
mPaint.setColor(Color.BLUE);
// Align the RIGHT side of the text with the origin.
mPaint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText(getContext().getString(R.string.clipping),
mClipRectRight, mTextOffset, mPaint);
}
6. If you run your app, you still only see the white screen, because you have not
overridden onDraw() and thus are not drawing anything yet.
When you use View classes provided by the Android system, the system clips views for
you to minimize overdraw. When you use custom View classes and override the onDraw()
method, clipping what you draw becomes your responsibility.
401
Introduction
deprecated. Depending on the version of Android you are using, you may need to adjust the
methods and operators you use to create the exact effects shown in the app screenshot.
See the Canvas and documentation for details.
1. Create the onDraw() method, if it is not already present as a code stub.
Next, add code to draw the first rectangle, which has no additional clipping.
canvas.drawColor(Color.GRAY);
Context maintains a stack of drawing states. Each state includes the currently applied
transformations and clipping regions. Undoing a transformation by reversing it is error-
prone, as well as chaining too many transformations relative to each other. Translation
is straightforward to reverse, but if you also stretch, rotate, or custom deform, it gets
complex quickly. Instead, you save the state of the canvas, apply your transformations,
draw, and then restore the previous state.
canvas.save();
4. Translate the origin of the canvas to the top-left corner of the first rectangle.
canvas.translate(mColumnOne, mRowOne);
drawClippedRectangle(canvas);
canvas.restore();
402
Introduction
7. Run your app. You should now see the first rectangle drawn on a gray background.
Next, add code to draw the second rectangle, which uses the difference between two
Next, add code to draw the third rectangle, which uses a circular clipping region created
403
Introduction
Next, combine shapes and draw any path to define a clipping region.
404
Introduction
// You can combine shapes and draw any path to define a clipping region.
canvas.save();
canvas.translate(mColumnOne, mRowThree);
mPath.rewind();
mPath.addCircle(mClipRectLeft+mRectInset+mCircleRadius,
mClipRectTop+mCircleRadius+mRectInset,
mCircleRadius, Path.Direction.CCW);
mPath.addRect(mClipRectRight/2-mCircleRadius,
mClipRectTop+mCircleRadius+mRectInset,
mClipRectRight/2+mCircleRadius,
mClipRectBottom-mRectInset,Path.Direction.CCW);
canvas.clipPath(mPath);
drawClippedRectangle(canvas);
canvas.restore();
405
Introduction
In the previous steps you used the translate transform to move the origin of the canvas. You
can apply transformations to any shape, including text, before you draw it, as shown in the
following example.
Solution code
Android Studio project: ClippingExample
406
Introduction
Summary
The Context of an activity maintains a state that preserves transformations and
clipping regions for the Canvas .
Use canvas.save() and canvas.restore() to draw and return to the original state of
your canvas.
To draw multiple shapes on a canvas, you can either calculate their location, or you can
move (translate) the origin of your drawing surface. The latter can make it easier to
create utility methods for repeated draw sequences.
Clipping regions can be any shape, combination of shapes or path.
You can add, subtract, and intersect clipping regions to get exactly the region you need.
You can apply transformations to text.
Related concepts
The related concept documentation is in The Canvas class.
Learn more
Android developer documentation:
Canvas class
Bitmap class
View class
Paint class
Bitmap.config configurations
Region.Op operators
Path class
407
Introduction
When you create a custom view and override its onDraw() method, all drawing happens on
the UI thread. Drawing on the UI thread puts an upper limit on how long or complex your
drawing operations can be, because your app has to complete all its work for every screen
refresh.
One option is to move some of the drawing work to a different thread using a SurfaceView .
All the views in your view hierarchy are rendered onto one Surface in the UI thread.
In the context of the Android framework, Surface refers to a lower-level drawing
surface whose contents are eventually displayed on the user's screen.
A SurfaceView is a view in your view hierarchy that has its own separate Surface , as
shown in the diagram below. You can draw to it in a separate thread.
To draw, start a thread, lock the SurfaceView 's canvas, do your drawing, and post it to
the Surface .
The following diagram shows a View Hierarchy with a Surface for the views and another
408
Introduction
App overview
The SurfaceViewExample app lets you search for an Android image on a dark phone screen
using a "flashlight."
1. At app startup, the user sees a black screen with a white circle, the "flashlight."
2. While the user drags their finger, the white circle follows the touch.
3. When the white circle intersects with the hidden Android image, the screen lights up to
reveal the complete image and a "win" message.
4. When the user lifts their finger and touches the screen again, the screen turns black and
the Android image is hidden in a new random location.
409
Introduction
The following is a screenshot of the SurfaceViewExample app at startup, and after the user
has found the Android image by moving around the flashlight.
Additional features:
Size of the flashlight is a ratio of the smallest screen dimension of the device.
Flashlight is not centered under the finger, so that the user can see what's inside the
circle.
MainActivity—Locks screen orientation, gets the display size, creates the GameView ,
and sets the GameView as its content view. Overrides onPause() and onResume() to
pause and resume the game thread along with the MainActivity .
410
Introduction
3. Add methods to get values for x, y, and the radius. You do not need any methods to set
them.
411
Introduction
7. Add a public void update() method. The method takes integer parameters newX
and newY , and it sets mX to newX and mY to newY .
3. Implement methods to add a stub for the only required method, run() .
@Override
public void run(){}
4. Add the stubs for the constructors and have each constructor call init() .
5. Add private init() method and set the mContext member variable to context .
412
Introduction
6. In the GameView class, add stubs for the pause() and resume( ) methods. Later, you
will manage your thread from these two methods.
2. Lock the screen orientation into landscape. Games often lock the screen orientation.
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
6. Still in MainActivity , override the onPause() method to also pause the mGameView
object. This onPause() method shows an error, because you have not implemented the
pause() method in the GameView class.
@Override
protected void onPause() {
super.onPause();
mGameView.pause();
}
413
Introduction
@Override
protected void onResume() {
super.onResume();
mGameView.resume();
}
android.graphics.Path .
1. Set mBitmapX and mBitmapY to random x and y positions that fall inside the screen.
2. Define a rectangular bounding box that contains the Android image.
3. Define the missing member variables.
414
Introduction
thread.
1. Add the pause() and r esume() methods using the code below. The mRunning
member variable tracks the thread status, so that you do not try to draw when the
activity is not running anymore.
Thread management can become a lot more complex after you have multiple threads in your
game. See Sending Operations to Multiple Threads for lessons in thread management.
415
Introduction
There are several ways in which to set up the view after the system has fully initialized the
view. The onSizeChangedMethod() is called every time the view changes.The view starts out
with 0 dimensions. When the view is first inflated, its size changes and
onSizeChangedMethod() is called. Unlike in onCreate() , the view's correct dimensions are
available.
1. Get the image of Android on a skateboard from github and add it to your drawable
folder, or use a small image of your own choice.
2. In GameView , override the onSizeChanged() method. Both the new and the old view
dimensions are passed as parameters as shown below.
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
3. Store the width and height in member variables mViewWidth and mViewHeight .
mViewWidth = w;
mViewHeight = h;
mPaint.setTextSize(mViewHeight / 5);
mBitmap = BitmapFactory.decodeResource(
mContext.getResources(), R.drawable.android);
setUpBitmap();
416
Introduction
Canvas canvas;
2. Create a loop that only runs while mRunning is true. All the following code must be
inside that loop.
while (mRunning) {
3. Check whether there is a valid Surface available for drawing. If not, do nothing.
if (mSurfaceHolder.getSurface().isValid()) {
4. Because you will use the flashlight cone coordinates and radius multiple times, create
local helper variables inside the if statement.
int x = mFlashlightCone.getX();
int y = mFlashlightCone.getY();
int radius = mFlashlightCone.getRadius();
In an app, with more threads, you must enclose this with a try/catch block to make
sure only one thread is trying to write to the Surface .
canvas = mSurfaceHolder.lockCanvas();
canvas.save();
canvas.drawColor(Color.WHITE);
417
Introduction
5. Set the circle as the clipping path using the DIFFERENCE operator, so that's what's inside
the circle is clipped (not drawn).
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
canvas.drawColor(Color.BLACK);
7. Check whether the the center of the flashlight circle is inside the winning rectangle. If
so, color the canvas white, redraw the Android image, and draw the winning message.
8. Drawing is finished, so you need to rewind the path, restore the canvas, and release the
lock on the canvas.
mPath.rewind();
canvas.restore();
mSurfaceHolder.unlockCanvasAndPost(canvas);
Run your app. It should display a black screen with a white circle at the center of the
screen.
1. In GameView , override the onTouchEvent() method and update the flashlight position on
the ACTION_DOWN and ACTION_MOVE events.
418
Introduction
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
Solution code
Android Studio project: SurfaceViewExample
Summary
To offload drawing to a different thread, create a custom view that extends SurfaceView
and implements Runnable . The SurfaceView is part of your view hierarchy but has a
drawing Surface that is separate from the rest of the view hierarchy.
Create an instance of your custom view and set it as the content view of your activity.
419
Introduction
Add pause() and resume() methods to the SurfaceView that stop and start a thread.
Override onPause() and onResume() in the activity to call the pause() and resume()
methods of the SurfaceView .
If appropriate, handle touch events, for example, by overriding onTouchEvent() .
Add code to update your data.
In the SurfaceView , implement the run() method to:
Related concept
The related concept documentation is in The SurfaceView class.
Learn more
Android developer documentation:
SurfaceView class
SurfaceHolder interface
Runnable interface
Path class
420
Introduction
The Property Animation system allows you to animate almost any property of an object. You
can define an animation to change any object property (a field in an object) over time,
regardless of whether the change draws to the screen or not. A property animation changes
a property's value over a specified length of time. For example, you can animate a circle to
grow bigger by increasing its radius.
With the property animation system, you assign animators to the properties that you want to
animate, such as color, position, or size. You also define aspects of the animation such as
interpolation. For example, you would create an animator for the radius of a circle whose
size you want to change.
The property animation system lets you define the following characteristics of an animation:
Duration: You can specify the duration of an animation. The default length is 300
milliseconds.
Time interpolation: You can specify how the values for the property are calculated as a
function of the animation's current elapsed time. You can choose from provided
interpolators or create your own.
Repeat count and behavior: You can specify whether or not to have an animation repeat
when it reaches the end of a duration, and how many times to repeat the animation. You
can also specify whether you want the animation to play back in reverse. Setting it to
reverse plays the animation forwards then backwards repeatedly, until the number of
repeats is reached.
Animator sets: You can group animations into logical sets that play together or
sequentially or after specified delays.
421
Introduction
Frame-refresh delay: You can specify how often to refresh frames of your animation.
The default is set to refresh every 10 ms, but the speed in which your application can
refresh frames ultimately depends on how busy the system is overall and how fast the
system can service the underlying timer.
App overview
422
Introduction
2. Add two two required constructors. When you create your custom view from code
exclusively, you only need the first constructor. When you inflate your custom view from
XML, the system calls the second constructor and if it's missing, you get an error.
423
Introduction
2. Add two two required constructors. When you create your custom view from code
exclusively, you only need the first constructor. When you inflate your custom view from
XML, the system calls the second constructor and if it's missing, you get an error.
<com.example.android.propertyanimation.PulseAnimationView
android:layout_width="match_parent"
android:layout_height="match_parent"/>
4. Run your app. It shows a white screen and the name of the app.
The property animator needs to be able to change the property that will be animated. It does
this through a "setter" method for the property. In order for the animator to find and use the
setter, the following conditions need to be met:
If the class whose property is being animated does not provide a setter property, you
have to create one. For the PulseAnimationView , you will create a member variable for
the radius and a setRadius() method to set the variable's value.
The property setter's name needs to be of the form set PropertyName (). The
PropertyName can be any valid string. When you create the animator, you will pass the
To cause the object to be redrawn after the property changes, you must call the
invalidate() method. Calling invalidate() tells the system that something about the
view has changed, and the system measures, lays out, and redraw the views.
424
Introduction
To make the animation more interesting, use the radius to affect other aspects of the
animation. For example, you could play a sound as the circle grows, and the sound
could change as a function of the radius. For the PropertyAnimation app, in addition to
changing the size, you are going to change the color of the circle as a function of the
radius.
Create a constant COLOR_ADJUSTER and set it to 5. You will use this constant in the next
step.
Inside the setRadius() method, after setting mRadius and before calling the
invalidate() method, change the color of the mPaint variable. Colors are integers
and can thus be used in integer operations. You can change the value of the
COLOR_ADJUSTER constant to see how it affects the color of the circle. You can also use a
1. In the custom view class, create private float member variables mX and mY to
store the event coordinates.
2. Override the onTouchEvent() method to get the event coordinates and store them in the
mX and mY variables.
425
Introduction
mX and mY variables.
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mX = event.getX();
mY = event.getY();
}
return super.onTouchEvent(event);
}
The animation is performed by an Animator object that, once started, changes the value of
a property from a starting value towards an end value over a given duration.
1. Create class constants for the animation duration and for a delay before the animation
starts. You can change the values of these constants later and explore how that affects
the appearance of the animation. The time is in milliseconds.
@Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {}
Inside the onSizeChanged() method, you will create three ObjectAnimator objects and
one AnimatorSet object. You cannot create them in the onCreate() method, because
the views have not been inflated, and so the call to getWidth() used below would not
return a valid value.
growAnimator.setDuration(ANIMATION_DURATION);
426
Introduction
5. Choose an interpolator for the animation. The interpolator affects the rate of change;
that is, the interpolator affects how the animated property changes from its starting
value to its ending value.
6. With a LinearInterpolator , the rate of change is constant; that is, the value changes
by the same amount for every animation step. For example, if the starting radius value
were 0 and your ending value were 10, every step of the animation might increase the
radius by 2. For example, 0 > 2 > 4 > 8 > 10.
growAnimator.setInterpolator(new LinearInterpolator());
shrinkAnimator.setDuration(ANIMATION_DURATION);
shrinkAnimator.setInterpolator(new LinearOutSlowInInterpolator());
11. Add a starting delay. When the animation is started, it will wait for the specified delay
before it runs.
shrinkAnimator.setStartDelay(ANIMATION_DELAY);
13. Add a repeat count to repeatAnimator . A repeat count of 0 is the default. With a
427
Introduction
repeat count of 0 , the animation plays once and does not repeat. With a repeat count
of 1 , the animation plays twice.
repeatAnimator.setRepeatCount(1);
14. Set the repeat mode to REVERSE . In this mode, every time the animation plays, it
reverses the beginning and end values. (The other possible value, which is the default,
is RESTART .)
repeatAnimator.setRepeatMode(ValueAnimator.REVERSE);
15. Create a private AnimatorSet member variable called mPulseAnimatorSet and initialize
the variable with an AnimatorSet .
An AnimatorSet allows you to combine several animations and to control in what order
they are played. You can have several animations play at the same time or in a
specified sequence. AnimatorSet objects can contain other AnimatorSet objects. See
the Property Animation guide for all the cool things you can do with animator sets.
16. The following AnimatorSet is very simple and specifies that the growAnimator should
play before the shrinkAnimator , followed by the repeatAnimator . Add it after you have
created the animators.
mPulseAnimatorSet.play(growAnimator).before(shrinkAnimator);
mPulseAnimatorSet.play(repeatAnimator).after(shrinkAnimator);
17. If you run your app now, you still only see the white screen.
428
Introduction
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mX, mY, mRadius, mPaint);
}
2. Run your app and tap the white screen to see the animations play. Have some fun and
experiment with the animation!
This example app creates and runs animations from the Java code because it uses data that
is not available until the view has been drawn. You can also define animators and animator
sets in XML. See the Property Animation guide and the Animations concept for instructions
and code examples.
Solution code
Android Studio project: PropertyAnimation.
Coding challenge
Write an app that demonstrates the use of the physics-based animation from the support
library.
You need to add the library dependency to your build.gradle file. Use the latest
version of the physics-based library as listed in the official documentation.
dependencies {
...
compile "com.android.support:support-dynamic-animation:26.0.0"
}
429
Introduction
Here is a code snippet for a simple vertical spring animation. See the Spring Animation
documentation and SpringAnimation class for more information.
Here is a code snippet for a simple rotation fling animation. See the Fling Animation
documentation and FlingAnimation class for more information.
Summary
With property animation, you can use almost any property of an object to create an
animation.
One way to create a property animation is to:
If the view does not have a setter for the property, create one and name it set
PropertyName . The setter is called by the Animator object to change the property
To create an Animator , set the object and property to be animated. For the property,
set a start value, an end value, and a duration.
Use interpolators to specify how the animated property changes over time. You can use
one of the many supplied interpolators or create your own.
You can combine several animations to run in sequence or at the same time using
430
Introduction
AnimatorSet objects.
See the PropertyAnimation guide for a complete description of all the cool things you
can do with property animations.
Related concept
The related concept documentation is in Animations.
Learn more
Android developer documentation:
View Animation
Property Animation
Drawable Animation
Physics based animation
See the Graphics Architecture series of articles for an in-depth explanation of how the
Android framework draws to the screen.
431
Introduction
A multimedia app that plays audio or video usually has two parts:
A player that, given a media source, downloads and decodes that media, and renders it
as video and/or audio.
A user interface (UI) with transport controls to run the player and optionally display the
player's state.
The architecture of a media app can get a lot more complicated, especially if your app's
entire purpose is to play audio or video. Android provides an overall architecture for audio
and video apps, and large number of media-related classes, which you can learn about in
Media Apps Overview.
The simplest possible way to play a video clip in your app, however, is to use the VideoView
class to play the video, and the MediaController class as the UI to control it. In this practical
you build a simple app that plays a video clip that is either embedded in the app's resources,
or available on the internet.
432
Introduction
App overview
433
Introduction
In this practical you build the SimpleVideoView app from scratch. SimpleVideoView plays a
The SimpleVideoView app includes a familiar set of media controls. The controls allow you
to play, pause, rewind, fast-forward, and use the progress slider to skip forward or back to
specific places in the video.
You start by playing a video file embedded with the app's resources. In the second task, you
modify the app to buffer and play a video file from the internet.
Your app can play media files from a variety of sources, including embedded in the app's
resources, stored on external media such as an SD card, or streamed from the internet. In
this example, to keep things simple, you embed the video file in the app itself.
434
Introduction
For the video playback UI, Android provides the MediaController view. The
MediaController view provides a set of common media-control buttons that can control a
In this task you build the first iteration of the SimpleVideoView app, which plays a video clip.
1. Create a new Android project. Call it SimpleVideoView and use the Empty activity
template. You can leave all other defaults the same.
2. Create a new raw resource directory to hold the video file the app will play: click the res
directory in the Project view and select File > New > Android resource directory.
3. Change both the Directory name and Resource type to "raw". Leave all other options
4. Download the Tacoma Narrows MP4 video file. If the video begins to play in your
browser rather than downloading, you can use File > Save Page As... to save the file.
5. Move the tacoma_narrows.mp4 file into the raw directory in your project.
6. In activity_main.xml , delete the existing TextView , and replace it with a VideoView
element with these attributes:
435
Introduction
<VideoView
android:id="@+id/videoview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="4:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
When layout_width and layout_height are 0dp , the size and position of the
VideoView are calculated dynamically based on the other constraints. The
size of the VideoView , the ratio of the width to the height should be 4:3. This constraint
keeps the aspect ratio of the video the same and prevents the view from being
stretched too far in either direction (depending on how the device is rotated).
2. Also in MainActivity , create a private method called getMedia() that takes a String
and returns a Uri . The implementation looks like this:
This method converts the name of the sample media file (a string) into a full URI object
representing the path to the sample in your app's resources. Note that although the
actual filename in the app's resource directory is tacoma_narrows.mp4 , the string name
and resulting resource name do not include the extension.
mVideoView = findViewById(R.id.videoview);
436
Introduction
4. Create a new private method called initializePlayer() that takes no arguments and
returns void .
5. Inside initializePlayer() , use the getMedia() and setVideoUri() methods to set the
media URI that the VideoView will play.
mVideoView.start();
7. Create a private method called releasePlayer() , and call the stopPlayback() method
on the VideoView . This stops the video from playing and releases all the resources held
by the VideoView .
@Override
protected void onStart() {
super.onStart();
initializePlayer();
}
437
Introduction
@Override
protected void onStop() {
super.onStop();
releasePlayer();
}
3. Override onPause() and add a test for versions of Android older than N (lower than 7.0,
API 24). If the app is running on an older version of Android, pause the VideoView
here.
@Override
protected void onPause() {
super.onPause();
This test is required because the behavior of onPause() and onStop() changed in
Android N (7.0, API 24). In older versions of Android, onPause() was the end of the
visual lifecycle of your app, and you could start releasing resources when the app was
paused.
In newer versions of Android, your app may be paused but still visible on the screen, as
with multi-window or picture-in-picture (PIP) mode. In those cases the user likely wants
the video to continue playing in the background. If the video is being played in multi-
window or PIP mode, then it is onStop() that indicates the end of the visible life cycle
of the app, and your video playback should indeed stop at that time.
If you only stop playing your video in onStop() , as in the previous step, then on older
devices there may be a few seconds where even though the app is no longer visible on
screen, the video's audio track continues to play while onStop() catches up. This test
for older versions of Android pauses the actual playback in onPause() to prevent the
sound from playing after the app has disappeared from the screen.
When the app starts, the video file is opened and decoded, begins playing, and plays to
the end. There is no way to control the media playback, for example using pause, play,
fast-forward, or rewind. You add these capabilities in the next task.
Note: If you test the SimpleVideoView app on an emulator, use an AVD that supports
API 23 or higher. Older versions of the emulator support fewer types of video formats,
438
Introduction
and may also suffer from degraded performance during playback. If you run the app on
a physical device that runs a version of Android older than API 23, you should not have
either of these problems.
combines the most common media control UI elements (buttons for play, pause, fast-
forward, and rewind, as well as a seek or progress bar) with the ability to control an
underlying media player, such as a VideoView .
To use a MediaController view, you don't define it in your layout as you would other views.
Instead you instantiate it programmatically in your app's onCreate() method and then
attach it to a media player. The controller floats above your app's layout and enables the
user to start, stop, fast-forward, rewind, and seek within the video.
2. A MediaController view, which includes UI elements for video transport controls (play,
pause, fast-forward, rewind, progress slider) and the ability to control the video
439
Introduction
Note: When you add the MediaController class make sure that you import
android.widget.MediaController , not android.media.session.MediaController .
2. Use setMediaController() to do the reverse connection, that is, to tell the VideoView
that the MediaController will be used to control it:
mVideoView.setMediaController(controller);
3. Build and run the app. As before, the video begins to play when the app starts. Tap the
VideoView to make the MediaController appear. You can then use any of the elements
in that controller to control media playback. The MediaController disappears on its own
440
Introduction
To get a baseline for comparison, run the app, if it's not already running. With your app in the
foreground, try the following tasks:
Note in each of these cases how the video restarts from the beginning.
In this task you keep track of the current playback position, in milliseconds, throughout the
app's lifecycle. If the video resources are released, the video can restart where it left off.
1. In MainActivity , add a member variable to hold the video's current position (initially 0).
The playback position is recorded in milliseconds from 0.
2. Add a member variable to hold the key for the playback position in the instance state
bundle:
if (mCurrentPosition > 0) {
mVideoView.seekTo(mCurrentPosition);
} else {
// Skipping to 1 shows the first frame of the video.
mVideoView.seekTo(1);
}
441
Introduction
If the current position is 0, the video has not yet played. Use seekTo() to set the
playback position to 1 millisecond. This will show the first frame of the video rather than
a black screen.
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(PLAYBACK_TIME, mVideoView.getCurrentPosition());
}
5. In onCreate() , check for the existence of the instance state bundle, and update the
value of mCurrentTime with the value from that bundle. Add these lines before you
create the MediaController .
if (savedInstanceState != null) {
mCurrentPosition = savedInstanceState.getInt(PLAYBACK_TIME);
}
6. Build and run the app. Repeat the baseline tasks and note how the playback position is
saved in each case.
The media has been prepared (buffered, decoded, uncompressed, and so on) and is
ready to play.
An error has occurred during media playback.
The media source has reported information such as a buffering delay during playback,
for example when media is streaming over the internet.
The media has finished playing.
Listeners for the preparation and completion events are the most common listeners to
implement in a media app. You haven't yet needed to handle media preparation in the
SimpleVideoView app, because the video clip is embedded in the app and is fairly small, so
it plays quickly. This may not be the case for larger video clips or those you play directly from
the internet. You come back to preparing media in a later task.
442
Introduction
When media playback is finished, the completion event occurs, and the onCompletion()
callback is called. In this task you implement a listener for onCompletion() events to display
a toast when the playback finishes.
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
// Implementation here.
}
});
2. Inside the onCompletion() method, add a Toast to display the message "Playback
completed."
3. After the toast, add a call to seekTo() to reset the playback and the MediaController
to the beginning of the clip.
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
Toast.makeText(MainActivity.this, "Playback completed",
Toast.LENGTH_SHORT).show();
mVideoView.seekTo(1);
}
});
By default, when the media finishes playing, both the VideoView and the
MediaController show the end state of the media. The code shown above resets both
the player and the controller to the start of the video so that the video can be played
again.
4. Build and run the app. Tap the video to display the MediaController , and move the
slider nearly all the way to the end. When the video finishes playing, the toast appears
and the controller resets to the beginning of the clip.
443
Introduction
If you play media from the internet, the VideoView class and the underlying MediaPlayer
implement a lot of the background work for you. When you use VideoView you don't need to
open a network connection, or set up a background task to buffer the media file.
444
Introduction
You do, however, need to handle the time period between when you tell your app to play the
media file and when the content in that file is actually available to play. If you do nothing,
your app may appear to freeze for quite a long time while the file is buffering, especially on
slow network connections. Even a message to the user that something is going on provides
a better user experience.
VideoView (and MediaPlayer ) provide a preparation event listener to handle this case. To
use the onPrepared() callback, set the URI of the media the VideoView will play, then
receive a callback when that media is ready to play.
In this task you modify the SimpleVideoView app to play a video from a URL on the internet.
You use the preparation event listener to handle updating the app's UI while the file is being
loaded.
1. In the main layout file, add a TextView element. The new TextView is centered in the
layout, same as the VideoView , and appears on top.
<TextView
android:id="@+id/buffering_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Buffering..."
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
2. In MainActivity , change the VIDEO_SAMPLE constant to indicate the URL for the media
file.
You can use any URL for any video that is available as a file on the internet.
Note: If you use your own URL for the video file, make sure that the video format is
supported by Android, by the specific version of Android your emulator or device is
445
Introduction
running. See Supported Media Formats for a list of video codecs and file types that
Android supports.
3. Add a member variable to hold the TextView that shows the buffering message.
mBufferingTextView = findViewById(R.id.buffering_textview);
5. In the getMedia() method, change the implementation to test whether the incoming
string is a URL or a raw resource. Return the appropriate URI.
1. In AndroidManifest.xml , add this line just before the <application> element, at the top
level of the manifest.
446
Introduction
mVideoView.setOnPreparedListener(
new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
// Implementation here.
}
});
2. Inside the onPrepared() method, set the visibility of the TextView to invisible. This
removes the "Buffering..." message.
mBufferingTextView.setVisibility(VideoView.INVISIBLE);
3. In initializePlayer() , locate the lines of code that test for the current position, seek to
that position, and start playback. Move those lines into the onPrepared() method,
placing them after the calls to setVisibility() . The final onPrepared() definition looks
like this:
mVideoView.setOnPreparedListener(
new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
mBufferingTextView.setVisibility(VideoView.INVISIBLE);
if (mCurrentPosition > 0) {
mVideoView.seekTo(mCurrentPosition);
} else {
mVideoView.seekTo(1);
}
mVideoView.start();
}
});
4. At the top of initializePlayer() , before setting the media URI to play, restore the
visibility of the buffering TextView :
mBufferingTextView.setVisibility(VideoView.VISIBLE);
Each time initializePlayer() is called, the buffering message should be turned on. It
is turned off only when onPrepared() is called and the media is ready to play.
5. Build and run the app. Initially the "Buffering..." message appears on the screen.
Depending on the speed of your network connection, after some moments the video will
appear and start playing.
447
Introduction
Solution code
Android Studio project: SimpleVideoView
Coding challenge
Note: All coding challenges are optional.
Challenge: Replace the MediaController in the SimpleVideoView app with a single button.
When the user taps the button, the text should toggle between "Play" and "Pause."
Change SimpleVideoView such that the video does not play automatically when the app
is started. Only start playing the media when the user clicks the "Play" button.
If the button is in the "Play" state the button should be disabled until the media has been
prepared and is ready to play.
Make sure that you update the button's Play/Pause state in response to activity state
changes.
Challenge: Add a "Skip 10s" button. When the user taps the button, the current position of
the media playback should skip ahead ten seconds.
Summary
Media apps in Android have a standard set of components, including a UI and a media
player.
Media (audio and video) files can be played from a variety of sources, including
embedded in the app's resources, stored on external media such as an SD card, or
played as a streaming media from the internet.
The easiest way to play video in your app is to use the VideoView class. The
VideoView class wraps a MediaPlayer and a SurfaceView . VideoView can be added
448
Introduction
common controls (play, pause, fast-forward, rewind, and a progress slider) for media
playback. MediaController is a ViewGroup that can be added programmatically to any
layout to control a VideoView .
Use MediaController.setMediaPlayer() to attach a media player such as a VideoView
to the controller.
Use VideoView.setMediaController() to attach a MediaController to the VideoView .
Use the onStart() and onStop() lifecycle methods to start and stop video playback in
your app. These methods represent the visible lifecycle of your app.
In versions of Android lower than Nougat, onPause() represents the end of the app's
visible lifecycle, but audio may continue to play for several seconds until onStop() is
called. Add a test for the Android version to pause video playback in onPause() on
older versions of Android.
The VideoView class does not preserve the state of video playback across any lifecycle
transition, including changes to background/foreground, device orientation, and multi-
window mode. To retain this state information, save and restore the current video
position in the activity instance state bundle.
Media playback uses a lot of system resources. Make sure that you release these
resources as soon as possible, usually in onStop() . Resources used by VideoView
are released when you call stopPlayback() .
The MediaPlayer class (and thus VideoView ) provides a set of media event callbacks
for various events, including onCompletion() and onPrepared() .
The onCompletion() callback is invoked when the media playback is complete. Use this
callback to announce the end of the media or reset the UI to a default state.
The onPrepared() callback is invoked when the media is ready to play. Depending on
the media and its location (embedded in the app, available on local storage, played from
the internet), it may take some time for the media to be available. Use the
onPrepared() callback to keep the user informed about the state of the media by
Related concept
The related concept documentation is Simple media playback.
Learn more
Android developer documentation:
449
Introduction
VideoView
MediaPlayer
MediaPlayer.OnCompletionListener
MediaPlayer.OnPreparedListener
MediaController
Other:
450
Introduction
The Android operating system provides a strong foundation for building apps that run well on
a wide range of devices and form factors. However, issues like complex lifecycles and the
lack of a recommended app architecture make it challenging to write robust apps.
Architecture Components provide libraries for common tasks such as lifecycle management
and data persistence to make it easier to implement the recommended architecture.
451
Introduction
Architecture Components help you structure your app in a way that is robust, testable, and
maintainable with less boilerplate code.
The diagram below shows a basic form of the recommended architecture for apps that use
Architecture Components. The architecture consists of a UI controller, a ViewModel that
serves LiveData , a Repository, and a Room database. The Room database is backed by a
SQLite database and accessible through a data access object (DAO). Each component is
described briefly below, in detail in the Architecture Components concept chapter, and you
will implement them in this practical.
Because all the components interact, you will encounter references to these components
throughout this practical, so here is a short explanation of each.
Entity: In the context of Architecture Components, the entity is an annotated class that
describes a database table.
SQLite database: On the device, data is stored in a SQLite database. The Room persistence
library creates and maintains this database for you.
DAO: Short for data access object . A mapping of SQL queries to functions. You used to
have to define these queries in a helper class. When you use a DAO, your code calls the
functions, and the components take care of the rest.
452
Introduction
Room database: Database layer on top of a SQLite database that takes care of mundane
tasks that you used to handle with a helper class. The Room database uses the DAO to
issue queries to the SQLite database based on functions called.
Repository: A class that you create for managing multiple data sources. In addition to a
Room database, the Repository could manage remote data sources such as a web server.
ViewModel : Provides data to the UI and acts as a communication center between the
Repository and the UI. Hides the backend from the UI. ViewModel instances survive device
configuration changes.
LiveData : A data holder class that follows the observer pattern, which means that it can be
observed. Always holds/caches latest version of data. Notifies its observers when the data
has changed. Generally, UI components observe relevant data. LiveData is lifecycle aware,
so it automatically manages stopping and resuming observation based on the state of its
observing activity or fragment.
Important: This practical implements the architecture defined in the Guide to App
Architecture and explained in the Architecture Components concepts chapter. It is highly
recommended that you read the concepts chapter.
453
Introduction
Design and construct an app using some of the Android Architecture Components.
You'll use Room, ViewModel , and LiveData .
App overview
In this practical you build an app that uses the Android Architecture Components collection
of libraries. The app, called RoomWordsSample, stores a list of words in a Room database
and displays the list in a RecyclerView . The RoomWordsSample app is basic, but
sufficiently complete that you can use it as a template to build on.
Note: Does this app sound familiar? In the Android Developer Fundamentals course you
built several versions of the WorldList app using a SQLiteDatabase, an SQLiteOpenHelper,
and then added a Contract and a ContentProvider. It was quite involved. Doing the same
thing using Architecture Components is less complex, less code, and much better
architecture.
The RoomWordsSample app does the following:
Works with a database to get and save words, and pre-populates the database with
some words.
Displays all the words in a RecyclerView in MainActivity .
Opens a second Activity when the user taps the + FAB button. When the user
enters a word, the app adds the word to the database and then the list updates
automatically.
454
Introduction
Tip: Print or open this diagram in a separate tab so you can refer to it as you build the code.
455
Introduction
456
Introduction
1. Add the following code to your build.gradle (Module: app) file, to the bottom of the the
dependencies block (but still inside it).
// Room components
implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
annotationProcessor "android.arch.persistence.room:compiler:$rootProject.roomVers
ion"
androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roo
mVersion"
// Lifecycle components
implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersi
on"
annotationProcessor "android.arch.lifecycle:compiler:$rootProject.archLifecycleVe
rsion"
2. In your build.gradle (Project: RoomWordsSample) file, add the version numbers at the
end of the file.
457
Introduction
ext {
roomVersion = '1.0.0'
archLifecycleVersion = '1.1.0'
}
Important: Get the latest version numbers from the Adding Components to your Project
page. Find the entry for the room and lifecycle libraries and the version number is at the
end of the name after the colon. In the following example, the version numbers is 1.0.0:
" android.arch.persistence.room:runtime: 1.0.0 "
The data for this app is words, and each word is represented by an entity in the database. In
this task you create the Word class and annotate it so Room can create a database table
from it. The diagram below shows a word_table database table. The table has one word
458
Introduction
column, which also acts as the primary key, and two rows, one each for "Hello" and "World."
table. Annotate your class declaration to indicate that the class is an entity. Specify the
name of the table if you want it to be different from the name of the class.
@PrimaryKey Every entity needs a primary key. To keep things simple, each word in the
RoomWordsSample app acts as its own primary key. To learn how to auto-generate
unique keys, see the tip below.
@NonNull Denotes that a parameter, field, or method return value can never be null .
The primary key should always use this annotation. Use this annotation for any
mandatory fields in your rows.
@ColumnInfo(name = "word" ) Specify the name of a column in the table, if you want
459
Introduction
the column name to be different from the name of the member variable.
Every field that's stored in the database must either be public or have a "getter" method.
This app provides a getWord() "getter" method rather than exposing member variables
directly.
For a complete list of annotations, see the Room package summary reference.
Update your Word class with annotations, as shown in the code below.
1. Add the @Entity notation to the class declaration and set the tableName to
"word_table" .
Note: If you type in the annotations, Android Studio auto-imports everything you need.
Here is the complete code:
@Entity(tableName = "word_table")
public class Word {
@PrimaryKey
@NonNull
@ColumnInfo(name = "word")
private String mWord;
If you get errors for the annotations, you can import them manually, as follows:
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
Tip on auto-generating keys: To auto-generate a unique key for each entity, you would add
and annotate a primary integer key with autoGenerate=true . See Defining data using Room
entities.
460
Introduction
The data access object, or Dao , is an annotated class where you specify SQL queries and
associate them with method calls. The compiler checks the SQL for errors, then generates
queries from the annotations. For common queries, the libraries provide convenience
annotations such as @Insert .
Note that:
461
Introduction
4. Annotate the insert() method with @Insert . You don't have to provide any SQL!
(There are also @ Delete and @ Update annotations for deleting and updating a row,
but you do not use these operations in the initial version of this app.)
5. Declare a method to delete all the words:
void deleteAll();
List<Word> getAllWords();
8. Annotate the getAllWords() method with a SQL query that gets all the words from the
word_table , sorted alphabetically for convenience:
@Dao
public interface WordDao {
@Insert
void insert(Word word);
Tip: For this app, ordering the words is not strictly necessary. However, by default,
return order is not guaranteed, and ordering makes testing straightforward.
To learn more about DAOs, see Accessing data using Room DAOs.
462
Introduction
When you display data or use data in other ways, you usually want to take some action
when the data changes. This means you have to observe the data so that when it changes,
you can react.
LiveData , which is a lifecycle library class for data observation, can help your app respond
to data changes. If you use a return value of type LiveData in your method description,
Room generates all necessary code to update the LiveData when the database is updated.
See the LiveData documentation to learn more about other ways to use LiveData , or
watch this Architecture Components: LiveData and Lifecycle video.
463
Introduction
Room is a database layer on top of a SQLite database. Room takes care of mundane tasks
that you used to handle with a database helper class such as SQLiteOpenHelper .
2. Annotate the class to be a Room database. Declare the entities that belong in the
database—in this case there is only one entity, Word . (Listing the entities class or
classes creates corresponding tables in the database.) Set the version number.
464
Introduction
3. Define the DAOs that work with the database. Provide an abstract "getter" method for
each @Dao .
5. Add code to create a database where indicated by the Create database here comment
in the code above.
The following code uses Room's database builder to create a RoomDatabas e object
named "word_database" in the application context from the WordRoomDatabase class.
In this practical you don't update the entities and the version numbers. However, if you
modify the database schema, you need to update the version number and define how to
handle migrations. For a sample app such as the one you're creating, destroying and
re-creating the database is a fine migration strategy. For a real app, you must
implement a non-destructive migration strategy. See Understanding migrations with
Room.
465
Introduction
Important: In Android Studio, if you get errors when you paste code or during the build
process, make sure you are using the full package name for imports. See Adding
Components to your Project. Then select Build > Clean Project. Then select Build >
Rebuild Project, and build again.
466
Introduction
A Repository is a class that abstracts access to multiple data sources. The Repository is not
part of the Architecture Components libraries, but is a suggested best practice for code
separation and architecture. A Repository class handles data operations. It provides a
clean API to the rest of the app for app data.
A Repository manages query threads and allows you to use multiple backends. In the most
common example, the Repository implements the logic for deciding whether to fetch data
from a network or use results cached in the local database.
467
Introduction
3. Add a constructor that gets a handle to the database and initializes the member
variables.
WordRepository(Application application) {
WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
mWordDao = db.wordDao();
mAllWords = mWordDao.getAllWords();
}
4. Add a wrapper method called getAllWords() that returns the cached words as
LiveData . Room executes all queries on a separate thread. Observed LiveData
LiveData<List<Word>> getAllWords() {
return mAllWords;
}
5. Add a wrapper for the insert() method. Use an AsyncTask to call insert() on a
non-UI thread, or your app will crash. Room ensures that you don't do any long-running
operations on the main thread, which would block the UI.
6. Create the insertAsyncTask as an inner class. You should be familiar with AsyncTask ,
so here is the insertAsyncTask code for you to copy:
insertAsyncTask(WordDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Word... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
468
Introduction
WordRepository(Application application) {
WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
mWordDao = db.wordDao();
mAllWords = mWordDao.getAllWords();
}
LiveData<List<Word>> getAllWords() {
return mAllWords;
}
insertAsyncTask(WordDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Word... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
}
Note: For this simple example, the Repository doesn't do much. For a more complex
implementation, see the BasicSample code on GitHub.
469
Introduction
The ViewModel is a class whose role is to provide data to the UI and survive configuration
changes. A ViewModel acts as a communication center between the Repository and the UI.
The ViewModel is part of the lifecycle library. For an introductory guide to this topic, see
ViewModel .
A ViewModel holds your app's UI data in a way that survives configuration changes.
Separating your app's UI data from your Activity and Fragment classes lets you better
follow the single responsibility principle: Your activities and fragments are responsible for
drawing data to the screen, while your ViewModel is responsible for holding and processing
all the data needed for the UI.
470
Introduction
In the ViewModel , use LiveData for changeable data that the UI will use or display.
Warning: Never pass context into ViewModel instances. Do not store Activity ,
Fragment , or View instances or their Context in the ViewModel .
An Activity can be destroyed and created many times during the lifecycle of a ViewModel ,
such as when the device is rotated. If you store a reference to the Activity in the
ViewModel , you end up with references that point to the destroyed Activity . This is a
memory leak. If you need the application context, use AndroidViewModel , as shown in this
practical. </div>
3. Add a constructor that gets a reference to the WordRepository and gets the list of all
words from the WordRepository .
4. Add a "getter" method that gets all the words. This completely hides the implementation
from the UI.
5. Create a wrapper insert() method that calls the Repository's insert() method. In
this way, the implementation of insert() is completely hidden from the UI.
471
Introduction
This practical assumes that you are familiar with creating layouts in XML, so the code is just
provided.
<resources>
<color name="colorPrimary">#2196F3</color>
<color name="colorPrimaryLight">#64b5f6</color>
<color name="colorPrimaryDark">#1976D2</color>
<color name="colorAccent">#FFFF9800</color>
<color name="colorTextPrimary">@android:color/white</color>
<color name="colorScreenBackground">#fff3e0</color>
<color name="colorTextHint">#E0E0E0</color>
</resources>
472
Introduction
<style name="text_view_style">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textAppearance">
@android:style/TextAppearance.Large</item>
<item name="android:background">@color/colorPrimaryLight</item>
<item name="android:layout_marginTop">8dp</item>
<item name="android:layout_gravity">center</item>
<item name="android:padding">16dp</item>
<item name="android:textColor">@color/colorTextPrimary</item>
</style>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/text_view_style"
tools:text="placeholder text" />
</LinearLayout>
android:background="@color/colorScreenBackground"
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
tools:listitem="@layout/recyclerview_item"
/>
473
Introduction
android:src="@drawable/ic_add_black_24dp"
474
Introduction
@Override
public WordViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = mInflater.inflate(R.layout.recyclerview_item, parent, false);
return new WordViewHolder(itemView);
}
@Override
public void onBindViewHolder(WordViewHolder holder, int position) {
if (mWords != null) {
Word current = mWords.get(position);
holder.wordItemView.setText(current.getWord());
} else {
// Covers the case of data not being ready yet.
holder.wordItemView.setText("No Word");
}
}
Note: The mWords variable in the adapter caches the data. In the next task, you add the
475
Introduction
2. Run your app to make sure the app compiles and runs. There are no items, because
you have not hooked up the data yet. The app should display the empty recycler view.
476
Introduction
There is no data in the database yet. You will add data in two ways: Add some data when
the database is opened, and add an Activity for adding words. Every time the database is
opened, all content is deleted and repopulated. This is a reasonable solution for a sample
app, where you usually want to restart on a clean slate.
If you need a refresher on AsyncTask , see AsyncTaks and AsyncTaskLoader in the Android
Fundamentals course.
@Override
public void onOpen (@NonNull SupportSQLiteDatabase db){
super.onOpen(db);
new PopulateDbAsync(INSTANCE).execute();
}
};
for the AsyncTask that deletes the contents of the database, then populates it with an
initial list of words. Feel free to use your own words!
477
Introduction
/**
* Populate the database in the background.
*/
private static class PopulateDbAsync extends AsyncTask<Void, Void, Void> {
PopulateDbAsync(WordRoomDatabase db) {
mDao = db.wordDao();
}
@Override
protected Void doInBackground(final Void... params) {
// Start the app with a clean database every time.
// Not needed if you only populate the database
// when it is first created
mDao.deleteAll();
3. Add the callback to the database build sequence in WordRoomDatabase , right before you
call .build() :
.addCallback(sRoomDatabaseCallback)
To display the current contents of the database, you add an observer that observes the
LiveData in the ViewModel . Whenever the data changes (including when it is initialized),
the onChanged() callback is invoked. In this case, the onChanged() callback calls the
adapter's setWord() method to update the adapter's cached data and refresh the displayed
list.
478
Introduction
mWordViewModel = ViewModelProviders.of(this).get(WordViewModel.class);
in this case, when the app opens, the initial data is added, so onChanged() method is
called.
4. Run the app. The initial set of words appears in the RecyclerView .
479
Introduction
480
Introduction
Now you will add an Activity that lets the user use the FAB to enter new words. This is what
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
3. Use the Empty Activity template to create a new activity, NewWordActivity. Verify that
481
Introduction
<activity android:name=".NewWordActivity"></activity>
<EditText
android:id="@+id/edit_word"
style="@style/text_view_style"
android:hint="@string/hint_word"
android:inputType="textAutoComplete" />
<Button
android:id="@+id/button_save"
style="@style/button_style"
android:text="@string/button_save" />
</LinearLayout>
5. Implement the NewWordActivity class. The goal is that when the user presses the Save
button, the new word is put in an Intent to be sent back to the parent Activity .
482
Introduction
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_new_word);
mEditWordView = findViewById(R.id.edit_word);
483
Introduction
3. In MainActivity, start NewWordActivity when the user taps the FAB. Replace the code
in the FAB's onClick() click handler with the following code:
4. RUN YOUR APP! When you add a word to the database in NewWordActivity , the UI
automatically updates.
5. Add a word that already exists in the list. What happens? Does your app crash? Your
app uses the word itself as the primary key, and each primary key must be unique. You
can specify a conflict strategy to tell your app what to do when the user tries to add an
existing word.
6. In the WordDao interface, change the annotation for the insert() method to:
@Insert(onConflict = OnConflictStrategy.IGNORE)
7. Run your app again and try adding a word that already exists. What happens now?
Solution code
Android Studio project: RoomWordsSample.
Summary
Now that you have a working app, let's recap what you've built. Here is the app structure
again, from the beginning:
484
Introduction
change, the observer's onChange() method is executed and updates mWords in the
WordListAdapter .
The data can be observed because it is LiveData . And what is observed is the
LiveData<List<Word>> that is returned by the WordViewModel object.
The WordViewModel hides everything about the backend from the user interface. It
provides methods for accessing the UI data, and it returns LiveData so that
MainActivity can set up the observer relationship. Views, activities, and fragments
only interact with the data through the ViewModel . As such, it doesn't matter where the
data comes from.
In this case, the data comes from a Repository. The ViewModel does not need to know
what that Repository interacts with. It just needs to know how to interact with the
Repository, which is through the methods exposed by the Repository.
The Repository manages one or more data sources. In the RoomWordsSample app,
that backend is a Room database. Room is a wrapper around and implements a SQLite
database. Room does a lot of work for you that you used to have to do yourself. For
example, Room does everything that you used to use a SQLiteOpenHelper class to do.
The DAO maps method calls to database queries, so that when the Repository calls a
method such as getAllWords() , Room can execute SELECT * from word_table ORDER BY
word ASC .
The result returned from the query is observed LiveData . Therefore, every time the
data in Room changes, the Observer interface's onChanged() method is executed and
the UI is updated.
Related concept
The related concept documentation is Architecture Components.
Learn more
Android developer documentation:
MutableLiveData
485
Introduction
ViewModel
ViewModelProviders
Codelabs:
Videos:
Code samples:
486
Introduction
This practical gives you more practice at using the API provided by the Room library to
implement database functionality. You will add the ability to delete specific items from the
database. The practical also includes a coding challenge, in which you update the app so
the user can edit existing data.
487
Introduction
retrieve data in Android's built-in SQLite database. You learned these topics in the
previous practical, 15.1A: Working with Architecture Components: Room, LiveData,
ViewModel.
Populate the database with data only if the database is empty (so users don't lose
changes they made to the data).
Delete data from a Room database.
Update existing data (if you build the challenge app).
App overview
You will extend the RoomWordsSample app that you created in the previous practical. So
far, that app displays a list of words, and users can add words. When the app closes and re-
opens, the app re-initializes the database, and words that the user has added are lost.
In this practical, you extend the app so that it only initializes the data in the database if there
is no existing data.
488
Introduction
Then you add a menu item that allows the user to delete all the data.
You also enable the user to swipe a word to delete it from the database.
489
Introduction
In this task you update the app so that when it opens, the initial data set is only added if the
database has no data.
To detect whether the database contains data already, you can run a query to get one data
item. If the query returns nothing, then the database is empty.
In a production app, you might want to allow users to delete all data without re-initializing the
data when the app restarts. But for testing purposes, it's useful to be able to delete all data,
then re-initialize the data when the app starts.
Room issues the database query when the getAnyWord() method is called and returns
an array containing one word. You don't need to write any additional code to implement
it.
doInBackground() method to check whether the database has any words before
490
Introduction
@Override
protected Void doInBackground(final Void... params) {
2. Run your app and add several new words. Close the app and restart it. You should see
the new words you added, as the words should now persist when the app is closed and
opened again.
through the ViewModel so that your app can call the method whenever it's needed.
Here are the general steps for implementing a method to use the Room library to interact
with the database:
Add the method to the DAO, and annotate it with the relevant database operation. For
the deleteAll() method, you already did this step in the previous practical.
Add the method to the WordRepository class. Write the code to run the method in the
background.
To call the method in the WordRepository class, add the method to the WordViewModel .
The rest of the app can then access the method through the WordViewModel .
491
Introduction
deleteAllWordsAsyncTask(WordDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(Void... voids) {
mAsyncTaskDao.deleteAll();
return null;
}
}
2. In the WordRepository class, add the deleteAll() method to invoke the AsyncTask
that you just defined.
Note: The production version of your app must provide safeguards so that users do not
accidentally wipe out their entire database. However, while you develop your app, it's helpful
492
Introduction
to be able to clear out test data quickly. This is especially true now that your app does not
clear out the data when the app opens.
<item
android:id="@+id/clear_data"
android:orderInCategory="100"
android:title="@string/clear_all_data"
app:showAsAction="never" />
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.clear_data) {
// Add a toast just for confirmation
Toast.makeText(this, "Clearing the data...",
Toast.LENGTH_SHORT).show();
return super.onOptionsItemSelected(item);
}
3. Run your app. In the Options menu, select Clear all data. All words should disappear.
4. Restart the app. (Restart it from your device or the emulator; don't run it again from
Android Studio) You should see the initial set of words.
After you clear the data, re-deploying the app from Android Studio will show the initial data
set again; opening the app will show the empty data set.
493
Introduction
Again, here are the general steps to implement a method to use the Room library to interact
with the database:
Add the method to the DAO, and annotate it with the relevant database operation.
Add the method to the WordRepository class. Write the code to run the method in the
background.
To call the method in the WordRepository class, add the method to the WordViewModel .
The rest of the app can then access the method through the WordViewModel .
@Delete
void deleteWord(Word word);
Because this operation deletes a single row, the @Delete annotation is all that is
needed to delete the word from the database.
deleteWordAsyncTask(WordDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Word... params) {
mAsyncTaskDao.deleteWord(params[0]);
return null;
}
}
2. In WordRepository , add the deleteWord() method to invoke the AsyncTask you just
defined.
494
Introduction
You have now implemented the logic to delete a word, but as yet there is no way to
invoke the delete-word operation from the app's UI. You fix that next.
Use the ItemTouchHelper class provided by the Android Support Library (version 7 and
higher) to implement swipe functionality in your RecyclerView . The ItemTouchHelper class
has the following methods:
onMove() is called when the user moves the item. You do not implement any move
495
Introduction
496
Introduction
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder,
int direction) {
int position = viewHolder.getAdapterPosition();
Word myWord = adapter.getWordAtPosition(position);
Toast.makeText(MainActivity.this, "Deleting " +
myWord.getWord(), Toast.LENGTH_LONG).show();
helper.attachToRecyclerView(recyclerView);
Given the position, you can get the word displayed by the ViewHolder by calling the
getWordAtPosition() method that you defined in the adapter:
mWordViewModel.deleteWord(myWord);
1. Run your app. You should be able to delete words by swiping them left or right.
497
Introduction
Solution code
Android Studio project: RoomWordsWithDelete
Coding challenge
Challenge: Update your app to allow users to edit a word by tapping the word and then
saving their changes.
Hints
Make changes in NewWordActivity
You can add functionality to NewWordActivity , so that it can be used either to add a new
word or edit an existing word.
The Word entity class uses the word field as the database key. However, when you update
a row in the database, the item being updated cannot be the primary key, because the
primary key is unique to each row and never changes. So you need to add an auto-
generated id to use as the primary key
@PrimaryKey(autoGenerate = true)
private int id;
@NonNull
@ColumnInfo(name = "word")
private String mWord;
Add a constructor to the Word entity class that takes id and word as parameters. Make
sure this additional constructor is annotated using @Ignore , because Room expects only
one constructor by default in an entity class.
@Ignore
public Word(int id, @NonNull String word) {
this.id = id;
this.mWord = word;
}
498
Introduction
To update an existing Word , create the Word using this constructor. Room will use the
primary key (in this case the id ) to find the existing entry in the database so it can be
updated.
@Update
void update(Word... word);
Summary
Writing database code
Room takes care of opening and closing the database connections each time a
database operations executes.
Annotate methods in the data access object (DAO) as @insert , @delete , @update ,
@query .
For simple insertions, updates and deletions, it is enough to add the relevant annotation
to the method in the DAO.
For example:
@Delete
void deleteWord(Word word);
@Update
void update(Word... word);
For queries or more complex database interactions, such as deleting all words, use the
@query annotation and provide the SQL for the operation.
For example:
499
Introduction
ItemTouchHelper
To enable users to swipe or move items in a RecyclerView , you can use the
ItemTouchHelper class.
Related concept
The related concept documentation is Architecture Components.
Learn more
Entities, data access objects (DAOs), and ViewModel :
Dao reference
ViewModel reference
ItemTouchHelper :
ItemTouchHelper reference
500
Introduction
501
Appendix: Setup
Appendix: Setup
Contents:
The practicals in this book assume that you are using the latest version of Android Studio,
and some of the practicals require at least Android Studio 3.0. For example, the
Performance chapters teach you how to use the Android Profiler tools.
This page summarizes how to set up the latest version of Android Studio.
If you need more help, see Install Android Studio and Run Hello World in the Android
Developer Fundamentals course and the resources at the end of this page.
502
Appendix: Setup
App overview
You can use the finished WordListSQLInteractive app from the Android Developer
Fundamentals course to test your setup, or you can use any other app that's not simplistic.
Note that you can keep two independent versions of Android Studio on your development
machine, if you want to.
Windows:
Mac:
Note: If you download version 2.3 or lower, the app name does not include the version
number. Rename the new version before moving it into your apps directory. Otherwise,
you might override your existing version of Android Studio.
Linux:
503
Appendix: Setup
1. Open Android Studio, then open an existing app of some complexity, such as
WordListSQLInteractive.
When you build an app that you built with a previous version of Android Studio, you may
get errors about components and libraries that are missing.
2. Click the links as prompted by the error messages, and install the needed components.
On Android 4.2 and higher, the Developer options screen is hidden by default. To
make the screen visible, go to Settings > About phone and tap Build number seven
times. Return to the previous screen to find Developer options at the bottom.
504
Appendix: Setup
3. Connect the mobile device to your development computer with a USB data cable.
4. In Android Studio, click Run.
5. You may be prompted to install HAXM and emulator images. If you have been using
several emulator images, this installation can take a while. Your largest update may be
over 1 GB and will take some time to download even on a fast connection. You can
postpone the system image downloads and run them later from the AVD manager.
6. After installation, choose your device and run the app.
If you don't have an emulator, click Create Virtual Device in the Select Deployment
Target dialog. Choose a phone and an existing system image, if you have one, because
additional system images are large to download.
Learn more
Android Studio Release Notes
Android Plugin for Gradle Release Notes
Install Android Studio and Run Hello World practical from Android Developer
Fundamentals
Run Apps on a Hardware Device
Android Studio Preview documentation, for information on how to get and install preview
versions of Android Studio
505
Appendix: Setup
506
Appendix: Homework
Appendix: Homework
Contents:
This appendix lists possible homework assignments that students can complete at the end
of each practical. It is the instructor's responsibility to do the following:
Instructors can use these suggested assignments as little or as much as they want.
Instructors should feel free to assign any other homework they feel is appropriate.
507
Appendix: Homework
Add the following functionality to the app, using SimpleFragment with SecondActivity :
1. Add a Next button under the Open button to navigate from the first activity to the
second activity.
2. Add a Previous button in the second activity to navigate back to the first activity.
Question 1
Which subclass of Fragment displays a vertical list of items that are managed by an
adapter?
RowsFragment()
PreferenceFragment()
DialogFragment()
ListFragment()
Question 2
Which of the following is the best sequence for adding a fragment to an activity that is
already running?
Declare the fragment inside the activity's layout file using a <fragment> view.
Declare a location for the fragment inside the activity's layout file using the
<FrameLayout> view group.
Declare the location for the fragment inside the activity's layout file using the
<FrameLayout> view group, get an instance of the fragment and FragmentManager , and
begin a transaction, use the add() transaction, and commit the transaction.
508
Appendix: Homework
Question 3
Which statement gets a reference to a fragment using the fragment's layout resource?
fragmentManager.findViewById(R.id.fragment_container);
fragmentManager.findFragmentById(R.id.fragment_container);
The app displays a second activity when the user taps the Next button.
The app's second activity includes an Open button. The Open button opens the same
fragment ( SimpleFragment ) that appears when the user taps Open in the first activity.
The fragment looks the same, with the X button.
Change the FragmentCommunicate app so that the Toast shows the "Yes" or "No" message
rather than "0" or "1."
Question 1
Which fragment-lifecycle callback draws the fragment's UI for the first time?
onAttach()
onActivityCreated()
509
Appendix: Homework
onCreate()
onCreateView()
Question 2
How do you send data from an activity to a fragment?
Set a Bundle and use the Fragment . setArguments(Bundle) method to supply the
construction arguments for the fragment.
Use the Fragment method getArguments() to get the arguments.
Define an interface in the Fragment class, and implement the interface in the activity.
Call addToBackStack() during a transaction that removes the fragment.
After the user taps Open and makes a choice, the Toast message displays "Choice is
Yes" or "Choice is No".
Add a 1x3 app widget that displays the current value of the string. Add a click handler to the
entire app widget such that the app opens when the user taps the app widget.
Question 1
Which of these app-widget components are required ? (Choose all that apply)
Provider-info file
Widget-provider class
510
Appendix: Homework
Configuration activity
Layout file
Question 2
Which of these layout and view classes can be used in an app widget?
Button
ConstraintLayout
LinearLayout
ImageButton
CardView
CalendarView
Question 3
In which method in your widget-provider class do you initialize the layout (remote views) for
the app widget?
onCreate()
onReceive()
onEnabled()
onUpdate()
The main activity of the app has an EditText view and a button. If you change the
string in the EditText and tap the button, the string is saved. Quitting and restarting the
app should load the current string from the shared preferences.
The app widget displays the current text in the app's EditText .
Tapping the app widget opens the app.
511
Appendix: Homework
Question 1
Which of the following features are provided by the SensorManager class? (Choose all that
apply)
Question 2
In which Activity lifecycle method should you register your sensor listeners?
onResume()
onCreate()
onStart()
onRestart()
Question 3
What are best practices for using sensors in your app? (Choose all that apply)
Register listeners for only for the sensors you're interested in.
Test to make sure that a sensor is available on the device before you use the sensor.
Check permissions for the sensor before you use it.
Register a sensor listener for the slowest possible data rate.
Don't block onSensorChanged() to filter or transform incoming data.
512
Appendix: Homework
When you run the app in the emulator and change the value of the humidity sensor, the
app's display should reflect the current value.
Question 1
Which sensors report values with respect to Earth's coordinate system, instead of reporting
values with respect to the device-coordinate system?
Question 2
Which sensors can you use to get the orientation of the device?
Gyroscope
Accelerometer and gravity sensor
Orientation sensor
Geomagnetic field sensor and accelerometer
513
Appendix: Homework
When you place the device flat on a table, a spot should appear in the middle of the
screen.
When you lift the top or bottom of the device, the spot should move to the upper or
lower edge of the screen. The position of the spot should be further away from the
center as you tilt the device at a greater angle.
Question 1
Select all of the following that are good basic performance tests you can perform.
Install your app on the lowest-end device that your target audience might have.
Observe your friends using the app and make note of their comments.
Run a small usability test.
Publish your app and look at the ratings and feedback you receive.
Question 2
Select all that are good ways to approach performance problems.
Guess at what might be the problem, make a change, and see whether it helps.
514
Appendix: Homework
Use a systematic, iterative approach, so that you can measure improvements resulting
from your changes to the app.
Use tools to inspect your app and acquire performance data measurements.
Run your code and have someone else run your code to evaluate it.
Question 3
Select all of the following that are performance tools available on your mobile device.
Question 4
What is the Performance Improvement Lifecycle technique for improving app performance?
515
Appendix: Homework
1. Use the same larger images for the thumbnails as for the details view . Run Profile GPU
Rendering with the app, take a screenshot of the bars.
2. Change the app to use small thumbnail images or text to list the items. Run Profile GPU
Rendering with the app again and take a screenshot of the bars.
3. Do the two screenshots look different? Comment on the difference or lack of difference.
4. Turn on Debug GPU Overdraw. Take a screenshot of your app. If there is a lot of red
color, use Layout Inspector to fix your app, then take another screenshot.
Question 1
How much time does your app have available to calculate and display one frame of content?
16 milliseconds
Less than 16 milliseconds, because the Android framework is doing work at the same
time as your app does work.
It depends on the device.
Question 2
What are some techniques you can use to make rendering faster?
Reduce overdraw.
Move work away from the UI thread.
Use AsyncTask .
Use smaller images.
Compress your data.
Flatten your view hierarchy.
Use as few views as possible.
Use loaders to load data in the background.
Use efficient views such as RecyclerView and ConstraintLayout .
Question 3
Which answer best describes the measure-and-layout stage of the rendering pipeline?
516
Appendix: Homework
Submit the two screenshots you took with Profile GPU Rendering turned on, and the final
screenshot you took with Debug GPU Overdraw turned on. Also submit your reflections on
why the two screenshots that show Profile GPU Rendering output might be (almost) the
same, or why they are different.
Student submitted two screenshots of the app with Profile GPU Rendering turned on
Screenshots are different.
Student submitted comments explaining the difference.
If there is no difference, student should reflect on why not.
If there is a difference, students should reflect on what may have caused it.
Student submitted one screenshot of the app with Debug GPU Overdraw turned on, and
this screenshot shows only a little bit of red coloring.
517
Appendix: Homework
Question 1
What tools are available in Android Studio for measuring memory performance?
Systrace
Heap dumps
Debug GPU Overdraw
Memory Profiler
Question 2
Looking at this Memory Monitor graph, which of the following statements is factually true?
The app is allocating an increasing amount of memory, and you should investigate for
possible memory leaks.
The app will run out of memory and crash.
The app will slow down over time because less memory is available.
More garbage collection events will happen, resulting in a slower app.
Question 3
Select all answers that describe features and benefits of a heap dump?
Question 4
What are the benefits of recording memory allocations? Select up to four.
518
Appendix: Homework
Submit the annotated screenshot that you took of the Memory graph, along with your
observations.
Student submitted annotated screenshot of the app with Memory Profiler turned on
Student submitted comments relating the graph to their app.
Student submitted a second, different screenshot, along with explanations on what they
did to cause this change, as well as reflections on the change.
Question 1
On mobile devices, what uses up battery power?
Mobile radio
Wi-Fi
Keeping the Display open
Running your device in airplane mode
Any hardware on the device that is actively in use
Question 2
519
Appendix: Homework
Your app can affect the amount of power that some device components use. Which of the
following device components does this include? Select up to three.
Question 3
Which of the following are best practices you should always incorporate in your app?
Defer all network requests until the user's device is on Wi-Fi and plugged in.
Use the most efficient image format for the type of images your app uses.
Compress all your data.
Respond to user actions immediately.
Always prefetch as much data as possible.
If you must poll the server, use an exponential back-off pattern.
To make your app work offline, cache data locally.
Question 4
What are best practices for working with images?
Use the smallest image possible. Resize images used for thumbnails.
Don't use images. They take up too much space.
Always use a quality setting that does not diminish the user experience but results in a
smaller image.
If you store images on a remote server, make multiple sizes available for each image.
That way, your app can request the appropriate size for the device it's running on.
WebP, PNG, and JPG image formats can be used interchangeably.
If possible, use the WebP image format, because it usually results in smaller, higher-
quality images.
520
Appendix: Homework
Question 1
Which of the following attributes should you add to the android:layout_marginLeft attribute
to support an RTL language?
android:layout_marginEnd
android:layout_marginStart
app:layout_constraintLeft_toRightOf
app:layout_constraintStart_toEndOf
Question 2
Which of the following attributes should you add to the app:layout_constraintLeft_toRightOf
attribute to support an RTL language?
app:layout_constraintRight_toLeftOf
app:layout_constraintStart_toEndOf
app:layout_constraintRight_toRightOf
app:layout_constraintEnd_toEndOf
521
Appendix: Homework
The LocaleText3 app calculates and shows a total after the user enters a quantity and taps
the Done button. Modify the LocaleText3 app as follows:
Question 1
Which of the following statements returns a general-purpose number format for the user-
selected language and locale?
Question 2
Which of the following statements converts a locale-formatted input quantity string to a
number?
myInputQuantity = numberFormat.parse(v.getText().toString()).intValue();
Question 3
Which of the following statements retrieves the country code for the user-chosen locale?
522
Appendix: Homework
If the user chooses Français (France), the app displays the total in euros.
If the user chooses Hebrew (Israel), the app displays the total in new shekels.
If the user chooses any other locale, the app displays the total in U.S. dollars.
Question 1
Why should you consider making your app accessible?
Question 2
Which of the following are accessibility features available in Android?
TalkBack
Switch Access
Text-to-speech
Closed captions
Magnification
Ability to change display or font size
Question 3
What is TalkBack?
523
Appendix: Homework
Question 4
How should you test your app for accessibility?
1. Add a description for the first image that describes the image. Ensure that the image is
focusable.
2. Add a decorative image with no description. Ensure that the image is not focusable.
3. For the third image, add a text label that describes the image.
Question 1
Which of the following attributes should you add to ImageView and ImageButton elements
to enable screen readers to describe the image?
android:text
android:contentDescription
android:hint
android:labelFor
Question 2
When should you add a content description to an ImageView or ImageButton ?
When the image's role on the screen is solely decorative and does not provide any
524
Appendix: Homework
function or meaning.
When an image's dimensions are very small and the image is difficult to see.
When the image is meaningful to the user in their use of the app.
When the image is also a button.
Question 3
When do you NOT need to add a content description to a view element?
The app should have three new images, of any kind. The third image should have a text
view that serves as a label.
The layout for the first new image should include the following:
An android:contentDescription attribute with a description of the image.
An android:focusable="true" attribute.
The layout for the second new image should include the following:
An android:contentDescription attribute with a null string ( "" ).
An android:focusable="false" attribute.
The layout for the third new image should include the following:
An android:focusable="true" attribute.
A TextView element with the android:labelFor attribute. The android:labelFor
attribute should indicate the ID of the image.
525
Appendix: Homework
1. Accuracy in meters.
2. Speed in meters per second.
Question 1
Which API do you use to request the last known location on the device?
Question 2
Which class do you use for handling geocoding and reverse geocoding?
GeoDecoder
Geocoder
ReverseGeocoder
GeocoderDecoder
Question 3
Which method do you use for periodic location updates ?
The app displays a second TextView , in addition to the TextView that shows the
address.
The second TextView shows speed and accuracy.
526
Appendix: Homework
1. Create a new Android image for shopping malls. (Hint: Use the Androdify tool.)
2. If the place type is TYPE_SHOPPING_MALL , replace the Android image with your image.
3. To test your app, pick a shopping mall from the PlacePicker dialog. Make sure that
your new Android image is animated, and that TextView object is updated with the
shopping mall's name and address.
Question 1
Which class displays a dialog that allows a user to pick a place using an interactive map ?
PlaceDetectionApi
GeoDataApi
PlacePicker
PlaceAutocomplete
Question 2
What are the two ways to add the autocomplete widget to your app?
Question 3
If your app uses the PlacePicker UI or the PlaceDetectionApi interface, what permission
does your app require?
ACCESS_COARSE_LOCATION
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_FINE_LOCATION
527
Appendix: Homework
1. When you tap the Pick a Place button, the place picker UI opens.
2. When you pick a shopping mall on the map, the new Android image is displayed.
3. The image is animated.
4. The TextView updates to show the name and address of the shopping mall.
Question 1
Which method is called when the map is loaded and ready to be used in the app ?
Question 2
Which Android components can you use to include Google Maps in your app ?
528
Appendix: Homework
Only MapFragment
Question 3
What types of maps does the Google Maps Android API offer?
Question 4
What interface do you implement to add on-click functionality to a point of interest (POI)?
GoogleMap.OnPoiListener
GoogleMap.OnPoiClickListener
GoogleMap.OnPoiClick
GoogleMap.OnPoiClicked
When the app is launched, the Google Map is displayed correctly, indicating that an API
key was generated properly.
After the Google Map loads, the camera moves to the student's home or school
location. In the code, this step should happen in the onMapReady (GoogleMap googleMap)
callback method.
Markers are displayed at the student's school location and another location, such as the
student's home.
The two markers are customized. For example, the markers use a color other than the
default red color, or they use a custom icon.
529
Appendix: Homework
1. In the layout, add a second version of the EditTextWithClear custom view underneath
the first version (the Last name field).
2. Use XML attributes to define the second version of the custom view as a phone number
field that accepts only numeric phone numbers as input.
Question 1
Which constructor do you need to inflate the layout for a custom view? Choose one:
Question 2
To define how your custom view fits into an overall layout, which method do you override?
onMeasure()
onSizeChanged()
invalidate()
onDraw()
Question 3
To calculate the positions, dimensions, and any other values when the custom view is first
assigned a size, which method do you override?
onMeasure()
onSizeChanged()
invalidate()
onDraw()
Question 4
To indicate that you'd like your view to be redrawn with onDraw() , which method do you call
from the UI thread, after an attribute value has changed?
530
Appendix: Homework
onMeasure()
onSizeChanged()
invalidate()
getVisibility()
The app displays a Phone number field with a clear (X) button on the right side of the
field, just like the Last name field.
The second version of the EditTextWithClear custom field (Phone number) should
use the android:inputType attribute so users can enter values with a numeric keypad.
You can use simple colored squares or shapes for the "cards."
If the user reveals two matching cards, show a congratulatory toast. If the user reveals
two cards that don't match, show an encouraging message telling them to tap to
continue.
Click handling: On the first tap, show the first card. On the second tap, show the second
card and display a message. On the next tap, restart.
Question 1
To display something to the screen, which one of the following draw and animation classes
is always required?
View
Drawable
Canvas
Bitmap
531
Appendix: Homework
Question 2
What are some properties of drawables?
Drawables are drawn into a view and the system handles drawing.
Drawables are best for simple graphics that do not change dynamically.
Drawables offer the best performance for game animations.
You can use drawables for frame-by-frame animations.
Question 3
Which of the following statements are true?
You use a Canvas object when elements in your app are redrawn regularly.
To draw on a Canvas , you must override the onDraw() method of a custom view.
Every view has a Canvas that you can access.
A Paint object holds style and color information about how to draw geometries, text,
and bitmaps.
Question 4
What is clipping?
A technique for defining regions on a Canvas that will not be drawn to the screen.
A technique for making the Canvas smaller so the Canvas uses less memory.
A way of telling the system which portions of a Canvas do not need to be redrawn.
A technique to consider when you're trying to speed up drawing.
A way to create interesting graphical effects.
532
Appendix: Homework
The MemoryGame app hides and reveals "cards" as the user taps on the screen. Use
clipping to implement the hide/reveal effect.
You can use simple colored squares or shapes for the "cards."
If the user reveals two matching cards, show a congratulatory toast. If the user reveals
two cards that don't match, show an encouraging message telling them to tap to
continue.
Click handling: On the first tap, show the first card. On the second tap, show the second
card and display a message. On the next tap, restart.
Question 1
What is a SurfaceView ?
A view in your app's view hierarchy that has its own separate surface.
A view that directly accesses a lower-level drawing surface.
A view that is not part of the view hierarchy.
A view that can be drawn to from a separate thread.
Question 2
What is the most distinguishing benefit of using a SurfaceView ?
Question 3
When should you consider using a SurfaceView ? Select up to three.
533
Appendix: Homework
Create an animation where text spins and recedes while getting smaller. Or text
appears from nowhere and spins to fill the screen. Combine these two animations.
Create an animation that simulates a ball that grows until it bursts into multiple smaller
balls.
Create a simple card game, where touching a card flips the card around.
Question 1
What types of animations are available with Android?
View animation
Property animation
Canvas animation
Drawable animation
Physics-based animation
534
Appendix: Homework
Question 2
Which of the following statements about property animation are true?
Property animation lets you define an animation to change any object property over
time.
Property animation lets you create objects with custom properties that you can animate.
A property animation tracks time and adapts its velocity to the time.
Property animation lets you animate multiple properties with animator sets.
The duration of a property animation is fixed.
Question 3
What are the advantages of using physics-based animation libraries? Select up to three
answers.
Physics-based animations are more realistic than other types of animations, because
physics-based animations appear more natural.
It is easier to use the physics-based support library than to implement adaptive
animations yourself.
Physics-based animations keep momentum when their target changes and end with a
smoother motion than other types of animations.
Physics-based animations are easier to combine with audio.
The main activity for the app contains a list of videos. Use a RecyclerView for the list.
Clicking a single item in the list starts the video playback activity containing the
VideoView and MediaController .
535
Appendix: Homework
At the end of the video playback, automatically start playing the next video in the list.
At the end of the last video in the list, finish the activity and return to the list of videos.
Question 1
VideoView is a wrapper for which two classes?
Question 2
Which of the following sources can VideoView play?
Question 3
Which of these callbacks are available for media events in the VideoView class?
onError()
onInfo()
onPrepared()
onBufferingUpdate()
The app's main activity contains a list of video samples to play, in a RecyclerView . The
videos may come from any source.
Clicking any video starts a new activity with a VideoView and a media controller.
Skip to the end of the video playback. The next video in the list should play.
536
Appendix: Homework
At the end of the playback of the last video, close the activity and return to the list of
videos.
The code should use the onComplete() callback to determine whether to play the next
video in the list or finish playing altogether.
Create a simple app that stores one text document whose contents are displayed in a
TextView . When the user edits the document, changes appear in the TextView .
Create a question-answer app. Start out with only questions and let users add new
questions and answers.
As a challenge, add a button to each answer in the question-answer app that displays
additional information that's stored in a different repository. The information could come
from a file on the device, or a page on the internet.
Question 1
What are the advantages of using a Room database?
Question 2
Which of the following are reasons for using a ViewModel ?
537
Appendix: Homework
Often used with LiveData for changeable data that the UI will use or display.
Prevents your data from being lost when the app crashes.
Acts as a communication center between the Repository and the UI.
ViewModel instances survive device configuration changes.
Question 3
What is the DAO?
Question 4
What are features of LiveData ?
Updates automatically when used with Room if all the intermediate levels also return
LiveData (DAO, ViewModel , Repository).
Uses the observer pattern and notifies its observers when its data has changed.
Automatically updates the UI when it changes.
Is lifecycle aware.
538
Appendix: Homework
You have learned about many ways to store data. Choosing the right storage option
depends on how large your data is, and how long the data has to survive.
Create an app that demonstrates how data stored in at least two different locations survives
configuration changes and the destruction of the app. You can do this by storing small
pieces of data, such as strings, in different data stores.
The app should demonstrate what happens to data that is not saved.
The app could demonstrate what happens to data that is preserved with
saveInstanceState , data that uses LiveData with a ViewModel , and data that is stored
in a file or database.
Question 1
Android Architecture Components provide some convenience annotations for DAOs. Which
of the following are available.
@Dao
@Insert
@Delete
@Update
@Query
@Select
Question 2
What are the benefits of using Architecture Components?
Architecture Components help you structure your app in a way that is robust and
testable.
Architecture Components help you create better UIs.
Architecture Components provide a simple, flexible, and practical approach to
structuring your app.
If you use the the provided libraries and architecture, your app is more maintainable
with less boilerplate code.
539
Appendix: Homework
540