碎片(fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf ·...

28
碎片(Fragment)的综合练习 ©by 宿宝臣 <[email protected]> 2018 9 1 目录 1 创建项目,建立模型 2 2 创建展示列表的 Fragment 5 3 创建 RecylerView 及其 Adapter 9 4 创建展示内容的 Fragment 18 5 创建展示图书详情的活动(Activity23 6 界面优化 25 6.1 设置 fragment_book_content 的可见性 .................................................... 25 6.2 设置图书列表的间距和文字大小 ............................................................. 26 7 进一步的拓展 28 8 FAQ 28 8.1 view.findViewById() 返回 null 是怎么回事? .............................................. 28 1

Upload: others

Post on 05-Sep-2020

11 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

碎片(Fragment)的综合练习

©by 宿宝臣 <[email protected]>

2018 年 9 月 1 日

目录

1 创建项目,建立模型 2

2 创建展示列表的 Fragment 5

3 创建 RecylerView 及其 Adapter 9

4 创建展示内容的 Fragment 18

5 创建展示图书详情的活动(Activity) 23

6 界面优化 256.1 设置 fragment_book_content 的可见性 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256.2 设置图书列表的间距和文字大小 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

7 进一步的拓展 28

8 FAQ 288.1 view.findViewById() 返回 null 是怎么回事? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

1

Page 2: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

1. 创建项目,建立模型 2

本文不是关于碎片的介绍或者教程,而是一个综合练习,采用步步为营的方式实现了一个简单的图书信息展示界面。对于手机而言,首先会展示一个图书列表,点击书名会跳转到图书详情页面,如图 1a所示。对于平板而言,展示一个两栏的页面,左栏显示图书列表,点击图书则在同一页的右栏显示图书详情,如图 1b所示。

通过这个综合练习,可以掌握以下知识点:

• 碎片(Fragment)是 Android 实现界面复用的重要手段。

• 每个碎片(Fragment)都是自治的,即都有自己独立的生命周期,都需要编写一个相应的碎片处理程序。

• 每个页面布局可以包含多个碎片(Fragment)。

• RecyclerView 是 Google 推荐的代替 ListView 的列表组件,每个 RecyclerView 都应该提供一个 Adapter 管理列表数据。

对于 Android 初学者而言,这个综合练习可能略微有点难度,但是只要跟随每一个步骤,相信不会遇到多少坑。个人给出两点建议,第一,练习中的每一个字母都要亲自敲入!当然,可借助 AndroidStudio 自动生成的文件模板和智能提示。正确输入程序,以及快速定位输入错误的的能力也是很重要的。第二,经常阶段性的验证程序是否正确运行,不要指望程序全部录入后一次执行成功,那样往往会遭受沉重打击。本文在不同的阶段都给出了测试运行的提示,当然这是作者的理解,你应该根据程序的功能点设置和自己的节奏决定在什么时候测试运行程序。

1 创建项目,建立模型

相信你已经对如何创建一个只包含空白活动(Empty Activity)的项目耳熟能详了,不再赘述。我们要编写一个管理书籍的 App,“书”是这个 App 中最基本的元素,根据面向对象的分析的基本方法,在创建项目后,首先创建一个模型类 Book 来描述一本书的基本属性。右键点击包名,即可方便的创建这个包下面的类了,如图 2所示。

Book 类很简单,这里只有 3 个属性及其 setter/getter 方法,如代码清单 1所示。

代码清单 1 – Book 类

package cn.edu.sdut.android.bookstore;

public class Book {private int id;private String title;private String desc;

public Book(int id, String title, String desc) {this.id = id;this.title = title;this.desc = desc;

}

public int getId() {return id;

}

Page 3: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

1. 创建项目,建立模型 3

(a) 手机运行效果图

(b) 平板运行效果图

图 1 – 碎片综合练习运行效果图

Page 4: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

1. 创建项目,建立模型 4

图 2 – 创建 Book 类

public void setId(int id) {this.id = id;

}

public String getTitle() {return title;

}

public void setTitle(String title) {this.title = title;

}

public String getDesc() {return desc;

}

public void setDesc(String desc) {this.desc = desc;

}

@Overridepublic String toString() {

return "Book{" +"id=" + id +", title='" + title + '\'' +", desc='" + desc + '\'' +'}';

}}

此时运行程序,和空白项目是没有区别的,下面我们创建一个仅仅展示书籍列表的页面。

Page 5: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

2. 创建展示列表的 FRAGMENT 5

2 创建展示列表的 Fragment

如果仅仅为了展示书籍列表,就创建一个 Activity 好了,简单直接。但是我们需要兼容平板和手机,希望完成后的 App 在平板上分两栏显示,在手机上只显示一个书名列表。显然,平板上的书名列表页面和手机的书名列表页面是可以完全相同的,因此有必要“复用”这个书名列表页面。碎片(Fragment)的设计目的就是为了页面的复用,因此在这里使用碎片(Fragment)实现书名列表页面很自然。首先创建碎片(Fragment)的 xml 定义文件 fragment_book_list.xml,在 res 上面点击右键,

选择 New Layout resource file,在接下来的图 3中填写碎片布局的文件名 fragment_book_list¬以及选择合适的布局管理器,这里使用最简单的 LinearLayout 即可。

图 3 – 创建碎片的布局文件

打开刚刚创建的碎片布局文件 fragment_book_list.xml,添加两个 TextView,分别用来展示图书的 id 和标题,如代码清单 2所示。 每一个布局文件中的

match_parent 和wrap_content 都值得认真琢磨和实验。

代码清单 2 – fragment_book_list.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"><TextView

android:id="@+id/book_id"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="1" />

<TextViewandroid:id="@+id/book_titile"android:layout_width="wrap_content"

¬文件名的选取也有讲究,这里以 fragment开头,一看便知这个布局文件是关于碎片的。比较一下,如果文件名是 book_list_fragment,目光移动到文件名的最后才能知道这个布局文件是关于碎片的,当然还是 fragment_book_list 更恰当。

Page 6: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

2. 创建展示列表的 FRAGMENT 6

android:layout_height="wrap_content"android:layout_marginLeft="15dp"android:text="book title here" />

</LinearLayout>

在这个碎片布局中,仅仅通过两个 TextView 来展示图书的 id 和书名即可。目前还不需要对布局做过多的优化,关于界面的优化参见节 6 [在第 25 页]:界面优化。下面创建使用这个碎片的 Java 类 BookListFragment,如图 4所示。BookListFragment 类需

要重写 onCreateView 方法,在 BookListFragment 类的空白处按下 Alt + Insert,随后选择 overridemethods...,如图 5所示,在接下来的窗口中选择 onCreateView 方法即可,如代码清单 3所示。

代码清单 3 – BookListFragment.java

package cn.edu.sdut.android.bookstore;

import android.os.Bundle;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v4.app.Fragment;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;

public class BookListFragment extends Fragment {

@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater,

@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {

return inflater.inflate(R.layout.fragment_book_list,container,false);}

}

BookListFragment 的意义显而易见,从 fragment_book_list 这个碎片的布局文件创建一个视图(View),显示其中的内容。显然,碎片是要依附于活动(Activity)的,因此还需要修改 main_activity.xml,使之包含 frag-

ment_book_list 这个碎片。为了简化问题,首先修改 main_activity.xml 的默认布局管理器为 Lin-earLayout,在 ConstrainLayout 上面右键点击并选择 Convert View...,在接下来的窗口选择 Lin-earLayout 即可,参见图 6。修改后的 main_activity.xml 内容如代码清单 4所示。

代码清单 4 – main_activity.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"

Page 7: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

2. 创建展示列表的 FRAGMENT 7

tools:context=".MainActivity"><fragment

android:id="@+id/book_list_fragment"android:name="cn.edu.sdut.android.bookstore.BookListFragment"android:layout_width="match_parent"android:layout_height="wrap_content"/>

</LinearLayout>

此时,可以运行一下 App 看看了!目前,我们还没有针对平板或者手机作任何特别的处理,因此在平板或者手机的模拟器上运行,其结果都是相同的,这里以手机运行结果例,见图 7。

图 4 – 创建碎片的支持类

图 5 – 自动插入重写方法

到这一步,我们验证了碎片(Fragment)的使用方法:首先需要创建碎片对应的布局文件(这里是 fragment_book_list.xml)和管理碎片的 Java 类(这里是 BookListFragment),然后在活动(Activity)的布局文件(这里是 main_activity.xml)中包含碎片(使用 fragment 标签),这样App 在显示这个活动界面的时候就会显示相应碎片的内容了。在一个活动的布局文件中可以包含多个碎片,在节 4中我们会看到这一点。

Page 8: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

2. 创建展示列表的 FRAGMENT 8

图 6 – 修改默认布局管理器

图 7 – 增加 Fragment 后的初步运行结果

Page 9: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 9

3 创建 RecylerView 及其 Adapter

显然,图 7所示的运行结果不是我们想要的:这应该是一个图书列表页面,应该分行列出图书的 id 和图书标题,而目前只是列出了 fragment_book_list.xml 碎片布局中写死了的 id 和 title 字段。当然,也不是全无用处,至少我们看出了一行这样的内容是什么样子。如何展示一个图书列表呢?当前 Google 推荐的方法是使用 RecyclerView,这部分内容相对比较多,下面我们一步一步走。首先,我们复制一下 fragment_book_list.xml 文件­,复制为 book_item.xml,这将是 Re-

cylerView中单条记录的显示样式。接着修改 fragment_book_list.xml布局文件,删除两个 TextView,增加 RecyclerView。增加 RecyclerView 时,最好通过设计器托放进去,这样可以自动增加 Recy-clerView 的依赖。如果是手工修改 fragment_book_list.xml 文件,需要在 app/build.properties 中手工增加 RecyclerView 的依赖声明,如下所示:

dependencies {...implementation 'com.android.support:recyclerview-v7:28.0.0-rc01'...

}

修改后的 fragment_book_list.xml 文件内容如代码清单 5所示,其中只是包含了一个 Recy-clerView。

代码清单 5 – fragment_book_list.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="match_parent">

<fragmentandroid:id="@+id/book_list_fragment"android:layout_width="match_parent"android:layout_height="match_parent"/>

</LinearLayout>

此时是可以运行 App 的,不过啥也看不到,甚至在上一步中能够显示的一行图书内容也看不到了:毕竟,book_item.xml 只是一个模板,在模板中写死的图书 id 和图书标题,将来会被替换为真实的 id 和标题。因此,book_item.xml 中的两个 android:text 属性实际上是可以删除的。那么,这个 RecyclerView和刚刚创建的 book_item.xml布局文件是什么关系呢?RecyclerView

又是如何展现一个图书列表的呢?图书列表中的图书信息从何而来?下面将一一揭开这几个谜团。可以看出,这个 RecylcerView 是在 fragment_book_list 布局中的,每个 RecyclerView 都需要

一个 Adapter 来绑定数据和模板文件,这里作为 BookListFragment 的内部类就可以了,因此修改BookListFragment,在其中增加 RecyclerView 的 Adapter,修改后的 BookListFragment 内容如代码清单 6所示。

­此时的 fragment_book_list.xml 布局,恰好仅仅显示了一本书,因此我们采取了“偷懒”的方法,直接拿来给 RecylerView 用。

Page 10: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 10

代码清单 6 – BookListFragment.java

package cn.edu.sdut.android.bookstore;

import android.os.Bundle;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v4.app.Fragment;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;

public class BookListFragment extends Fragment {

@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater,

@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {

return inflater.inflate(R.layout.fragment_book_list,container,false);}

}

和代码清单 3比较一下可以看出,此时的 BookListFragment膨胀了很多,我们需要认真消化一下。首先,在 BookListFragment 内部创建 Adapter 类的框架:

public class BookListFragment extends Fragment {....class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{}

}

此时,由于 Adapter.ViewHolder 还没有创建,Android Studio 会报告出错,因此鼠标移动到ViewHolder上面,同时按下 Alt + Enter,选择“Create class ViewHolder”自动创建Adapter.ViewHolder类:

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{public class ViewHolder {}

}

此时,Android Studio 还是会报错,因为 Adapter 还需要实现 Recycler.Adapter 必须的三个方法。再次在 Adapter.ViewHolder 上同时按下 Alt + Enter,选择“Implements methods”,自动创建三个方法:

Page 11: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 11

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {

return null;}@Overridepublic void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {}@Overridepublic int getItemCount() {

return 0;}public class ViewHolder {}

}

此时,Android Studio还是报错,报告:ViewHolder应该继承自 android.support.v7.widget.RecyclerView.ViewHolder,再次把鼠标放到 Adapter.ViewHolder(已经第三次把鼠标放到这个位置了),再次同时按下 Alt +

Enter,选择“Make ViewHolder extends android.support.v7.widget.RecyclerView.ViewHolder”:

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {

return null;}@Overridepublic void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {}@Overridepublic int getItemCount() {

return 0;}public class ViewHolder extends RecyclerView.ViewHolder {}

}

接着会发现,Android Studio报告ViewHolder缺少默认的构造器,于是在 RecyclerView.ViewHolder上面按下 Alt + Enter,选择“Create constructor matching super”,至此 Android Studio 不再报错,我们看一下在 Android Studio 帮助下获得的 Adapter 框架:

Page 12: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 12

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {

return null;}@Overridepublic void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {}@Overridepublic int getItemCount() {

return 0;}public class ViewHolder extends RecyclerView.ViewHolder {

public ViewHolder(@NonNull View itemView) {super(itemView);

}}

}

下面首先实现 ViewHolder类。ViewHolder作为一个缓存容器,表示了 RecyclerView中的每一行。本例中,每一行图书有两个元素:id和 title,因此 ViewHolder类应该添加两个私有变量 bookId和 bookTitle,并在构造方法中实例化这两个私有变量,所以完整的 ViewHolder 类为:

public class ViewHolder extends RecyclerView.ViewHolder {private TextView bookId;private TextView bookTitle;public ViewHolder(@NonNull View itemView) {

super(itemView);bookId = (TextView)itemView.findViewById(R.id.book_id);bookTitle = (TextView)itemView.findViewById(R.id.book_titile);

}}

有了 ViewHolder 的基础,下面我们看一下 onCreateViewHolder 方法的实现。顾名思义,这个方法是在创建 ViewHolder 的时候被调用的,因此在这里要根据 RecyclerView 的布局模板创建合适的 View对象作为 ViewHolder构造方法的参数,具体来说,我们根据给定的 ViewGroup的 Context创建一个 layoutInflater 对象,然后根据 book_item 布局模板创建一个 View 对象表征这个布局模板:

Page 13: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 13

public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {View view = LayoutInflater.from(viewGroup.getContext())

.inflate(R.layout.book_item,viewGroup,false);final ViewHolder viewHolder = new ViewHolder(view);return viewHolder;

}

至此,我们解决了在 Adapter 中列表中的每一行如何表达:在 onCreateViewHolder 中创建ViewHolder对象来缓存列表中的每一行。那么列表中的数据从哪里来呢?一般的,首先需要在Adapter类中添加一个列表对象表示列表元素,然后通过 onBindViewHolder 方法来完成数据和 ViewHolder的绑定:

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{private List<Book> bookList;....public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {

Book book = bookList.get(i);viewHolder.bookId.setText(String.valueOf(book.getId()));viewHolder.bookTitle.setText(book.getTitle());

}....

}

getItemCount 方法就比较简单了,Adapter 类通过这个方法了解列表元素的个数:

public int getItemCount() {return bookList.size();

}

Adapter 自然需要知晓所有的图书信息,那么如何传递所有图书信息给 Adapter 呢?自然在构造方法中最方便:

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{private List<Book> bookList;....public BookAdapter(List<Book> list) {

bookList = list;}....

}

Page 14: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 14

至此,我们完成了 BookAdapter 类的几乎所有代码,我们看一下 BookAdapter 目前的完整代码®,参见代码清单 7。

代码清单 7 – BookListFragment.java

package cn.edu.sdut.android.bookstore;

import android.os.Bundle;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v4.app.Fragment;import android.support.v7.widget.RecyclerView;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.TextView;

import java.util.List;

public class BookListFragment extends Fragment {

@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater,

@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {

return inflater.inflate(R.layout.fragment_book_list,container,false);}

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{private List<Book> bookList;

public BookAdapter(List<Book> list) {bookList = list;

}

@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {

View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.book_item,viewGroup,false);

final ViewHolder viewHolder = new ViewHolder(view);return viewHolder;

}

@Overridepublic void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {

Book book = bookList.get(i);viewHolder.bookId.setText(book.getId());

®由于 BookAdapter 是 BookListFragment 的内部类,这里依然列出的是 BookListFragment 的完整代码。

Page 15: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 15

viewHolder.bookTitle.setText(book.getTitle());}

@Overridepublic int getItemCount() {

return bookList.size();}

public class ViewHolder extends RecyclerView.ViewHolder {private TextView bookId;private TextView bookTitle;

public ViewHolder(@NonNull View itemView) {super(itemView);bookId = (TextView)itemView.findViewById(R.id.book_id);bookTitle = (TextView)itemView.findViewById(R.id.book_titile);

}}

}

}

写了半天的代码,你一定迫不及待想测试一下代码是否正常吧?这是很好的思想,稳打稳扎,步步为营,但是如果现在在模拟器上运行 App,发现还是白茫茫一片啥也看不到!是的,我们还没有给BookAdapter 提供数据呢。显然,在 BookListFragment 的 onCreateView 方法中提供数据比较方便:此时正需要合适的数据填充图书列表页面,改造后的 BookListFragment 参见代码清单 8。

代码清单 8 – BookListFragment.java

package cn.edu.sdut.android.bookstore;

import android.os.Bundle;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v4.app.Fragment;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.LinearLayout;import android.widget.TextView;

import java.util.ArrayList;import java.util.List;import java.util.Random;

public class BookListFragment extends Fragment {

@Nullable

Page 16: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 16

@Overridepublic View onCreateView(@NonNull LayoutInflater inflater,

@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.fragment_book_list,container,false);RecyclerView recyclerView = view.findViewById(R.id.book_list_recycler_view);LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());recyclerView.setLayoutManager(layoutManager);BookAdapter bookAdapter = new BookAdapter(getBooks());recyclerView.setAdapter(bookAdapter);return view;

}

private List<Book> getBooks() {List<Book> list = new ArrayList<>();for(int i = 0; i < 50; i++) {

Book book = new Book(i,"book title" + i, getRandomBookTitle("book title" + i));list.add(book);

}return list;

}

private String getRandomBookTitle(String s) {Random random = new Random(20);int length = random.nextInt();StringBuilder builder = new StringBuilder();for(int i = 0; i < length; i++){

builder.append(s);}

return builder.toString();}

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{private List<Book> bookList;

public BookAdapter(List<Book> list) {bookList = list;

}

@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {

View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.book_item,viewGroup,false);

final ViewHolder viewHolder = new ViewHolder(view);return viewHolder;

}

@Override

Page 17: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

3. 创建 RECYLERVIEW 及其 ADAPTER 17

public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {Book book = bookList.get(i);viewHolder.bookId.setText(String.valueOf(book.getId()));viewHolder.bookTitle.setText(book.getTitle());

}

@Overridepublic int getItemCount() {

return bookList.size();}

public class ViewHolder extends RecyclerView.ViewHolder {private TextView bookId;private TextView bookTitle;

public ViewHolder(@NonNull View itemView) {super(itemView);bookId = (TextView)itemView.findViewById(R.id.book_id);bookTitle = (TextView)itemView.findViewById(R.id.book_titile);

}}

}

}

可以看出,为了获得理想的测试数据,我们在 BookListFragment中增加了 getBooks和 getRan-domBookDesc 方法,这两个方法很容易理解,不再赘述。现在可以测试一下了!目前我们只是实现了图书列表功能,因此在手机和平板运行的结果是类

似的,这里以平板为例,如图 8所示,根据平板尺寸的大小,显示了若干列图书的 id 和书名。这个界面太丑了!别着急,让我们逐步完善它。记住,步步为营,每一步都可运行可测试是很重要的。

图 8 – 在平板上只显示图书列表的运行界面

Page 18: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

4. 创建展示内容的 FRAGMENT 18

4 创建展示内容的 Fragment

到目前为止,图书列表的展示功能算是基本完成了,下面看一下如何实现在平板上展示图书详情。在手机上展示图书详情的实现方法参见节 5 [在第 23 页]。在平板上,图书列表在左栏,图书详情在右栏,显然左右两栏分别是两个 Fragment 比较合理:

这两个 Fragment 会在手机界面的设计中被复用。图书列表的 Fragment 我们已经基本设计完成了,下面看一下展示图书详情的右栏的设计。首先在res layout目录下创建一个新的碎片(fragment)布局文件 fragment_book_content,如代码清单 9所示。

代码清单 9 – fragment_book_content.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#00ff00"android:id="@+id/book_content_layout">

<TextViewandroid:id="@+id/book_title"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center" />

<Viewandroid:layout_width="match_parent"android:layout_height="1dp"android:background="#000" />

<TextViewandroid:id="@+id/book_desc"android:layout_width="match_parent"android:layout_height="wrap_content"/>

</LinearLayout>

接着,需要创建管理这个碎片的 BookContentFragment.java文件,有了编写 BookListFragment的基础,我们不再赘述,如代码清单 10所示。

代码清单 10 – BookContentFragment.java

package cn.edu.sdut.android.bookstore;

import android.os.Bundle;import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v4.app.Fragment;import android.util.Log;import android.view.LayoutInflater;

Page 19: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

4. 创建展示内容的 FRAGMENT 19

import android.view.View;import android.view.ViewGroup;import android.widget.TextView;

public class BookContentFragment extends Fragment {private View view;

@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater,

@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {

view = inflater.inflate(R.layout.fragment_book_content,container,false);return view;

}

public void refresh(Book book){view.setVisibility(View.VISIBLE);TextView bookTitle = view.findViewById(R.id.book_title);TextView bookDesc = view.findViewById(R.id.book_desc);bookTitle.setText(book.getTitle());bookDesc.setText(book.getDesc());

}

}

这个碎片布局是包含在哪个 Activity 的布局中呢?显然应该是在 activity_main.xml 布局文件中。为了更好的适用平板布局,可以采用 Android的限定府,让平板布局和手机布局区分开来,比如在res 目录下创建 layout-sw600dp 目录,即为当平板的宽度超过 600dp 时 Android 自动采用本目录下的界面布局。因此,我们首先创建目录res layout-sw600dp,在这个目录下创建 activity_main.xml布局文件,内容如代码清单 11所示。

代码清单 11 – activity_main.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/two_column_layout"><fragment

android:id="@+id/book_list_fragment"android:name="cn.edu.sdut.android.bookstore.BookListFragment"android:layout_width="0dp"android:layout_weight="1"android:layout_height="match_parent"/>

<Viewandroid:layout_width="1dp"android:layout_height="match_parent"

Page 20: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

4. 创建展示内容的 FRAGMENT 20

android:background="#000" />

<fragmentandroid:id="@+id/book_content_fragment"android:name="cn.edu.sdut.android.bookstore.BookContentFragment"android:layout_width="0dp"android:layout_weight="3"android:layout_height="match_parent"/>

</LinearLayout>

现在可以运行一下看看了,如图 9所示。为了清楚起见,我把 fragment_book_content 的背景色设置为 #00ff00,参见代码清单 9中的 android:background=#00ff00。

图 9 – 初步的图书列表和图书详情页面

在图 9中,图书详情是空的!是的,到目前为止,我们并没有实现图书条目的点击逻辑,图书详情从何而来呢?下面在 BookListFragment 中实现图书列表的点击逻辑,完成图书详情的展示设计。再次打开 BookListFragment,我们发现图书列表的每一行是有 book_item布局模板决定的,在

BookListFragment 的 onCreateView 中我们已经获得了相应于 book_item 模板的 View 对象,因此在这个 View 对象上绑定 click 事件响应相关代码即可,如代码清单 12所示,在 onClick 中做了三件事:

1. 获取当前的 book 对象,以便传递给 BookContentFragment 展示图书详情。

2. 获取 BookContentFragment 对象。

3. 调用 BookContentFragment 的 refresh 方法刷新碎片内容。

代码清单 12 – BookListFragment.java

package cn.edu.sdut.android.bookstore;

import android.os.Bundle;

Page 21: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

4. 创建展示内容的 FRAGMENT 21

import android.support.annotation.NonNull;import android.support.annotation.Nullable;import android.support.v4.app.Fragment;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.TextView;

import java.util.ArrayList;import java.util.List;import java.util.Random;

public class BookListFragment extends Fragment {

@Nullable@Overridepublic View onCreateView(@NonNull LayoutInflater inflater,

@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.fragment_book_list,container,false);RecyclerView recyclerView = view.findViewById(R.id.book_list_recycler_view);LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());recyclerView.setLayoutManager(layoutManager);BookAdapter bookAdapter = new BookAdapter(getBooks());recyclerView.setAdapter(bookAdapter);return view;

}

private List<Book> getBooks() {List<Book> list = new ArrayList<>();for(int i = 0; i < 50; i++) {

Book book = new Book(i,"book title" + i, getRandomBookDesc("book desc " + i));list.add(book);

}return list;

}

private String getRandomBookDesc(String s) {Random random = new Random();int length = random.nextInt(20) + 1;StringBuilder builder = new StringBuilder();for(int i = 0; i < length; i++){

builder.append(s);}

return builder.toString();}

Page 22: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

4. 创建展示内容的 FRAGMENT 22

class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{private List<Book> bookList;

public BookAdapter(List<Book> list) {bookList = list;

}

@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {

View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.book_item,viewGroup,false);

final ViewHolder viewHolder = new ViewHolder(view);

view.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {

Book book = bookList.get(viewHolder.getAdapterPosition());BookContentFragment bookContentFragment =

(BookContentFragment) getFragmentManager().findFragmentById(R.id.book_content_fragment);

bookContentFragment.refresh(book);}

});return viewHolder;

}

@Overridepublic void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {

Book book = bookList.get(i);viewHolder.bookId.setText(String.valueOf(book.getId()));viewHolder.bookTitle.setText(book.getTitle());

}

@Overridepublic int getItemCount() {

return bookList.size();}

public class ViewHolder extends RecyclerView.ViewHolder {private TextView bookId;private TextView bookTitle;

public ViewHolder(@NonNull View itemView) {super(itemView);bookId = (TextView)itemView.findViewById(R.id.book_id);bookTitle = (TextView)itemView.findViewById(R.id.book_title);

}}

Page 23: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

5. 创建展示图书详情的活动(ACTIVITY) 23

}

}

现在在平板模拟器上运行一下,已经可以看到大致的模样了,如图 10所示,点击左栏的图书标题,右栏即可以显示相应的图书详情了。 可以尝试点击图书的

序号、当前行的空白处看看?

图 10 – 初步实现图书的点击事件,展示图书详情

不过,当你尝试放在手机模拟器上运行时,却得不到期望的结果,更糟糕的是,程序“闪退”,崩溃了!下面我们就着力解决程序的兼容性问题:让程序在手机和平板上都能运行,并且自动适应不同的屏幕大小。

5 创建展示图书详情的活动(Activity)

要让程序也能够在手机上正常运行,需要作两个方面的工作:

1. 编写一个展示图书详情的活动(Activity)。显然,展示图书详情的活动(Activity)所使用的布局文件应该复用 fragment_book_content.xml 这个碎片(Fragment),这也是碎片的主要存在价值。

2. 在左栏的点击事件处理中,需要判断程序当前是运行在平板上还是手机上,以便决定图书详情的显示方式:在平板上直接在右栏展示图书详情,在手机上需要启动另外一个活动(Activity)展示图书详情。显然,手机和平板的入口不一样,手机是通过 res/layout/activity_main.xml进入的,平板是通过 res/layout-sw600dp/activity_main.xml 进入的。观察这两个不同的布局文件可以发现,如果我们能够识别 res/layout-sw600dp/activity_main.xml中特有的双栏布局,就可以区分当前是运行在手机还是平板了:设置一个 isTwoPane 逻辑变量,当进入双栏布局时设置此变量为 true 即可。

下面我们首先编写图书详情的活动(Activity),创建一个空的活动 BookContentActivity即可,将自动生成的布局文件 activity_book_content修改为如代码清单 13所示,直接嵌入 fragment_book_content即可。

Page 24: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

5. 创建展示图书详情的活动(ACTIVITY) 24

代码清单 13 – activity_book_content.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><fragment

android:id="@+id/book_content_fragment"android:name="cn.edu.sdut.android.bookstore.BookContentFragment"android:layout_width="match_parent"android:layout_height="match_parent"/>

</LinearLayout>

活动(Activity)的实现如代码清单 14所示¯。由于我们复用了 fragment_book_content,因此需要在活动 BookContentActivity 的 onCreate 方法中获取 BookContentFragment 对象并调用其refresh 方法刷新碎片的内容。另外,在 BookContentActivity 中也设计了一个方便的类方法 action-Start 用来启动 Activity,这是一种很好的启动 Activity 的策略,值得效仿。

代码清单 14 – BookContentActivity.java

package cn.edu.sdut.android.bookstore;

import android.content.Context;import android.content.Intent;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;

public class BookContentActivity extends AppCompatActivity {

@Overrideprotected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);setContentView(R.layout.activity_book_content);

int bookId = Integer.parseInt(getIntent().getStringExtra("bookId"));String bookTitle = getIntent().getStringExtra("bookTitle");String bookDesc = getIntent().getStringExtra("bookDesc");BookContentFragment bookContentFragment = (BookContentFragment)

getSupportFragmentManager().findFragmentById(R.id.book_content_fragment);bookContentFragment.refresh(new Book(bookId,bookTitle,bookDesc));

}

public static void actionStart(Context context, String bookId, String bookTitle, StringbookDesc){

Intent intent = new Intent(context,BookContentActivity.class);intent.putExtra("bookId",bookId);intent.putExtra("bookTitle",bookTitle);

¯这里的 actionStart 方法参照了《第一行代码 Android》

Page 25: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

6. 界面优化 25

intent.putExtra("bookDesc",bookDesc);context.startActivity(intent);

}}

然后,需要在 res/layout-sw600dp/activity_main.xml 中作一个特殊的标记,这里采用的方法是将最外层的 LinearLayout 标记为“two_column_layout”:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/two_column_layout"><fragment

android:id="@+id/book_list_fragment"....

这样就可以在 BookListFragment 中通过识别这个标记来决定 isTwoPane 的值了,关键代码如下所示:

public class BookListFragment extends Fragment {....@Overridepublic void onActivityCreated(@Nullable Bundle savedInstanceState) {

super.onActivityCreated(savedInstanceState);if(getActivity().findViewById(R.id.two_column_layout) != null) {

isTwoPane = true;} else

isTwoPane = false;}....

}

6 界面优化

手机 App 的界面优化不完全是美工的活:你必须熟练的将美工的效果通过 Android 的语法表现出来。下面我们一步一步来优化这个简单 App 的界面,也借此复习一下 xx 章学过的内容。

6.1 设置 fragment_book_content 的可见性

初次在平板上运行时,默认并没有选中任何书目,因此右栏是空白的,显得不太协调,这里有两种常见的处理策略:

1. 初始情况下设置右栏为隐藏状态,只有选择书目后才显示右栏。

Page 26: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

6. 界面优化 26

2. 默认选中一条书目,使得右栏显示默认的书目详情。

第二种处理策略留给读者自行思考实现,下面我们实现第一种处理策略,很简单,首先修改 frag-ment_book_content.xml 碎片布局文件,设置为默认不显示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#00ff00"android:id="@+id/book_content_layout"android:visibility="invisible"><TextView

android:id="@+id/book_title"....

然后修改 BookContentFragment 的 refresh 方法中设置 fragment_book_content 可见即可:

public void refresh(Book book){view.setVisibility(View.VISIBLE);TextView bookTitle = view.findViewById(R.id.book_title);....

6.2 设置图书列表的间距和文字大小

适当增大书目列表之间的间距,字体调大一些,效果会更美观一些,参见图 11。

图 11 – 调整间距和字体后的显示效果

调整方法主要是使用和 android:layout_marginXXX 的若干属性,参见代码清单 15和代码清单

Page 27: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

6. 界面优化 27

16。也可以通过 android:paddingXXX 达到相似的效果,请读者自行测试。

代码清单 15 – book_item.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"><TextView

android:id="@+id/book_id"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginLeft="15dp"android:layout_marginTop="10dp"android:textSize="18sp"android:text="1" />

<TextViewandroid:id="@+id/book_title"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:layout_marginLeft="15dp"android:layout_marginStart="15dp"android:textSize="18sp"android:text="book title here" />

</LinearLayout>

代码清单 16 – fragment_book_content.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#00ff00"android:id="@+id/book_content_layout"android:visibility="invisible">

<TextViewandroid:id="@+id/book_title"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:layout_marginBottom="10dp"android:textSize="18sp"android:gravity="center" />

<Viewandroid:layout_width="match_parent"

Page 28: 碎片(Fragment)的综合练习softlab.sdut.edu.cn/.../2018/08/fragment-step-by-step-1.pdf · 2018. 8. 31. · 碎片(Fragment)的综合练习 ©by 宿宝臣

7. 进一步的拓展 28

android:layout_height="1dp"android:background="#000" />

<TextViewandroid:id="@+id/book_desc"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="10dp"android:layout_marginLeft="15dp"android:layout_marginRight="15dp"android:layout_marginBottom="10dp"android:textSize="18sp"/>

</LinearLayout>

7 进一步的拓展

支持数据库存储、支持图书信息的增查删改、支持用户身份验证、支持网络操作等

8 FAQ

8.1 view.findViewById() 返回 null 是怎么回事?

首先要检查拼写错误!笔者曾经遇到这样的语句:view.findViewById(R.id.book_titile) 返回了null 导致程序闪退,百思不得其解。一顿折腾之后才发现,原来在另外一个 layout 文件中,错误的命名图书标题 id 为 book_titile(应该是 book_title),这样在系统中就存在两个非常类似的图书标题的 id,很容易混淆,导致莫名的错误。细致,是一个程序员必须具备的素质。其次,要考虑指定的组件和 view真的是包含关系吗?是否考虑使用 getActivity().findViewById

扩大查找的范围?