I. 引言
本文将主要实现笔记的可删除功能: 用户能够长按笔记进行删除操作。
II. 前情回顾
轻松上手:<Android Studio笔记应用开发>(一)入门与笔记应用浅开发
轻松上手:<Android Studio笔记应用开发>(二)笔记可显示Part1:实现逻辑与textView
轻松上手:<Android Studio笔记应用开发>(二)笔记可显示Part2:定义笔记的数据结构类型
轻松上手:<Android Studio笔记应用开发>(二)笔记可显示Part3:适配器
轻松上手:<Android Studio笔记应用开发>(二)大功告成!添加新笔记!
轻松上手:<Android Studio笔记应用开发>(三)笔记可再编辑
为了创建一个简单的Android笔记应用,前文已经成功实现了以下主要功能:
-
笔记的展示: 主活动(
MainActivity
)中通过一个列表视图(ListView
)展示了所有笔记的内容和创建时间。 -
笔记的添加: 用户通过悬浮按钮(
FloatingActionButton
)可以添加新的笔记,进入编辑页面(EditActivity
),并在该页面输入笔记内容后保存。 -
笔记的保存和显示: 新添加的笔记会保存在 SQLite 数据库中,主活动在每次启动时从数据库读取笔记列表,并通过适配器(
NoteAdapter
)将笔记显示在列表视图中。 -
笔记的点击编辑: 用户可以点击笔记列表中的项,进入编辑页面,编辑该笔记的内容。
III. 适配器的改进
1. 引入笔记项点击的回调接口 OnNoteItemLongClickListener
用于处理笔记项的长按事件。
2. 添加 OnNoteItemLongClickListener
接口
在 NoteAdapter
类中添加一个接口 OnNoteItemLongClickListener
,用于定义笔记项长按的回调方法。
public interface OnNoteItemLongClickListener {
void onNoteItemLongClick(long noteId);
}
3. 注册点击事件监听器
在 NoteAdapter
类中添加一个 OnNoteItemLongClickListener
成员变量,并在构造函数中接收它。
private OnNoteItemLongClickListener onNoteItemLongClickListener;
public NoteAdapter(Context context, List<Note> noteList, OnNoteItemClickListener listener, OnNoteItemLongClickListener longClickListener) {
this.context = context;
this.noteList = noteList;
this.onNoteItemClickListener = listener;
this.onNoteItemLongClickListener = longClickListener;
}
4. 在 getView
中触发回调
在 getView
方法中,当笔记项被长按时,触发回调接口的方法,将长按事件传递给主活动类处理。
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
// 触发笔记项长按事件
if (onNoteItemLongClickListener != null) {
onNoteItemLongClickListener.onNoteItemLongClick(noteList.get(position).getId());
}
return true; // 消耗长按事件
}
});
5. 适配器更新后的代码
package com.example.my_notes_record;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.List;
public class NoteAdapter extends BaseAdapter {
public interface OnNoteItemClickListener {
void onNoteItemClick(long noteId);
}
public interface OnNoteItemLongClickListener {
void onNoteItemLongClick(long noteId);
}
private Context context;
private List<Note> noteList;
private OnNoteItemClickListener onNoteItemClickListener;
private OnNoteItemLongClickListener onNoteItemLongClickListener;
// 默认构造函数
public NoteAdapter(){
}
// 带参数的构造函数,接受上下文和笔记列表
public NoteAdapter(Context Context,List<Note> noteList){
this.context=Context;
this.noteList=noteList;
}
public NoteAdapter(Context context, List<Note> noteList, OnNoteItemClickListener onNoteItemClickListener) {
this.context = context;
this.noteList = noteList;
this.onNoteItemClickListener = onNoteItemClickListener;
}
public NoteAdapter(Context context, List<Note> noteList, OnNoteItemClickListener listener, OnNoteItemLongClickListener longClickListener) {
this.context = context;
this.noteList = noteList;
this.onNoteItemClickListener = listener;
this.onNoteItemLongClickListener = longClickListener;
}
// 获取列表项数量
@Override
public int getCount() {
return noteList.size();
}
// 获取指定位置的笔记对象
@Override
public Object getItem(int position){
return noteList.get(position);
}
// 获取指定位置的笔记ID
@Override
public long getItemId(int position){
return position;
}
// 创建并返回每个列表项的视图
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 从XML布局文件实例化视图
View view = View.inflate(context, R.layout.note_list_item, null);
// 获取布局中的TextView控件
TextView tv_content = (TextView) view.findViewById(R.id.tv_content);
TextView tv_time = (TextView) view.findViewById(R.id.tv_time);
// 从笔记对象中获取内容和时间信息
String allText = noteList.get(position).getContent();
// 设置TextView的文本内容
tv_content.setText(allText.split("\n")[0]);
tv_time.setText(noteList.get(position).getTime());
// 将笔记ID作为视图的标签
view.setTag(noteList.get(position).getId());
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 触发笔记项点击事件
if (onNoteItemClickListener != null) {
onNoteItemClickListener.onNoteItemClick(noteList.get(position).getId());
}
}
});
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
// 触发笔记项长按事件
if (onNoteItemLongClickListener != null) {
onNoteItemLongClickListener.onNoteItemLongClick(noteList.get(position).getId());
}
return true; // 消耗长按事件
}
});
return view;
}
}
IV. 主活动的更新
1. 实现接口 NoteAdapter.OnNoteItemLongClickListener
public class MainActivity extends AppCompatActivity implements NoteAdapter.OnNoteItemClickListener , NoteAdapter.OnNoteItemLongClickListener{
// ...
}
2. 适配器初始化
在主活动中初始化适配器时,同时传递实现了 OnNoteItemLongClickListener
接口的当前活动实例。
adapter = new NoteAdapter(getApplicationContext(), noteList , this , this);
3. 实现接口方法处理笔记项长按事件
在这一部分中,我们将实现 OnNoteItemLongClickListener
接口的方法,以处理笔记项的长按事件。这个过程主要包括弹出确认删除对话框和在用户确认后删除笔记。
3.1 实现 onNoteItemLongClick
方法
@Override
public void onNoteItemLongClick(long noteId) {
// 当笔记项长按时触发,显示删除确认对话框
showDeleteConfirmationDialog(noteId);
}
3.2 显示删除确认对话框
private void showDeleteConfirmationDialog(final long noteId) {
// 创建一个AlertDialog.Builder实例,用于构建对话框
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// 设置对话框消息和按钮
builder.setMessage("确定要删除此笔记吗?")
.setPositiveButton("删除", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// 在用户确认后删除笔记
deleteNoteById(noteId);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// 用户取消对话框,不执行任何操作
}
});
// 创建并显示对话框
builder.create().show();
}
在这里,AlertDialog.Builder
被用于构建一个确认删除的对话框。其中,setMessage
方法设置对话框的消息,setPositiveButton
和 setNegativeButton
方法分别设置确认和取消按钮,并通过 OnClickListener
处理点击事件。
3.3 删除笔记的实现
// 通过 ID 删除笔记的方法
private void deleteNoteById(long noteId) {
// 创建一个 CRUD 实例
CRUD op = new CRUD(this);
op.open();
// 调用 CRUD 类中的 deleteNoteById 方法执行删除操作
op.deleteNoteById(noteId);
// 关闭数据库连接
op.close();
// 删除后刷新笔记列表
refreshListView();
}
4. 主活动更新后的代码
package com.example.my_notes_record;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.ListView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
// 创建名为 "MainActivity" 的主活动类
public class MainActivity extends AppCompatActivity implements NoteAdapter.OnNoteItemClickListener , NoteAdapter.OnNoteItemLongClickListener{
private Context context = this; // 上下文对象,用于数据库操作
private NoteDatabase dbHelper; // 数据库帮助类
private NoteAdapter adapter; // 笔记适配器
private List<Note> noteList = new ArrayList<>(); // 笔记列表
private FloatingActionButton btn; // 悬浮按钮
private ListView lv; // 列表视图
// 定义一个 ActivityResultLauncher,用于处理其他活动的结果
private ActivityResultLauncher<Intent> someActivityResultLauncher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//调用父类的 onCreate 方法,用于执行一些初始化操作
//savedInstanceState 参数用于恢复之前的状态
setContentView(R.layout.activity_main);//设置当前 Activity 的布局
btn = findViewById(R.id.floatingActionButton); // 悬浮按钮
lv = findViewById(R.id.lv); // 列表视图,用于显示数据列表
adapter = new NoteAdapter(getApplicationContext(), noteList , this , this);//初始化一个笔记适配器,并将应用的上下文对象和笔记列表传递给适配器
refreshListView(); // 刷新笔记列表
lv.setAdapter(adapter); // 将适配器与列表视图关联,从而显示笔记列表中的数据在界面上
// 初始化 ActivityResultLauncher,用于处理启动其他活动的结果
someActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
Intent data = result.getData();
if (data != null) {
// 从 EditActivity 返回的内容和时间
String content = data.getStringExtra("content");
String time = data.getStringExtra("time");
long noteId = data.getLongExtra("note_id", -1);
// 检查是否是新笔记还是更新现有笔记
if (noteId == -1L) {
// 如果是新笔记,调用添加新笔记的方法
if(!content.isEmpty()) addNewNote(content, time);
} else {
// 如果是现有笔记,调用更新现有笔记的方法
updateExistingNote(noteId, content, time);
}
refreshListView(); // 刷新笔记列表
}
}
}
);
// 设置悬浮按钮的点击事件监听器
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 启动 EditActivity 并等待结果
Intent intent = new Intent(MainActivity.this, EditActivity.class);
someActivityResultLauncher.launch(intent);
}
});
}
// 刷新笔记列表
public void refreshListView() {
// 创建数据库操作对象,打开数据库连接
CRUD op = new CRUD(context);
op.open();
if (noteList.size() > 0) noteList.clear(); // 清空笔记列表
noteList.addAll(op.getAllNotes()); // 获取数据库中所有笔记
op.close(); // 关闭数据库连接
adapter.notifyDataSetChanged(); // 通知适配器数据已更改,刷新列表视图
}
@Override
public void onNoteItemClick(long noteId) {
// 处理项点击,启动 EditActivity 并传递选定笔记以进行编辑
Intent intent = new Intent(MainActivity.this, EditActivity.class);
intent.putExtra("note_id", noteId);
someActivityResultLauncher.launch(intent);
}
// 添加新笔记
private void addNewNote(String content, String time) {
CRUD op = new CRUD(this);
op.open();
Note newNote = new Note(content, time);
op.addNote(newNote);
op.close();
}
// 更新现有笔记
private void updateExistingNote(long noteId, String content, String time) {
CRUD op = new CRUD(this);
op.open();
Note updatedNote = new Note(content, time);
updatedNote.setId(noteId);
op.updateNote(updatedNote);
op.close();
}
@Override
public void onNoteItemLongClick(long noteId) {
// 当笔记项长按时触发,显示删除确认对话框
showDeleteConfirmationDialog(noteId);
}
private void showDeleteConfirmationDialog(final long noteId) {
// 创建一个AlertDialog.Builder实例,用于构建对话框
AlertDialog.Builder builder = new AlertDialog.Builder(this);
// 设置对话框消息和按钮
builder.setMessage("确定要删除此笔记吗?")
.setPositiveButton("删除", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// 在用户确认后删除笔记
deleteNoteById(noteId);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// 用户取消对话框,不执行任何操作
}
});
// 创建并显示对话框
builder.create().show();
}
// 通过 ID 删除笔记的方法
private void deleteNoteById(long noteId) {
// 创建一个 CRUD 实例
CRUD op = new CRUD(this);
op.open();
// 调用 CRUD 类中的 deleteNoteById 方法执行删除操作
op.deleteNoteById(noteId);
// 关闭数据库连接
op.close();
// 删除后刷新笔记列表
refreshListView();
}
}
V. 数据库操作的扩展
1. 实现根据 ID 删除笔记的方法
public void deleteNoteById(long noteId) {
// 执行删除操作,根据 ID 删除指定笔记
db.delete(
NoteDatabase.TABLE_NAME,
NoteDatabase.ID + "=?",
new String[]{String.valueOf(noteId)}
);
}
2. 数据库操作扩展后的代码
package com.example.my_notes_record;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
import java.util.List;
public class CRUD {
SQLiteOpenHelper dbHandler; // SQLiteOpenHelper 实例用于处理数据库连接
SQLiteDatabase db; // SQLiteDatabase 实例用于执行数据库操作
// 定义数据库表的列名
private static final String[] columns = {
NoteDatabase.ID,
NoteDatabase.CONTENT,
NoteDatabase.TIME
};
// 构造方法,接受上下文参数
public CRUD(Context context) {
dbHandler = new NoteDatabase(context); // 初始化数据库处理器
}
// 打开数据库连接
public void open() {
db = dbHandler.getWritableDatabase(); // 获取可写的数据库连接
}
// 关闭数据库连接
public void close() {
dbHandler.close(); // 关闭数据库处理器
}
// 添加一条笔记记录
public Note addNote(Note note) {
ContentValues contentValues = new ContentValues(); // 创建一个用于存储数据的 ContentValues 对象
contentValues.put(NoteDatabase.CONTENT, note.getContent()); // 添加内容
contentValues.put(NoteDatabase.TIME, note.getTime()); // 添加时间
long insertId = db.insert(NoteDatabase.TABLE_NAME, null, contentValues); // 将数据插入数据库
note.setId(insertId); // 将插入后的 ID 设置到笔记对象中
return note; // 返回包含新数据的笔记对象
}
public List<Note> getAllNotes() {
Cursor cursor = db.query(
NoteDatabase.TABLE_NAME, // 表名
columns, // 要查询的列(在这里是ID、内容、时间)
null, // 查询条件(null表示无特殊条件)
null, // 查询条件参数(null表示无特殊条件)
null, // 分组方式(null表示不分组)
null, // 过滤方式(null表示不过滤)
null // 排序方式(null表示不排序)
);
List<Note> notes = new ArrayList<>(); // 创建一个笔记列表用于存储查询结果
if (cursor.getCount() > 0) {
while (cursor.moveToNext()) {
Note note = new Note(); // 创建笔记对象
note.setId(cursor.getLong(cursor.getColumnIndex(NoteDatabase.ID))); // 设置 ID
note.setContent(cursor.getString(cursor.getColumnIndex(NoteDatabase.CONTENT))); // 设置内容
note.setTime(cursor.getString(cursor.getColumnIndex(NoteDatabase.TIME))); // 设置时间
notes.add(note); // 将笔记对象添加到列表中
}
}
cursor.close(); // 关闭游标
return notes; // 返回包含所有笔记记录的列表
}
// 根据 ID 获取笔记
public Note getNoteById(long noteId) {
// 查询数据库,获取指定 ID 的笔记记录
Cursor cursor = db.query(
NoteDatabase.TABLE_NAME, // 表名
columns, // 要查询的列(在这里是ID、内容、时间)
NoteDatabase.ID + "=?", // 查询条件(通过 ID 进行查询)
new String[]{String.valueOf(noteId)}, // 查询条件参数(指定要查询的 ID 值)
null, // 分组方式(null表示不分组)
null, // 过滤方式(null表示不过滤)
null // 排序方式(null表示不排序)
);
Note note = null;
if (cursor.moveToFirst()) {
// 如果查询到结果,则创建新的笔记对象并设置其属性
note = new Note();
note.setId(cursor.getLong(cursor.getColumnIndex(NoteDatabase.ID))); // 设置 ID
note.setContent(cursor.getString(cursor.getColumnIndex(NoteDatabase.CONTENT))); // 设置内容
note.setTime(cursor.getString(cursor.getColumnIndex(NoteDatabase.TIME))); // 设置时间
}
cursor.close(); // 关闭游标,释放资源
return note; // 返回获取到的笔记对象,如果未找到则返回 null
}
// 更新笔记
public void updateNote(Note note) {
// 创建一个 ContentValues 对象,用于存储要更新的数据
ContentValues values = new ContentValues();
values.put(NoteDatabase.CONTENT, note.getContent());
values.put(NoteDatabase.TIME, note.getTime());
// 执行数据库更新操作
db.update(
NoteDatabase.TABLE_NAME, // 表名
values, // 更新的内容值
NoteDatabase.ID + "=?", // 更新条件(通过 ID 进行更新)
//`"=?"` 是一个占位符,它表示在 SQL 查询中使用参数。这是一种防止 SQL 注入攻击的方式。在这里,它表示将在这个位置上填入具体的数值。
new String[]{String.valueOf(note.getId())} // 更新条件参数(指定要更新的 ID 值)
//创建一个字符串数组,数组中包含了要替代占位符 `"=?"` 的具体数值。在这里,它包含了笔记对象的 ID。
);
}
// 根据 ID 删除笔记
public void deleteNoteById(long noteId) {
// 执行删除操作,根据 ID 删除指定笔记
db.delete(
NoteDatabase.TABLE_NAME,
NoteDatabase.ID + "=?",
new String[]{String.valueOf(noteId)}
);
}
}
VI. 一些不太容易理解的点
1. 为什么要引入接口OnNoteItemLongClickListener
,以及它的作用是什么?
此处接口的引入主要是为了实现解耦。
将长按事件的处理逻辑从适配器中抽离,使得适配器可以更灵活地应对不同的事件处理需求。
通过接口,我们将长按事件的处理权力交给了主活动类。
这样一来,可以保障代码的结构良好和可维护性。
2. 为什么在 NoteAdapter
构造函数中要传递一个 OnNoteItemLongClickListener
实例?
为适配器提供了一个接口,允许主活动类定义长按事件的处理逻辑。
3. 为什么要在 getView
方法中设置长按事件监听器,并且传递 noteId
?
通过在
getView
方法中设置长按事件监听器, 可以为每个笔记项设置独立的长按事件监听器。
传递
noteId
,我们把笔记项的标识符传递给主活动类,让主活动类负责实际的业务逻辑。
4. 对AlertDialog.Builder
不熟悉怎么办?
AlertDialog.Builder
是 Android 提供的一个用于构建对话框的类,它允许我们以链式调用的方式设置对话框的各种属性,包括标题、消息、按钮等。主要步骤包括创建构建器实例、设置属性、创建对话框。
在文章中,我们使用了
AlertDialog.Builder
来构建确认删除的对话框。以下是相关代码:
private void showDeleteConfirmationDialog(final long noteId) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("确定要删除此笔记吗?")
.setPositiveButton("删除", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// 在确认后删除笔记
deleteNoteById(noteId);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// 用户取消对话框,不执行任何操作
}
});
builder.create().show();
}
以下作简单解释
-
AlertDialog.Builder builder = new AlertDialog.Builder(this);
:创建一个对话框构建器实例,传入当前活动的上下文。 -
builder.setMessage("确定要删除此笔记吗?")
:设置对话框的消息,即要显示的文本内容。 -
.setPositiveButton("删除", new DialogInterface.OnClickListener() {...})
:设置对话框的确认按钮,以及确认按钮的点击事件处理逻辑。 -
.setNegativeButton("取消", new DialogInterface.OnClickListener() {...})
:设置对话框的取消按钮,以及取消按钮的点击事件处理逻辑。 -
builder.create().show();
:通过构建器创建对话框实例并显示。
如果读者想更多了解
AlertDialog.Builder
,不妨查阅官方文档并自行实践。
VII. 拓展
- 有没有更好的方法来优化和扩展现有的代码?
- 是否可以引入其他设计模式或框架来改进应用的结构和性能?
- 除了删除确认对话框外,是否还有其他方式来提高用户交互的友好性和便利性?
VI. 结语
搞定啦!现在你学会了如何用适配器模式改进 Android 应用,更灵活处理长按事件。别忘了思考文章结尾的提问哦,欢迎交流!🚀