这篇文章分享个人 Android 开发(入门)课程 的第七个和第八个实战项目:书籍列表应用和新闻应用。这两个项目都托管在个人 GitHub 上,分别是 BookListing 和 NewsApp 这两个 Repository,项目介绍已详细写在 README 上,欢迎你们 star 和 fork。html
这两个实战项目的主要目的是练习从 Web API 获取应用数据,不过在实际 coding 的过程当中使用了不少其它有意思的 Android 组件,这篇文章就逐个分享给你们。文章内容不会按应用的开发流程进行,各部份内容相对独立,你们能够利用浏览器的查找 (cmd/ctrl+F) 功能按需取用。为了精简篇幅,文中的代码有删减,请以 GitHub 中的代码为准。java
Android 提供了 SwipeRefreshLayout 类实现下拉刷新的手势操做,在 BookListing 和 NewsApp 这两个应用中都使用了 SwipeRefreshLayout。例以下面的 XML 代码,应用的主要内容显示在 RecyclerView 中,要想实现它的下拉刷新功能,须要将 SwipeRefreshLayout 做为它的父视图 (Parent View),可是 SwipeRefreshLayout 只能有一个子视图,因此在 RecyclerView 以外还须要用 RelativeLayout 这个 ViewGroup 包括。另外,SwipeRefreshLayout 是由 Android 支持库提供的,因此使用前确保在项目的 Gradle 中添加了正确的依赖库。node
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center" />
</RelativeLayout>
</android.support.v4.widget.SwipeRefreshLayout>
复制代码
SwipeRefreshLayout 的 ID 设置为 swipe_container,用于在 Java 中查找这个 Android 组件,并设置监听器实现具体的刷新操做。例以下面的 Java 代码,在 onCreate
中设置 OnRefreshListener
监听器,并在其中 override onRefresh
method,它会在用户完成下拉手势后调用,因此这里就是刷新应用内容须要执行的代码。另外,刷新动画的颜色序列能够在 setColorSchemeResources
中设置。android
SwipeRefreshLayout swipeContainer = findViewById(R.id.swipe_container);
swipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// ToDo: Handles the pull to refresh event here.
}
});
// Configure the refreshing colors.
swipeContainer.setColorSchemeResources(
android.R.color.holo_blue_light,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
复制代码
SwipeRefreshLayout 的刷新动画一般由用户的下拉手势触发,应用在完成刷新操做后中止刷新动画,经过设置如下 method 实现:git
swipeContainer.setRefreshing(false);
复制代码
若是设置 setRefreshing
为 true
就能够主动开始刷新动画,因此 SwipeRefreshLayout 也能够用做加载指示符 (Loading Indicator),在加载数据的时候开始刷新动画,数据加载完成后中止刷新动画,在 BookListing 和 NewsApp 这两个应用中都是这么作的。github
更多 SwipeRefreshLayout 内容能够参考这个 CodePath 教程。编程
Navigation Drawer 是 Android 应用中一种经常使用的导航模式,在 NewsApp 中用它来切换不一样主题的新闻。使用 Android Studio 为应用添加 Navigation Drawer 很是简单,只须要在新建 Activity 时选择 Navigation Drawer Activity 就会自动建立好不少样板代码 (Boilerplate Code),样式符合 Material Design 风格,开发者仅需根据需求修改。以 NewsApp 为例:浏览器
In activity_main.xml缓存
<android.support.v4.widget.DrawerLayout
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:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer" />
</android.support.v4.widget.DrawerLayout>
复制代码
match_parent
,由于 Navigation Drawer 一般是隐藏的,不占用屏幕空间。android:layout_gravity
属性,即设置 Navigation Drawer 的呼出方向,一般是从左边滑出。这里设置为 start
而不是 left
,是由于支持了从右至左 (RTL) 的设计语言,例如用户设备为 RTL 风格时,Navigation Drawer 是从右边滑出的。match_parent
,宽度设置为 wrap_content
,实现抽屉的画面效果,并且一般宽度不会大于 320dp 以保证在抽屉打开时,部分应用内容仍可见。app:headerLayout
属性设置)和 Menu(经过 app:menu
属性设置)。注意二者的文件路径不一样。tools:openDrawer
能够利用 DesignTime Layout Attributes 实时预览 Navigation Drawer 的显示效果。设置好 Navigation Drawer 的布局后,接下来就在 Java 中初始化:bash
In MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
DrawerLayout drawer = findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.addDrawerListener(toggle);
toggle.syncState();
NavigationView navigationView = findViewById(R.id.nav_view);
navigationView.setCheckedItem(R.id.nav_overview);
navigationView.setNavigationItemSelectedListener(this);
...
}
复制代码
NavigationItemSelectedListener
设置为 this
表示 MainActivity 是实现这个监听器接口的类。例如在 NewsApp 中,在 MainActivity 中 override onNavigationItemSelected
method 处理 item 的选中事件。In MainActivity.java
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
...
@Override
public boolean onNavigationItemSelected(MenuItem item) {
// Handle navigation view item clicks here.
Toolbar toolbar = findViewById(R.id.toolbar);
switch (item.getItemId()) {
case R.id.nav_overview:
toolbar.setTitle(R.string.app_name);
section = null;
break;
case R.id.nav_news:
toolbar.setTitle(R.string.menu_news);
section = "news";
break;
case R.id.nav_opinion:
toolbar.setTitle(R.string.menu_opinion);
section = "commentisfree";
break;
default:
Log.e(LOG_TAG, "Something wrong with navigation drawer items.");
}
// Close navigation drawer after handling item click event.
DrawerLayout drawer = findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
return true;
}
复制代码
NavigationItemSelectedListener
接口的类,因此在类名后面须要添加 implements
参数。除此以外,还须要修改 onBackPressed
method 来指定当 Navigation Drawer 打开时,用户点击“返回”按钮 (Back buttons) 时的行为。
@Override
public void onBackPressed() {
DrawerLayout drawer = findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else {
super.onBackPressed();
}
}
复制代码
当用户在Navigation Drawer 打开时点击“返回”按钮的操做应该是关闭 Navigation Drawer。这部分代码是由 Android Studio 自动生成的。
SearchView 是一种 Android 组件,至关于在应用栏放入一个 EditText,提供了不少搜索相关的功能,例如显示候选词等。在 BookListing App 中,使用 SearchView 来获取用户输入的搜索关键词,用于向 Web API 发送请求。
1、提供 menu 资源
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_search"
android:icon="@android:drawable/ic_menu_search"
android:title="@string/search_title"
app:actionViewClass="android.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView"
android:orderInCategory="1" />
</menu>
复制代码
android:icon
属性设置 SearchView 出如今应用栏的图标。android:title
属性设置 SearchView 的标题。若未设置 SearchView 的图标时,就会在应用栏显示它的标题;用户长按图标时也会弹出标题文本消息。app:showAsAction
属性设置 SearchView 的显示策略,其中 ifRoom
表示SearchView 图标仅在应用栏有空间时才显示,不然会显示在溢出菜单 (Overflow Menu) 中;collapseActionView
表示 SearchView 会包含在一个二级菜单中。android:orderInCategory
属性设置 SearchView 的显示优先级,数字越小优先级越高。在应用栏有多个 item 时,若是它们的 app:showAsAction
属性都设置为 ifRoom
,那么在应用栏没有空间时会按照这个属性仅显示优先级最高的菜单项。2、在 Java 实现代码
与其它菜单项相似,SearchView 的操做也是在 onCreateOptionsMenu
中进行。
In MainActivity.java
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.options_menu, menu);
searchMenuItem = menu.findItem(R.id.menu_search);
searchView = (SearchView) searchMenuItem.getActionView();
searchView.setQueryHint(getString(R.string.search_hint));
searchView.setIconifiedByDefault(false);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
// Todo: Get the submitted query text here.
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
return true;
}
复制代码
setQueryHint
method 设置 SearchView 的提示文字。setIconifiedByDefault
设置 SearchView 是否默认显示图标,若真则仅显示图标,若假则显示带有文本输入框的完整 SearchView。在 BookListing App 中,因为在 menu 资源中设置了 app:showAsAction="collapseActionView"
将 SearchView 放入了二级菜单,因此在这里将 setIconifiedByDefault
设为 false
也仅显示 SearchView 的图标。onQueryTextSubmit
会在用户点击回车键后获取提交的文本;onQueryTextChange
则每当文本发生变化时就获取新的文本。3、点击 TextView 自动打开 SearchView
在 BookListing App 中,提供了点击 Empty View 直接打开 SearchView,弹出输入法 (IME) 供用户输入搜索关键词的功能。
mEmptyStateView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchMenuItem.expandActionView();
searchView.setIconified(false);
}
});
复制代码
onClick
method 添加打开 SearchView 的代码。expandActionView()
打开 SearchView 所在的应用栏二级菜单;再设置 SearchView 的 setIconified
为 false
显示完整的 SearchView,系统就自动聚焦到 SearchView 的输入框,弹出输入法供用户输入搜索关键词了。在 RecyclerView 列表滑到底部以前,应用提早加载数据添加到列表中,实现无限滚动列表的效果。所以,这里要添加 OnScrollListener
并 override onScrolled
method 来监控列表的滚动状况,当应用判断列表快要滑到底时,会加载更多数据。
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (isLoading) {
return;
}
if (dy > 0) {
visibleItemCount = layoutManager.getChildCount();
totalItemCount = layoutManager.getItemCount();
pastVisibleItems = layoutManager.findFirstVisibleItemPosition();
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
isLoading = true;
// Todo: Fetch new data here.
}
}
}
});
复制代码
onScrolled
中,首先判断 isLoading 是否为真,如果则提早返回。isLoading 是一个全局的布尔类型变量,默认为 false。它表示当前状态下数据是否正在加载中,因此在开始加载数据时须要将它设置为 true,数据加载完成时设为 false。onScrolled
的参数 dy 大于零(表示屏幕的滚动方向为向上)时分别获取三个参数。因为这三个变量是在匿名类中使用的,因此要声明为全局变量。 (1)visibleItemCount:获取 RecyclerView 的 item 数目,但不包括已回收的视图,因此它能够看做是当前屏幕可见的 item 数目。 (2)totalItemCount:获取 RecyclerView 的全部 item 数目。 (3)pastVisibleItems:获取 RecyclerView.Adapter 第一个可见的 item 的位置,也就是当前屏幕可见的第一个 item 的位置,因此它能够看做是已滑出屏幕的 item 数目。因为 RecyclerView 没有提供与相似 ListView 的 clear 和 addAll method,因此须要开发者自行实现,一般是在 RecyclerView.Adapter 中添加辅助方法 (Helper Method)。
In NewsAdapter.java
public void clear() {
mBooksList.clear();
notifyDataSetChanged();
}
public void addAll(List<News> newsList) {
mBooksList.addAll(newsList);
notifyDataSetChanged();
}
复制代码
上面两个辅助方法都调用了同一个 method,告知适配器列表数据有变化。列表数据变化一般有两种类型:一种是子项变化 (Item Change),指 item 的数据变化,列表没有任何位置上的变化;另外一种是结构变化 (Structural Change),指列表中有 item 插入、移除、移动。常见的 notify 类 method 有如下几种:
Method | Description |
---|---|
notifyDataSetChanged() | 未指定数据变化的类型,适配器认为全部的原先数据已不可用,LayoutManager 会从新捆绑 (rebind) 和从新布局 (relayout) 视图,这种方式效率较低,一般不优先考虑使用。 |
notifyItemChanged (int position) | 列表中指定位置 (position) 的 item 发生数据变化,这属于子项变化,适配器仅更新该位置的 item,其它 item 不受影响。 |
notifyItemInserted (int position) | 列表中在指定位置 (position) 插入 item,原先该位置的 item 日后移一位 (position + 1),其它 item 仅改变位置,不会从新布局。这属于结构变化。 |
notifyItemMoved (int fromPosition, int toPosition) | 列表中一个 item 从原先位置 (fromPosition) 移动到另外一位置 (toPosition),其它 item 仅改变位置,不会从新布局。这属于结构变化。 |
notifyItemRemoved (int position) | 列表中指定位置 (position) 的 item 被移除,该位置后面的 item 位置前移一位 (position - 1),其它 item 仅改变位置,不会从新布局。这属于结构变化。 |
notifyItemRangeChanged (int positionStart, int itemCount) | 从指定位置 (positionStart) 开始,共计 itemCount 个数的 item 发生数据变化,这属于子项变化,适配器仅更新相应的 item,其它 item 不受影响。 |
根据不一样的情景使用不一样的 notify 类 method 以达到更高效率,更多信息能够到 RecyclerView.Adapter 文档查看。
DividerItemDecoration 属于 RecyclerView.ItemDecoration 的子类,它可用于为 LinearLayoutManager 下的 item 添加分隔线,支持垂直和水平方向。
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
recyclerView.getContext(), layoutManager.getOrientation());
recyclerView.addItemDecoration(dividerItemDecoration);
复制代码
setDrawable
能够为分隔线设置 Drawable 资源。在 BookListing App 中,RecyclerView 使用了 CardView 做为其子项的主要布局,而且实现了可扩展的 CardView 效果。实现这一功能有三个要点。
1、OnItemClickListener
RecyclerView 没有相似 ListView 可直接调用的类来处理 item 的点击事件,RecyclerView 只提供了 OnItemClickListener 接口,因此首先须要在 RecyclerView.Adapter 中实现 OnItemClickListener,以 BookListing App 为例,代码以下。
In BookAdapter.java
private OnItemClickListener mOnItemClickListener;
public void setOnItemClickListener(OnItemClickListener OnItemClickListener) {
mOnItemClickListener = OnItemClickListener;
}
public interface OnItemClickListener {
void onItemClick(View view, int position);
}
复制代码
而后在 Mainactivity 中设置监听器,代码以下。
In MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
mAdapter.setOnItemClickListener(new BookAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
}
});
...
}
复制代码
针对 BookListing App 的状况,CardView 的点击事件不须要在 MainActivity 中进行任何操做,因此这里留空,但必须在 MainActivity 中设置监听器。全部操做放在监听器内进行,所以又回到 RecyclerView.Adapter 中去。
In BookAdapter.java
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
...
if (mOnItemClickListener != null) {
holder.cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
cardViewIndex = holder.getAdapterPosition();
notifyItemChanged(holder.getAdapterPosition());
}
});
}
...
}
复制代码
在 onBindViewHolder
中设置监听器并经过 override onClick
method 添加 CardView 点击事件触发后执行的代码。因为 BookListing App 要实现 CardView 的展开和折叠功能,因此在这里使用了一个全局变量记录当前用户点击的 CardView 的位置,并经过 notifyItemChanged
告知监听器更新该位置的 item 数据。注意 cardViewIndex 是全局变量,默认值为 -1,使其默认状况下无做用 (unreachable),直到发生点击事件时对它赋值。
2、展开和折叠 CardView
接下来适配器会更新发生点击事件的 item 数据,也就是从新执行一次 onBindViewHolder
,position 参数为 cardViewIndex。因此,此时就要往 onBindViewHolder
添加扩展 CardView 的代码了。
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
...
if (cardViewIndex == position) {
ViewGroup.LayoutParams cardViewLayoutParams = holder.cardView.getLayoutParams();
if (isCardExpanded.get(position).equals(false)) {
cardViewLayoutParams.height = (int) mContext.getResources().
getDimension(R.dimen.card_expanded_height);
int expandedHorizontalMargin = mContext.getResources().
getDimensionPixelOffset(R.dimen.card_expanded_horizontal_margin);
int expandedVerticalMargin = mContext.getResources().
getDimensionPixelOffset(R.dimen.card_expanded_vertical_margin);
setMargins(holder.cardView, expandedHorizontalMargin, expandedVerticalMargin,
expandedHorizontalMargin, expandedVerticalMargin);
isCardExpanded.set(position, true);
} else {
cardViewLayoutParams.height = (int) mContext.getResources().
getDimension(R.dimen.card_height);
int originVerticalMargin = mContext.getResources().
getDimensionPixelOffset(R.dimen.card_vertical_margin);
int originHorizontalMargin = mContext.getResources().
getDimensionPixelOffset(R.dimen.card_horizontal_margin);
setMargins(holder.cardView, originHorizontalMargin, originVerticalMargin,
originHorizontalMargin, originVerticalMargin);
isCardExpanded.set(position, false);
}
holder.cardView.setLayoutParams(cardViewLayoutParams);
cardViewIndex = -1;
}
...
}
复制代码
setMargins
改变 CardView 与屏幕边缘的距离,达到放大和缩小的效果。其中 setMargins
的输入参数为像素值 (px),可利用 mContext.getResources().getDimensionPixelOffset()
实现独立像素 (dp) 对像素 (px) 的转换。/**
* Helper method that set margins of views, using {@link ViewGroup.MarginLayoutParams}.
*
* @param view is the view whom set margins to.
* @param leftMargin is the left margin of the view.
* @param topMargin is the top margin of the view.
* @param rightMargin is the right margin of the view.
* @param bottomMargin is the bottom margin of the view.
*/
private void setMargins(View view, int leftMargin, int topMargin,
int rightMargin, int bottomMargin) {
if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
ViewGroup.MarginLayoutParams params =
(ViewGroup.MarginLayoutParams) view.getLayoutParams();
params.setMargins(leftMargin, topMargin, rightMargin, bottomMargin);
view.requestLayout();
}
}
复制代码
CardView 在展开和折叠过程当中的动画效果是由 DefaultItemAnimator 提供的,在 MainActivity 中添加如下指令便可。
recyclerView.setItemAnimator(new DefaultItemAnimator());
复制代码
设置好须要修改的 LayoutParams 参数后,最后不要忘记执行如下指令,使修改设置生效。
holder.cardView.setLayoutParams(cardViewLayoutParams);
复制代码
你们确定注意到,与 CardView 展开和折叠相关的参数不止有 cardViewIndex,还有一个全局布尔类型变量 isCardExpanded,它其实是一个 ArrayList,记录了 RecyclerView 列表的每一个 item 的展开和折叠状况,CardView 展开时为真,折叠时为假。所以,在展开某个位置的 CardView 后须要将该位置的 isCardExpanded 设为 true,折叠后则设为 false。如何获取一个与 RecyclerView 列表等长的 ArrayList 并将全部项默认为 false(由于 CardView 默认是折叠的)就是第三个要点。
3、isCardExpanded
因为 RecyclerView.Adapter 必须 override getItemCount
method,在这个 method 中会获得 RecyclerView 列表的全部 item 数目,所以能够在 getItemCount
内初始化 isCardExpanded,代码以下。
private List<Boolean> isCardExpanded = new ArrayList<>();
@Override
public int getItemCount() {
int listItemCount = mBooksList.size();
if (isCardExpanded.size() < listItemCount) {
isCardExpanded.clear();
for (int index = 0; index < listItemCount; index++) {
isCardExpanded.add(false);
}
}
return listItemCount;
}
复制代码
getItemCount
内,首先判断当前 isCardExpanded 是否已有值,若无才对其赋值,并在赋值以前清除列表,最后经过 for 循环语句向 isCardExpanded 添加与 RecyclerView 列表等长的 item 并将全部项默认为 false。在 BookListing App 中,列表中的每一本图书都包含标题、做者、评分等文字,还有一张图片。由于应用的内容是经过 AsyncTaskLoader 从 Web API 获取的,文字与图片的数据大小量级不一样,为了尽快为用户提供有意义的内容,因此 BookListing App 采起了“先显示文字,后显示图片”的策略,这就要求图书的文字和图片分开两个线程加载,用到两个 AsyncTaskLoader。 以 BookListing App 为例,在 MainActivity 中引入两个 AsyncTaskLoader,它们的 LoaderCallback 做为一个类定义,在操做 Loader 时传入的参数也须要更改。
In MainActivity.java
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
...
getLoaderManager().initLoader(BOOK_LOADER_ID, null, new BookLoaderCallback());
}
private class BookLoaderCallback implements LoaderManager.LoaderCallbacks<List<Book>> {
@Override
public Loader<List<Book>> onCreateLoader(int i, Bundle bundle) {
...
}
@Override
public void onLoadFinished(Loader<List<Book>> loader, List<Book> books) {
...
loaderManager.restartLoader(IMAGE_LOADER_ID, null, new ImageLoaderCallback());
}
}
private class ImageLoaderCallback implements LoaderManager.LoaderCallbacks<List<Drawable>> {
@Override
public Loader<List<Drawable>> onCreateLoader(int i, Bundle bundle) {
return new ImageLoader(getApplicationContext());
}
@Override
public void onLoadFinished(Loader<List<Drawable>> loader, List<Drawable> drawables) {
mAdapter.setImage(drawables);
}
}
}
复制代码
implements
参数。而在 BookListing App 中就须要在 MainActivity 内分别定义两个 BookLoaderCallback 和 ImageLoaderCallback 类,并在类名后面添加 implements
参数。在调用 initLoader
和 restartLoader
时第三个参数也要由 this
改成各自的 LoaderCallback 类实例,如 new BookLoaderCallback()
和 new ImageLoaderCallback()
。onLoadFinished
执行 restartLoader
指令,开启 ImageLoader。onCreateLoader
中,ImageLoader 直接跳到后台线程 loadInBackground
将 Web API 返回的图片 URL (QueryUtils.image) 转换为 Drawable 资源。返回值的数据类型为 List。In ImageLoader.java
@Override
public List<Drawable> loadInBackground() {
List<Drawable> drawables = new ArrayList<>();
List<String> image = QueryUtils.image;
if (image != null && !image.isEmpty()) {
for (int index = 0; index < image.size(); index++) {
drawables.add(getImageDrawable(image.get(index)));
}
}
return drawables;
}
复制代码
这里用到了辅助方法 getImageDrawable
,涉及到显示网络图片的内容,主要是应用了 InputStream 缓存并转换为 Drawable 资源,返回值的数据类型为 Drawable。
private static Drawable getImageDrawable(String imageUrlString) {
Drawable imageResource = null;
try {
URL url = new URL(imageUrlString);
InputStream content = (InputStream) url.getContent();
imageResource = Drawable.createFromStream(content, "src");
} catch (MalformedURLException e) {
Log.e(LOG_TAG, "Problem building the URL ", e);
} catch (IOException e) {
Log.e(LOG_TAG, "Problem getting the URL content ", e);
}
return imageResource;
}
复制代码
onLoadFinished
中调用 RecyclerView.Adapter 的 setImage
辅助方法,向列表中添加图片。public void setImage(List<Drawable> drawables) {
if (drawables != null && !drawables.isEmpty()) {
for (int index = 0; index < drawables.size(); index++) {
mBooksList.get(index).setImageResource(drawables.get(index));
notifyItemChanged(index);
}
}
}
复制代码
经过 for 循环语句为 RecyclerView 列表的每一项添加图片,并通知适配器每一项的数据变化,使其得以更新。
在 BookListing App 中,除 RecyclerView 以外还有其它视图须要随着 RecyclerView 的列表一块儿实现滚动效果,例如图书列表上面的两个分别显示图书总数和页码信息的 TextView,因此这里引入 NestedScrollView。
<android.support.v4.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadeScrollbars="true"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/result_count"
style="@style/resultTextView"
android:gravity="start|center_vertical" />
<TextView
android:id="@+id/result_page"
style="@style/resultTextView"
android:gravity="end|center_vertical" />
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="@dimen/recycler_view_bottom_padding" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
复制代码
android:scrollbars
属性为 vertical
使其拥有一个垂直方向的滚动条,默认在右侧显示;同时设置 android:fadeScrollbars
为 true
使滚动条在列表静止不动时隐藏。这两个属性并非 NestedScrollView 专有的,事实上它是在 View 类定义的,因此理论上全部视图均可以设置这两个属性。android:paddingBottom
使列表的最后一个 item 距离屏幕底部有必定的距离,可是这会致使内容滚动时在 padding 区域出现一个空白横条,很是影响美观。因此这里还须要设置 android:clipToPadding
为 false
使 padding 的空白区域在内容滚动时消失,仅在列表滚动到底部时出现。将 RecyclerView 放在 NestedScrollView 内可能会出现 RecyclerView 列表滚动卡顿不流畅的现象,根据 stack overflow 的高票答案来看,在 Java 中添加如下代码便可解决问题。
recyclerView.setNestedScrollingEnabled(false);
复制代码
不过在 stack overflow 的答案下面也有评论指出,执行这条代码后 RecyclerView 将不会回收视图,致使资源浪费。因为这条指令在 RecyclerView 文档中没有详细介绍,我经过 Android Profiler 也没有观察到异常,因此就没有深究下去,有了解的各位请不吝赐教。
BookListing 和 NewsApp 这两个应用的数据都是从 Web API 获取的,因此在设备无网络链接或无数据的状况下,要用 Empty View 显示当前应用的状态,提醒用户进行下一步操做。
在 XML 布局中,一般把 RecyclerView 与 Empty View 放入 RelativeLayout 中,彼此不用设置相对位置关系,由于二者在同一时间只会显示其一。
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:padding="@dimen/activity_spacing" />
</RelativeLayout>
复制代码
设置 Empty View 须要在多处实现,将重复使用的代码封装成一个 Helper method 供其它地方调用是一个好的作法。
private void setEmptyView(boolean visibility, @Nullable Integer textStringId,
@Nullable Integer imageDrawableId) {
TextView emptyView = findViewById(R.id.empty_view);
if (visibility && textStringId != null && imageDrawableId != null) {
emptyView.setText(textStringId);
emptyView.setCompoundDrawablesWithIntrinsicBounds(null,
ContextCompat.getDrawable(getApplicationContext(), imageDrawableId),
null, null);
emptyView.setCompoundDrawablePadding(getResources().
getDimensionPixelOffset(R.dimen.compound_image_spacing));
emptyView.setVisibility(View.VISIBLE);
} else {
emptyView.setVisibility(View.GONE);
}
}
复制代码
setEmptyView
设置了三个输入参数,第一个是设置 Empty View 是否可见的布尔类型参数;第二个是 Empty View 的文本字符串 ID,能够为 null;第三个是 Empty View 的图片资源 ID,能够为 null。注意设置为 @Nullable
的输入参数不能是原始数据类型,因此这里须要将 int 换成其对象类型 Integer。
若是要设置 Empty View 为不可见,能够调用如下代码。
setEmptyView(false, null, null);
复制代码
仅当依次传入 true、字符串 ID、以及图片资源 ID 后,Empty View 才会开始设置相应的属性,最后设置为可见。其中,设置 TextView 的组合图片 (Compound Drawable) 须要调用 setCompoundDrawablesWithIntrinsicBounds
并经过 ContextCompat.getDrawable()
获取 Drawable 资源传入第二个参数,表示在 TextView 上方显示一张图片。
调用 setCompoundDrawablePadding
设置图片与文本之间的间隔,它传入的参数是像素值 (px),能够经过 getResources().getDimensionPixelOffset()
实现独立像素 (dp) 对像素 (px) 的转换。
在面对设备旋转等会致使 Activity 重启的状况时,能够将一些变量在 Activity 被杀死 (killed) 以前保存起来,而后 Activity 重启时在 onCreate 或 onRestoreInstanceState 中取回变量。例如在 BookListing App 中,经过 override onSaveInstanceState
method 保存了 resultOffset 整数以及 requestKeywords 字符串。
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt("resultOffset", resultOffset);
savedInstanceState.putString("requestKeywords", requestKeywords);
super.onSaveInstanceState(savedInstanceState);
}
复制代码
onSaveInstanceState
的超级类。变量能够在 onCreate 中取回,例如在 BookListing App 中,当 savedInstanceState 不为空时,按字符串键取回 resultOffset 整数以及 requestKeywords 字符串。注意在 onCreate 的输入参数就是 savedInstanceState。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState != null) {
resultOffset = savedInstanceState.getInt("resultOffset");
requestKeywords = savedInstanceState.getString("requestKeywords");
}
...
}
复制代码
变量也能够在 onRestoreInstanceState 中取回,只不过它是在 onCreate 以后执行的,所以若是变量是须要在 onCreate 中用到的,就不能在 onRestoreInstanceState 中取回变量了。
@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
resultOffset = savedInstanceState.getInt("resultOffset");
requestKeywords = savedInstanceState.getString("requestKeywords");
}
复制代码
注意 onSaveInstanceState 和 onRestoreInstanceState 调用各自的超级类的时机是不同的。
在 BookListing App 中,采用了底部横滑切换上下页的导航模式,实现方法主要参考了这个 stack overflow 帖子,主要是应用了 OnTouchListener 中的 SimpleOnGestureListener 来捕捉左滑和右滑手势操做。 不过在 BookListing App 中的应用不够理想,例如局部的横滑一般是面向局部操做的,例如移除屏幕中的一个卡片。另外设置了 OnTouchListener 的视图会让 Android Studio 认为该视图是一个自定义视图,提示无障碍 (Accessibility) 方面的警告。所以,这部份内容仅做为备忘,不做讨论。
在 BookListing 和 NewsApp 这两个应用中,在进行数据加载以前都须要检查网络状态。面对这种常常用到的功能,封装成一个 Helper method 供其它地方调用是一个好的作法。
private boolean isConnected() {
// Get a reference to the ConnectivityManager to check state of network connectivity.
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
// Get details on the currently active default data network.
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
// Return true if the device is connected, vice versa.
return networkInfo != null && networkInfo.isConnected();
}
复制代码
该辅助方法返回的数据类型是布尔类型,当检查到设备已链接网络时返回值为真,无链接时为假。这样一来 isConnected()
就能够轻易地放入 if/else 流控语句应用。
在 NewsApp 中,使用的 The Guardian API 返回的时间数据是 ISO-8601 格式的,具体来讲是 UTC 日期与时间结合 (Combined date and time in UTC) 的形式。这种格式会在时间前面加一个大写字母 T,显示 UTC 时间时在末尾加一个大写字母 Z。这只是复杂的时间问题的冰山一角,你们有兴趣能够观看这个 YouTube 视频。所幸在 Android 中可使用 SimpleDateFormat 来格式化时间,例如格式化 ISO-8601 时间能够利用以下代码:
try {
SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault());
Date dateIn = inFormat.parse(news.getTime());
SimpleDateFormat outFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
String dateOut = outFormat.format(dateIn);
} catch (ParseException e) {
Log.e(LOG_TAG, "Problem parsing the news time.", e);
}
复制代码
format
method,得到预期格式的时间字符串。以前的课程中提到,为视图提供触摸反馈,最简单的方法是设置视图的背景:
android:background="?android:attr/selectableItemBackground"
复制代码
它其实是应用了 R.attr 类提供的 Drawable 资源,在视图聚焦或点击 (focus/pressed) 状态下显示圆形涟漪的动画触摸反馈。经常使用的还有另一个资源。
android:background="?android:attr/selectableItemBackgroundBorderless"
复制代码
因为它是从 API Level 21 引入的,因此对于 minSdkVersion
在 API Level 21 如下的应用能够在 styles 中分开定义,在 BookListing App 中就是这么作的。它能够忽略视图的边界,在聚焦或点击 (focus/pressed) 时显示完整的圆形涟漪动画。这在一些不想因为显示视图边界而破坏界面完整性的场景颇有帮助。
字体 属于 Android 应用的一类资源,它能够像图片、音频等资源同样引用。例如在 NewsApp 中,新闻标题的首字母采用了 Hansa Gotisch 字体(来源:Font Meme),实现方法是在 res/font 目录下存放 TTF 文件,而后在 TextView 中设置 android:fontFamily
属性为对应的 TTF 文件名便可。
实战项目 7&8 BookListing 和 NewsApp 这两个应用的分享完毕,欢迎你们到个人 GitHub 交流,文中有遗漏的要点也能够提醒我,我很乐意解答。