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:
="1.0"="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:
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
}
}
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>
<style name="AppTheme" parent="Base.Theme.AppCompat.Light.DarkActionBar">
<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:
="1.0"="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:
="1.0"="utf-8"
<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:
="1.0"="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':
="1.0"="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:
="1.0"="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 {
}
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() {
((ViewGroup)m_SearchView.findViewById
(android.support.v7.appcompat.R.id.search_mag_icon).
getParent()).setBackgroundColor(Color.parseColor("#ffffff"));
this.setDefaultSearchIcon(); this.setupIconifiedByDefault();
m_SearchView.setQueryHint("TYPE HERE...");
m_SearchView.setQuery("", false); getRootView().requestFocus();
m_SearchView.findViewById(android.support.v7.appcompat.R.id.search_mag_icon).
setOnClickListener(new SearchableViewListener());
ViewGroup llSearchView = ((ViewGroup)m_SearchView.findViewById(
android.support.v7.appcompat.R.id.search_mag_icon).getParent());
EditText searchEditText = llSearchView.findViewById(
android.support.v7.appcompat.R.id.search_src_text);
searchEditText.setSelected(false);
searchEditText.setOnClickListener(new SearchableViewListener());
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) {
if (android.support.v7.appcompat.R.
id.search_mag_icon == view.getId()) {
if (!isDefaultIcon) {
setDefaultSearchIcon();
return;
}
m_ClickListener.onClick(view);
}
else setNavBackSearchIcon();
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
m_TextWatcherListener.beforeTextChanged(charSequence, i, i1, i2);
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
setNavBackSearchIcon();
m_TextWatcherListener.onTextChanged(charSequence, i, i1, i2);
}
@Override
public void afterTextChanged(Editable editable) {
if (editable.toString().isEmpty())
setDefaultSearchIcon();
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() {
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() {
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() {
if (this.isDefaultIcon == true) {
this.isDefaultIcon = false;
this.replaceSearchIcon(R.drawable.ic_arrow_back_black_24dp);
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) {
((ImageView)m_SearchView.findViewById
(android.support.v7.appcompat.R.id.search_mag_icon)).
setImageDrawable(m_Context.getDrawable(resDefaultIcon));
this.setupAnimation();
}
This method is used to set up the animation for the search view icon:
private void setupAnimation() {
final ImageView searchIconView = m_SearchView.findViewById(
android.support.v7.appcompat.R.id.search_mag_icon);
int searchIconWidth = searchIconView.getWidth();
int searchIconHeight = searchIconView.getHeight();
RotateAnimation searchIconAnimation = new RotateAnimation(0f, 360f,
searchIconWidth / 2, searchIconHeight / 2);
searchIconAnimation.setInterpolator(new LinearInterpolator());
searchIconAnimation.setRepeatCount(Animation.INFINITE);
searchIconAnimation.setDuration(700);
searchIconView.startAnimation(searchIconAnimation);
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:
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) {
m_ClickListener = clickListener;
}
public void setTextWatchListener(@Nullable TextWatcher textWatchListener) {
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);
m_DrawerLayout = findViewById(R.id.airport_drawer_layout);
m_navigationView = findViewById(R.id.airport_navigation_view);
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);
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) {
if (!m_DrawerLayout.isDrawerOpen(GravityCompat.START))
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
{
public boolean onNavigationItemSelected(MenuItem menuItem) {
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:
="1.0"="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)
{
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);
}
@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() {
}
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) {
View FlightsFragmentView =
inflater.inflate(R.layout.fragment_flights, container, false);
m_TabLayout = FlightsFragmentView.findViewById(R.id.flights_destination_tabs);
m_ViewPager = FlightsFragmentView.findViewById(R.id.flights_destination_pager);
FlightsDestPagerAdapter pagerAdapter = new FlightsDestPagerAdapter(
getChildFragmentManager(), m_TabLayout.getTabCount());
m_ArrivalsFragment = ArrivalsFragment.newInstance();
m_DeparturesFragment = DeparturesFragment.newInstance();
pagerAdapter.add(m_ArrivalsFragment);
pagerAdapter.add(m_DeparturesFragment);
m_ViewPager.setAdapter(pagerAdapter);
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 {
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) {
menuItem.setChecked(true);
m_FragmentTran = m_FragmentMgr.beginTransaction();
if (menuItem.getItemId() == .Rid.flights)
m_FragmentTran.replace(R.id.airport_fragment_container,
FlightsFragment.newInstance());
else if (menuItem.getItemId() == R.id.about) {}
m_FragmentTran.addToBackStack(null); m_FragmentTran.commit();
if (m_DrawerLayout.isDrawerOpen(GravityCompat.START))
m_DrawerLayout.closeDrawers();
return true;
}
public void setupInitialFragment()
{
if (m_FragmentMgr == null)
m_FragmentMgr = getSupportFragmentManager();
m_FragmentTran = m_FragmentMgr.beginTransaction();
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_DrawerLayout = findViewById(R.id.airport_drawer_layout);
m_navigationView = findViewById(R.id.airport_navigation_view);
m_flightsNavigationView = findViewById(R.id.flights_navigation);
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);
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
="1.0"="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
="1.0"="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() {
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) {
View ArrivalsView =
inflater.inflate(R.layout.fragment_arrivals, container, false);
m_FlightsFragment =
(FlightsFragment) this.getParentFragment();
m_ArrivalsRecyclerView =
ArrivalsView.findViewById(R.id.arrivals_recycler_view);
m_FlightsFragment.setupFlightsRecyclerView(m_ArrivalsRecyclerView, m_ArrivalsDataSet);
return ArrivalsView;
}
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 {
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() {
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) {
View DeparturesView =
inflater.inflate(R.layout.fragment_arrivals, container, false);
m_FlightsFragment =
(FlightsFragment) this.getParentFragment();
m_DeparturesRecyclerView =
DeparturesView.findViewById(R.id.arrivals_recycler_view);
m_FlightsFragment.setupFlightsRecyclerView
(m_DeparturesRecyclerView, m_DeparturesDataSet);
return DeparturesView;
}
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 {
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
="1.0"="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> {
private ArrayList<AirportDataModel> m_DataModel;
private Context m_context;
public static class ViewHolder extends RecyclerView.ViewHolder {
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);
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);
}
}
public FlightsRecyclerAdapter(ArrayList<AirportDataModel> m_dataModel, Context context) {
m_DataModel = m_dataModel; m_context = context;
}
@Override
public FlightsRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.flights_item, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
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));
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)
{
Animation flightAnimation = android.view.animation.
AnimationUtils.loadAnimation(m_context, R.anim.fade_interpolator);
flightAnimation.setDuration(700);
view.startAnimation(flightAnimation);
}
@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();
AirportFlightsDestData flightsData = new AirportFlightsDestData();
int flight_rnd_index = rand_obj.nextInt(
flightsData.m_DestCities.size() - 1);
String destCity = flightsData.m_DestCities.get(flight_rnd_index);
String destResId = flightsData.getFlagResourceByDestCity(destCity);
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";
airline_code += new StringBuilder().append(airline_code_let1).toString();
airline_code += new StringBuilder().append(airline_code_let2).toString();
String flight_code = "\0";
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();
flight_code = airline_code + " " + flight_code;
Airlines airlines = new Airlines(flightsData.m_airlinesResName.get(rand_obj.nextInt(
flightsData.m_airlinesResName.size() - 1)), flight_code);
String flight_status = flightsData.m_Status.get(
rand_obj.nextInt(flightsData.m_Status.size() - 1));
int time_hours_sign = rand_obj.nextInt(2);
int time_hours_offset = rand_obj.nextInt(48);
long currTimeMillis = System.currentTimeMillis();
if (time_hours_sign > 0)
currTimeMillis += time_hours_offset * 3.6e+6;
else currTimeMillis -= time_hours_offset * 3.6e+6;
return new AirportDataModel(currTimeMillis,
flight_status, destCity, destResId, airlines);
}
public ArrayList<AirportDataModel> InitModel(int numOfItems)
{
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)
{
long currTimeMillis = System.currentTimeMillis();
currTimeMillis += new Random().nextInt(48) * 3.6e+6;
for (int index = 0; index < dataSet.size(); index++) {
AirportDataModel item = dataSet.get(index);
if (item.m_Time <= currTimeMillis) {
dataSet.remove(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<>();
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;
for (int index = 0; index < m_DestCities.size(); index++) {
if (m_DestCities.get(index) == destCity) {
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() {
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;
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;
}
m_AirportDataModel.Simulate(dataSet);
FlightsRecyclerAdapter recyclerAdapter
= new FlightsRecyclerAdapter(dataSet, getBaseContext());
recyclerView.setAdapter(recyclerAdapter);
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) {
FlightsFragment flightsFragment = (FlightsFragment)
m_FragmentMgr.findFragmentById(R.id.airport_fragment_container);
RecyclerView flightsRecyclerView = null;
ArrayList<AirportDataModel> DataSet, oldDataSet = null;
TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);
if (tabLayout.getTabAt(0).isSelected()) {
flightsRecyclerView =
flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;
oldDataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
}
else if (tabLayout.getTabAt(1).isSelected()) {
flightsRecyclerView =
flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;
oldDataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
}
if (!charSequence.toString().isEmpty()) {
DataSet = new FlightsIndexedSearch().
doSearch(charSequence.toString(), oldDataSet);
if (DataSet.size() == 0) {
DataSet = oldDataSet;
}
}
else DataSet = oldDataSet;
FlightsRecyclerAdapter recyclerAdapter
= new FlightsRecyclerAdapter(DataSet, getBaseContext());
flightsRecyclerView.setAdapter(recyclerAdapter);
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) {
ArrayList<AirportDataModel> targetDataset = new ArrayList<>();
for (int index = 0; index < dataSet.size(); index++) {
AirportDataModel currItem = dataSet.get(index);
boolean dest = Pattern.compile(".*" + text + ".*",
Pattern.CASE_INSENSITIVE).matcher(currItem.m_Destination).matches();
boolean flight = Pattern.compile(".*" + text + ".*",
Pattern.CASE_INSENSITIVE).matcher(currItem.m_Airlines.m_Flight).matches();
boolean status = Pattern.compile(".*" + text + ".*",
Pattern.CASE_INSENSITIVE).matcher(currItem.m_Status).matches();
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;
TabLayout tabLayout = findViewById(R.id.flights_destination_tabs);
if (tabLayout.getTabAt(0).isSelected()) {
recyclerView = flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;
dataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
}
else if (tabLayout.getTabAt(1).isSelected()) {
recyclerView =
flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView;
dataSet = flightsFragment.m_DeparturesFragment.m_DeparturesDataSet;
}
long curr_time = System.currentTimeMillis();
if (menuItem.getItemId() == R.id.flights_prev)
{
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)
{
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)
{
recyclerAdapter = new FlightsRecyclerAdapter
(m_AirportDataModel.filterByTime(dataSet,
curr_time, curr_time + (long)3.6e+6 * 48), getBaseContext());
}
recyclerView.setAdapter(recyclerAdapter);
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...