Android 4 - Apps
Mortagage Calculator
This the the app we're going to make. It's one of the simplest apps but a little bit more complicated that "hello" app.
In this chapter, we'll create a subclass of Android's Activity class to tell what should happen when our app starts. The class also defines the logic of our Mortgage Calculator. For GUI, we'll create EditText (text field), TextView, and a SeekBar (slider).
The followings are the specifications for our new project:
- Build Target: 2.3.3
- Application name: Quick Mortgage Calculator
- Package name: com.bogotobogo.qmortgagecalc
- Min SDK Version: 10
To range UI components, TableLayout is used, and to create the rows TableRow objects are used. The number of columns in the TableLayout is decided by the TableRow that contains the most components, and each row's height is decided by the tallest component of that row.
So, we need to replace the default layout, main.xml with a new one that uses TableLayout. It places components relative to one another.
After deleting the res/layout/main.xml, right click the layout folder and select New > Other.... Then, in the New dialog, from the Android node, select Android XML File, and then hit Next to open the New Android XML File dialog.
Type in main.xml for the file name and choose TableLayout.
Open the Properties window by selecting the TableLayout from the Outline window.
Then, we want to set:
- Background: #FF
- id: @+id/tableLayout
- Padding: 6dp
- Stretch columns: 1,2,3
The Stretch columns indicates that columns 1,2, and 3 should stretch horizontally to fill the layout's width while column 0 will be as wide as its widest element.
Next, we're going to add 4 rows to the TableLayout using Outline window. Right click TableLayout and select Add Row:
Repeating this 3 more times, and then rename the Ids to start from index 0 by selecting Edit Id... from the right click on each item.
Then, we need to set the Layout width propertie of the TableRows to match_parent. This makes the rows to fill the width of the layout.
We'll add TextViews and EditTexts to each row. For the first row (row 0), drag a TextView from the Palette's Form Widgets. Then, drag a EditText from the Text Fields. Do the similar tasks to the other rows, then set the id with Edit ID.... To set the Text, we use Edit Text.... After setting all, we should have something like this:
The layout we got:
As we can see from the Layout, there are couple of issues that should be addressed:
- The loanEditText and customSeekBar are not spanned across te columns.
- The texts of the TextViews are not easily readable.
- The 10yr, 15yr, and 30yr are in the wrong columns.
- The texts are left aligned.
When we select multiple components, the Properties window shows us only the common items. This is the way we set the color for all the TextView to black (#000).
As we discussed in the section 2.4, the year text are all off by one column. Because we cannot specify the column using the Properties window, we do it directly on XML.
<TextView android:id="@+id/tenTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/ten_year" android:layout_column = "1" android:textColor="#000"/> <TextView android:id="@+id/fifteenTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/fifteen_year" android:layout_column = "2" android:textColor="#000"/> <TextView android:id="@+id/thirtyTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/thirty_year" android:layout_column = "3" android:textColor="#000"/>
Once we manually ad the layout_column to the XML, it now appears in the Properties window under the Misc section:
This is what we got so far:
Note that we switched raw input from "0.0" to string zero with value of "0.00".
- Let's place the texts in the TableRow2 at the center. To do that we neet to set the gravity in the Properties window to center.
- Next turn is for the EditTexts in the TableRow3. Make the Text size as 14sp
- Let's make the EditText of the TableRow0 to span across the column. We need to add the following line to the main.xml:
android:layout_span = "3"
For our SeekBar, it is:android:layout_span = "2"
- Set TextViews (Loan Amount, Interest Rate, and Monthly Payment) can use 2 lines by setting it lines property, and by modifying the strings.xml as shown in the following example:
<string name="monthly_payment">Monthly\nPayment</string>
Also, we want to right-align those by setting Gravity to right, with Padding right to 5dp. - Re-positioning the component can be done using Gravity, Padding, or Layout_height.
Time to set the SeekBar which will be used to set the interest rate. Set the Max value to 2000(=20%), and Progress property to 500(=5%). Then, we want add Padding around it.
We do not want users to mess with results displayed such as Month Payments in the EditTexts of TableRow3. So, we want to disable 3 features: Focusable, LongClickable, and Cursor visible are set to false.
Importance of components of GUI is represented by Layout weight which determines how each component should be sized with respect to other components. In our application, all components have Layout weight value 1 except the TextView in the left column, which has default value of 0. This means when we have wider screen, other component would stretch to fill it while the TextViews would only occupy the space for the widest component of TextView. A component with a value of 3 will be stretched 3 times that of the component that has value of 1.
In this application, we will get Monthly Payments for 3 terms (10yr, 15yr, and 30yr) depending on the interest rate which we can set using the SeekBar.
Here is the list of packages we need to import:
import android.app.Activity; import android.os.Bundle; import android.widget.TextView; import android.widget.EditText; import android.widget.SeekBar; import android.text.Editable; import android.text.TextWatcher; import android.widget.SeekBar.OnSeekBarChangeListener;
Summary for each packages is the following:
- The Activity class provides us the basic lifecycle methods of our application.
- The Bundle class from android.os gives state information of an app. As an example, when the user launches another app, our app that was running can save its state when it's sent to the background by the Android OS.
- The package android.widget is needed for EditText, TextView, and SeekBar.
- We can implement TextWatcher to respond to the event from EditText.
- When the user moves SeekBar's thumb, we can respond to it by implementing
SeekBar.OnSeekBarChangeListener interface.
The ADT Plugin generated the QuickMortgageCalculator class as a subclass of Activity
when we created QuickMortgageCalculator project:
public class QuickMortgageCalculator extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
We implement two lifecycle methods in our app:
- onCreate()
This is called by OS when an Activity is starting, in other words, when its GUI is about to be displayed. So, the user can interact with the Activity. - onSaveInstanceState()
This is called by OS when the configuration of the device changes (like rotating the device) during the execution of the app. This method can be used to save state information that we want to restore when our app's onCreate() method is called as part of the configuration change.
Here is QuickMortgageCalculator.java.
package com.bogotobogo.qmortgagecalc; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; import android.widget.EditText; import android.widget.SeekBar; import android.text.Editable; import android.text.TextWatcher; import android.widget.SeekBar.OnSeekBarChangeListener; public class QuickMortgageCalculator extends Activity { /** Called when the activity is first created. */ // constants used when saving/restoring state private static final String LOAN_AMOUNT = "LOAN_AMOUNT"; private static final String CUSTOM_INTEREST_RATE = "CUSTOM_INTEREST_RATE"; private double currentLoanAmount ; // loan amount entered by the user private EditText loanEditText; // accepts user input for loan amount private double currentCustomRate; // interest rate % set with the SeekBar private TextView customRateTextView; // custom rate private EditText tenYearEditText; // 10 yr monthly private EditText fifteenYearEditText; // 15 yr monthly private EditText thirtyYearEditText; // 30 yr monthly // Called when the activity is first created. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // call superclass's version setContentView(R.layout.main); // inflate the GUI // check if app just started or is being restored from memory if ( savedInstanceState == null ) // the app just started running { currentLoanAmount = 0.0; // initialize the loan amount to zero currentCustomRate = 5.00; // initialize the custom rate to 5.00% } // end if else // app is being restored from memory, not executed from scratch { // initialize the loan amount to saved amount currentLoanAmount = savedInstanceState.getDouble(LOAN_AMOUNT); // initialize the custom rate to saved interest rate currentCustomRate = savedInstanceState.getDouble(CUSTOM_INTEREST_RATE); } // end else // get the TextView displaying the custom interest rate customRateTextView = (TextView) findViewById(R.id.customRateTextView); // get the loanEditText loanEditText = (EditText) findViewById(R.id.loanEditText); // loanEditTextWatcher handles loanEditText's onTextChanged event loanEditText.addTextChangedListener(loanEditTextWatcher); // get the SeekBar used to set the custom interest rate SeekBar customSeekBar = (SeekBar) findViewById(R.id.customSeekBar); customSeekBar.setOnSeekBarChangeListener(customSeekBarListener); // get references to the 10yr, 15yr and 30yr EditTexts tenYearEditText = (EditText) findViewById(R.id.tenYearEditText); fifteenYearEditText = (EditText) findViewById(R.id.fifteenYearEditText); thirtyYearEditText = (EditText) findViewById(R.id.thirtyYearEditText); } // calculate monthly payment private double formula(double loan, double rate, int term) { double c = rate/100/12.; double n = term *12 ; return loan * (c * Math.pow(1 + c, n )) / ( Math.pow(1 + c,n)-1); } // updates 10, 15 and 30 yr EditTexts private void updateMonthlyPayment() { // calculate monthly payment double tenYearMonthly = formula(currentLoanAmount,currentCustomRate, 10 ); double fifteenYearMonthly = formula(currentLoanAmount,currentCustomRate, 20 ); double thirtyYearMonthly = formula(currentLoanAmount,currentCustomRate, 30 ); // 10, 15 and 30 yr monthly payment EditTexts tenYearEditText.setText("$" + String.format("%.0f", tenYearMonthly)); fifteenYearEditText.setText("$" + String.format("%.0f", fifteenYearMonthly)); thirtyYearEditText.setText("$" + String.format("%.0f", thirtyYearMonthly)); } // updates the custom rate and monthly payment EditTexts private void updateCustomRate() { // set customRateTextView's text to match the position of the SeekBar customRateTextView.setText(String.format("%.02f", currentCustomRate) + "%"); updateMonthlyPayment(); // update the 10, 15 and 30 yr EditTexts } // save values of loanEditText and customSeekBar @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putDouble(LOAN_AMOUNT, currentLoanAmount); outState.putDouble(CUSTOM_INTEREST_RATE, currentCustomRate); } // called when the user changes the position of SeekBar private OnSeekBarChangeListener customSeekBarListener = new OnSeekBarChangeListener() { // update currentCustomRate, then call updateCustomRate public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // get currentCustomRate from the position of the SeekBar's thumb currentCustomRate = seekBar.getProgress() / 100.0; // update EditTexts for custom rate and monthly updateCustomRate(); } public void onStartTrackingTouch(SeekBar seekBar) {} public void onStopTrackingTouch(SeekBar seekBar) {} }; // event-handling object that responds to loanEditText's events private TextWatcher loanEditTextWatcher = new TextWatcher() { // called when the user enters a number public void onTextChanged(CharSequence s, int start, int before, int count) { // convert loanEditText's text to a double try { currentLoanAmount = Double.parseDouble(s.toString()); } catch (NumberFormatException e) { currentLoanAmount = 0.0; // default if an exception occurs } // update the Monthly Payment updateMonthlyPayment(); // update the 10, 15 and 30 yr EditTexts } public void afterTextChanged(Editable s) {} public void beforeTextChanged(CharSequence s, int start, int count, int after) {} }; } // end
main.xml
<?xml version="1.0" encoding="utf-8"?> <TableLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/tableLayout" android:padding="6dp" android:stretchColumns="1,2,3" android:background="#FFF"> <TableRow android:id="@+id/tableRow0" android:layout_width="fill_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/loanTextView" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="right|center_vertical" android:paddingBottom="5dp" android:paddingRight="5dp" android:text="@string/loan_amount" android:textColor="#000" android:lines="2" android:layout_weight="0"/> <EditText android:id="@+id/loanEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_span = "3" android:inputType="numberDecimal" android:hint="@string/zero" android:textSize="14sp" android:layout_weight="1"/> </TableRow> <TableRow android:id="@+id/tableRow1" android:layout_width="fill_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/customTextView" android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/rate" android:textColor="#000" android:lines="2" android:paddingRight="5dp" android:gravity="right|center_vertical" android:layout_weight="0"/> <SeekBar android:id="@+id/customSeekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_span="2" android:max="2000" android:paddingBottom="5dp" android:paddingLeft="5dp" android:paddingRight="5dp" android:paddingTop="5dp" android:progress="500" android:layout_weight="1"/> <TextView android:id="@+id/customRateTextView" android:layout_width="wrap_content" android:layout_height="match_parent" android:text="@string/five_percent" android:textColor="#000" android:gravity="center_vertical" android:layout_weight="1"/> </TableRow> <TableRow android:id="@+id/tableRow2" android:layout_width="fill_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/tenTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/ten_year" android:layout_column = "1" android:textColor="#000" android:gravity="center" android:layout_weight="1"/> <TextView android:id="@+id/fifteenTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/fifteen_year" android:layout_column = "2" android:textColor="#000" android:gravity="center" android:layout_weight="1"/> <TextView android:id="@+id/thirtyTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/thirty_year" android:layout_column = "3" android:textColor="#000" android:gravity="center" android:layout_weight="1"/> </TableRow> <TableRow android:id="@+id/tableRow3" android:layout_width="fill_parent" android:layout_height="wrap_content" > <TextView android:id="@+id/momthlyTextView" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="right|center_vertical" android:lines="2" android:paddingRight="5dp" android:text="@string/monthly_payment" android:textColor="#000" android:layout_weight="0"/> <EditText android:id="@+id/tenYearEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="@string/zero" android:inputType="numberDecimal" android:textSize="14sp" android:focusable="false" android:cursorVisible="false" android:longClickable="false" android:layout_weight="1"/> <EditText android:id="@+id/fifteenYearEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="numberDecimal" android:hint="@string/zero" android:textSize="14sp" android:focusable="false" android:cursorVisible="false" android:longClickable="false" android:layout_weight="1"/> <EditText android:id="@+id/thirtyYearEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="numberDecimal" android:hint="@string/zero" android:textSize="14sp" android:focusable="false" android:cursorVisible="false" android:longClickable="false" android:layout_weight="1"/> </TableRow> </TableLayout>
strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, QuickMortgageCalculator!</string> <string name="app_name">Quick Mortgage Calculator</string> <string name="loan_amount">Loan\nAmount</string> <string name="rate">Interest\nRate</string> <string name="ten_year">10 yr</string> <string name="fifteen_year">15 yr</string> <string name="thirty_year">30 yr</string> <string name="five_percent">5%</string> <string name="monthly_payment">Monthly\nPayment</string> <string name="term">Term</string> <string name="zero">0.00</string> </resources>
The onCreate() method is called by the OS when an Activity is started. In general, this is the place we initializes the instance variables of Activity as well as UI components. However, because OS will give us ANR(Application Not Responding) if the app takes longer that five seconds to load,we need to make the app load quickly by doing background work for time-consuming initialization instead of doing it within onCreate().
When the user changes device's configuration (such as rotation) while app is running, the user still expects our app continues to run seamlessly. When the system calls onCreate, it passes a Bundle to savedInstanceState:
@Override public void onCreate(Bundle savedInstanceState) { ...
savedInstanceState contains the activity's saved state by the onSaveInstanceState() method. Note that we call the super class's onCreate() method when we are overriding any Activity method.
setContentView inflates the activity's GUI by loading and parsing R.layout.main.
An app can have two distinct states:
- savedInstanceState == null
The app just started running. - savedInstanceState != null
The app is being restored, and calls the savedInstanceState object's getDouble() method to get the saved value.
Once the layout is inflated, we can get references to the widgets. We do it by calling findViewById() method, which is one of the methods of Activity. The name of each UI component's constant in the R.id is determined by the android:id attribute in the layout file, main.xml.
customRateTextView = (TextView) findViewById(R.id.customRateTextView); ...
The following lines of code override class Activity's onSaveInstanceState() method.
// save values of loanEditText and customSeekBar @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putDouble(LOAN_AMOUNT, currentLoanAmount); outState.putDouble(CUSTOM_INTEREST_RATE, currentCustomRate); }
The onSaveInstanceState() is called by OS when the configuration of the device changes. In the method, we first call the superclass's onSaveInstanceState() method, and then we store key-value pairs in the Bundle that was passed to the method. The saved values are using to restore the app state from onCreate() method.
Now, let look at the method OnSeekBarChangeListener(). It creates the anonymous inner-class object customSeekBarListener which respondes to customSeekBar's events. In onCreate() method, we registered customSeekBarListener as customSeekBar's event handler:
customSeekBar.setOnSeekBarChangeListener(customSeekBarListener);
We override onProgressChanged() method in the inner class.
// called when the user changes the position of SeekBar private OnSeekBarChangeListener customSeekBarListener = new OnSeekBarChangeListener() { // update currentCustomRate, then call updateCustomRate public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { ....
To reflect the changes in EditText, we defined an anonymous inner class that implements the TextWatcher interface. In that way, when the user changes the loan amount, we can calculate new monthly payments.
Files used in this app, QuickMortgageCalculator.zip
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization