Android Programming By An Example: Creating An Airport Schedule Simulator Application





5.00/5 (21 votes)
In this article, we will discuss about the advanced Android application development based on the example of creating a responsive Airport schedule simulator application.
Introduction
In this article, we will demonstrate how to use Android Studio and Java programming language to create a sample Android application implementing the functionality of the advanced responsive user interface from "scratch". The app discussed in this article will implement the functionality of airport flights schedule simulation. During the development lifecycle, we will implement an Android app's responsive user interface used to render lists of either 'arrivals' and 'departures' flights, as well as provide the functionality for dynamically generating and updating the information about flights in the real-time mode.
We will make a large emphasis on several Java language programming aspects, as well as delve into the number of programming techniques that allow us to deliver an advanced Android app, including the aspects of creating a responsive app's drawer and navigation bar app from the very beginning, delivering our own custom views and layouts such as a custom search view bar with action button, overriding the default functionality of generic app's action bar, maintaining the tabbed layout, rendering recycler views that unlike listviews or gridviews allow to create a custom look for items in the lists of data being rendered by the application, creating various layouts with multiple nested fragments, using bottom navigation view, etc.
Besides the app's interface-specific topics, we will also find out how to create an efficient code written in Java to implement the functionality that generates and manipulates the data contents, as well as how to provide the interaction between the code that manipulates the data and the app's user interface.
Specifically, we will implement the functionality of airport flights schedule simulator that generates a dataset of random flights and manipulates these data by simulating the flights arrival and departure time-line by filtering out flights in the real-time mode, dynamically updating the list of flights being rendered. For that purpose, we will use and discuss such topics as using Android app's background tasks, using timers, etc.
Background
Prerequisites (Before We Begin…)
Before we begin the discussion, let’s spend a moment and take a closer look at what development tools and libraries we particularly need so far to build and run our first Android app.
Since, we’re about to use Java programming language to deploy our first application running Android, we must have Java SE installed. For that purpose, we need to download and install Java Standard Edition – SE platform from http://www.java.com/. In turn, Java SE platform contains all libraries and modules required to build and run the code written in Java on your PC.
After we’ve successfully installed Java SE platform, we also need to properly install an IDE and specific libraries needed to create an Android app project and build the code running our application being deployed. There’s the number of various IDEs, programming languages and libraries, such as either Microsoft Visual Studio / C#.NET Xamarin or Android Studio empowered by Android development community, that can be effectively used to create and deploy Android apps.
In this article, to provide an efficiency of the Android apps development lifecycle, platform compatibility, as well as to slipstream the development process, we will particularly use Android Studio and Java programming language for that purpose.
That’s actually why it’s required and highly recommended to download and install Android Studio (https://developer.android.com/studio/) at the development machine after we’ve installed Java SE platform during the previous configuration step.
As we might have already noticed, Android Studio, being installed, consists of the number of development tools including IDE, Java SDK and NDK libraries, Android system emulators, Gradle/Maven – Java compiler’s “make
” utility that makes it easier to compile and link codes written in Java programming language.
In turn, Android Studio’s IDE is an efficient and responsive tool used to easily create and edit Android apps’ resources and Java codes implementing the basic app’s functionality.
Besides an efficient and convenient IDE, Android Studio bundle also includes Java SDK libraries required to develop Android app for various targets (phones, tablets, wearables, Android TV,...). Specifically, Android Studio IDE allows to download and install SDKs for the variety of Android system releases via SDK manager, which is a part of Android Studio, or, optionally, by regularly using native SDK manager from Java SDK distribution.
For compiling and linking an app being created, Android Studio’s bundle also includes Gradle/Maven ‘make
’ utility mentioned above. While creating our first Android app project in Android Studio, Gradle component is downloaded and configured to be used along with Android Studio’s IDE. Every time, when we’re building and running an Android app’s project, Gradle utility is performing the compilation- and linking-specific tasks, such as creating an apk-package
containing the built Android app, ready to be run on either the emulator or an Android device. During the development lifecycle, since a project has been created and configured, we can use multiple versions of Gradle utility, the way as it was discussed in the project creating sections of this article.
To make it possible to run an app during the debugging development phase, Android Studio also includes an Android device emulator supporting various Android system releases, downloadable via Android Studio’s emulators manager, from Google and Android development community website. Running an app on the emulator is much similar to running it on a target Android device.
In the next section of this article, we will demonstrate how to create our first Android app project in the Android Studio’s environment being installed.
Creating Your First Android App Project
The first thing that we have to do after we’ve successfully met all installation and configuration requirements, discussed above, is to run Android Studio and create a project that will implement our airport flight schedule simulation Android app functionality. To do this, we will use Android Studio main dialog by toggling Start a new Android Studio project option:
After this, the Android project creating dialog will appear on screen:
Here, in this dialog, we must specify an application name (in this case, it’s `AirportApp
`), company domain (for example, `epsilon.com
`) to properly configure application package, project location, and, particularly, the package name, which, in our case, is `com.epsilon.airportapp
`. After we’ve provided all information needed to create a project, click on Next button located at the bottom of this dialog.
After this step, we must properly select and specify our application’s targeting devices, including the proper form factors (either `phone ` or `tablet `), minimal SDK and its version, as well as Android system release version:
After we've successfully selected target device and Android release version, for which the following application will be deployed, we also must select a type of app's activity. An activity is normally a Java-class implementing functionality responsible for app's main window creation, events handling as well as accomplishing other user interaction-specific tasks. In fact, a Java-class extending the generic Activity
class, or other derived classes, is the main class for any existing Android apps:
In this particular case, we start our first Android app development lifecycle with selecting an empty activity as the main activity for our Airport schedule simulator app. Further, we will customize and enhance the default empty activity to provide functionality needed to perform airport schedule simulation tasks.
The final step in Android app creating phase is to configure an activity-based Java class alias, generate a specific activity layout, as well as to configure app's backward compatibility libraries. To do this, we must proceed with the next configuration dialog:
During the final step, we must specify an app's activity-based Java-class name that will correspond to the specific activity layout xml-file name being generated. Also, we must specify whether we want to provide the app's backwards compatibility with the older Android releases.
Since we've configured the app's activity, during the final phase, the specific project is being generated and the Android Studio's IDE main window is opened:
In the next section of this article, we'll take a short glance at the Android app's project structure created with Android Studio.
Android App's Project Structure
At this point, let's take a closer look at the app's solution tree located at the upper left corner of the Android Studio's IDE main window opened after the app's project has been created. Normally, the solution tree displays the contents of the project being created that exactly corresponds its directory structure saved to a specific location (for example, 'D:\AirportApp').
AndroidManifest.xml
The folder 'manifests' is the first folder that appears at the top of app's solution tree. It basically includes only one file 'AndroidManifest.xml'. The following file primarily contains all configuration data provided in XML format needed to run the application being created. AndroidManifest.xml file has the following structure, that is exactly the same for all Android apps:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.epsilon.arthurvratz.airportapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme.NoActionBar">
<activity android:name=".AirportActivity"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
The second line of AndroidManifest.xml file contains manifest
tag, the attributes of which provide the namespace and app's package name information. It contains also a nested tag application
having the number of attributes that define label, text orientation and a pair of icons for the app created. The icons and label, specified by the attributes of application tag are basically displayed in the app's main window. Also, the application tag contains an attribute that defines the default app's theme (for example, android:theme="@style/AppTheme"
). Optionally, we might want to modify existing or add more attributes to the application tag, in order to provide a custom look and behavior of the app's main window. For example, we might want to change the value android:theme
attribute so that our app will override the default generic and use its own implementation of the app's action bar. For that purpose, we need to change the value of the following tag to android:theme="@style/AppTheme.NoActionBar"
.
Normally, the application
tag has the number of nested tags such as the activity
tag, used to provide a set of configuration attributes of the main app's activity. By default, the activity
tag has only one attribute that defines the name of the main app's activity (e.g. android:name=".AirportActivity"
). To modify the app's main activity configuration parameters, we might have a need to add more attributes to the following tag:
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden">
In this particular case, we've added the following configuration attributes to our airport schedule simulator app main activity
tag listed above. The first attribute is a duplicate of the attribute we've previously specified in the application
tag above. The following attribute is used to specify that there's no default generic app's actionbar in the running app will be displayed. The second attribute android:windowSoftInputMode="stateHidden"
is used to specify a soft input method will not be automatically rendered when the app is launched. The last attribute android:configChanges="orientation|screenSize|keyboard|keyboardHidden"
provides the list of configuration changes overridden by the app. It means that the following changes will be handled by the app, rather than the Android system. Specifically, the app will handle the screen rotation and render a proper interface layout variation depending on the current screen orientation (e.g., either 'portrait
' or 'landscape
').
The application tag also has the number of innermost nested tags such as intent-filter
, action
and category
. The action
and category
intent tags inside intent-filter
tag specify the main application entry-point. Particularly, these tags specify that the current '.AirportActivity'
is the main app's activity:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
Gradle Scripts
Now, let's take a look at the 'Gradle Scripts' sibling located at the bottom of our app's solution tree. The following folder contains all script files needed to configure gradle 'make
' utility mentioned in the previous section, including two instances of 'build.gradle
' files for either project 'AirportApp
' or the 'app
' module. The first build.gradle file has the following contents:
// Top-level build file where you can add configuration options
// common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
The following file is the non-XML file containing the basic configuration for Gradle repositories, including its build version (e.g., 'com.android.tools.build:gradle:3.1.3'
). During the project configuration, the contents of the following file typically remain unchanged.
However, there's a special interest in the second build.gradle file. The second build.gradle file basically contains the definition of the app's project modules dependencies. For example:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.epsilon.arthurvratz.airportapp"
minSdkVersion 24
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
implementation 'com.android.support:support-v4:28.0.0-alpha3'
implementation 'com.android.support:support-v13:28.0.0-alpha3'
implementation 'com.android.support:design:28.0.0-alpha3'
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'org.jetbrains:annotations-java5:15.0'
}
To be able to use Android Support Libraries such as v.4,v.7,v.13
as well as RecyclerView
and ConstraintLayout
, we must add the following lines to the dependencies
section of this file:
implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
implementation 'com.android.support:support-v4:28.0.0-alpha3'
implementation 'com.android.support:support-v13:28.0.0-alpha3'
implementation 'com.android.support:design:28.0.0-alpha3'
implementation 'com.android.support:recyclerview-v7:28.0.0-alpha3'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
In turn, both 'gradle-wrapper.properties' and 'local.properties' files are another special interest:
gradle-wrapper.properties
#Thu Jul 26 06:49:16 EEST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
local.properties
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Jul 26 15:02:12 EEST 2018
sdk.dir=C\:\\AndroidSDK
In these files, we can specify the either gradle utility version or the absolute path to the Android SDK location. To do this, we must modify the following lines of both these files:
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
sdk.dir=C\:\\Android\AndroidStudio\SDK
Notice: If you change the version of gradle to > gradle-4.6-all.zip, then you'll also need to disable 'Configure on demand' option in 'File' > 'Settings' > 'Build, Execution, Deployment' > 'Compiler'.
App's Activity And Layout File
After we've exactly conformed to all app's project configuration step, let's take a look at our future app's activity Java implementation file and the main app's layout xml-file. The main app's layout file is located under 'res/layout' folder and has the name of 'activity_airport.xml'. The following file initially contains the 'android.support.constraint.ConstraintLayout
' tag, which is the default layout for an empty app.
To modify the main app's layout and add our Android app's interface components such as other inline layouts or controls (i.e., 'views'), we must use the Android Studio's layout designer or manually edit the following layout file:
To be able to edit the layout in the Android Studio's designer, you must also modify 'styles.xml' that can be located at 'res/values' folder of the app's project:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Base.Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Specifically, you must change the value of 'parent
' attributes of the 'style
' tag from parent="Theme.AppCompat.Light.DarkActionBar"
to parent="Base.Theme.AppCompat.Light.DarkActionBar"
.
The following layout is the default empty app's layout which will be changed during the app's development lifecycle being discussed. Optionally, we can add changes to the app's layout contents by manually editing the 'activity_airport.xml' layout file:
<?xml version="1.0" encoding="utf-8"?>
<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=".AirportActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="19dp"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
Further, we'll provide the detailed guidelines on how to use constraint layouts to build a responsive app's interface in one of the succeeding sections of this article.
The final aspect that we're about to discuss at this point is the app's main activity implementation file 'com.epsilon.airportapp/AirportActivity.java':
package com.epsilon.airportapp;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class AirportActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_airport);
}
}
The following file is contained within the first 'com.epsilon.airportapp' folder and contains the declaration of the 'AirportActivity
' Java-class extending the generic 'AppCompatActivity
' class. Initially, the AirportActivity
class contains only one overridden method 'OnCreate
', implementing the functionality for rendering the app's layout as the main content view for the app being created. The following method implements the invocation of either 'onCreate
' method in super-class, or setContentView
method that accepts the main app's layout resource-id 'R.layout.activity_airport
' and provides the basic rendering functionality for the main app's layout. In the future, we will modify the 'AirportActivity
' class and add the required functionality to perform airport's flights schedule simulation.
The App's Main Layout Blueprint
At this point, our primary goal is to create a sketch of the airport schedule simulation app's main layout design. To be more specific, the main app's layout will have the following look:
As you can see from the figure above, the entire main airport app's layout consists of an advanced variant of 'SearchView'
at the topmost, 'TabLayout'
, in which two lists of either arrival and departure flight will be rendered. Each tab will render a 'RecyclerView'
to display lists of flights, 'BottomNavigationView
' that allows to navigate through the list of flights that will take place 'yesterday', 'now' and 'tomorrow'. The 'TabLayout
' and 'RecyclerView
' are rendered by specific fragment layouts that are displayed after toggling the app's drawers navigation menu items or selecting one of the specific tabs.
The main app's layout is mainly based on the 'DrawerLayout
' pattern, which means that the app's drawer will be rendered in case when a user's toggling the action bar button at the upper-left corner of the app's main window. The app's drawer regularly might contain the drawer's header based on 'NavigationView
', app's main menu, etc.
Beforehand, let's recall that this is not a standard app's layout generated by the project creation wizard. Further, we will discuss about how to implement the airport app's custom layout programmatically.
Designing the App's Main Layout
Now, we've finally maintained the airport app's main layout blueprint, now it's time to create and edit one or more app's layout files. The first file we're about to modify is 'activity_airport.xml'. Since our airport app is intended to have an app's drawer, we're choosing the 'DrawerLayout
' as the main app's layout type:
<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<android.support.v4.widget.DrawerLayout xmlns:
android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/airport_drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<include layout="@layout/content_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.design.widget.NavigationView
android:id="@+id/airport_navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="@menu/main_menu"
app:headerLayout="@layout/nav_header_frame"/>
</android.support.v4.widget.DrawerLayout>
In this case, we use the 'android.support.v4.widget.DrawerLayout
' as the root tag in our activity_airport.xml file. After this, we also need to create two nested tags such as either the 'include
' tag which will include the another portion of the following layout contained in a separate file 'content_frame.xml', or the 'android.support.design.widget.NavigationView
' tag, that declares the airport app's drawer layout. Unfortunately, since the drawer layout is used, we cannot modify the layout shown above by using Android Studio's layout designer, but we can manually edit this layout by using a Android Studio's IDE text editor.
The included fragment of the app's main layout is stored in content frame file and is looks like follows:
<?xml version="1.0" encoding="utf-8"?>
<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">
<LinearLayout
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/airport_fragment_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.02"
android:focusable="true"
android:focusableInTouchMode="true">
<requestFocus />
<android.support.v7.widget.SearchView
android:id="@+id/searchable"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<FrameLayout
android:id="@+id/airport_fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toTopOf="@+id/flights_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_bar">
</FrameLayout>
<android.support.design.widget.BottomNavigationView
android:id="@+id/flights_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/airport_fragment_container"
app:menu="@menu/flights_navigation"
android:theme="@style/AppTheme"/>
</android.support.constraint.ConstraintLayout>
In this file, we normally use 'android.support.constraint.ConstraintLayout
' tag as the root tag for this layout. The following tag has the number of inline tags, including 'LinearLayout
', in which 'android.support.v7.widget.SearchView
' tag is declared, 'FrameLayout
' that actually declares a frame that will be programmatically replaced with a specific fragment rendering 'RecyclerView
', displaying a list of flights, 'BottomNavigationView
' rendering options for filtering out flights by its time. Since we're using constraint layout as the root for the entire content frame, the all nested views and layouts must be properly constrained. Unlike the previous layout, content frame layout can be successfully edited with Android Studio's layout designer. That's actually why we're having an option whether to edit the specific content frame file or use the layout designer to provide the specific constraints to all views within the following layout.
In this case, the best way to interconnect the views in the content frame is to add specific attributes such as 'app:layout_constraintTop_toBottomOf
' to each view-tag as it's shown in the source code above. In this fragment of code, we're adding layout constraint attributes to each view-tag
vertically and horizontally starting at the uppermost 'LinearLayout
' view-tag
, to chain all of them in vertical orientation.
At this point, let's get back to the fragment of code that defines the drawer layout for our app. Another view declared inside of 'DrawerLayout
' tag is 'android.support.design.widget.NavigationView
'. The following view is basically used to render the app's drawer and its menu as it's shown on the blueprint figure above. The using of the navigation view normally requires that we create another app's drawer layout and specific menu declaring items for the app's drawer menu.
To create these layouts, we basically need to create a sub-folder in the '/res' folder of our project and create the specific menu layout resource file called 'main_menu.xml':
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<group android:checkableBehavior="single">
<item
android:id="@+id/flights"
android:icon="@drawable/ic_flight_black_24dp"
android:title="@string/flights" />
<item
android:id="@+id/about"
android:icon="@drawable/ic_star_black_24dp"
android:title="@string/about" />
</group>
</menu>
In this file, we must declare the 'menu
' tag and also create the 'group
' of items inside of it. In this case, the following layout contains a group of two items for each 'flights' or 'about' menu options, displayed in the app's drawer, below its header.
Another layout file 'nav_header_frame.xml' contains the layout rendered in the app's drawer when toggled by a user:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="240dp"
android:background="@drawable/airport_nav_header"
android:gravity="bottom"
android:orientation="vertical"
android:padding="16dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
android:id="@+id/imageView"
android:layout_width="103dp"
android:layout_height="99dp"
app:srcCompat="@mipmap/ic_launcher_round" />
<Space
android:layout_width="352dp"
android:layout_height="10dp" />
<TextView
android:id="@+id/airport_app_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="Verdana"
android:text="@string/nav_header"
android:textColor="@android:color/background_light"
android:textIsSelectable="false"
android:textSize="30sp" />
<TextView
android:id="@+id/airport_app_author"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/airport_app_author" />
</LinearLayout>
To create the app's drawer layout, we will use the specific 'LinearLayout
' tag. The linear layout, unlike other layouts such as 'CostraintLayout
' allows to position all views in the vertical orientation only, and does not require setting up any constraints between views.
To define the proper drawer's layout, we must place the following view tags to inside the linear layout, as well as to provide the background image the app's drawer header. To do this, we specify the following linear layouts attribute: 'android:background="@drawable/airport_nav_header"
'. Normally, our linear layout created will contain the following inline views:
- '
ImageView
' - is used to show the airport app's icon - '
Space
' - to create a gap between the specific views in the linear layout - '
TextView
' - to print either airport app's title or author's details
Finally, the app's drawer layout rendered by the 'NavigationView
' as well as its blueprint will have the following look:
In the next section of this article, we'll find out how to implement the functionality of the main airport app's activity.
Creating Custom SearchView With Action Button
SearchView
is the first control of the airport schedule simulator app that appears on the top of main app's window. At this point, let's get back to the fragment of '
content_frame.xml'
. The 'android.support.v7.widget.SearchView
' tag declaring the search view is located prior to all other views of the following layout file, wrapped up by the 'LinearLayout
:
<LinearLayout
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/airport_fragment_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.02"
android:focusable="true"
android:focusableInTouchMode="true">
<requestFocus />
<android.support.v7.widget.SearchView
android:id="@+id/searchable"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
We use the linear layout to ensure that the search view does not gain focus after the app has started.
Since we've declared the 'SearchView
' tag within the content frame, our goal, at this point, is to provide the functionality and behavior (e.g., make the search view responsive) by implementing the specific code in Java that will instantiate and handle events of our search view.
As we've probably know, in this project, we will not use the generic search view and the app's bar, but will create our own custom search view that combines the basic functionality of the generic search view and the app's action bar.
To create a custom search view with action button, we need to create a new java-class and name it as 'SearchableWithButtonView
' that extends the generic 'View
' class:
public class SearchableWithButtonView extends View {
// SearchView basic functionality implementation java-code goes here...
}
In this class, we need to implement the following methods. setupSearchableWithButton()
is the very first method that we need to implement to provide the specific look and behavior to our custom search view:
public void setupSearchableWithButton() {
// Set background color of the search view
((ViewGroup)m_SearchView.findViewById
(android.support.v7.appcompat.R.id.search_mag_icon).
getParent()).setBackgroundColor(Color.parseColor("#ffffff"));
// Set default custom search of the search view button icon
// and look of the custom search view
this.setDefaultSearchIcon(); this.setupIconifiedByDefault();
// Set default search hint displayed in the search view's edit text view
m_SearchView.setQueryHint("TYPE HERE...");
// Set default query text and remove focus from the search view
m_SearchView.setQuery("", false); getRootView().requestFocus();
// Instantiate the search view object and
// set default action button click event listener
m_SearchView.findViewById(android.support.v7.appcompat.R.id.search_mag_icon).
setOnClickListener(new SearchableViewListener());
// Instantiate the search view object
ViewGroup llSearchView = ((ViewGroup)m_SearchView.findViewById(
android.support.v7.appcompat.R.id.search_mag_icon).getParent());
// Instantiate object of the text editable inside the search view
EditText searchEditText = llSearchView.findViewById(
android.support.v7.appcompat.R.id.search_src_text);
// Remove the search view text editable default selection
searchEditText.setSelected(false);
// Set text editable click event listener
searchEditText.setOnClickListener(new SearchableViewListener());
// Set text editable onTextChange listener
searchEditText.addTextChangedListener(new SearchableViewListener());
}
In this method, we change the appearance and behavior of the generic search view by modifying the background color, search view button icon, removing default selection and focus from the search view when the app starts, and, also, set handlers (i.e., listeners) of various search view events such as clicking on the search view button that serves as the app's main action button, text editing and text editable view clicking, etc.
Those events handlers are implemented as the 'SearchableWithButtonView' child class, declared inside of it:
public class SearchableViewListener
implements OnClickListener, TextWatcher {
@Override
public void onClick(View view) {
// Check if the custom search view button was clicked
if (android.support.v7.appcompat.R.
id.search_mag_icon == view.getId()) {
// If so, perform a check if the default action bar icon was set
if (!isDefaultIcon) {
// If not, set the default icon by invoking setDefaultSearchIcon() method
setDefaultSearchIcon();
// Terminate the onClick handler method execution
return;
}
// Invoke onClick(...) method from the main app's activity class
m_ClickListener.onClick(view);
}
// Otherwise, set navigation-back search icon
else setNavBackSearchIcon();
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
// Invoke the beforeTextChange(...) method from app's activity class
// (e.g. its parent)
m_TextWatcherListener.beforeTextChanged(charSequence, i, i1, i2);
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
// Set navigation-back icon and invoke the onTextChanged(...) method
// from app's activity class (e.g. its parent)
setNavBackSearchIcon();
m_TextWatcherListener.onTextChanged(charSequence, i, i1, i2);
}
@Override
public void afterTextChanged(Editable editable) {
// Perform a check if the editable string is empty
if (editable.toString().isEmpty())
// If so, set default search view icon
setDefaultSearchIcon();
// Invoke the afterTextChanged(...) method from its parent
m_TextWatcherListener.afterTextChanged(editable);
}
}
Also the 'SearchableWithButtonView
' class has the following methods:
The method listed below changes the look of the custom search view to uniconfied:
private void setupIconifiedByDefault() {
// Disable the iconfied mode to make the search view
// fill the entire area horizontally
m_SearchView.setIconified(false);
m_SearchView.setIconifiedByDefault(false);
}
The following method replaces the default icon of the generic search view with the custom icon of the app's action bar button:
private void setDefaultSearchIcon() {
// Replace the default search view icon with the action button icon
this.isDefaultIcon = true;
this.replaceSearchIcon(R.drawable.ic_dehaze_white_24dp);
}
The following method replaces the default action bar button icon with the navigation-back icon:
private void setNavBackSearchIcon() {
// Check if the default icon was set
if (this.isDefaultIcon == true) {
// If so, replace search view icon with navigation-back icon
this.isDefaultIcon = false;
this.replaceSearchIcon(R.drawable.ic_arrow_back_black_24dp);
// Run the search view icon animation
this.setupAnimation();
}
}
The following method replaces the default search view button icon with an icon retrieved from the app's resources:
private void replaceSearchIcon(int resDefaultIcon) {
// Instantiate search view button icon object and set the custom icon
// by calling setImageDrawable method that accepts the icon object retrieved
// from the app's resources by calling the context's getDrawable(...) method
((ImageView)m_SearchView.findViewById
(android.support.v7.appcompat.R.id.search_mag_icon)).
setImageDrawable(m_Context.getDrawable(resDefaultIcon));
// Start animating icon
this.setupAnimation();
}
This method is used to set up the animation for the search view icon:
private void setupAnimation() {
// Instantiate search view icon object
final ImageView searchIconView = m_SearchView.findViewById(
android.support.v7.appcompat.R.id.search_mag_icon);
// Compute the icon's width and height values
int searchIconWidth = searchIconView.getWidth();
int searchIconHeight = searchIconView.getHeight();
// Instantiate RotateAnimation class object and specify the rotation params
RotateAnimation searchIconAnimation = new RotateAnimation(0f, 360f,
searchIconWidth / 2, searchIconHeight / 2);
// Set animation interpolator
searchIconAnimation.setInterpolator(new LinearInterpolator());
// Set animation repeat count
searchIconAnimation.setRepeatCount(Animation.INFINITE);
// Set animation duration
searchIconAnimation.setDuration(700);
// Start animating the icon
searchIconView.startAnimation(searchIconAnimation);
// Perform a delay for 700ms after the icon animation ends
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
searchIconView.setAnimation(null);
}
}, 700);
}
By using the following method, we override the basic functionality of findViewById(...)
method to be used with search view object:
// Override the default findViewById method to be used to Instantiate
// search view object
private SearchView findSearchViewById(int resId) {
return ((Activity)m_Context).findViewById(resId);
}
By calling these two methods, we set the click event listener and text change event listener used in main app's activity class:
public void setSearchButtonClickListener(@Nullable OnClickListener clickListener) {
// Set click listener class object of its parent
m_ClickListener = clickListener;
}
public void setTextWatchListener(@Nullable TextWatcher textWatchListener) {
// Set text change watcher listener class object of its parent
m_TextWatcherListener = textWatchListener;
}
Now, since we've implemented the customized search view with action button, it's time to add its functionality to the main app's activity as follows:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_airport);
// Instantiating the drawer layout object
m_DrawerLayout = findViewById(R.id.airport_drawer_layout);
// Instantiating the navigation view object
m_navigationView = findViewById(R.id.airport_navigation_view);
// Instantiating our custom search view object
m_searchableWithButtonView =
new SearchableWithButtonView(AirportActivity.this, R.id.searchable);
// Setting up our custom search view
m_searchableWithButtonView.setupSearchableWithButton();
// Adding the text change watcher listener
m_searchableWithButtonView.setTextWatchListener(new SearchableWithButtonListener());
// Adding the search view action button click event listener
m_searchableWithButtonView.setSearchButtonClickListener
(new SearchableWithButtonListener());
// Setup app's drawer menu click event listener
m_navigationView.setNavigationItemSelectedListener(m_NavigationBarListener);
// ...
In the overridden onCreate
method, we normally perform the instantiation of drawer layout and navigation view objects, setting up our custom search view and add specific events handlers. To handle various search view's events, we must declare a child class 'SearchableWithButtonListener
' implementing the either 'View.OnClickListener
' or 'TextWatcher
' event handling generic classes:
public class SearchableWithButtonListener implements View.OnClickListener, TextWatcher
{
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
}
@Override
public void onClick(View view) {
// Perform a check if the app's drawer open
if (!m_DrawerLayout.isDrawerOpen(GravityCompat.START))
// If not, open the app's drawer
m_DrawerLayout.openDrawer(GravityCompat.START);
}
}
The functionality implemented by methods of the following class discussed in one of the next sections of this article. In this case, we will discuss only one implementation of onClick(...)
method from this class. The following method implements the app's drawer open functionality by invoking the DrawerLayout.openDrawer(...)
method.
As we've already discussed after firing the openDrawer(...)
method while the custom action bar click event is handled, the app's drawer is open displaying the app's main menu. At this point, we also must provide the menu items click event handling by calling 'm_navigationView.setNavigationItemSelectedListener(m_NavigationBarListener)'
method that accept the object of listener class as its single parameter. The following code implements the overridden navigation menu items click event listener class:
private class NavigationBarListener implements
NavigationView.OnNavigationItemSelectedListener
{
// This method handles the navigation menu item click events
public boolean onNavigationItemSelected(MenuItem menuItem) {
// set item as selected to persist highlight
menuItem.setChecked(true);
//...
if (m_DrawerLayout.isDrawerOpen(GravityCompat.START))
m_DrawerLayout.closeDrawers();
return true;
}
}
Creating Tabbed App's Layout
As we've already discussed, the airport app is intended to response the user's input and display various content depending on what options from the app's drawer navigation menu or tabs were toggled by a user. Specifically after toggling the 'flights' menu item in the app's drawer navigation menu, it normally renders the tabbed layout. Each tab basically displays a list of flights rendered by the recycler view. To implement this, we will use fragments. A 'Fragment
' is a dynamically created and rendered portion of the app's layout containing other layouts or views, or both.
In this case, what we have to do so far is to create specific fragment layouts and our own java-classes implementing the content rendering functionality. As we've already discussed, the two tabs 'arrivals' and 'departures' will appear in the main app's window. In each of these tabs, we will render 'RecyclerView
' showing up a list of flights scheduled. To provide a tabbed layout functionality, we will use 'TabbedLayout
' rendered inside 'LinearLayout
', which is the root layout for the 'FlightsFragment
' shown up when a user toggles the first menu item 'flights' in the app's drawer navigation menu. The flights fragment layout is implemented in 'res/layout/ fragment_flights.xml' file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/flights_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".FlightsFragment">
<android.support.design.widget.TabLayout
android:id="@+id/flights_destination_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabMaxWidth="0dp"
app:tabMode="fixed"
app:tabGravity="fill">
<android.support.design.widget.TabItem
android:id="@+id/arrivals_tab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_flight_land_black_24dp"
android:text="@string/arrivals_tab" />
<android.support.design.widget.TabItem
android:id="@+id/departures_tab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_flight_takeoff_black_24dp"
android:text="@string/departures_tab" />
</android.support.design.widget.TabLayout>
<android.support.v4.view.ViewPager
android:id="@+id/flights_destination_pager"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<requestFocus/>
</LinearLayout>
Inside the flights fragment linear layout, we declare two tags: 'android.support.design.widget.TabLayout
' and 'android.support.v4.view.ViewPager
'. The first tag basically defines the tabbed layout containing two tabs for either 'arrivals' or 'departures' flights rendering, that appear under the search view in the main app's window. By declaring the second tag 'ViewPager
', we provide the functionality for sliding between one entire screen rendering flights to another.
Since the tabbed layout and view pager are rendered as a fragment, we must create a separate java-class 'FlightsFragment
' extending the generic 'android.support.v4.app.Fragment
' class:
FlightsFragmentImpl.java
package com.epsilon.arthurvratz.airportapp;
import android.net.Uri;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import java.util.ArrayList;
public class FlightsFragmentImpl extends android.support.v4.app.Fragment implements
ArrivalsFragment.OnFragmentInteractionListener,
DeparturesFragment.OnFragmentInteractionListener
{
public RecyclerView m_RecyclerView;
public RecyclerView.Adapter m_RecyclerAdapter;
public RecyclerView.LayoutManager m_LayoutManager;
public void setupFlightsRecyclerView
(RecyclerView recyclerView, ArrayList<AirportDataModel> dataSet)
{
// Setting the recycler view object
m_RecyclerView = recyclerView;
// Setting the recycler view has a fixed size
m_RecyclerView.setHasFixedSize(true);
// Instantiating the linear layout manager object
m_LayoutManager = new LinearLayoutManager(getContext());
// Setting up the recycler view's layout manager
m_RecyclerView.setLayoutManager(m_LayoutManager);
// Instantiating the flights recycler view's adapter object
// and adding the flights dataset to the flights recycler view's adapter
m_RecyclerAdapter = new FlightsRecyclerAdapter(dataSet, getContext());
// Setting up the flights recycler view's adapter object</span>
m_RecyclerView.setAdapter(m_RecyclerAdapter);
}
@Override
public void onFragmentInteraction(Uri uri) {
}
}
FlightsFragment.java
package com.epsilon.arthurvratz.airportapp;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class FlightsFragment extends FlightsFragmentImpl
{
private TabLayout m_TabLayout;
private ViewPager m_ViewPager;
final private TabSelectedListener
m_TabSelListener = new TabSelectedListener();
public ArrivalsFragment m_ArrivalsFragment;
public DeparturesFragment m_DeparturesFragment;
private class TabSelectedListener implements TabLayout.OnTabSelectedListener
{
@Override
public void onTabSelected(TabLayout.Tab tab) {
m_ViewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
}
private OnFragmentInteractionListener mListener;
public FlightsFragment() {
// Required empty public constructor
}
public static FlightsFragment newInstance() {
return new FlightsFragment();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflating the flights fragment view's object
View FlightsFragmentView =
inflater.inflate(R.layout.fragment_flights, container, false);
// Instantiating the tab layout object
m_TabLayout = FlightsFragmentView.findViewById(R.id.flights_destination_tabs);
// Instantiating view pager object
m_ViewPager = FlightsFragmentView.findViewById(R.id.flights_destination_pager);
// Instantiating the tab layout's pager adapter
FlightsDestPagerAdapter pagerAdapter = new FlightsDestPagerAdapter(
getChildFragmentManager(), m_TabLayout.getTabCount());
// Instantiating the arrivals fragment object
m_ArrivalsFragment = ArrivalsFragment.newInstance();
// Instantiating the departures fragment object
m_DeparturesFragment = DeparturesFragment.newInstance();
// Adding the arrivals and departure fragment objects to the view pager adapter
pagerAdapter.add(m_ArrivalsFragment);
pagerAdapter.add(m_DeparturesFragment);
// Setting up the view pager adapter
m_ViewPager.setAdapter(pagerAdapter);
// Adding the generic page sliding event listener
m_ViewPager.addOnPageChangeListener(
new TabLayout.TabLayoutOnPageChangeListener(m_TabLayout));
m_TabLayout.addOnTabSelectedListener(m_TabSelListener);
return FlightsFragmentView;
}
public void onButtonPressed(Uri uri) {
if (mListener != null) {
mListener.onFragmentInteraction(uri);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnFragmentInteractionListener) {
mListener = (OnFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnFragmentInteractionListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
@Override
public void onFragmentInteraction(Uri uri) {
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
}
}
To implement the flights fragment functionality, we actually define two java-classes. The first class 'FlightsFragmentImpl
' extends the generic 'android.support.v4.app.Fragment
' and implements the 'OnFragmentInteractionListener
' functionality for both 'ArrivalsFragment
' and 'DepartureFragment
' classes discussed below. The following class implements just one method 'setupFlightsRecyclerView(...)
', that accepts two arguments of either a recycler view's object or the dataset 'ArrayList
' object discussed later on in this article. The main purpose of this method is to setup the recycler view's adapter that is used to hold the data rendered in the recycler view shown in one of the selected tabs.
Another class 'FlightsFragment
' extends the functionality of the 'FlightsFragmentImpl' and provides the basic functionality for dynamically setting up tab layout and view pager in 'OnCreateView
' overridden method by adding specific arrivals and departures fragments objects to the view page adapter. The 'FlightsDestPagerAdapter
' java-class implements the basic functionality of view pager adapter:
package com.epsilon.arthurvratz.airportapp;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import java.util.ArrayList;
public class FlightsDestPagerAdapter extends FragmentPagerAdapter {
private ArrayList<Fragment> m_Fragments = new ArrayList<Fragment>();
public FlightsDestPagerAdapter(FragmentManager FragmentMgr, int NumberOfTabs) {
super(FragmentMgr);
}
public void add(Fragment fragment)
{
m_Fragments.add(fragment);
}
@Override
public Fragment getItem(int position) {
return m_Fragments.get(position);
}
@Override
public int getCount() {
return m_Fragments.size();
}
}
The implementation of the following class is mainly based on using 'ArrayList<Fragment>
' functionality used to store an array of generic 'Fragment
' class objects.
Finally, to render the flights fragment, we must override the 'onNavigationItemSelected(...)
' method in the 'AirportActivity.NavigationBarListener
' class. The following method is basically used to handle event from the app's drawer navigation menu and has the following implementation:
private class NavigationBarListener implements
NavigationView.OnNavigationItemSelectedListener
{
public boolean onNavigationItemSelected(MenuItem menuItem) {
// set item as selected to persist highlight
menuItem.setChecked(true);
// Instantiate the fragment manager transaction coordinator object
m_FragmentTran = m_FragmentMgr.beginTransaction();
// Perform a check if the flights menu item was selected
if (menuItem.getItemId() == .Rid.flights)
// If so, replace the airport_fragment_container frame layout
// with specific flight fragment by using its object.
m_FragmentTran.replace(R.id.airport_fragment_container,
FlightsFragment.newInstance());
else if (menuItem.getItemId() == R.id.about) {}
m_FragmentTran.addToBackStack(null); m_FragmentTran.commit();
// Check if the app's drawer is still open
if (m_DrawerLayout.isDrawerOpen(GravityCompat.START))
// If so, close the app's drawer
m_DrawerLayout.closeDrawers();
return true;
}
public void setupInitialFragment()
{
if (m_FragmentMgr == null)
// Instantiate the support fragment manager object
m_FragmentMgr = getSupportFragmentManager();
// Begin fragments transaction
m_FragmentTran = m_FragmentMgr.beginTransaction();
// Add the default flights fragment object and commit transaction
m_FragmentTran.add(R.id.airport_fragment_container,
FlightsFragment.newInstance()).commit();
}
}
The following class also implements one more method 'setupInitialFragment(...)
' that is used to setup initial fragment when invoked from the app's main activity code, in the overridden method 'OnCreate(...)
', when the main app's activity is instantiated:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_airport);
//m_ActionToolBar = findViewById(R.id.airport_actionbar);
m_DrawerLayout = findViewById(R.id.airport_drawer_layout);
m_navigationView = findViewById(R.id.airport_navigation_view);
m_flightsNavigationView = findViewById(R.id.flights_navigation);
//setSupportActionBar(m_ActionToolBar);
//this.setupActionBar(R.drawable.ic_dehaze_white_24dp);
m_searchableWithButtonView =
new SearchableWithButtonView(AirportActivity.this, R.id.searchable);
m_searchableWithButtonView.setupSearchableWithButton();
m_searchableWithButtonView.setTextWatchListener(new SearchableWithButtonListener());
m_searchableWithButtonView.setSearchButtonClickListener
(new SearchableWithButtonListener());
m_navigationView.setNavigationItemSelectedListener(m_NavigationBarListener);
m_flightsNavigationView.setSelectedItemId(R.id.flights_now);
// Setting up initial fragment to be rendered in the main app's window
m_NavigationBarListener.setupInitialFragment(); this.hideSoftInputKeyboard();
//...
}
In the next section of this article, we will discuss how to render recycler views inside the flights fragment, showing the lists of either 'arrival' or 'departure' flights.
Rendering Flights In RecyclerView
Rendering lists of flights in the recycler view is the final airport app's GUI topic we're about to discuss in this article. As we already know, our airport application displays two lists of either 'arrival' or 'departure' flights and programmatically does it a similar way. To render lists of flights, all that we have to do is create two fragments that will render either the arrival flights or departure flights recycler views:
fragment_arrivals.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/arrivals_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|center_horizontal"
tools:context=".ArrivalsFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/arrivals_recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
fragment_departures.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/departures_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|center_horizontal"
tools:context=".DeparturesFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/departures_recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
We also create two java-classes of either 'ArrivalsFragment
' and 'DeparturesFragment
' that implement those fragments listed above functionality.
ArrivalsFragment.java
package com.epsilon.arthurvratz.airportapp;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
public class ArrivalsFragment extends android.support.v4.app.Fragment {
public RecyclerView m_ArrivalsRecyclerView;
public ArrayList<AirportDataModel> m_ArrivalsDataSet;
public FlightsFragment m_FlightsFragment;
private OnFragmentInteractionListener mListener;
public ArrivalsFragment() {
// Instantiate the airport app's data model and generate set of random flights
m_ArrivalsDataSet = new AirportDataModel().InitModel(20);
}
public static ArrivalsFragment newInstance() {
return new ArrivalsFragment();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the flights fragment layout
View ArrivalsView =
inflater.inflate(R.layout.fragment_arrivals, container, false);
// Get flights fragment layout object
m_FlightsFragment =
(FlightsFragment) this.getParentFragment();
// Instantiate arrivals recycler view object
m_ArrivalsRecyclerView =
ArrivalsView.findViewById(R.id.arrivals_recycler_view);
// Invoke setupFlightsRecyclerView method,
// which is the member of flight fragment class
m_FlightsFragment.setupFlightsRecyclerView(m_ArrivalsRecyclerView, m_ArrivalsDataSet);
return ArrivalsView;
}
// TODO: Rename method, update argument and hook method into UI event
public void onButtonPressed(Uri uri) {
if (mListener != null) {
mListener.onFragmentInteraction(uri);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnFragmentInteractionListener) {
mListener = (OnFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnFragmentInteractionListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
}
}
DeparturesFragment.java
package com.epsilon.arthurvratz.airportapp;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
public class DeparturesFragment extends android.support.v4.app.Fragment {
public RecyclerView m_DeparturesRecyclerView;
public ArrayList<AirportDataModel> m_DeparturesDataSet;
public FlightsFragment m_FlightsFragment;
private OnFragmentInteractionListener mListener;
public DeparturesFragment() {
// Instantiate the airport app's data model and generate set of random flights
m_DeparturesDataSet = new AirportDataModel().InitModel(20);
}
public static DeparturesFragment newInstance() {
return new DeparturesFragment();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the flights fragment layout
View DeparturesView =
inflater.inflate(R.layout.fragment_arrivals, container, false);
// Get flights fragment layout object
m_FlightsFragment =
(FlightsFragment) this.getParentFragment();
// Instantiate departures recycler view object
m_DeparturesRecyclerView =
DeparturesView.findViewById(R.id.arrivals_recycler_view);
// Instantiate arrivals recycler view object
m_FlightsFragment.setupFlightsRecyclerView
(m_DeparturesRecyclerView, m_DeparturesDataSet);
return DeparturesView;
}
// TODO: Rename method, update argument and hook method into UI event
public void onButtonPressed(Uri uri) {
if (mListener != null) {
mListener.onFragmentInteraction(uri);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof OnFragmentInteractionListener) {
mListener = (OnFragmentInteractionListener) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement OnFragmentInteractionListener");
}
}
@Override
public void onDetach() {
super.onDetach();
mListener = null;
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
}
}
In both these java-classes, we override the functionality of onCreateView(...)
method by inflating the flights fragment layout object to invoke the setupFlightsRecyclerView(...)
method of 'FlightFragmentImpl
' class:
public void setupFlightsRecyclerView
(RecyclerView recyclerView, ArrayList<AirportDataModel> dataSet)
{
m_RecyclerView = recyclerView;
m_RecyclerView.setHasFixedSize(true);
m_LayoutManager = new LinearLayoutManager(getContext());
m_RecyclerView.setLayoutManager(m_LayoutManager);
m_RecyclerAdapter = new FlightsRecyclerAdapter(dataSet, getContext());
m_RecyclerView.setAdapter(m_RecyclerAdapter);
}
Another important aspect of using recycler views to render lists of flights is the implementation of a recycler view adapter. Since both recycler views for either 'arrival' or 'departure' flights perform the data rendering in a similar way, we just need to implement a single flights recycler view adapter for both specific recycler views.
Also, we must create a layout for each flight item displaying the flight-specific information such as time, destination, airlines code, airlines logo, country flag and status.
flights_item.xml
<?xml version="1.0" encoding="utf-8"?>
<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="wrap_content">
<TextView
android:id="@+id/flight_time"
android:layout_width="51dp"
android:layout_height="18dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="24dp"
android:text="3:07pm"
android:textAppearance="@style/TextAppearance.AppCompat.Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/airlines_logo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<ImageView
android:id="@+id/airlines_logo"
android:layout_width="55dp"
android:layout_height="48dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/flight_code"
app:layout_constraintStart_toEndOf="@+id/flight_time"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@color/text_color_secondary" />
<TextView
android:id="@+id/flight_code"
android:layout_width="59dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="24dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/flight_destination"
app:layout_constraintStart_toEndOf="@+id/airlines_logo"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/flight_destination"
android:layout_width="68dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="24dp"
android:text="TextView"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/country_flag"
app:layout_constraintStart_toEndOf="@+id/flight_code"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<ImageView
android:id="@+id/country_flag"
android:layout_width="51dp"
android:layout_height="42dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/flight_status"
app:layout_constraintStart_toEndOf="@+id/flight_destination"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@android:color/black" />
<TextView
android:id="@+id/flight_status"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="24dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/country_flag"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
</android.support.constraint.ConstraintLayout>
FlightsRecyclerAdapter.java
package com.epsilon.arthurvratz.airportapp;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
public class FlightsRecyclerAdapter
extends RecyclerView.Adapter<FlightsRecyclerAdapter.ViewHolder> {
// Provide a reference to the views for each data item
// Complex data items may need more than one view per item, and
// you provide access to all the views for a data item in a view holder
private ArrayList<AirportDataModel> m_DataModel;
private Context m_context;
public static class ViewHolder extends RecyclerView.ViewHolder {
// each data item is just a string in this case
public TextView m_TimeView;
public TextView m_FlightCodeView;
public TextView m_DestView;
public TextView m_StatusView;
public ImageView m_AirlinesLogoView;
public ImageView m_CountryFlagView;
public ViewHolder(View v) {
super(v);
// Instantiate each view object in the flights_item layout
m_TimeView = v.findViewById(R.id.flight_time);
m_FlightCodeView = v.findViewById(R.id.flight_code);
m_DestView = v.findViewById(R.id.flight_destination);
m_StatusView = v.findViewById(R.id.flight_status);
m_AirlinesLogoView = v.findViewById(R.id.airlines_logo);
m_CountryFlagView = v.findViewById(R.id.country_flag);
}
}
// Provide a suitable constructor (depends on the kind of dataset)
public FlightsRecyclerAdapter(ArrayList<AirportDataModel> m_dataModel, Context context) {
m_DataModel = m_dataModel; m_context = context;
}
// Create new views (invoked by the layout manager)
@Override
public FlightsRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// create a new view
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.flights_item, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// Retrieve specific data for each item rendered in the flights recycler view
// and display these values in the specific views in the flights_item layout
holder.m_TimeView.setText(new SimpleDateFormat("HH:mm")
.format(m_DataModel.get(position).m_Time));
holder.m_StatusView.setText(m_DataModel.get(position).m_Status);
holder.m_DestView.setText(m_DataModel.get(position).m_Destination);
holder.m_FlightCodeView.setText(m_DataModel.get(position).m_Airlines.m_Flight);
Context airlines_logo_context = holder.m_AirlinesLogoView.getContext();
String airlines_logo = m_DataModel.get(position).m_Airlines.m_logoResId;
holder.m_AirlinesLogoView.setImageResource(this.getResourceIdFromString
(airlines_logo_context, airlines_logo));
Context flag_context = holder.m_CountryFlagView.getContext();
String flag = "flag" + m_DataModel.get(position).m_DestResId;
holder.m_CountryFlagView.setImageResource
(this.getResourceIdFromString(flag_context, flag));
// Launching animation of each view in the flights_item layout
setAnimation(holder.m_TimeView, position);
setAnimation(holder.m_StatusView, position);
setAnimation(holder.m_DestView, position);
setAnimation(holder.m_FlightCodeView, position);
setAnimation(holder.m_AirlinesLogoView, position);
setAnimation(holder.m_CountryFlagView, position);
}
public void setAnimation(View view, int pos)
{
// Instantinating the animation object
Animation flightAnimation = android.view.animation.
AnimationUtils.loadAnimation(m_context, R.anim.fade_interpolator);
// Set animation duration
flightAnimation.setDuration(700);
// Starting the animation
view.startAnimation(flightAnimation);
}
// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
return m_DataModel.size();
}
public int getResourceIdFromString(Context context, String resource)
{
return context.getResources().getIdentifier(resource,
"drawable", context.getPackageName());
}
}
The 'FlightsRecyclerViewAdapter
' is the java-class that extends the functionality of the specialization of generic 'RecyclerView.Adapter<FlightsRecyclerAdapter.ViewHolder>
'. It implement the basic functionality needed for binding the data to the recycler view. Specifically, it implement a child java-class 'ViewHolder
' responsible for rendering each flight item by invoking instantiating the object of each view in flights_item
layout. To render specific items, the app is calling onBindViewHolder(...)
overridden method to programmatically set specific values to be displayed by the various views inside the flights_item
layout.
Adding Flights Schedule Simulation Functionality
As we've already discussed at the very beginning in this article, besides the user interface intended to render the specific flights data, we must implement the functionality responsible for generating the flights data and time-line simulation. Throughout this application, we use the pattern which is something like model-view-controller frequently used in other programming languages and frameworks. Specifically, in this particular case, we're combining our data model with a certain data controller that perform the actual flights dataset manipulation.
AirportDataModel.java
package com.epsilon.arthurvratz.airportapp;
import java.util.ArrayList;
import java.util.Random;
public class AirportDataModel {
long m_Time;
String m_Status;
String m_Destination;
String m_DestResId;
Airlines m_Airlines;
public class Airlines
{
public Airlines(String logoResId, String flight)
{
this.m_Flight = flight;
this.m_logoResId = logoResId;
}
public String m_logoResId;
public String m_Flight;
}
public AirportDataModel() { }
public AirportDataModel(long curr_time, String status, String dest,
String destResId, Airlines airlines)
{
this.m_Airlines = airlines;
this.m_Status = status;
this.m_Destination = dest;
this.m_Time = curr_time;
this.m_DestResId = destResId;
}
public AirportDataModel getRandomFlight() {
Random rand_obj = new Random();
// Instantiate airport flights destination data class object
AirportFlightsDestData flightsData = new AirportFlightsDestData();
// Generate random destination city index
int flight_rnd_index = rand_obj.nextInt(
flightsData.m_DestCities.size() - 1);
// Get a string value of a destination city by its random index
String destCity = flightsData.m_DestCities.get(flight_rnd_index);
// Get specific country flag resource id associated with the name of the city
String destResId = flightsData.getFlagResourceByDestCity(destCity);
// Generate letters in the flight code
char airline_code_let1 = (char) (rand_obj.nextInt('Z' - 'A') + 'A');
char airline_code_let2 = (char) (rand_obj.nextInt('Z' - 'A') + 'A');
String airline_code = "\0";
// Append letters to the airline_code string value
airline_code += new StringBuilder().append(airline_code_let1).toString();
airline_code += new StringBuilder().append(airline_code_let2).toString();
String flight_code = "\0";
// Generate four digits of the flight code
flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();
flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();
flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();
flight_code += new StringBuilder().append(rand_obj.nextInt(9)).toString();
// Construct a string containing the full airline code
flight_code = airline_code + " " + flight_code;
// Instantiate and construct airlines data object
Airlines airlines = new Airlines(flightsData.m_airlinesResName.get(rand_obj.nextInt(
flightsData.m_airlinesResName.size() - 1)), flight_code);
// Get random status string value
String flight_status = flightsData.m_Status.get(
rand_obj.nextInt(flightsData.m_Status.size() - 1));
// Generate a random flight time
int time_hours_sign = rand_obj.nextInt(2);
// Generate an hours offset from the current system time
int time_hours_offset = rand_obj.nextInt(48);
// Get the current system time
long currTimeMillis = System.currentTimeMillis();
// Determine the random flight time in ms
if (time_hours_sign > 0)
currTimeMillis += time_hours_offset * 3.6e+6;
else currTimeMillis -= time_hours_offset * 3.6e+6;
// Instantiate and return flight item data object based on the
// data previously generated
return new AirportDataModel(currTimeMillis,
flight_status, destCity, destResId, airlines);
}
public ArrayList<AirportDataModel> InitModel(int numOfItems)
{
// Init model by generated a list of random flight items
ArrayList<AirportDataModel> newDataModel = new ArrayList<>();
for (int index = 0; index < numOfItems; index++) {
newDataModel.add(this.getRandomFlight());
}
return newDataModel;
}
public ArrayList<AirportDataModel> Simulate(ArrayList<AirportDataModel> dataSet)
{
// Get current system time in ms
long currTimeMillis = System.currentTimeMillis();
// Get a random current time being simulated
currTimeMillis += new Random().nextInt(48) * 3.6e+6;
// Perform a linear search to filter out all flights that already have taken place
for (int index = 0; index < dataSet.size(); index++) {
AirportDataModel item = dataSet.get(index);
if (item.m_Time <= currTimeMillis) {
// Remove current flight item
dataSet.remove(item);
// Generate and add new flight item
dataSet.add(new Random().nextInt(dataSet.size()), getRandomFlight());
}
}
return dataSet;
}
public ArrayList<AirportDataModel> filterByTime(
ArrayList<AirportDataModel> dataSet, long time_start, long time_end) {
ArrayList<AirportDataModel> targetDataSet = new ArrayList<>();
// Perform a linear search to filter out flights which time belongs to a given range
for (int index = 0; index < dataSet.size(); index++) {
AirportDataModel item = dataSet.get(index);
if (item.m_Time > time_start && item.m_Time < time_end)
targetDataSet.add(item);
}
return targetDataSet;
}
}
In this class, we implement all the methods required to generate and manipulate flights data. The first thing that we need to do is to implement a getRandomFlight(...)
method generating a random flight data. The following method basically relies on using a statically declared data. For that purpose, we create another class that also defines and manipulates the flights-specific data.
AirportFlightsDestData.java
package com.epsilon.arthurvratz.airportapp;
import java.util.Arrays;
import java.util.List;
public class AirportFlightsDestData
{
public class CountryCityRel
{
public CountryCityRel(int countryId, int[] cityIds)
{
this.m_cityIds = cityIds;
this.m_countryId = countryId;
}
private int m_countryId;
private int[] m_cityIds;
}
public String getFlagResourceByDestCity(String destCity)
{
int countryId = -1;
// Performing a linear search to find the dest city index
for (int index = 0; index < m_DestCities.size(); index++) {
if (m_DestCities.get(index) == destCity) {
// Performing a linear search to find the dest country and return country-id
for (int country = 0; country < m_CountryCityRelTable.size(); country++) {
int[] cityIds = m_CountryCityRelTable.get(country).m_cityIds;
for (int city = 0; city < cityIds.length && cityIds != null; city++)
countryId = (cityIds[city] == index) ?
m_CountryCityRelTable.get(country).m_countryId : countryId;
}
}
}
return m_countryResName.get(countryId);
}
public List<String> m_DestCities = Arrays.asList(
"Atlanta", "Beijing", "Dubai", "Tokyo", "Los Angeles", "Chicago",
"London", "Hong Kong",
"Shanghai", "Paris", "Amsterdam", "Dallas", "Guangdong",
"Frankfurt", "Istanbul", "Delhi", "Tangerang",
"Changi", "Incheon", "Denver", "New York", "San Francisco",
"Madrid", "Las Vegas", "Barcelona", "Mumbai", "Toronto");
public List<String> m_countryResName = Arrays.asList(
"peoplesrepublicofchina", "unitedstates",
"unitedarabemirates", "japan", "unitedkingdom",
"hongkong", "france", "netherlands", "germany", "turkey", "india", "indonesia",
"singapore", "southkorea", "spain", "canada");
public List<String> m_airlinesResName = Arrays.asList(
"aa2", "aeromexico", "airberlin", "aircanada",
"airfrance2", "airindia2", "airmadagascar",
"airphillipines", "airtran",
"alaskaairlines3", "alitalia", "austrian2", "avianca1",
"ba2", "brusselsairlines2",
"cathaypacific21", "china_airlines", "continental",
"croatia2", "dagonair", "delta3", "elal2",
"emirates_logo2", "ethiopianairlines4",
"garudaindonesia", "hawaiian2", "iberia2",
"icelandair", "jal2", "klm2", "korean",
"lan2", "lot2", "lufthansa4", "malaysia",
"midweat", "newzealand", "nwa1", "oceanic",
"qantas2", "sabena2", "singaporeairlines",
"southafricanairways2", "southwest2",
"spirit", "srilankan", "swiss", "swissair3",
"tap", "tarom", "thai4", "turkish",
"united", "varig", "vietnamairlines", "virgin4", "wideroe1");
public List<CountryCityRel> m_CountryCityRelTable =
Arrays.asList(new CountryCityRel(0, new int[] { 1, 8, 12, }),
new CountryCityRel(1, new int[] { 0, 4, 5, 11, 19, 20, 21,23 }),
new CountryCityRel(2, new int[] { 2 }),
new CountryCityRel(3, new int[] { 3 }),
new CountryCityRel(4, new int[] { 6 }),
new CountryCityRel(5, new int[] { 7 }),
new CountryCityRel(6, new int[] { 9 }),
new CountryCityRel(7, new int[] { 10 }),
new CountryCityRel(8, new int[] { 13 }),
new CountryCityRel(9, new int[] { 14 }),
new CountryCityRel(10, new int[] { 15, 22, 25 }),
new CountryCityRel(11, new int[] { 16 }),
new CountryCityRel(12, new int[] { 17 }),
new CountryCityRel(13, new int[] { 18 }),
new CountryCityRel(14, new int[] { 21, 24 }),
new CountryCityRel(15, new int[] { 26 }));
public List<String> m_Status =
Arrays.asList("Check-In", "Canceled", "Expected", "Delayed");
}
The following class contains a set of generic 'List
' objects declared and statically initialized to hold the various data on flights destination cities, as well as the lists with names of resources containing airlines logos and countries flags. Also the following class has the declaration of 'getFlagResourceByDestCity
' method used to retrieve data on specific resources names by the name of destination city.
In the airport data model class, we must declare the specific data field variables to hold the data on each flight:
long m_Time;
String m_Status;
String m_Destination;
String m_DestResId;
Airlines m_Airlines;
public class Airlines
{
public Airlines(String logoResId, String flight)
{
this.m_Flight = flight;
this.m_logoResId = logoResId;
}
public String m_logoResId;
public String m_Flight;
}
Also, in this class, we implement the following methods including getRandomFlight(...)
, InitModel(...)
, Simulate(...)
and filterByTime(...)
. As we've already discussed, getRandomFlight
method is used to generate a random data for a random flight that later will be added to the list of lights. For that purpose, we invoke the InitModel
method in the ArrivalsFragment
and DeparturesFragment
classes constructor respectively so that each of these constructors will instantiate the airport data model class object, invoke this method and receive its own copy of the array list object containing a list of either arrival or departure flights:
public ArrivalsFragment() {
m_ArrivalsDataSet = new AirportDataModel().InitModel(20);
}
To provide the list of flights dynamic update during the flights schedule simulation process, we must override the default onResume
method for our app's activity class as follows:
@Override
protected void onResume() {
super.onResume();
this.findViewById(R.id.search_bar).requestFocus();
startSimulation();
}
In this method, we're invoking another Simulate(...)
method to launch a simulation process:
public void Simulate() {
simTask = new TimerTask() {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
// Instantiate flights fragment object
FlightsFragment flightsFragment = (FlightsFragment)
m_FragmentMgr.findFragmentById(R.id.airport_fragment_container);
m_flightsNavigationView.setSelectedItemId(R.id.flights_now);
ArrayList<AirportDataModel> dataSet = null;
RecyclerView recyclerView = null;
// Determine the currently selected tab
TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);
if (tabLayout.getTabAt(0).isSelected()) {
dataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
recyclerView =
flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;
}
else if (tabLayout.getTabAt(1).isSelected()) {
dataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
recyclerView =
flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;
}
// Invoking airport data model's Simulate method
m_AirportDataModel.Simulate(dataSet);
// Instantinating the new object for FlightsRecyclerAdapter class and
// pass the dataset object as
// one of the adapter's constructor parameters
FlightsRecyclerAdapter recyclerAdapter
= new FlightsRecyclerAdapter(dataSet, getBaseContext());
// For the current recycler view object setting the new adapter
recyclerView.setAdapter(recyclerAdapter);
// Updating the data bound to the new recycler adapter
recyclerAdapter.notifyDataSetChanged();
recyclerAdapter.notifyItemRangeChanged(0, dataSet.size());
}
});
}
};
}
In this method listed above, we're creating a timer task thread spawned by an instance of timer:
private void startSimulation()
{
this.Simulate(); new Timer().schedule(simTask, 50, 10000);
}
Every time when the system timer scheduled ticks, the run(...)
method is invoked. In the following method, we're invoking the airport data model's Simulate(...)
method listed above. The following method determines the system time and filters out all flights items having time value less than the current system time. After that, we create a new instance of recycler view controller previously discussed and pass it the new list of flights as the argument of its constructor. After that, we finally invoke notifyDataSetChange(...)
and notifyItemRangeChanged(...)
method of the adapter to invalidate the data being update and reflect its changes in the recycler view.
Adding Custom Search View Functionality
As we've already discussed above, our airport app implements the search view rendered at the topmost of the app's main window, to perform an indexed search of flights data by a partial match. At this point, all that we have to do is to add the search functionality to the following custom search view. To do this, we implement onTextChanged
method in the main app's activity class searchable with button listener:
public class SearchableWithButtonListener implements View.OnClickListener, TextWatcher
{
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
// Instantiating the flights fragment object
FlightsFragment flightsFragment = (FlightsFragment)
m_FragmentMgr.findFragmentById(R.id.airport_fragment_container);
RecyclerView flightsRecyclerView = null;
ArrayList<AirportDataModel> DataSet, oldDataSet = null;
// Determining the currently select tab
TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);
if (tabLayout.getTabAt(0).isSelected()) {
// Instantiating the currently active recycler view's object
flightsRecyclerView =
flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;
// Retrieving a list of arrival flights
oldDataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
}
else if (tabLayout.getTabAt(1).isSelected()) {
// Instantiating the currently active recycler view's object
flightsRecyclerView =
flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;
// Retrieving a list of departure flights</span>
oldDataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
}
// Perform a check if the string is not empty
if (!charSequence.toString().isEmpty()) {
// If so instantiate the flights indexed search object
// and invoke doSearch method
// to obtain the list flights which data matches by the partial match
DataSet = new FlightsIndexedSearch().
doSearch(charSequence.toString(), oldDataSet);
if (DataSet.size() == 0) {
DataSet = oldDataSet;
}
}
else DataSet = oldDataSet;
// Instantiate the new adapter object and
// pass the new filtered dataset as argument
FlightsRecyclerAdapter recyclerAdapter
= new FlightsRecyclerAdapter(DataSet, getBaseContext());
// Setting up the new recycler view's adapter
flightsRecyclerView.setAdapter(recyclerAdapter);
// Reflect changes in recycler view
recyclerAdapter.notifyDataSetChanged();
recyclerAdapter.notifyItemRangeChanged(0, DataSet.size());
}
@Override
public void afterTextChanged(Editable editable) {
}
@Override
public void onClick(View view) {
if (!m_DrawerLayout.isDrawerOpen(GravityCompat.START))
m_DrawerLayout.openDrawer(GravityCompat.START);
}
}
When a user types in a text in the search view, the overridden event handling method onTextChanged
is invoked. In this method, we're determining the currently active recycler view and execute doSearch
method to obtain the list of filtered flight items by a partial match. After that, we're instantiating the new adapter and pass the dataset obtained to the following adapter. Finally, we're invalidating this data in the currently active recycler view. The fragment of code listed below contains the implementation of doSearch
method:
package com.epsilon.arthurvratz.airportapp;
import java.util.ArrayList;
import java.util.regex.Pattern;
public class FlightsIndexedSearch {
public ArrayList<AirportDataModel> doSearch(String text,
ArrayList<AirportDataModel> dataSet) {
// Instantinating the empty flights array list object
ArrayList<AirportDataModel> targetDataset = new ArrayList<>();
// Performing a linear search to find all flight items which data values
// match the specific pattern
for (int index = 0; index < dataSet.size(); index++) {
AirportDataModel currItem = dataSet.get(index);
// Applying search pattern to the flight destination string value
boolean dest = Pattern.compile(".*" + text + ".*",
Pattern.CASE_INSENSITIVE).matcher(currItem.m_Destination).matches();
// Applying search pattern to the airlines flight code string value
boolean flight = Pattern.compile(".*" + text + ".*",
Pattern.CASE_INSENSITIVE).matcher(currItem.m_Airlines.m_Flight).matches();
// Applying search pattern to the flight status string value
boolean status = Pattern.compile(".*" + text + ".*",
Pattern.CASE_INSENSITIVE).matcher(currItem.m_Status).matches();
// If one of these values matches the pattern add the current item to the
// target dataset
if (dest != false || flight != false || status != false) {
targetDataset.add(currItem);
}
}
return targetDataset;
}
}
Adding Bottom Navigation Bar Functionality
The functionality of the bottom navigation bar is much similar to the functionality provided to perform the flights indexed search. To provide this functionality, we must set the navigation item selected listener in the main app's activity class:
m_flightsNavigationView.setOnNavigationItemSelectedListener(
new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
FlightsFragment flightsFragment = (FlightsFragment)
m_FragmentMgr.findFragmentById(R.id.airport_fragment_container);
RecyclerView recyclerView = null;
ArrayList<AirportDataModel> dataSet = null;
FlightsRecyclerAdapter recyclerAdapter = null;
// Determining the currently selected tab
TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);
if (tabLayout.getTabAt(0).isSelected()) {
// Getting the currently active recycler view object
recyclerView = flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;
// Getting the dataset of the currently active recycle view
// (e.g. arrival flights dataset)
dataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
}
else if (tabLayout.getTabAt(1).isSelected()) {
// Getting the currently active recycler view object
recyclerView =
flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;
// Getting the dataset of the currently active recycle view
// (e.g. departure flights dataset
dataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
}
// Get current system time value
long curr_time = System.currentTimeMillis();
if (menuItem.getItemId() == R.id.flights_prev)
{
// Instantinating the new recycler adapter object
// and pass the filtered list of previous flights items
// returned by the filterByTime method
recyclerAdapter = new FlightsRecyclerAdapter
(m_AirportDataModel.filterByTime(dataSet,
curr_time - (long)3.6e+6 * 48, curr_time), getBaseContext());
}
else if (menuItem.getItemId() == R.id.flights_now)
{
// Instantinating the new recycler adapter object and
// pass the filtered list of current flights items
// returned by the filterByTime method
recyclerAdapter = new FlightsRecyclerAdapter
(m_AirportDataModel.filterByTime(dataSet,
curr_time - (long)3.6e+6 * 24, curr_time +
(long)3.6e+6 * 24), getBaseContext());
else if (menuItem.getItemId() == R.id.flights_next)
{
// Instantinating the new recycler adapter object and
// pass the filtered list of next flights items
// returned by the filterByTime method
recyclerAdapter = new FlightsRecyclerAdapter
(m_AirportDataModel.filterByTime(dataSet,
curr_time, curr_time + (long)3.6e+6 * 48), getBaseContext());
}
// Setting up the new recycler view's adapter
recyclerView.setAdapter(recyclerAdapter);
// Reflect changes in recycler view
recyclerAdapter.notifyDataSetChanged();
recyclerAdapter.notifyItemRangeChanged(0, dataSet.size());
return true;
}
});
In this method, we're first determining the currently active recycler view and receive its object. After that, we're performing a check if a user toggled a specific bottom navigation buttons and filtering out all flights that match the given time-line criteria by invoking filterByTime
method. Finally, we create a new recycler view adapter and pass the dataset to its constructor, invalidating the currently active recycler view.
Points of Interest
In this article, we've discussed about the several aspects of creating and developing an advanced Android application using various Android and Java programming language techniques including creating custom views and layouts, delivering navigation drawer-based apps, working with fragments and recycler views, implementing custom data adapters and controllers, etc.
History
- 2nd August, 2018 - The first revision of article was published...