This section provides an overview (read hints without details) of the design
and implementation of the user interfaces.
The main activity is a navigation interface to access different interfaces by clicking different tabs on the top of the screen. The tabs use an TabLayout to add multiple Tabs to one activity. Each Tab is associated with a class inherited from the Fragment class. When a tab is clicked, the corresponding interface will be rendered below the TabLayout.
The start tab allows the user to enter exercise information manually as discussed earlier. The root layout could be LinearLayout. You can also use other layout types if you wish. You will need spinner widgets for the layout to create a drop-down list for both the input type and activity type options.
You
also need to implement the setOnClickListener for the
"Start" and "Sync" buttons. Once the "Start"
button is clicked, the app should fire the different activities according to
the "Input Type". If the input type is "Manual Entry", then
the ManualInputActivity allows the user to enter
exercise stats; when "GPS" or "Automatic" is selected, the MapDisplayActivity should be shown to the user. The
interfaces for both activities are discussed in Start
Tab section. The "Activity Type" should be passed to the new
activities by putting the value to the intent’s extras. Activity types may
include Running, Walking, Standing , Cycling, Hiking,
Downhill Skiing, Cross-Country Skiing, Snowboarding, Skating, Swimming,
Mountain Biking, Wheelchair, Elliptical and Other.
The
user enters exercise information using dialogs driven by the ManualInputActivity . Therefore, you need to implement all
the related dialogs in a DialogFragment , say MyRunsDialogFragment , or implement a DialogFragment
for each dialog. After the user is done with inputting information using
dialogs, the ManualInputActivity will store the input
temporarily in an ExerciseEntry object. When the user clicks the
"Save" button, the app saves the temporary data associated with the
exercise stats in the database – so an insert in the database is going to occur
when an exercise object representing all the information associated with a
manually input single exercise is inserted as a new row in to the database.
That was a torturous sentence class ;-) In dialog fragment's onCreateDialog()
,
you should set which activity's method should be called when the user clicks
the OK button.
Please read Android API documents for details on DialogFragment .
The MapDisplayActivity layout has three part: map view, status and buttons. In order to overlay the status on the map, you can use FrameLayout and put both mapfragment and a LinearLayout which contains the textviews needed to show the status. Buttons can be put in another LinearLayout . The following layout skeleton shows such design.
<FrameLayout>
<fragment
android:id="@+id/map"
class="com.google.android.gms.maps.MapFragment" >
</fragment>
<LinearLayout>
<TextView
android:id="@+id/type_stats"
android:text=" " />
</LinearLayout>
<LinearLayout
android: layout_gravity="bottom" >
<Button
android:id="@+id/btnSave"
android:onClick="onSaveClicked" />
<Button
android:id="@+id/btnCancel"
android:onClick="onCancelClicked"
android:text="@string/ui_button_cancel_title" />
</LinearLayout>
</FrameLayout>
In order to show the map properly, you need to get a Google MAP API key for your app. You can check Android developers to find out how to do that.
The location trace needs to be updated once a location update is available and processed. You need to create a starting marker marking the start position, ending marker marking your current location and al line showing your trace. The starting marker needs to be place at the first location coordinate. Draw a single polyline along all the collected location coordinate. Place the ending marker at the last location coordinate.
Exercise status include activity type, average speed, current speed, climb, calories and distance. Please refer to the demo apk for details.
MapDisplay has two modes: displaying history entry and
showing the live location trace. To display a history entry, you need to
figure out a way to retrieve the data from the database. To show the live
location trace, you need to receive an update notification from the TrackingService and update the map accordingly. Please
refer to History
Fragment and Service
Implementation for more details.
The
History Fragment loads all exercise entries from the database then
displays the
entries as a ListView . As mentioned in class, a ListView uses an
adapter to show data. You can implement an custom adapter class, for
example, ActivityEntriesAdapter ,
which extends ArrayAdapter < ExerciseEntry >
that exposes data from an array to the widget. You need to implement
how the ListView displays each record. This can be done by using
the override of the getView()
method in ActivityEntriesAdapter.
There should be two rows for each record. The format of the first row is as
follows: <Activity Type> <Date>. The second row's format is:
<Distance> <Duration>. One such example is showing in User
Interface Walk-through (middle picture). You need to handle user's unit
preference as well. You need to show the distance in user's selected unit
(metric or imperial units, see Settings
Fragment)
ListView’s onListItemClick should be implemented when the user clicks on history entries. When the selected entry’s input type is manual entry, the app opens DisplayEntryActivity to show the details. Otherwise it opens MapDisplayActivity to show the trace along with the status. You need to pass exercise entry’s unique database id to next activity, so that they can retrieve the entry from the database.
History
list also needs to respond to history entry updates. When the user delete an
entry through the web interface (see later), the cloud will send a message to
the app and the app needs to update the database. When the user is viewing a
history fragment, the app should be able to update the view to reflect any
changes.
The DisplayEntryActivity has two jobs: 1) retrieve and display all the columns of a specific exercise entry to the list of TextView ; and 2) setup a button click listener to delete the selected entry (see database section).
The user can view a summarized list of all exercises using the tab history fragment. If the user clicks on one of the summaries on the list view then detailed information associated with the single exercise is displayed by DisplayEntryActivity.
Importantly, the user can delete the whole exercise entry by clicking on the
"DELETE" button.
The
Settings Fragment is quite different from the Start Fragment and History
Fragment classes. Because the Settings Fragment sets up various application
preferences settings, it inherits from PreferenceFragment .
We only need to call addPreferencesFromResource in
the onCreate()
method to load the preferences UI from an XML resource.
This XML file is called preference.xml in directory: res/xml. The preference UI
is displayed when the "settings" Tab is clicked.
Most of the elements in the Settings Fragment are common widgets, e.g., CheckBoxPreference , ListPreference . There are two other elements in the design: user profile and class homepage. When clicking these two elements a new activity should pop-up. This is achieved using the PreferenceScreen XML tag – see PreferenceFragment for more details.
Clicking on class homepage should invoke and open a browser and go to our class webpage. This is a nice example of one of your components using other apps on your phone; that is the browser.
The UI in Android is controlled by an XML file in the res/layout folder; in this case you need to specify the layout, for example, in the profile.xml. The profile.xml layout should consist of a linear hierarchical of widgets and layouts. Android provides a “drag and drop” method as part of the graphical layout – so you can either directly edit the xml file or use the graphical tool to design your layout, or both: you can easily switch between both modes.
The layout should be a ScrollView element. Inside the ScrollView use a vertical LinearLayout . You need to program all elements contained the layout. The titles for all elements (e.g. Name, Email, Phone etc.) are TextView widgets. The editable boxes are EditText widgets. And the buttons for gender element are RadioButtons grouped as a RadioGroup . You will assign IDs to your widgets in the XML code, which you will refer later in your Kotlin code. It’s not necessary to give every item an ID. For example the TextViews item, you won’t use them in the future, so their ID fields are not important. The grey hints in each EditText are specified by property “ android :hint ”. Please set different keyboard layout for numerical and text input box using android :inputType property of EditText. By doing this, when you first interact with the text box, the keyboard layout is optimized for your type of input: for phone number field, the keyboard will be all numbers with larger buttons, etc. For email, the keyboard will be a Qwerty keyboard. Note that all properties specified in the XML file can also be specified and modified in the Kotlin code. But use XML as much as possible rather than programming the UI. If the appearance of the UI changes as the program executes, you will need to do some UI work in the Kotlin code.
In profile.xml, specify event handlers or callbacks for the two buttons. The callback logic for the save button captures the current information in the EditText and RadioButton elements and stores the user data; in the case of cancel button nothing is done other than to exit the activity.
You
should implement two private helper function methods in the activity to help
load user data that has already been saved - called loadProfile( )
- and one to
save the user data – called saveProifle()
. Consider saveProifle( )
:
this function saves the user input data using a SharedPreference
object. After calling the helper function the activity
simply displays some toast to the screen letting the user know that the data is
saved. Similarly, when the application is started (either for the first time or
restarted) the activity needs to load the user data using the loadProfile ( )
helper. Your helper function calls loadProfile( )
method in onCreate()
and uses
the same SharedPreference object to load the data and
display it to the screen. Think about the edge case the first time that the app
runs when no previous data is saved. You will need to make sure some default
data (e.g., empty string elements) are displayed.
You
will need to add a ImageView
for displaying the photo and a button to trigger a Dialog, which will ask the
user to either use camera or existing photos as shown above (central image).
Also, you need to handle screen rotations.
Recall in the class, a screen rotation will trigger onCreate( )
, which will
remove the unsaved pictures in this case. You can utilize onSaveInstanceState( )
to
save the location of the temporary profile picture and reload it in onCreate()
.
Note, the triggered dialog invokes MyRunsDialogFragment that inherits DialogFragment. We will reuse the MyRunsDialogFragment class in future labs to handle all customized dialog boxes for the MyRuns app. In MyRunsDialogFragment, you can differentiate various dialog fragments by supplying distinctive dialog IDs in the onCreateDialog method.
Setting the profile image is challenging. When user clicks the "Change"
change button, the activity shows a dialog asking if the user wants to take a
picture or select one from the gallery as discussed above. Based on the users
input the activity starts the camera or galley apps. You need to use StartActivityForResult()
to get results back from the camera or galley app. Consider the following workflow: start an activity to get
an image (either by camera or gallery), the activity returns the
results to the
profile activity, which displays it in the image view. You can use MediaStore.ACTION_IMAGE_CAPTURE
to open the camera, Intent.ACTION_PICK to open the gallery.