Android编程示例:创建机场计划模拟器应用程序

时间:2023-03-08 19:37:12
Android编程示例:创建机场计划模拟器应用程序

在本文中,我们将演示如何使用Android Studio和Java编程语言创建一个示例Android应用程序,从“临时”实现高级响应用户界面的功能。本文中讨论的应用程序将实现机场航班时刻表模拟的功能。在开发生命周期中,我们将实现Android应用程序的响应式用户界面,用于呈现“到达”和“离开”航班的列表,并提供动态生成和更新实时模式中的航班信息的功能。

我们将大力强调几个Java语言编程方面,并深入研究允许我们提供高级Android应用程序的编程技术的数量,包括从一开始就创建响应app's drawer和navigation bar应用程序的方面,提供我们自己的custom views并且layouts,如custom search view bar with action button,覆盖默认功能通用app's action bar,保持tabbed layout,渲染recycler views ,不同于listviews或gridviews允许创建的自定义外观的项目lists由应用程序所呈现的数据,与创建各种布局多重嵌套fragments,使用 bottom navigation view等

除了应用程序的界面特定主题,我们还将了解如何创建一个用Java编写的高效代码来实现生成和操作数据内容的功能,以及如何提供操作数据的代码和应用程序的用户界面。

具体来说,我们将实施机场航班时刻表模拟器的功能,该模拟器生成随机航班数据集并通过在实时模式中过滤掉航班来模拟航班到达和延长时间线来操纵这些数据,动态更新航班列表被渲染。为此,我们将使用和讨论诸如使用Android应用程序的后台任务,使用计时器等主题。

背景

先决条件(在我们开始之前...)

在我们开始讨论之前,让我们花点时间仔细研究一下到目前为止我们特别需要的开发工具和库来构建和运行我们的第一个Android应用程序。

因为,我们即将使用Java编程语言来部署我们运行Android的第一个应用程序,我们必须安装Java SE。为此,我们需要从http://www.codesocang.com/下载并安装Java Standard Edition - SE平台。反过来,Java SE平台包含在PC上构建和运行用Java编写的代码所需的所有库和模块。

在我们成功安装Java SE平台之后,我们还需要正确安装IDE和创建Android应用程序项目所需的特定库,并构建运行我们正在部署的应用程序的代码。各种IDE,编程语言和库的数量,例如由Android开发社区授权的Microsoft Visual Studio / C#.NET Xamarin或Android Studio,可以有效地用于创建和部署Android应用程序。

在本文中,为了提供Android应用程序开发生命周期的效率,平台兼容性以及开发流程,我们将特别使用Android Studio和Java编程语言来实现此目的。

这就是为什么,在我们在之前的配置步骤中安装Java SE平台之后,需要并强烈建议在开发机器上下载并安装Android Studio(https://developer.android.com/studio/)。

我们可能已经注意到,安装的Android Studio包含许多开发工具,包括IDE,Java SDK和NDK库,Android系统模拟器,Gradle / Maven - java编译器的“make”实用程序,可以更轻松地编译和编译用Java编程语言编写的链接代码。

反过来,Android Studio的IDE是一个高效且响应迅速的工具,用于轻松创建和编辑Android源码以及实现基本应用程序功能的Java代码。

除了高效便捷的IDE之外,Android Studio软件包还包括为各种目标(手机,平板电脑,可穿戴设备,Android电视......)开发Android应用程序所需的Java SDK库。具体来说,Android Studio IDE允许通过SDK管理器下载和安装适用于各种Android系统版本的SDK,SDK Manager是Android Studio的一部分,或者可选地定期使用Java SDK发行版中的本机SDK管理器。

为了编译和链接正在创建的应用程序,Android Studio的软件包还包括上面提到的Gradle / Maven'make'实用程序。在Android Studio中创建我们的第一个Android应用程序项目时,Gradle组件已下载并配置为与Android Studio的IDE一起使用。每次,当我们构建和运行Android应用程序的项目时,Gradle实用程序正在执行编译和链接特定的任务,例如创建包含内置Android应用程序的apk包,可以在模拟器上运行或者一个Android设备。在开发生命周期中,由于已经创建和配置了项目,因此我们可以使用Gradle实用程序的多个版本,就像在本文的项目创建部分中讨论的那样。

为了能够在调试开发阶段运行应用程序,Android Studio还包括一个支持各种Android系统版本的Android设备模拟器,可通过Android Studio的模拟器管理器从Google和Android开发社区网站源码下载。在模拟器上运行应用程序与在目标Android设备上运行应用程序非常相似。

在本文的下一部分中,我们将演示如何在安装的Android Studio环境中创建我们的第一个Android应用程序项目。

摘要

创建您的第一个Android App项目

在我们成功满足上面讨论的所有安装和配置要求之后,我们要做的第一件事就是运行Android Studio并创建一个项目来实现我们的机场航班时刻表模拟Android应用程序功能。为此,我们将使用Android Studio主对话框切换启动新的Android Studio项目选项:

Android编程示例:创建机场计划模拟器应用程序

在此之后,Android项目创建对话框将出现在屏幕上:

Android编程示例:创建机场计划模拟器应用程序

在这个对话框中,我们必须指定一个应用程序名称(在这种情况下,它是` AirportApp`),公司域(例如,` epsilon.com`)来正确配置应用程序包,项目位置,特别是包name,在我们的例子中是` com.epsilon.airportapp`。在我们提供了创建项目所需的所有信息后,单击此对话框底部的下一个按钮。

在此步骤之后,我们必须正确选择并指定我们的应用程序的目标设备,包括正确的外形(“手机”或“平板电脑”),最小的SDK及其版本,以及Android系统发布版本:

Android编程示例:创建机场计划模拟器应用程序

在我们成功选择了目标设备和Android发布版本之后,我们还必须选择一种应用程序的活动。活动通常是java类实现功能,负责应用程序的主窗口创建,事件处理以及完成其他用户交互特定任务。实际上,扩展泛型Activity类或其他派生类的java 类是任何现有Android应用程序的主类:

Android编程示例:创建机场计划模拟器应用程序

在这种特殊情况下,我们开始我们的第一个Android应用程序开发生命周期,选择一个空活动作为我们的机场计划模拟器应用程序的主要活动。此外,我们将定制和增强默认的空活动,以提供执行机场计划模拟任务所需的功能。

Android应用程序创建阶段的最后一步是配置基于活动的java类别名,生成特定的活动布局,以及配置应用程序的向后兼容库。为此,我们必须继续下一个配置对话框:

Android编程示例:创建机场计划模拟器应用程序

在最后一步中,我指定一个应用程序基于活动的java类名称,该名称将对应于正在生成的特定活动布局xml文件名。此外,我们必须指定是否要提供应用程序与旧版Android的向后兼容性。

由于我们已经配置了应用程序的活动,因此在最后阶段生成特定项目并打开Android Studio的IDE主窗口:

Android编程示例:创建机场计划模拟器应用程序

在本文的下一部分中,我们将简要介绍使用Android Studio创建的Android应用程序的项目结构。

Android App的项目结构

在这一点上,让我们仔细看看应用程序项目创建后打开的Android Studio IDE主窗口左上角的应用程序解决方案树。通常,解决方案树显示正在创建的项目的内容,该内容与保存到特定位置的目录结构完全对应(例如,“ D:\ AirportApp ”)。

AndroidManifest.xml中

文件夹“清单”是显示在应用解决方案树顶部的第一个文件夹。它基本上只包含一个文件' AndroidManifest.xml '。以下文件主要包含运行正在创建的应用程序所需的xml格式的所有配置数据。AndroidManifest.xml文件具有以下结构,对于所有Android应用程序都完全相同:

<?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>

AndroidManifest.xml文件的第二行包含manifest标记,其属性提供命名空间和应用程序的包名称信息。它还包含一个嵌套标签application ,该标签具有定义标签,文本方向和创建的应用程序的一对图标的属性数。由应用程序标签的属性指定的图标和标签基本上显示在应用程序的主窗口中。应用程序标记还包含一个定义默认应用程序主题的属性(例如,android:theme="@style/AppTheme")。或者,我们可能希望修改现有的或向应用程序标记添加更多属性,以便提供应用程序主窗口的自定义外观和行为。例如,我们可能想要更改值android:theme属性,以便我们的应用程序将覆盖默认的泛型并使用其自己的应用程序操作栏实现。为此,我们需要将以下标记的值更改为android:theme="@style/AppTheme.NoActionBar".

通常,application标记具有嵌套标记的数量,例如activity标记,用于提供主应用程序活动的一组配置属性。默认情况下,activity标记只有一个属性,用于定义主应用程序活动的名称(例如android:name=".AirportActivity")。要修改应用程序的主要活动配置参数,我们可能需要向以下标记添加更多属性:

android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="orientation|screenSize|keyboard|keyboardHidden">

在这种特殊情况下,我们将以下配置属性添加到activity上面列出的机场计划模拟器应用主标签中。第一个属性是我们之前在application 上面标记中指定的属性的副本。以下属性用于指定将显示正在运行的应用程序中的默认通用应用程序操作栏。第二个属性android:windowSoftInputMode="stateHidden"用于指定在启动应用程序时不会自动呈现的软输入方法。最后一个属性 android:configChanges="orientation|screenSize|keyboard|keyboardHidden"提供应用程序覆盖的配置更改列表。这意味着应用程序将处理以下更改,而不是Android系统。具体来说,应用程序将处理屏幕旋转并根据当前屏幕方向呈现正确的界面布局变化(例如' portrait'或' landscape')。

应用程序标记还包含最内层嵌套标记的数量,例如intent-filter,action和category。在action和category里面的意图标签intent-filter标签指定的主应用程序的入口点。特别是,这些标签指定当前'.AirportActivity' 是主应用程序的活动:

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Gradle脚本

现在,让我们来看看位于应用解决方案树底部的' Gradle Scripts '兄弟。以下文件夹包含配置上一节中提到的gradle'make'实用程序所需的所有脚本文件,包括项目' '或' '模块的两个' build.gradle '文件实例。第一个build.gradle文件包含以下内容:AirportAppapp

// 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
}
以下文件是包含Gradle存储库的基本配置的非xml文件,包括其构建版本(例如 'com.android.tools.build:gradle:3.1.3')。在项目配置期间,以下文件的内容通常保持不变。
但是,第二个build.gradle文件特别感兴趣。第二个build.gradle文件基本上包含应用程序的项目模块依赖项的定义。例如:
apply plugin: 'com.android.application'

android {
compileSdkVersion
defaultConfig {
applicationId "com.epsilon.arthurvratz.airportapp"
minSdkVersion
targetSdkVersion
versionCode
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'
}
为了能够使用Android支持库,我们必须v.4,v.7,v.13将以下行添加到此文件的部分:RecyclerViewConstraintLayout dependencies
<span Courier New",Courier,mono; font-size: 12px; font-size-adjust: none; font-stretch: 100%; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; line-height: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: pre; word-spacing: 0px;">    
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'</span>
反过来,' gradle-wrapper.properties '和' local.properties '文件都是另一个特别的兴趣:
gradle-wrapper.properties:
#Thu Jul  :: EEST
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

在此文件中,我们可以指定gradle实用程序版本或Android SDK位置的绝对路径。为此,我们必须修改这两个文件的以下行:

distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
sdk.dir=C\:\\Android\AndroidStudio\SDK
注意:如果您的gradle更改的版本> gradle-4.6-all.zip,,那么你就还需要禁用“ 配置需求 ”中的“选项文件”>“ 设置 ”>“ 建立,实施,部署 “> 编译器 ”。

应用程序的活动和布局文件

在我们完全符合所有应用程序的项目配置步骤之后,让我们看看我们未来应用程序的活动java实现文件和主应用程序的布局xml文件。主应用程序的布局文件位于“ res / layout ”文件夹下,名称为“ activity_airport.xml ”。以下文件最初包含' android.support.constraint.ConstraintLayout'标记,这是空应用程序的默认布局。
要修改的主要应用程序的布局,并添加我们的Android应用程序的界面组件,如其他直列布局或控制(即“若干意见”),我们必须使用Android Studio的布局设计或手动编辑以下布局文件:

Android编程示例:创建机场计划模拟器应用程序

为了能够在Android Studio的设计器中编辑布局,您还必须修改可以位于应用程序项目的“ res / values ”文件夹中的“ styles.xml ” :
<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>
具体来说,您必须parent将' style'标签的' '属性值更改parent="Theme.AppCompat.Light.DarkActionBar"为  parent="Base.Theme.AppCompat.Light.DarkActionBar"。
以下布局是默认的空应用程序布局,将在讨论应用程序的开发生命周期期间进行更改。或者,我们可以通过手动编辑' activity_airport.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="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>
此外,我们将在本文的后续部分之一中提供有关如何使用约束布局来构建响应式应用程序界面的详细指南。
我们此时要讨论的最后一个方面是应用程序的主要活动实现文件' 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);
}
}
以下文件包含在第一个' com.epsilon.airportapp '文件夹中,并包含' AirportActivity'扩展泛型AppCompatActivity'类的java类的声明。最初,AirportActivity类只包含一个重写方法' OnCreate',实现将应用程序布局呈现为正在创建的应用程序的主内容视图的功能。以下方法实现了调用onCreate超类中的' '方法,或者接受主应用程序的布局resource-id' R.layout.activity_airport'的setContentView方法,并为主应用程序的布局提供了基本的呈现功能。将来,我们会修改' AirportActivity''

应用程序的主要布局Bluep rint

此时,我们的主要目标是创建机场计划模拟应用程序的主要布局设计草图。更具体地说,主应用程序的布局将具有以下外观:

Android编程示例:创建机场计划模拟器应用程序

从上图中可以看出,整个主要机场应用程序的布局包括'SearchView'最顶层的高级变体'TabLayout',其中将呈现两个到达和离开航班列表。每个选项卡将呈现一个'RecyclerView'显示航班列表,' BottomNavigationView'允许导航将在'昨天','现在'和'tommorow'发生的航班列表中。“TabLayout”和“RecyclerView”由特定片段布局呈现,这些布局在切换应用程序的抽屉导航菜单项或选择其中一个特定选项卡后显示。
主应用程序的布局主要基于' DrawerLayout'模式,这意味着应用程序的抽屉将在用户切换应用程序主窗口左上角的操作栏按钮时呈现。应用程序的抽屉定期可能包含基于'NavigationView',应用程序主菜单等的抽屉标题。 
在此之前,让我们回想一下,这不是项目创建向导生成的标准应用程序布局。此外,我们将讨论如何以编程方式实现机场应用程序的自定义布局。

设计应用程序的主要布局

现在,我们终于维护了机场应用程序的主要布局蓝图,现在是时候创建和编辑一个或多个应用程序的布局文件了。我们要修改的第一个文件是' activity_airport.xml '。由于我们的机场应用程序旨在拥有应用程序的抽屉,我们选择' DrawerLayout'作为主应用程序的布局类型:
<?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>
在这种情况下,我们android.support.v4.widget.DrawerLayout在activity_airport.xml文件中使用' '作为根标记。在此之后,我们还需要创建两个嵌套标签,例如' include'标签,它将包含单独文件' content_frame.xml'中包含的以下布局的另一部分,或' android.support.design.widget.NavigationView'标签,声明机场应用程序的标签抽屉布局。遗憾的是,由于使用了抽屉布局,我们无法使用Android Studio的布局设计器修改上面显示的布局,但我们可以使用Android Studio的IDE文本编辑器手动编辑此布局。
应用程序主要布局的包含片段存储在内容框架文件中,如下所示:
<?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>
在这个文件中,我们通常使用' android.support.constraint.ConstraintLayout'标签作为此布局的根标签。以下标记具有内联标记的数量,包括' LinearLayout',其中' android.support.v7.widget.SearchView'标记被声明',FrameLayout'实际上声明了一个框架,将以编程方式替换为特定的片段渲染' RecyclerView',显示一个航班列表,'BottomNavigationView'按时间过滤掉航班的渲染选项。由于我们使用约束布局作为整个内容框架的根,因此必须正确约束所有嵌套视图和布局。与之前的布局不同,可以使用Android Studio的布局设计器成功编辑内容框架布局。这就是我们选择是否编辑特定内容框架文件或使用布局设计器为以下布局中的所有视图提供特定约束的原因。 
在这种情况下,互连内容框架中视图的最佳方法是app:layout_constraintTop_toBottomOf向每个视图标记添加特定属性,例如' ',如上面的源代码所示。在这段代码中,我们从最上面的'LinearLayout'视图标签开始垂直和水平地向每个视图标签添加布局约束属性,以垂直方向链接所有这些属性。
在这一点上,让我们回到定义应用程序抽屉布局的代码片段。在' DrawerLayout'标签内部声明的另一个视图是' android.support.design.widget.NavigationView'。以下视图主要用于渲染应用程序的抽屉及其菜单,如上图所示。使用导航视图通常需要我们创建另一个应用程序的抽屉布局和特定菜单,为应用程序的抽屉菜单声明项目。
要创建这些布局,我们基本上需要在项目的' / res '文件夹中创建一个子文件夹,并创建名为' 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>
在这个文件中,我们必须声明' menu'标签,并在其中创建group项目的' '。在这种情况下,以下布局包含每个“航班”或“关于”菜单选项的两个项目组,显示在应用程序的抽屉中,标题下方。
另一个布局文件' nav_header_frame.xml '包含用户切换时在应用程序抽屉中呈现的布局:
<?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>
要创建应用程序的抽屉布局,我们将使用特定的' LinearLayout'标签。与其他布局(例如' CostraintLayout')不同,线性布局允许仅在垂直方向上定位所有视图,并且不需要在视图之间设置任何约束。 
要定义正确的抽屉布局,我们必须将以下视图标记放置在线性布局内,以及为应用程序的抽屉标题提供背景图像。为此,我们指定以下线性布局属性:' android:background="@drawable/airport_nav_header"'。通常,我们创建的线性布局将包含以下内联视图:
  • ' ImageView' - 用于显示机场应用程序的图标;
  • ' Space' - 在线性布局中创建特定视图之间的间隙;
  • ' TextView' - 打印机场应用程序的标题或作者的详细信息;

最后,由' NavigationView'及其蓝图呈现的应用程序的抽屉布局将具有以下外观:

Android编程示例:创建机场计划模拟器应用程序

在本文的下一部分中,我们将了解如何实现主要机场应用程序活动的功能。

使用操作按钮创建自定义SearchView

SearchView是出现在主应用程序窗口顶部的机场计划模拟器应用程序的第一个控件。此时,让我们回到他标记的'content_frame.xml片段,声明搜索视图位于以下布局文件的所有其他视图之前,由' :包裹起来:'.Tandroid.support.v7.widget.SearchViewLinearLayout<font color="#007000" face=""Segoe UI",Arial,Sans-Serif">'</font>

<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>
我们使用线性布局用于确保应用程序启动后搜索视图不会获得焦点。
由于我们SearchView在内容框架中声明了' '标签,因此我们的目标是通过在java中实现特定代码来提供功能和行为(例如,使搜索视图响应),这些代码将即时处理和处理事件。我们的搜索视图。
我们可能知道,在这个项目中我们不会使用通用搜索视图和应用程序栏,但会创建我们自己的自定义搜索视图,它结合了通用搜索视图的基本功能和应用程序的操作栏。
要使用操作按钮创建自定义搜索视图,我们需要创建一个新的java类,并将其命名为“ SearchableWithButtonView扩展泛型View”类:
public class SearchableWithButtonView extends View {

         // SearchView basic functionality implementation java-code goes here...

}
在本课程中,我们需要实现以下方法。setupSearchableWithButton()是我们需要实现的第一个方法,以便为我们的自定义搜索视图提供特定的外观和行为:
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(); // Instantinate 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()); // Instantinate the search view object
ViewGroup llSearchView = ((ViewGroup)m_SearchView.findViewById(
android.support.v7.appcompat.R.id.search_mag_icon).getParent()); // Instantinate 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());
}
在此方法中,我们通过修改背景颜色,搜索视图按钮图标,在应用程序启动时从搜索视图中删除默认选择和焦点来更改通用搜索视图的外观和行为,并且还设置处理程序(即侦听器)各种搜索视图事件,例如单击用作应用程序主操作按钮的搜索视图按钮,文本编辑和文本可编辑视图点击等。
这些事件处理程序实现为' SearchableWithButtonView '子类,在其中声明:
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) {
<span Segoe UI",Arial,Sans-Serif; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;"> // Set navigation-back icon and invoke the onTextChanged(...) method </span>
<span Segoe UI",Arial,Sans-Serif; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;">// from app's activity class (e.g. its parent)</span>
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);
}
}

另外,  “ SearchableWithButtonView”类有以下方法:

下面列出的方法将自定义搜索视图的外观更改为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);
}

以下方法使用应用程序操作栏按钮的自定义图标替换通用搜索视图的默认图标

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);
}

以下方法使用导航后退图标替换默认操作栏按钮图标:

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();
}
}

以下方法将默认搜索视图按钮图标替换为从应用程序资源中检索到的图标:

private void replaceSearchIcon(int resDefaultIcon) {
// Instantinate 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();
}

此方法用于设置搜索视图图标的动画

private void setupAnimation() {

    // Instantinate 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(); // Instantinate RotateAnimation class object and specify the rotation params
RotateAnimation searchIconAnimation = new RotateAnimation(0f, 360f,
searchIconWidth / , searchIconHeight / );
// Set animation interpolator
searchIconAnimation.setInterpolator(new LinearInterpolator());
// Set animation repeat count
searchIconAnimation.setRepeatCount(Animation.INFINITE);
// Set animation duration
searchIconAnimation.setDuration(); // 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);
}
}, ); }

通过使用以下方法,我们覆盖了与搜索视图对象一起使用的findViewById(...)方法的基本功能:

// Override the default findViewById method to be used to instantinate
// search view object
private SearchView findSearchViewById(int resId) {
return ((Activity)m_Context).findViewById(resId);
}

通过调用这两个方法,我们设置了主应用程序的activity类中使用的click事件监听器和文本更改事件监听器:

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;
}

现在,由于我们已经使用操作按钮实现了自定义搜索视图,现在是时候将其功能添加到主应用程序的活动中,如下所示:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_airport); // Instantinating the drawer layout object
m_DrawerLayout = findViewById(R.id.airport_drawer_layout);
// Instantinating the navigation view object
m_navigationView = findViewById(R.id.airport_navigation_view); // Instantinating 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); // ...

在overriden onCreate方法中,我们通常执行抽屉布局和导航视图对象的即时化,设置我们的自定义搜索视图并添加特定的事件处理程序。要处理各种搜索视图的事件,我们必须声明一个子类' SearchableWithButtonListener'实现' View.OnClickListener'或' TextWatcher'事件处理泛型类:

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);
}
}

通过本文下一节中讨论的以下类的方法实现的功能。在这种情况下,我们将仅讨论onClick(...)此类中方法的一种实现。以下方法通过调用方法实现应用程序的抽屉打开功能DrawerLayout.openDrawer(...) 。

正如我们在openDrawer(...)处理自定义操作栏单击事件后触发该方法时已经讨论的那样,应用程序的抽屉打开,显示应用程序的主菜单。此时我们还必须通过调用'm_navigationView.setNavigationItemSelectedListener(m_NavigationBarListener)'接受侦听器类对象作为其单个参数的方法来提供菜单项单击事件处理。以下代码实现了重写导航菜单项click事件监听器类:

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;
}
}

创建选项卡式应用程序的布局

正如我们已经讨论过的,机场应用程序旨在响应用户的输入并显示各种内容,具体取决于应用程序的抽屉导航菜单中的选项或用户切换的选项卡。特别是在应用程序的抽屉导航菜单中切换“飞行”菜单项后,它通常会呈现选项卡式布局。每个选项卡基本上显示由回收者视图呈现的航班列表。为实现这一点,我们将使用片段。' Fragment'是应用程序布局的动态创建和渲染部分,包含其他布局或视图,或两者兼而有之。

在这种情况下,到目前为止我们要做的是创建特定的片段布局和我们自己的实现内容呈现功能的java类。正如我们已经讨论过的那样,两个标签“到达”和“离开”将出现在主应用程序的窗口中。在每个选项卡中,我们将呈现' RecyclerView'显示已安排的航班列表。为了提供选项卡式布局功能,我们将使用' TabbedLayout'inside inside' LinearLayout',这是FlightsFragment当用户在应用程序的抽屉导航菜单中切换第一个菜单项'flight'时显示的根布局。航班片段布局在' res / layout / fragment_flights.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" 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>
在航班片段线性布局中,我们声明了两个标签:' android.support.design.widget.TabLayout'和' android.support.v4.view.ViewPager'。第一个标签基本上定义了标签式布局,其中包含两个选项卡,用于“到达”或“离开”航班渲染,显示在主应用程序窗口的搜索视图下方。通过声明第二个标签' ViewPager'我们提供了在一个整个屏幕之间滑动渲染到另一个屏幕的功能。
由于选项卡式布局和视图分页器呈现为片段,我们必须创建一个单独的java类FlightsFragment“扩展泛型android.support.v4.app.Fragment”类:
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); // Instantinating the linear layout manager object
m_LayoutManager = new LinearLayoutManager(getContext()); // Setting up the recycler view's layout manager
m_RecyclerView.setLayoutManager(m_LayoutManager); // Instantinating 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 <span Segoe UI",Arial,Sans-Serif; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;">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); // Instantinating the tab layout object
m_TabLayout = FlightsFragmentView.findViewById(R.id.flights_destination_tabs); // Instantinating view pager object
m_ViewPager = FlightsFragmentView.findViewById(R.id.flights_destination_pager); // Instantinating the tab layout's pager adapter
FlightsDestPagerAdapter pagerAdapter = new FlightsDestPagerAdapter(
getChildFragmentManager(), m_TabLayout.getTabCount()); // Instantinating the arrivals fragment object
m_ArrivalsFragment = ArrivalsFragment.newInstance(); // Instantinating 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);
}
}
为了实现航班片段功能,我们实际上定义了两个java类。第一个类' FlightsFragmentImpl'扩展了泛型' android.support.v4.app.Fragment '并实现了下面讨论的OnFragmentInteractionListener' ArrivalsFragment'和' DepartureFragment'类的' '功能。下面的类只实现了一个方法' setupFlightsRecyclerView(...)',它接受了一个回收器视图对象或ArrayList本文后面讨论的数据集对象的两个参数。此方法的主要目的是设置回收器视图的适配器,该适配器用于保存在其中一个选定选项卡中显示的回收器视图中呈现的数据。
另一个类' FlightsFragment'扩展' FlightsFragmentImpl '的功能,并OnCreateView通过向视图页面适配器添加特定的到达和离开片段对象,提供动态设置选项卡布局和在'覆盖方法中查看寻呼机的基本功能。' FlightsDestPagerAdapter'java-class实现了view pager适配器的基本功能:
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();
}
}
以下类的实现主要基于使用' ArrayList<Fragment>'用于存储通用' Fragment'类对象数组的功能。
最后,要渲染航班片段,我们必须覆盖onNavigationItemSelected(...)' AirportActivity.NavigationBarListener'类中的' '方法。以下方法主要用于处理应用程序抽屉导航菜单中的事件,并具有以下实现:
private class NavigationBarListener implements
NavigationView.OnNavigationItemSelectedListener
{
public boolean onNavigationItemSelected(MenuItem menuItem) {
// set item as selected to persist highlight
menuItem.setChecked(true); // Instantinate the fragment manager transaction coordinator object
m_FragmentTran = m_FragmentMgr.beginTransaction(); // Perform a check if the flights menu item was selected
if (menuItem.getItemId() == R.id.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) // Instantinate 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();
}
}
以下类还实现了另一个方法' setupInitialFragment(...)',用于在应用程序的主活动代码中调用初始片段时,在重写方法' OnCreate(...)'中,当主应用程序的活动被即时化时:
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(); //...
}
在本文的下一部分中,我们将讨论如何在航班片段内呈现​​回收者视图,显示“到达”或“离开”航班的列表。

在RecyclerView中渲染航班

在回收站视图中渲染航班列表是我们即将在本文中讨论的最终机场应用程序的GUI主题。正如我们已经知道的那样,我们的机场应用程序显示两个“到达”或“离开”航班列表,并以编程方式以类似方式执行。为了呈现航班列表,我们所要做的就是创建两个片段,这些片段将呈现到达航班或离港航班的回收者视图:

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>
我们还创建了两个' ArrivalsFragment'和' DeparturesFragment'的java类,它们实现了上面列出的那些片段的功能:
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() { // Instatinate the airport app's data model and generate set of random flights
m_ArrivalsDataSet = new AirportDataModel().InitModel();
} 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(); // Instantinate 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() {
// Instatinate the airport app's data model and generate set of random flights
m_DeparturesDataSet = new AirportDataModel().InitModel();
} 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</span>
m_FlightsFragment =
(FlightsFragment) this.getParentFragment(); // Instantinate departures recycler view object</span>
m_DeparturesRecyclerView =
DeparturesView.findViewById(R.id.arrivals_recycler_view); // Instantinate arrivals recycler view object</span>
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);
}
}
在这两个java类中,我们onCreateView(...)通过膨胀航班片段布局对象来调用setupFlightsRecyclerView(...)方法的功能,以调用' FlightFragmentImpl'类的方法:
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);
}
使用回收站视图呈现航班列表的另一个重要方面是回收站视图适配器的实施。由于“到达”或“离开”航班的回收者视图都以类似的方式执行数据呈现,我们只需要为特定的回收站视图实施单个航班回收站视图适配器。
此外,我们必须为每个航班项目创建一个布局,显示特定航班的信息,如时间,目的地,航空公司代码,航空公司徽标,国家/地区标志和状态:
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); // Instantinate 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(); // 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()); }
}
' FlightsRecyclerViewAdapter'是扩展泛型' RecyclerView.Adapter<FlightsRecyclerAdapter.ViewHolder>' 专业化功能的java类。它实现了将数据绑定到回收器视图所需的基本功能。具体来说,它实现了一个子java类“ViewHolder”,它负责通过调用flight_item布局中的每个视图的对象来呈现每个航班项目。要呈现特定项目,应用程序正在调用onBindViewHolder(...)overriden方法,以编程方式设置要由flights_item布局内的各种视图显示的特定值。

添加航班时刻表模拟功能

正如我们在本文最开始讨论的那样,除了用于呈现特定航班数据的用户界面外,我们还必须实施负责生成航班数据和时间线模拟的功能。在这个应用程序中,我们使用的模式类似于在其他编程语言和框架中经常使用的模型 - 视图 - 控制器。具体来说,在这种特殊情况下,我们将我们的数据模型与执行实际飞行数据集操作的某个数据控制器相结合:
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(); // Instantinate 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() - ); // 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()).toString();
flight_code += new StringBuilder().append(rand_obj.nextInt()).toString();
flight_code += new StringBuilder().append(rand_obj.nextInt()).toString();
flight_code += new StringBuilder().append(rand_obj.nextInt()).toString(); // Construct a string containing the full airline code flight_code = airline_code + " " + flight_code; // Instantinate and construct airlines data object Airlines airlines = new Airlines(flightsData.m_airlinesResName.get(rand_obj.nextInt(
flightsData.m_airlinesResName.size() - )), flight_code); // Get random status string value String flight_status = flightsData.m_Status.get(
rand_obj.nextInt(flightsData.m_Status.size() - )); // Generate a random flight time int time_hours_sign = rand_obj.nextInt(); // Generate an hours offset from the current system time
int time_hours_offset = rand_obj.nextInt(); // Get the current system time long currTimeMillis = System.currentTimeMillis(); // Determine the random flight time in ms if (time_hours_sign > )
currTimeMillis += time_hours_offset * 3.6e+6;
else currTimeMillis -= time_hours_offset * 3.6e+6; // Instantinate 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 = ; 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() * 3.6e+6; // Perform a linear search to filter out all flights that already have taken place
for (int index = ; 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 = ; 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;
}
}
在本课程中,我们实现了生成和操纵航班数据所需的所有方法。我们需要做的第一件事就是实现一个getRandomFlight(...)生成随机飞行数据的方法。以下方法基本上依赖于使用静态声明的数据。为此,我们创建了另一个类,它还定义和操作特定于航班的数据:
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 = -; // Performing a linear search to find the dest city index
for (int index = ; 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 = ; country < m_CountryCityRelTable.size(); country++) {
int[] cityIds = m_CountryCityRelTable.get(country).m_cityIds;
for (int city = ; 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", "*",
"Shanghai", "Paris", "Amsterdam", "Dallas", "Guangdong", "Frankfurt", "Istanbul", "Delhi", "Tangerang",
"Changi", "Incheon", "Denver", "New York", "San Francisco", "Madrid", "Las Vegas", "Barcelona", "Mumbai", "Torronto"); 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(, new int[] { , , , }),
new CountryCityRel(, new int[] { , , , , , , , }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { , , }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { }),
new CountryCityRel(, new int[] { , }),
new CountryCityRel(, new int[] { })); public List<String> m_Status = Arrays.asList("Check-In", "Canceled", "Expected", "Delayed");
}
以下类包含一组List声明和静态初始化的通用“ 对象”,用于保存航班目的地城市的各种数据,以及包含航空公司徽标和国家/地区标志的资源名称列表。此外,以下类具有' getFlagResourceByDestCity'方法的声明,该方法用于通过目标城市的名称检索特定资源名称上的数据。
在机场数据模型类中,我们必须声明特定的数据字段变量来保存每个航班上的数据:
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;
}
此外,在这个类中,我们实现下面的方法包括getRandomFlight(...),InitModel(...),Simulate(...)和filterByTime(...)。正如我们已经讨论的那样,getRandomFlight方法用于为随机飞行生成随机数据,稍后将添加到灯光列表中。为此,我们分别在ArrivalsFragment和DeparturesFragment类构造函数中调用InitModel方法,以便这些构造函数中的每一个都将立即启动机场数据模型类对象,调用此方法并接收包含到达列表的数组列表对象的自己的副本或者出发航班:
public ArrivalsFragment() {
m_ArrivalsDataSet = new AirportDataModel().InitModel();
}
要在航班时刻表模拟过程中提供航班动态更新列表,我们必须覆盖应用程序活动类的默认onResume方法,如下所示:
@Override
protected void onResume() {
super.onResume();
this.findViewById(R.id.search_bar).requestFocus(); startSimulation();
}
在这个方法中,我们调用另一个Simulate(...)方法来启动模拟过程:
public void Simulate() {
simTask = new TimerTask() {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() { // Instantinate 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().isSelected()) {
dataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet;
recyclerView = flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView;
} else if (tabLayout.getTabAt().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(, dataSet.size());
}
});
}
};
}
在上面列出的这个方法中,我们创建了一个由timer实例生成的计时器任务线程:
private void startSimulation()
{
this.Simulate(); new Timer().schedule(simTask, , );
}
每次系统计时器调度时,都会调用run(...)方法。在下面的方法中,我们调用上面列出的机场数据模型的Simulate(...)方法。以下方法确定系统时间并过滤掉时间值小于当前系统时间的所有航班项目。之后我们创建一个前面讨论过的Recycler视图控制器的新实例,并将新的航班列表作为其构造函数的参数传递给它。之后,我们最终调用适配器的notifyDataSetChange(...)和notifyItemRangeChanged(...)方法,使正在更新的数据无效并在Recycler视图中反映其更改。

添加自定义搜索视图功能

正如我们上面已经讨论过的,我们的机场应用程序实现了在应用程序主窗口最顶层呈现的搜索视图,以通过部分匹配执行航班数据的索引搜索。此时,我们要做的就是将搜索功能添加到以下自定义搜索视图中。为此,我们onTextChanged在主应用程序的活动类中实现方法,可以使用按钮监听器进行搜索:

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) { // Instantinating 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().isSelected()) { // Instantinating the currently active recycler view's object
flightsRecyclerView = flightsFragment.m_ArrivalsFragment.m_ArrivalsRecyclerView; // Retriving a list of arrival flights
oldDataSet = flightsFragment.m_ArrivalsFragment.m_ArrivalsDataSet; } else if (tabLayout.getTabAt().isSelected()) {
<span Segoe UI",Arial,Sans-Serif; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;">
// Instantinating the currently active recycler view's object</span>
flightsRecyclerView = flightsFragment.m_DeparturesFragment.m_DeparturesRecyclerView; <span Segoe UI",Arial,Sans-Serif; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;"> // Retriving 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 instantinate 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() == ) {
DataSet = oldDataSet;
}
} else DataSet = oldDataSet; // Instantinate the new adapter object and pass the new filtered dataset as argument FlightsRecyclerAdapter recyclerAdapter
= new FlightsRecyclerAdapter(DataSet, getBaseContext()); <span Segoe UI",Arial,Sans-Serif; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; line-height: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;"> // Setting up the new recycler view's adapter</span> flightsRecyclerView.setAdapter(recyclerAdapter); <span Segoe UI",Arial,Sans-Serif; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-shadow: none; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px;"> // Reflect changes in recycler view</span> recyclerAdapter.notifyDataSetChanged(); recyclerAdapter.notifyItemRangeChanged(, 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);
}
}
当用户在搜索视图中键入文本时,将onTextChanged 调用overriden事件处理方法。在此方法中,我们确定当前活动的回收器视图并执行doSearch方法以通过部分匹配获得已过滤的航班物品列表。之后,我们即时启动新适配器并将获取的数据集传递给以下适配器。最后,我们在当前活动的回收站视图中使这些数据无效。下面列出的代码片段包含doSearch方法的实现:
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 = ; 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;
}
}

添加底部导航栏功能

底部导航栏的功能与为执行航班索引搜索而提供的功能非常相似。要提供此功能,我们必须在主应用程序的活动类中设置选定侦听器的导航项:

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().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().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 * , 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 * , curr_time + (long)3.6e+6 * ), 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 * ), getBaseContext());
} // Setting up the new recycler view's adapter recyclerView.setAdapter(recyclerAdapter); // Reflect changes in recycler view recyclerAdapter.notifyDataSetChanged();
recyclerAdapter.notifyItemRangeChanged(, dataSet.size()); return true;
}
});

在这种方法中,我们首先确定当前活动的回收器视图并接收其对象。之后,我们正在检查用户是否切换了特定的底部导航按钮,并通过调用filterByTime方法过滤掉与给定时间线条件匹配的所有航班。最后,我们创建一个新的回收器视图适配器并将数据集传递给它的构造函数,使当前活动的回收器视图无效。

兴趣点

在本文中,我们讨论了使用各种Android和Java编程语言技术创建和开发高级Android应用程序的几个方面,包括创建自定义视图和布局,提供基于导航抽屉的应用程序,处理片段和回收器视图,实现自定义数据适配器和控制器等