<< Go Back To MyRuns Document

User Interface Implementation

This section provides an overview (read hints without details) of the design and implementation of the user interfaces.

Main Activity

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.

Start Fragment

Main Interface

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.

ManualInputActivity

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 .

MapDisplayActivity 

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.

History Fragment

Main Interface

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.

DisplayEntryActivity

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.

Settings Fragment

Main Interface

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.

Profile Activity

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.