轻松上手:<Android Studio笔记应用开发>(四)笔记可删除

I. 引言

点击查看该项目GitHub地址

本文将主要实现笔记的可删除功能:

用户能够长按笔记进行删除操作。

II. 前情回顾

轻松上手:<Android Studio笔记应用开发>(一)入门与笔记应用浅开发

轻松上手:<Android Studio笔记应用开发>(二)笔记可显示Part1:实现逻辑与textView

轻松上手:<Android Studio笔记应用开发>(二)笔记可显示Part2:定义笔记的数据结构类型

轻松上手:<Android Studio笔记应用开发>(二)笔记可显示Part3:适配器

轻松上手:<Android Studio笔记应用开发>(二)大功告成!添加新笔记!

轻松上手:<Android Studio笔记应用开发>(三)笔记可再编辑

为了创建一个简单的Android笔记应用,前文已经成功实现了以下主要功能:

  1. 笔记的展示: 主活动(MainActivity)中通过一个列表视图(ListView)展示了所有笔记的内容和创建时间。

  2. 笔记的添加: 用户通过悬浮按钮(FloatingActionButton)可以添加新的笔记,进入编辑页面(EditActivity),并在该页面输入笔记内容后保存。

  3. 笔记的保存和显示: 新添加的笔记会保存在 SQLite 数据库中,主活动在每次启动时从数据库读取笔记列表,并通过适配器(NoteAdapter)将笔记显示在列表视图中。

  4. 笔记的点击编辑: 用户可以点击笔记列表中的项,进入编辑页面,编辑该笔记的内容。

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 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 noteList;
    private OnNoteItemClickListener onNoteItemClickListener;
    private OnNoteItemLongClickListener onNoteItemLongClickListener;
    // 默认构造函数
    public NoteAdapter(){ }
    // 带参数的构造函数,接受上下文和笔记列表
    public NoteAdapter(Context Context,List noteList){ this.context=Context;
        this.noteList=noteList;
    }
    public NoteAdapter(Context context, List noteList, OnNoteItemClickListener onNoteItemClickListener) { this.context = context;
        this.noteList = noteList;
        this.onNoteItemClickListener = onNoteItemClickListener;
    }
    public NoteAdapter(Context context, List 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 noteList = new ArrayList<>(); // 笔记列表
    private FloatingActionButton btn; // 悬浮按钮
    private ListView lv; // 列表视图
    // 定义一个 ActivityResultLauncher,用于处理其他活动的结果
    private ActivityResultLauncher 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 getAllNotes() { Cursor cursor = db.query(
                NoteDatabase.TABLE_NAME,  // 表名
                columns,                // 要查询的列(在这里是ID、内容、时间)
                null,                   // 查询条件(null表示无特殊条件)
                null,                   // 查询条件参数(null表示无特殊条件)
                null,                   // 分组方式(null表示不分组)
                null,                   // 过滤方式(null表示不过滤)
                null                    // 排序方式(null表示不排序)
        );
        List 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 应用,更灵活处理长按事件。别忘了思考文章结尾的提问哦,欢迎交流!🚀

      持续爆肝更新中…🫡🫡🫡

      求点赞👍求关注🩷求转发💕