前面提到,笔记本列表页面具有以下功能:
- 展示全部笔记本
- 创建新的笔记本
- 选择笔记本,并向调用NotebooksActivity的上一级activity返回对应的笔记本ID
可以做如下设计:
- 用RecyclerView来显示笔记本列表,其中最上面一项是“全部笔记”,意思是,不选中任何笔记本时,则令笔记列表显示所有笔记,否则仅显示选中的笔记本中的内容
- 添加一个漂浮按钮来触发新建笔记本操作。点击漂浮按钮后,弹出一个对话。对话框包含一个编辑框,用户手动输入笔记本名字后确认,则向数据库中新插入一条记录。插入操作完成后刷新笔记本列表
- 点击列表中的某一条目之后,关闭页面,并向上一级页面返回:
a. 选择“全部笔记”则返回0
b. 选择其它笔记本条目则返回笔记本id
1. 创建Notebook类
之前我们用Note类来表示一条笔记。同样的道理,我们创建一个Notebook类来表示笔记本。
在“model”包下新建Notebook类,并添加如下的属性:
- id:笔记本的唯一id
- name:笔记本名称
打开新建的Notebook类,编写代码如下:
public class Notebook {
private long id;
private String name;
public Notebook(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2. 向NoteDAO类增加笔记本数据访问函数
打开NoteDAO类,添加以下方法
- insertNotebook():向数据库插入一个笔记本对象
- queryAllNotebooks():查询全部笔记本
- queryNotebookById():根据id查找笔记本对象
insertNotebook()
/**
* 向数据库中插入一个笔记本
* @param noteBook 被添加到数据库的笔记本对象
* @return 插入成功后更新noteBook的id并将note对象返回;插入失败则返回null
*/
public Notebook insertNotebook(Notebook noteBook) {
if (noteBook == null) {
return noteBook;
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COL_NOTEBOOK_NAME, noteBook.getName());
long id = db.insert(TABLE_NOTEBOOK, null, values);
if (id <= 0) {
return null;
}
noteBook.setId(id);
return noteBook;
}
queryAllNotebooks()
/**
* 获取全部笔记本
* @return 全部笔记本的列表。如果没有任何笔记本,则返回的列表长度为0
*/
public ArrayList<Notebook> queryAllNotebooks() {
ArrayList<Notebook> nbs = new ArrayList<>();
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NOTEBOOK, null, null, null, null, null, null);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String name = cursor.getString(1);
Notebook nb = new Notebook(id, name);
nbs.add(nb);
}
} finally {
cursor.close();
}
}
return nbs;
}
queryNotebookById
/**
* 根据id获取对应的笔记本
* @param id 笔记本id
* @return 如果存在id对应的笔记本,则创建对象并返回,否则返回null
*/
public Notebook queryNotebookById(long id) {
if (id <= 0) {
return null;
}
SQLiteDatabase db = dbHelper.getReadableDatabase();
String selection = COL_ID + "=" + id;
Cursor cursor = db.query(TABLE_NOTEBOOK, null, selection, null, null, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
String name = cursor.getString(1);
Notebook nb = new Notebook(id, name);
return nb;
}
} finally {
cursor.close();
}
}
return null;
}
这样,对笔记本数据进行操作的几个方法就完成了。
2. 展示笔记本列表
展示笔记本列表与原来的全部笔记页面是大体相似的。唯一的不同,是我们在列表的最前面保留一个“全部笔记”固定项。
在NotebooksActivity的布局文件activity_notebooks.xml中增加RecyclerView:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="com.jing.app.sn.NotebooksActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/notebook_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
接下来,为笔记本列表项创建布局,新建布局文件notebook_list_item.xml:
在这里,简单的让它只显示笔记本的名字,因此,只包含一个TextView,以及一条分隔线:
<?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="72dp"
android:orientation="vertical"
android:clickable="true"
android:background="@drawable/note_list_item_bg">
<!--笔记本名字-->
<TextView
android:id="@+id/tv_nb_name"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:gravity="center_vertical"
android:text="这里是笔记本名字"
android:maxLines="2"
android:textColor="@color/black"
android:textSize="16sp" />
<!--分隔线-->
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#999999"/>
</LinearLayout>
从布局文件中,我们可以看到,显示笔记本名字的TextView的id叫做“tv_nb_name”。
现在,我们再去NotebooksActivity中实现这个列表。
先创建内部类NotebookViewHolder以实现我们自己的ViewHolder:
private class NotebookViewHolder extends RecyclerView.ViewHolder {
// 只有一个文本视图用以显示名字
private TextView notebookName;
public NotebookViewHolder(View itemView) {
super(itemView);
notebookName = itemView.findViewById(R.id.tv_nb_name);
}
}
接下来,创建适配器类NotebookAdapter:
private class NotebookAdapter extends RecyclerView.Adapter<NotebookViewHolder> {
// 笔记本列表
private ArrayList<Notebook> noteBooks = new ArrayList<>();
// 设置笔记本列表内容
public void setNotes(ArrayList<Notebook> nbs) {
// 现将原列表清空,再将传入的列表元素全部加入
this.noteBooks.clear();
if (nbs != null) {
this.noteBooks.addAll(nbs);
}
}
@Override
public NotebookViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 根据列表项布局文件创建视图对象
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(R.layout.notebook_list_item, parent, false);
// 基于上面的视图对象创建ViewHolder对象并返回
NotebookViewHolder vh = new NotebookViewHolder(view);
return vh;
}
@Override
public void onBindViewHolder(NotebookViewHolder holder, int position) {
// 取对应位置的笔记对象
final Notebook nb = noteBooks.get(position);
// 设置对应ViewHolder对象中各视觉元素的值
holder.notebookName.setText(nb.getName());
// 响应点击事件
// 处理列表项点击事件
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onSelectNotebook(nb);
}
});
}
@Override
public int getItemCount() {
return noteBooks.size();
}
}
可以注意到,在处理列表项点击事件时,调用了onSelectNotebook()方法,但是目前并没有创建它,因此会出现报错。
在NotebooksActivity中(适配器类之外)创建onSelectNotebook()方法,它的参数就是当前点击的笔记本对象,它执行的操作就是关闭笔记本列表页面,并向上一级activity返回这个对象的id和名字。编写代码如下:
private void onSelectNotebook(Notebook notebook) {
// 创建一个Intent对象,用来携带要返回的参数
Intent intent = new Intent();
intent.putExtra("notebookId", notebook.getId());
intent.putExtra("notebookName", notebook.getName());
// 设置返回结果,返回码为1
setResult(1, intent);
//关闭页面
finish();
}
接下来,我们查询数据库,获取笔记本列表,并使其显示出来。
首先,为NotebooksActivity添加RecyclerView及其适配器类型的成员:
public class NotebooksActivity extends AppCompatActivity {
private RecyclerView mRecyclerView;
private NotebookAdapter mAdapter;
...
}
然后找到onCreate()方法,添加代码以对RecyclerView进行初始化:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notebooks);
// 添加代码:初始化并设置RecyclerView
mRecyclerView = findViewById(R.id.notebook_list);
// 设定为垂直列表
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(layoutManager);
mAdapter = new NotebookAdapter();
mRecyclerView.setAdapter(mAdapter);
}
这里仅仅是初始化了RecyclerView,还没有加载数据。重写NotebooksActivity类的onResume()方法,并通过AsyncTask对笔记本数据进行异步加载:
@Override
protected void onResume() {
super.onResume();
asyncLoadNotebooks();
}
private void asyncLoadNotebooks() {
AsyncTask<Void, Void, ArrayList<Notebook>> task = new AsyncTask<Void, Void, ArrayList<Notebook>>() {
@Override
protected ArrayList<Notebook> doInBackground(Void... voids) {
ArrayList<Notebook> nbs = NoteDAO.getInstance(NotebooksActivity.this.getApplicationContext()).queryAllNotebooks();
// 在列表最前添加"全部笔记"项,id设为0
Notebook allNotes = new Notebook(0, getString(R.string.all_notes));
nbs.add(0, allNotes);
return nbs;
}
@Override
protected void onPostExecute(ArrayList<Notebook> notebooks) {
mAdapter.setNotes(notebooks);
mAdapter.notifyDataSetChanged();
}
};
task.execute();
}
在这里,我们通过创建一个匿名的AsyncTask子类来实现笔记本数据的异步加载:
- 在doInBackground()方法中,首先从数据库中查询全部的笔记本,得到一个列表。然后创建一个特殊的Notebook对象,将其id设置为0(从数据库中来的笔记本id不可能为0),名字设置为“全部笔记”,并插入到列表最前面。
- 在onPostExecute()方法中,更新适配器类持有的数据,并通知数据集变化。
3. 处理返回结果
之前,我们讲到,从笔记列表页面调用笔记本列表页面,选择笔记本后将会把它的id返回过来。下面来处理这个返回值。处理逻辑如下:
分析返回值,将页面标题设置为返回的笔记本名字,判断返回的id:
- 如果为0,则加载全部笔记,并设置页面标题为“全部笔记”
- 如果大于0,则读取该笔记本中所有的笔记,显示在列表中
经过这个处理,原来仅仅显示全部笔记的主页面,现在可以根据我们所选择的笔记本,动态的变换内容了。
进行以下修改:
为NoteListActivity类添加notebookId成员:
notebookId成员缺省为0,表明加载全部笔记。当我们从笔记本列表中返回时,用返回的新id值将其重新设置,以切换笔记本:
private long notebookId;
修改NoteListActivity类的异步加载类:
找到内部类LoadAllNotesTask,修改如下:
private class LoadAllNotesTask extends AsyncTask<Void, Void, ArrayList<Note>> {
private long notebookId;
public LoadAllNotesTask(long notebookId) {
this.notebookId = notebookId;
}
public LoadAllNotesTask() {
this(0);
}
@Override
protected void onPreExecute() {
mLoadingView.setVisibility(View.VISIBLE);
}
@Override
protected void onPostExecute(ArrayList<Note> notes) {
// 为适配器设置新的笔记列表
adapter.setNotes(notes);
// 通知RecyclerView刷新
adapter.notifyDataSetChanged();
// 关闭加载等待视图
mLoadingView.setVisibility(View.GONE);
}
@Override
protected ArrayList<Note> doInBackground(Void... voids) {
ArrayList<Note> notes = noteRepository.getAllNotes();
// 从列表中去掉笔记本id不匹配的笔记对象
if (notebookId > 0) {
for (Iterator<Note> iterator = notes.iterator(); iterator.hasNext();) {
if (iterator.next().getNotebookId() != notebookId) {
iterator.remove();
}
}
}
return notes;
}
}
在原来的基础上进行了如下改动:
- 添加了私有成员notebookId,表示当前页面针对哪个笔记本
- 添加了两个构造方法,一个接收指定的笔记本id来初始化notebookId成员;另一个缺省的将notebookId设置为0,即全部笔记
- 在doInBackground()中,当notebookId>0,也就是指定了当前页面对应的笔记本时,进行一次循环处理,去掉从数据库中取得的笔记列表中不属于此笔记本的项目
修改onResume()方法中对LoadAllNotesTask的创建和调用
@Override
protected void onResume() {
super.onResume();
// 修改:创建对象时加上笔记本id参数
LoadAllNotesTask task = new LoadAllNotesTask(notebookId);
task.execute();
}
重写NoteListActivity类的onActivityResult()方法
onActivityResult()方法专门用于处理被调用activity的返回值。
打开NoteListActivity类,重写onActivityResult()方法:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == 1 && resultCode == 1) {
// 取得返回参数
notebookId = data.getLongExtra("notebookId", 0);
String notebookName = data.getStringExtra("notebookName");
// 设置页面标题
setTitle(notebookName);
}
}
4. 实现新建笔记本
回到NotebooksActivity,先在它的布局文件中添加漂浮按钮。可以简单的把原来在笔记列表页面中添加的漂浮按钮拷贝过来。完毕后布局内容如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="com.jing.app.sn.NotebooksActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/notebook_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_add_notebook"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:clickable="true"
android:onClick="onNewNotebook"
app:srcCompat="@drawable/ic_add" />
</FrameLayout>
这里指定按钮点击处理函数为onNewNotebook(),则在NotebooksActivity类中添加此方法:
public void onNewNotebook(View view) {
}
在这个方法里面,我们弹出一个对话框,里面包含一个编辑框,用来填写新建的笔记本的名字,点击确认后将其保存到数据库。
为onNewNotebook()方法添加代码如下:
public void onNewNotebook(View view) {
// 创建编辑框对象
final EditText notebookNameEdit = new EditText(this);
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle(R.string.new_notebook)
.setView(notebookNameEdit) // 将编辑框对象加入对话框
.setNegativeButton("取消", null)
.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String name = notebookNameEdit.getEditableText().toString();
if (!TextUtils.isEmpty(name)) {
Notebook nb = new Notebook(0, name);
nb = NoteDAO.getInstance(NotebooksActivity.this).insertNotebook(nb);
if (nb != null) {
// 插入成功,刷新笔记本列表
asyncLoadNotebooks();
} else {
// 插入失败,提示用户
Toast.makeText(NotebooksActivity.this, "保存笔记本失败", Toast.LENGTH_SHORT).show();
}
}
}
});
builder.show();
}
这样,到目前阶段,可以添加新的笔记本了。选择新的笔记本之后,UI切回笔记列表页面,且标题改为当前选中的笔记本标题:
然而,新的笔记本里面并没有任何笔记,这一点要通过修改新建笔记页面来实现。