在本文中,我们将演示如何使用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项目创建对话框将出现在屏幕上:
在这个对话框中,我们必须指定一个应用程序名称(在这种情况下,它是` AirportApp`),公司域(例如,` epsilon.com`)来正确配置应用程序包,项目位置,特别是包name,在我们的例子中是` com.epsilon.airportapp`。在我们提供了创建项目所需的所有信息后,单击此对话框底部的下一个按钮。
在此步骤之后,我们必须正确选择并指定我们的应用程序的目标设备,包括正确的外形(“手机”或“平板电脑”),最小的SDK及其版本,以及Android系统发布版本:
在我们成功选择了目标设备和Android发布版本之后,我们还必须选择一种应用程序的活动。活动通常是java类实现功能,负责应用程序的主窗口创建,事件处理以及完成其他用户交互特定任务。实际上,扩展泛型Activity类或其他派生类的java 类是任何现有Android应用程序的主类:
在这种特殊情况下,我们开始我们的第一个Android应用程序开发生命周期,选择一个空活动作为我们的机场计划模拟器应用程序的主要活动。此外,我们将定制和增强默认的空活动,以提供执行机场计划模拟任务所需的功能。
Android应用程序创建阶段的最后一步是配置基于活动的java类别名,生成特定的活动布局,以及配置应用程序的向后兼容库。为此,我们必须继续下一个配置对话框:
在最后一步中,我指定一个应用程序基于活动的java类名称,该名称将对应于正在生成的特定活动布局xml文件名。此外,我们必须指定是否要提供应用程序与旧版Android的向后兼容性。
由于我们已经配置了应用程序的活动,因此在最后阶段生成特定项目并打开Android Studio的IDE主窗口:
在本文的下一部分中,我们将简要介绍使用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
}
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'
}
<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>
#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
## 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
应用程序的活动和布局文件
<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>
<?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>
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);
}
}
应用程序的主要布局Bluep rint
此时,我们的主要目标是创建机场计划模拟应用程序的主要布局设计草图。更具体地说,主应用程序的布局将具有以下外观:
设计应用程序的主要布局
<?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>
<?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>
<?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>
<?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>
- ' ImageView' - 用于显示机场应用程序的图标;
- ' Space' - 在线性布局中创建特定视图之间的间隙;
- ' TextView' - 打印机场应用程序的标题或作者的详细信息;
最后,由' NavigationView'及其蓝图呈现的应用程序的抽屉布局将具有以下外观:
使用操作按钮创建自定义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>
public class SearchableWithButtonView extends View { // SearchView basic functionality implementation java-code goes here... }
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());
}
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>
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) { }
}
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);
}
}
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();
}
}
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();
}
}
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>
<?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>
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);
}
}
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);
}
<?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>
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()); }
}
添加航班时刻表模拟功能
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;
}
}
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");
}
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 ArrivalsFragment() {
m_ArrivalsDataSet = new AirportDataModel().InitModel();
}
@Override
protected void onResume() {
super.onResume();
this.findViewById(R.id.search_bar).requestFocus(); startSimulation();
}
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());
}
});
}
};
}
private void startSimulation()
{
this.Simulate(); new Timer().schedule(simTask, , );
}
添加自定义搜索视图功能
正如我们上面已经讨论过的,我们的机场应用程序实现了在应用程序主窗口最顶层呈现的搜索视图,以通过部分匹配执行航班数据的索引搜索。此时,我们要做的就是将搜索功能添加到以下自定义搜索视图中。为此,我们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);
}
}
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应用程序的几个方面,包括创建自定义视图和布局,提供基于导航抽屉的应用程序,处理片段和回收器视图,实现自定义数据适配器和控制器等