----------------[ View, Actions, Intents ]----------------
In class today we saw an example of a very simple "Hello World"
application that reacts to a button click to launch into a different
screen (i.e., starts a new Activity). That screen offers a TextEdit
box, takes input, and reports that input back to the initial Activity
as it exits (when the user clicks another button). The initial
activity then changes a TextView label to the string entered.
Note that clicking the first button replaces the entire content of the
app's screen area, i.e., in Android's terms, the entire tree of nested
Views in the Window area the app owns. Then, as the editing activity
exits, that whole tree is restored. This is how Activities work: they
replace the entire hierarchy of visible elements in the app's window
area when called, and restore their own when returned to.
The code is posted in
http://www.cs.dartmouth.edu/~sergey/cs65/examples/Greeter/
For explanations, see comments and
https://developer.android.com/training/basics/intents/result.html
This directory is flat; you will need to place these files in the
appropriate places in the Studio Project). For example, on my
system, the paths are
AndroidStudioProjects/Greeter/app/src/main/java/com/netfluke/sergey/greeter/
for .java files,
AndroidStudioProjects/Greeter/app/src/main/res/layout/
for .xml layout files
AndroidStudioProjects/Greeter/app/src/main/AndroidManifest.xml
for the manifest file
Study the directory structure of your Android Studio projects from
the terminal/shell. Ignore the contents of the build/ directory,
except for the ./app/build/outputs/apk/debug/ directory, where
you will find the APK of the application that is uploaded to
your phone or your emulator. We'll talk about dissecting APKs
next week.
================[ Windows, Surfaces, Bitmaps ]================
In Android, the visible area of the screen is divided between Windows,
which are rectangle areas handled by specific applications. This is
where Activities in these applications will draw their hierarchies of
Views such as text, images, buttons, list and checkbox widgets, etc.,
including really complex ones such as embedded Google maps frames.
Activities get their rectangle allocation from the WindowManager
and fill them with their tree of Views (typically described in XML
files such as layout files; Java classes for the Views are created
from XML elements). Views draw and redraw themselves into the
Surface and Canvas associated with the windows.
Good & concise explanations of Android terminology are here:
https://stackoverflow.com/questions/9451755/what-is-an-android-window#answers
https://stackoverflow.com/questions/4576909/understanding-canvas-and-surface-concepts#answers
You can see the View objects that make up Android's home/launcher
screen with the Android Device Monitor tool. In the Studio menu bar,
click "Tools > Android > Android Device Monitor", then click the DDMS
button in the tool that starts up. The tool will close the Studio's
adb connection and start its own; this is normal. Select the device,
and then use the button tool-tipped "Dump View Hierarchy for UI
Automator" (see device-monitor-ddms.png and
device-monitor-view-hierarchy.png). Now you can navigate the tree of
Views, and find exactly how the screen is laid out. On Monday, we will
use the tool called APKtool to extract complete XML layout files from
app packages.
================[ Views and Activities ]================
Once Views are measured, laid out, and drawn on the screen, they are
ready to receive clicks and other screen-related events (e.g., long
clicks, focus changes, screen touches, etc.). Technically, every
View occupies a particular rectangle on the screen; a touch or a
click in that area gets delivered to the View.
A View might not have any callbacks registered for the event. In
that case, all you'd get is a little animation of the Button being
depressed and released. However, such a button is useless. The
click is meant to change something about the state of the app,
and so it must be communicated back to the Activity. This is
what callbacks do.
Callbacks on Views are a part of the famous design pattern called
Model-View-Controller. The logic for measuring, laying out, and
drawing Views is contained in the View objects; the Activity acts the
"controller", which keeps the application states, receives the events
via callbacks, queries the application-specific data (the "model"),
and updates views with such data.
NOTE: Callbacks are registered with the Views, but are defined as
methods of the Activity. This means that any variables in the Activity
class are in the lexical scope of these callbacks, and are accessible
to these functions and shared by them. This is very important:
although called in response to events in Views, callbacks run in the
context of Activity, and share that context---where the app's state
is, and from where they can call to a local or remote database, which
implements the model for the application data.
================[ What Views do ]================
Views go through three phases---each one a recursive traversal of the
view tree---before they are ready. In each case, each View receives
some data and passes some data to its child views. First comes
measurement, then figuring out the relative coordinates of child views
if any, then, finally, the drawing. These recursive traversals
are described in
https://developer.android.com/guide/topics/ui/overview.html
and
https://developer.android.com/reference/android/view/View.html
https://developer.android.com/reference/android/view/ViewGroup.html
Note that you can define your own views, and overload that methods
that are responsible for measuring, drawing, handling touches, etc.
================[ Creating the View tree ]================
So Activities request a Window and fill it in with their tree of Views
(one tree for an activity). The typical way an Activity does this
is by calling setContentView() and passing it the ID of the
XML layout that describes the View elements and how they nest; recall
that a Layout is also a View, just one that knows how to position
its child Views.
So you typically see Activity code like:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}
R.layout.activity_main needs some explaining (which I didn't do in
class). All resource files in an Android Studio project and all the
XML elements that specify the android:id attribute get an integer ID
by which the objects created from them can be retrieved via
findViewByID() function. These IDs are gathered in an automatically
generated file called R.java, and include members called R.layout.*
for every layout file, R.id.* for every XML element with an ID,
R.string.* for strings in res/values/string.xml and suchlike,
R.drawable.* from any images, R.raw.* for binary files like sounds,
and so on. You can look for these R.java files in the terminal.
Android Studio keeps track of these and will warn you if you try to
use an R.* name that has not actually been generated.
================[ Intents ]================
So what kicks off the onCreate() function for an Activity and causes
all of the above? Normally, it's an Intent sent by the Launcher app
when you click on your app's icon in the Launcher drawer or in
a special place on the Launcher screen like the "hot bar" that shows
up to 5 app icons (on my phone; your Launcher may be configured
differently).
There are two kinds of Intents: those that explicitly name the Java
class to be launched, and those that don't, but rather specify the
Action, the Type, and the Category of the intent. By these three
pieces of data, the Intent is _resolved_, i.e., all activities of all
the apps on the system that specified intent-filters in their
manifests are searched for a match.
Implicit intents can launch powerful actions such as taking pictures
with the camera, calling phones, sending SMSes, setting alarms, etc.
See https://developer.android.com/guide/components/intents-common.html
for a long list.
If several activities match, a Chooser dialog can be displayed; this
is what you see when you are asked which app you'd like to use to
complete an action. This happens, e.g., when you have several browsers
installed and registered for a "web view" action, and some app wants
to display a web page in a browser view, with the ACTION_VIEW (in
full, "android.intent.action.VIEW"). That app's intent would then
match all of your browsers, and bringing up a Chooser dialog would
make sense. The same happens when you have both the Dialer and Skype
apps that can make phone calls, and the action is ACTION_CALL (in
full, "android.intent.action.CALL").
Details: https://developer.android.com/training/basics/intents/sending.html
https://developer.android.com/training/sharing/index.html
https://developer.android.com/training/sharing/send.html --sending
https://developer.android.com/training/sharing/receive.html --receiving
https://developer.android.com/guide/components/intents-filters.html --in depth
The Intent sent to start apps from the Launcher is _implicit_: its
action is set to "android.intent.action.MAIN", and the category is
"android.intent.category.LAUNCHER":
================[ Connecting callbacks ]================
Callbacks of Views can be connected to the Activity that controls
those Views either via XML (if the Views are created from an
XML description such as a layout file) or from Java code at
runtime. The effect is the same: the callback gets connected
to a method inside the Activity class.
Connecting a Button in XML to a function onButtonClick()
[the name of the function can be anything you like; even
youPressedOnMeOuch() or some such].
What happens when the class given in the layout's context attribute
has no onButtonClick() function? [Hint: use the LogCat tab to see.]
Connection from Java is a bit more complex. Suppose there's no
last line in the above XML. Then clicking on the Button will do
nothing, or maybe a little First, you need to
get the View object that was built from the XML for the Button.
How to find it? The classic way is,
Button b2 = (Button) findViewById(R.id.button2);
b2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
onButtonClick(view);
}
}); */
Here R.id.button2 is the auto-generated integer ID in the
auto-generated class "R" ("r" for resource), made from the
XML's android:id="@+id/button2" attribute. @+ means that
the ID is new and should be added to R. This is in contrast
to Android's standard built-in resources that are called
names like "@android/id:content" and android.R.id.content
respectively. Incidentally, this ID corresponds to your
app's root ViewGroup, under which the rest of your view
tree goes.
There's another, newer way:
https://medium.com/google-developers/no-more-findviewbyid-457457644885
Note that Android Studio will show you this code folded, as
b2.setOnClickListener( (view) -> { onButtonClick(view); } );
which is much shorter. In fact, Kotlin's way of defining this
callback is just this, even without the semicolons!
Only one way of connecting up the View to the Activity is needed.
On Monday we'll see what else can break :)