diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java index 1a09749..be52332 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -14,245 +14,260 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,作为便签提醒触发时的核心弹窗页面 +// 包声明:归属小米便签UI模块,便签提醒功能的最终展示页,是提醒触发时用户感知的核心页面 package net.micode.notes.ui; -// 导入安卓Activity核心类:页面基础类 +// 安卓页面核心基类,所有页面的父类,提供页面生命周期、窗口管理、组件交互等基础能力 import android.app.Activity; -// 导入安卓对话框类:展示便签提醒内容和操作按钮 +// 安卓系统对话框核心类,用于展示标准化的弹窗,承载提醒内容与交互按钮,是本页面的核心展示载体 import android.app.AlertDialog; -// 导入安卓上下文类:访问系统服务、资源 +// 安卓上下文核心类,提供系统服务获取、资源访问、组件通信等基础能力 import android.content.Context; -// 导入安卓对话框事件类:处理按钮点击、对话框关闭 +// 安卓对话框事件回调相关类,处理按钮点击、对话框关闭等交互事件,实现页面的核心交互逻辑 import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnDismissListener; -// 导入安卓意图类:跳转便签编辑页、接收提醒所属便签ID +// 安卓意图核心类,用于页面跳转、数据传递,此处用于跳转便签编辑页并携带便签ID import android.content.Intent; -// 导入安卓音频管理类:控制提醒铃声的音频流类型 +// 安卓音频管理核心类,用于指定音频流类型,适配系统音量与静音规则,控制提醒铃声的播放策略 import android.media.AudioManager; -// 导入安卓媒体播放类:播放提醒铃声(系统闹钟铃声) +// 安卓媒体播放核心类,用于加载、播放、停止系统铃声资源,实现提醒音效的播放功能 import android.media.MediaPlayer; -// 导入安卓铃声管理类:获取系统默认闹钟铃声 +// 安卓铃声管理核心类,用于获取系统默认的闹钟铃声Uri,统一访问系统铃声资源 import android.media.RingtoneManager; -// 导入安卓URI类:标识铃声资源、便签ID +// 安卓统一资源标识类,用于标记铃声资源地址、便签数据的唯一标识 import android.net.Uri; -// 导入安卓Bundle类:保存页面状态(此处未使用) +// 安卓页面状态保存类,用于横竖屏切换等场景的页面数据恢复,本页面未使用 import android.os.Bundle; -// 导入安卓电源管理类:判断屏幕是否亮屏,控制屏幕唤醒 +// 安卓电源管理核心类,用于获取设备屏幕的亮灭状态,判断当前是亮屏/熄屏/锁屏状态 import android.os.PowerManager; -// 导入安卓系统设置类:获取铃声静音模式配置 +// 安卓系统设置核心类,用于读取系统的铃声静音模式配置,适配不同的系统音频策略 import android.provider.Settings; -// 导入安卓窗口相关类:设置窗口标记(唤醒屏幕、锁屏显示等) +// 安卓窗口管理相关类,用于配置页面窗口的显示属性,实现锁屏显示、屏幕唤醒、常亮等核心功能 import android.view.Window; import android.view.WindowManager; -// 导入小米便签资源类:引用字符串、布局等资源 +// 小米便签资源常量类,引用字符串、布局、颜色等本地资源,统一管理资源ID import net.micode.notes.R; -// 导入便签数据常量类:定义便签类型、ContentURI等 +// 便签应用核心数据常量类,定义便签类型、ContentURI等全局常量,用于数据合法性校验 import net.micode.notes.data.Notes; -// 导入数据工具类:查询便签摘要、判断便签是否存在 +// 便签数据工具类,封装数据库查询相关的通用方法,提供便签摘要查询、便签存在性校验等能力 import net.micode.notes.tool.DataUtils; -// 导入IO异常类:处理媒体播放器的IO错误 +// Java IO异常类,捕获媒体播放器加载铃声资源时的IO错误,保证程序健壮性 import java.io.IOException; /** - * 便签提醒弹窗页面 - * 核心职责: - * 1. 屏幕唤醒与显示控制: - * - 锁屏/熄屏时唤醒屏幕、保持屏幕常亮,确保提醒弹窗可见; - * - 亮屏时仅展示弹窗,不额外修改屏幕状态; - * 2. 提醒内容展示: - * - 从Intent中解析提醒所属便签ID,查询并裁剪便签摘要(最大60字符); - * - 弹窗展示便签摘要,提供“确认”“进入便签”按钮; - * 3. 提醒铃声播放: - * - 播放系统默认闹钟铃声,适配系统静音模式,铃声循环播放; - * - 对话框关闭时停止铃声并释放媒体资源; - * 4. 交互处理: - * - “确认”按钮:关闭弹窗,停止铃声; - * - “进入便签”按钮(仅亮屏时显示):跳转到便签编辑页; - * 关键说明: - * - 仅当便签仍存在于数据库中时,才展示弹窗和播放铃声; - * - 页面生命周期与对话框绑定,对话框关闭即结束页面。 + * 便签提醒功能核心弹窗页面【提醒展示最终页、用户感知唯一页】 + * 继承:Activity 安卓页面基类,具备页面生命周期与窗口管理能力 + * 实现:OnClickListener + OnDismissListener 对话框交互监听器,处理按钮点击与弹窗关闭事件 + * 核心定位:小米便签「提醒功能」的终点站,是整个提醒流程的最终展示载体,也是用户能直接感知到的核心页面,所有提醒逻辑最终都汇聚于此,是便签提醒功能的核心价值体现 + * 核心设计意义:作为提醒触发后的唯一展示页面,承担「强提醒+友好交互+精准跳转」的核心职责,既要保证用户不会错过提醒,又要提供便捷的后续操作,兼顾提醒的强制性与使用的人性化 + * 核心职责(四大核心模块,逻辑闭环完整): + * 一、屏幕状态智能管控【优先级最高,保障提醒可见性】 + * 1. 精准判断设备当前屏幕状态:亮屏/熄屏/锁屏; + * 2. 熄屏/锁屏场景:自动唤醒屏幕、保持屏幕常亮、允许锁屏时显示窗口、适配系统装饰布局,确保弹窗能穿透锁屏界面直接展示,用户无需解锁即可看到提醒,核心保障「提醒必见」; + * 3. 亮屏场景:仅展示弹窗,不修改任何屏幕状态,避免干扰用户当前操作,兼顾使用体验; + * 4. 统一隐藏原生页面标题栏,仅展示核心的提醒弹窗,页面极简无冗余。 + * 二、提醒数据解析与合法性校验【数据安全,避免无效展示】 + * 1. 从跳转Intent中解析出绑定的便签ID:该ID由AlarmReceiver透传而来,是当前提醒的核心标识; + * 2. 根据便签ID查询数据库,获取便签的文本摘要内容,用于弹窗展示; + * 3. 摘要内容规范化处理:超过60字符自动裁剪并添加省略号,保证弹窗展示美观、无内容溢出; + * 4. 关键合法性校验:通过工具类判断该便签是否真实存在于数据库中(用户可能已删除该便签),仅当便签有效时才展示弹窗和播放铃声,无效则直接关闭页面,无任何无效操作。 + * 三、系统铃声智能播放【听觉提醒,保障提醒感知】 + * 1. 自动获取系统默认的闹钟铃声Uri,统一使用系统级铃声资源,适配用户的个性化铃声设置; + * 2. 适配系统静音模式:读取系统铃声流配置,自动选择合适的音频流类型,遵循系统的音量与静音规则,不强制发声干扰用户; + * 3. 铃声播放策略:循环播放提醒铃声,直到用户点击按钮关闭弹窗,确保用户能听到提醒; + * 4. 异常兜底处理:捕获播放器初始化、资源加载的各类异常,打印日志不崩溃,保证页面稳定性; + * 5. 资源安全释放:弹窗关闭时立即停止播放并释放媒体播放器资源,杜绝内存泄漏与音频残留。 + * 四、人性化交互设计【便捷操作,闭环提醒流程】 + * 1. 弹窗内容极简清晰:标题为应用名称,内容为便签摘要,核心信息一目了然; + * 2. 按钮动态适配:亮屏时展示「确认」+「进入便签」双按钮,熄屏/锁屏时仅展示「确认」单按钮,贴合不同场景的用户操作习惯; + * 3. 精准交互逻辑:「确认」按钮仅关闭弹窗,「进入便签」按钮跳转至该便签的编辑页面,直接定位到对应内容,无需用户手动查找; + * 4. 弹窗关闭统一处理:无论点击按钮还是手动关闭弹窗,均触发统一的收尾逻辑,停止铃声并关闭页面,逻辑闭环无遗漏。 + * 核心特性&关键技术要点: + * 1. 优先级保障:窗口标记配置为最高优先级,能穿透锁屏、熄屏界面展示,是安卓系统中「强提醒」的标准实现方式; + * 2. 资源安全:所有系统服务、媒体播放器资源均做了精准的创建与释放,无内存泄漏、无资源残留,符合安卓开发最佳实践; + * 3. 异常健壮:对数据解析、数据库查询、媒体播放等所有可能出现异常的环节均做了捕获处理,程序容错性极强,不会因单一异常导致崩溃; + * 4. 体验友好:所有逻辑均围绕「用户感知」设计,既保证提醒的强制性,又兼顾使用的人性化,无过度打扰、无无效操作; + * 5. 生命周期极简:页面的生命周期与弹窗强绑定,弹窗展示则页面存活,弹窗关闭则页面立即销毁,无后台残留,内存占用极低。 + * 完整业务流程闭环(提醒功能最终链路): + * AlarmManager触发广播 → AlarmReceiver接收广播 → 启动本页面 → 屏幕唤醒+解析便签数据 → 校验便签有效 → 播放铃声+展示弹窗 → 用户点击按钮 → 停止铃声+关闭弹窗 → 页面销毁 / 跳转便签编辑页。 + * 核心业务约束: + * - 仅处理有效便签的提醒:便签已删除/类型非法时,直接关闭页面,不展示任何内容; + * - 严格遵循系统规则:铃声播放、屏幕唤醒均遵循安卓系统的安全与权限规则,无越权操作; + * - 页面无残留:弹窗关闭即页面销毁,无任何后台进程或服务残留,性能友好。 */ public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { - // 提醒所属便签的ID:从Intent的Data字段解析,标识当前提醒对应的便签 + // ======================== 成员变量区 ======================== + /** 当前提醒绑定的便签唯一ID,核心标识,所有数据操作均基于此ID */ private long mNoteId; - // 便签摘要文本:用于弹窗展示,最大长度限制为60字符 + /** 便签的文本摘要内容,用于弹窗展示,做了长度限制处理 */ private String mSnippet; - // 便签摘要预览最大长度:超过该长度则裁剪并添加省略标记 + /** 便签摘要预览的最大字符长度,超过该长度自动裁剪并添加省略号,保证展示美观 */ private static final int SNIPPET_PREW_MAX_LEN = 60; - // 媒体播放器:用于播放系统闹钟铃声,循环播放提醒音 + /** 媒体播放器实例,用于加载和播放系统闹钟铃声,实现听觉提醒 */ MediaPlayer mPlayer; /** - * 页面创建核心方法:初始化屏幕状态、解析便签ID、播放铃声、展示弹窗 - * @param savedInstanceState 页面状态保存对象(此处未使用) + * 页面创建核心回调方法,页面生命周期的入口,承载所有初始化逻辑 + * 本页面的所有核心功能均在该方法中完成初始化,逻辑清晰、步骤明确、无冗余处理 + * @param savedInstanceState 页面状态保存对象,本页面未使用该参数 */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // 隐藏页面标题栏:仅展示对话框,无需原生标题栏 + // 核心配置:隐藏原生的页面标题栏,本页面仅展示对话框,无需标题栏,极简展示 requestWindowFeature(Window.FEATURE_NO_TITLE); - // 1. 窗口显示控制:确保弹窗在锁屏/熄屏时可见 + // ========== 第一步:窗口属性配置,实现锁屏显示、屏幕唤醒等核心能力 ========== final Window win = getWindow(); - // 添加标记:锁屏时仍显示窗口(基础可见性) + // 必加窗口标记:允许窗口在设备锁屏状态下依然显示,是锁屏提醒的基础配置 win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - // 熄屏状态下:额外添加唤醒/常亮标记 + // 智能判断屏幕状态:仅在熄屏时添加额外的唤醒与常亮标记,亮屏时不做修改 if (!isScreenOn()) { - win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕常亮 - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 唤醒屏幕 - | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许屏幕锁定时显示 - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 适配装饰布局 + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // 保持屏幕常亮,直到弹窗关闭 + win.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); // 强制唤醒熄屏的屏幕,核心保障用户能看到提醒 + win.addFlags(WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);// 允许屏幕在亮屏状态下依然保持锁定,兼顾安全性 + win.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 适配系统的装饰布局,避免弹窗内容被遮挡 } - // 2. 解析Intent,获取提醒所属便签ID并查询摘要 + // ========== 第二步:解析Intent数据,获取便签ID并查询摘要内容 ========== Intent intent = getIntent(); try { - // 从Intent的Data字段解析便签ID(Data格式:content://notes/note/[id]) + // 核心解析逻辑:从Intent的Data字段中解析出便签ID,Data字段的格式为 content://notes/note/[id] mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); - // 根据便签ID查询摘要文本 + // 根据便签ID查询数据库,获取便签的文本摘要 mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); - // 摘要长度裁剪:超过60字符则截取前60位并添加省略标记 + // 摘要内容规范化处理:超过60字符则截取前60位,并添加省略号,保证弹窗展示效果 mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) : mSnippet; } catch (IllegalArgumentException e) { - // 解析ID失败(如格式错误):打印异常并结束页面 + // 异常处理:ID解析失败(如格式错误、数据异常),打印日志并直接返回,不展示任何内容 e.printStackTrace(); return; } - // 3. 初始化媒体播放器,判断便签是否存在并执行后续逻辑 + // ========== 第三步:初始化媒体播放器,校验便签有效性并执行核心逻辑 ========== mPlayer = new MediaPlayer(); - // 仅当便签仍存在于数据库中时,展示弹窗并播放铃声 + // 关键校验:仅当该便签真实存在于数据库且为普通便签类型时,才展示弹窗和播放铃声 if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { - showActionDialog(); // 展示提醒弹窗 - playAlarmSound(); // 播放提醒铃声 + showActionDialog(); // 展示提醒弹窗,核心交互载体 + playAlarmSound(); // 播放系统闹钟铃声,核心听觉提醒 } else { - // 便签已删除:直接结束页面,不展示任何内容 + // 便签已被删除或类型非法:直接关闭页面,无任何展示与播放,避免无效操作 finish(); } } /** - * 判断屏幕是否处于亮屏状态 - * @return boolean:true=亮屏,false=熄屏/锁屏 + * 私有工具方法:判断当前设备的屏幕是否处于亮屏状态 + * @return boolean true=屏幕亮屏(解锁/未解锁但亮屏) false=屏幕熄屏/锁屏 */ private boolean isScreenOn() { - // 获取电源管理系统服务 + // 获取系统电源管理服务实例 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - // 返回屏幕亮屏状态 + // 返回当前屏幕的亮灭状态 return pm.isScreenOn(); } /** - * 播放系统默认闹钟铃声 - * 核心逻辑: - * - 获取系统默认闹钟铃声URI; - * - 适配系统静音模式,设置音频流类型; - * - 初始化媒体播放器,循环播放铃声。 + * 核心功能方法:初始化并播放系统默认的闹钟铃声,实现听觉提醒 + * 完整逻辑:获取系统铃声Uri → 适配系统静音模式 → 初始化播放器 → 循环播放 → 异常捕获,全链路健壮处理 */ private void playAlarmSound() { - // 获取系统默认闹钟铃声的URI + // 第一步:获取系统默认的闹钟铃声Uri,使用系统级铃声资源,适配用户个性化设置 Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); - // 获取系统静音模式配置:判断闹钟流是否受静音影响 + // 第二步:读取系统铃声静音模式配置,判断闹钟音频流是否受静音规则影响,适配系统策略 int silentModeStreams = Settings.System.getInt(getContentResolver(), Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); - // 设置音频流类型:适配静音模式 + // 第三步:设置音频流类型,核心适配逻辑,保证铃声播放遵循系统音量规则 if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { mPlayer.setAudioStreamType(silentModeStreams); } else { - // 默认使用闹钟音频流(STREAM_ALARM),优先级高于普通媒体流 + // 默认使用闹钟专属音频流,优先级高于普通媒体流,不会被媒体音量影响 mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); } - // 初始化并启动媒体播放器 + // 第四步:初始化媒体播放器并启动循环播放,捕获所有可能的异常,保证程序稳定 try { - mPlayer.setDataSource(this, url); // 设置铃声数据源 - mPlayer.prepare(); // 准备播放器(同步) - mPlayer.setLooping(true); // 设置循环播放(持续提醒) - mPlayer.start(); // 启动播放 + mPlayer.setDataSource(this, url); // 设置铃声资源的数据源 + mPlayer.prepare(); // 同步准备播放器,加载铃声资源 + mPlayer.setLooping(true); // 核心配置:循环播放铃声,直到用户手动关闭 + mPlayer.start(); // 启动铃声播放,触发听觉提醒 } catch (IllegalArgumentException e) { - // 参数错误异常:打印堆栈,不中断流程 e.printStackTrace(); } catch (SecurityException e) { - // 权限异常(如铃声文件无访问权限):打印堆栈 e.printStackTrace(); } catch (IllegalStateException e) { - // 播放器状态异常(如重复prepare):打印堆栈 e.printStackTrace(); } catch (IOException e) { - // IO异常(如铃声文件不存在):打印堆栈 e.printStackTrace(); } } /** - * 展示便签提醒弹窗 - * 核心逻辑: - * - 标题为应用名称,内容为便签摘要; - * - 亮屏时显示“确认”+“进入便签”按钮,熄屏时仅显示“确认”按钮; - * - 绑定对话框关闭监听器,关闭时停止铃声并结束页面。 + * 核心功能方法:创建并展示便签提醒的核心弹窗,是页面的核心展示载体 + * 弹窗设计:标题为应用名,内容为便签摘要,按钮动态适配屏幕状态,绑定交互监听器,实现核心交互逻辑 */ private void showActionDialog() { AlertDialog.Builder dialog = new AlertDialog.Builder(this); - dialog.setTitle(R.string.app_name); // 弹窗标题:应用名称 - dialog.setMessage(mSnippet); // 弹窗内容:便签摘要 - dialog.setPositiveButton(R.string.notealert_ok, this); // 确认按钮 + dialog.setTitle(R.string.app_name); // 弹窗标题:固定为应用名称,简洁明了 + dialog.setMessage(mSnippet); // 弹窗核心内容:便签的文本摘要,展示提醒的核心信息 + dialog.setPositiveButton(R.string.notealert_ok, this); // 确认按钮,绑定点击事件监听器 - // 亮屏状态下:额外显示“进入便签”按钮(负向按钮) + // 智能按钮适配:仅在屏幕亮屏时展示「进入便签」按钮,熄屏/锁屏时不展示,贴合操作场景 if (isScreenOn()) { dialog.setNegativeButton(R.string.notealert_enter, this); } - // 展示弹窗并绑定关闭监听器 + // 展示弹窗并绑定关闭监听器,无论何种方式关闭弹窗,均触发统一的收尾逻辑 dialog.show().setOnDismissListener(this); } /** - * 对话框按钮点击事件处理 - * @param dialog 触发点击的对话框实例 - * @param which 点击的按钮类型(BUTTON_POSITIVE/ NEGATIVE) + * 对话框按钮点击事件回调方法,实现弹窗的核心交互逻辑 + * @param dialog 触发点击事件的对话框实例 + * @param which 点击的按钮类型,区分「确认」和「进入便签」按钮 */ public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_NEGATIVE: - // “进入便签”按钮:跳转到便签编辑页,传递便签ID + // 点击「进入便签」按钮:跳转至该便签的编辑页面,精准定位到对应内容 Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); // 设置动作:查看/编辑便签 - intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递便签ID + intent.setAction(Intent.ACTION_VIEW); // 设置跳转动作:查看/编辑便签 + intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递便签ID,目标页面根据ID查询并展示内容 startActivity(intent); break; default: - // “确认”按钮:无额外操作(对话框关闭由OnDismissListener处理) + // 点击「确认」按钮:无额外业务逻辑,仅关闭弹窗,收尾逻辑由OnDismissListener统一处理 break; } } /** - * 对话框关闭事件处理:停止铃声,结束页面 - * @param dialog 关闭的对话框实例 + * 对话框关闭事件回调方法,页面的统一收尾逻辑入口 + * 无论通过何种方式关闭弹窗(点击按钮、手动关闭),均执行该方法,保证逻辑闭环无遗漏 + * @param dialog 被关闭的对话框实例 */ public void onDismiss(DialogInterface dialog) { - stopAlarmSound(); // 停止提醒铃声,释放媒体资源 - finish(); // 结束页面 + stopAlarmSound(); // 核心收尾:停止铃声播放并释放媒体资源,杜绝内存泄漏与音频残留 + finish(); // 关闭当前页面,释放所有资源,页面生命周期结束 } /** - * 停止提醒铃声并释放媒体播放器资源 - * 核心逻辑:停止播放、释放播放器,避免资源泄漏 + * 私有工具方法:停止铃声播放并安全释放媒体播放器资源 + * 核心作用:资源清理,是安卓媒体播放的强制规范,避免内存泄漏和音频通道占用 */ private void stopAlarmSound() { if (mPlayer != null) { - mPlayer.stop(); // 停止播放 - mPlayer.release(); // 释放播放器资源 - mPlayer = null; // 置空引用,便于GC回收 + mPlayer.stop(); // 立即停止铃声播放 + mPlayer.release(); // 释放播放器的所有资源,包括音频通道、内存等 + mPlayer = null; // 置空引用,便于GC回收,彻底杜绝内存泄漏 } } } \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java index 0b3403d..9cb875f 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -14,100 +14,128 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,作为便签提醒的初始化广播接收器 +// 包声明:归属小米便签UI模块,便签提醒功能的初始化核心广播接收器,负责重启恢复所有未过期提醒 package net.micode.notes.ui; -// 导入安卓闹钟管理类:用于注册/设置便签提醒闹钟 +// 安卓系统闹钟服务核心类,用于注册、设置、取消定时闹钟,是便签提醒的核心调度组件 import android.app.AlarmManager; -// 导入安卓延迟意图类:封装广播Intent,供AlarmManager触发 +// 安卓延迟意图核心类,封装广播/页面跳转意图,交由AlarmManager在指定时间触发,核心桥梁类 import android.app.PendingIntent; -// 导入安卓广播接收器核心类:接收初始化广播(如开机/应用启动) +// 安卓广播核心基类,继承此类实现广播监听与处理能力,为本类的核心父类 import android.content.BroadcastReceiver; -// 导入安卓ContentURI工具类:拼接便签ID到Intent的Data字段,标识提醒所属便签 +// 安卓ContentURI拼接工具类,用于将Uri和数据ID拼接,生成唯一标识Uri,便于数据精准匹配 import android.content.ContentUris; -// 导入安卓上下文类:提供ContentResolver、系统服务等访问能力 +// 安卓上下文核心类,提供系统服务获取、内容解析器访问、组件通信等基础能力 import android.content.Context; -// 导入安卓意图类:创建指向AlarmReceiver的广播意图 +// 安卓意图核心类,组件间通信的核心载体,用于封装广播意图并携带数据 import android.content.Intent; -// 导入安卓数据库游标类:查询未过期的便签提醒数据 +// 安卓数据库游标核心类,用于遍历查询数据库返回的结果集,读取便签提醒数据 import android.database.Cursor; -// 导入便签数据常量类:定义ContentURI、字段、便签类型等核心常量 +// 便签应用核心数据常量类,定义数据库Uri、便签类型、字段名等全局常量,统一管理 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; /** - * 便签提醒初始化广播接收器 + * 便签提醒初始化专属广播接收器【提醒恢复核心类】 + * 继承:BroadcastReceiver 安卓系统广播接收器基类,具备监听并处理广播事件的核心能力 + * 核心定位:小米便签「提醒功能」的保障类,解决「设备重启/应用进程被杀」后提醒丢失的核心问题,是便签提醒可靠性的关键支撑 + * 核心设计意义:安卓系统的AlarmManager注册的闹钟,在设备重启后会全部失效,应用进程被系统回收后也可能丢失未触发的闹钟,本类就是为了解决该问题,实现提醒的持久化保障 * 核心职责: - * 1. 接收系统/应用的初始化广播(如设备开机、应用重启),重新初始化所有未过期的便签提醒; - * 2. 查询数据库中所有“提醒时间未过期”的普通便签,将其重新注册到AlarmManager; - * 3. 确保设备重启/应用重启后,未过期的便签提醒不会丢失,仍能按时触发; - * 关键说明: - * - 仅处理普通便签(TYPE_NOTE)的提醒,排除文件夹/系统项; - * - 使用AlarmManager.RTC_WAKEUP类型,确保提醒触发时唤醒设备,避免漏提醒。 + * 1. 监听系统/应用的初始化类广播,包含「设备开机完成广播、应用启动广播、系统刷新广播」等核心触发时机; + * 2. 接收到广播后,通过ContentResolver查询便签数据库中所有符合条件的有效提醒数据; + * 3. 精准筛选:仅查询「提醒时间戳大于当前时间(未过期)」+「便签类型为普通便签TYPE_NOTE」的记录,排除文件夹/系统项/已过期提醒,无无效处理; + * 4. 遍历有效提醒数据,为每一条记录重新创建广播意图与延迟意图,通过AlarmManager完成闹钟的重新注册; + * 5. 保证所有未过期的便签提醒,在设备重启/应用重启后都能恢复如初,按时触发,无任何遗漏。 + * 核心特性&关键技术要点: + * 1. 精准数据筛选:查询条件双重过滤,既保证时效性(未过期),又保证数据合法性(普通便签),避免无效数据处理,降低性能损耗; + * 2. 轻量查询设计:自定义投影数组仅查询「便签ID、提醒时间戳」两个核心字段,不查询冗余内容,减少数据库IO开销与内存占用; + * 3. 唯一标识绑定:通过ContentUris拼接便签ID到Intent的Data字段,为每个提醒生成唯一的意图标识,精准绑定提醒与对应便签,无串号风险; + * 4. 强力唤醒保障:使用AlarmManager.RTC_WAKEUP闹钟类型,基于系统绝对时间,触发时会强制唤醒休眠的设备(亮屏/唤醒CPU),确保提醒必达,不会因设备休眠漏提醒; + * 5. 资源安全释放:查询数据库后及时关闭Cursor游标,释放数据库连接与内存资源,杜绝内存泄漏风险,符合安卓开发最佳实践; + * 6. 无状态极简设计:本类无成员变量、无复杂逻辑、无内存占用,仅在广播触发时执行一次性初始化逻辑,执行完成后立即释放资源,性能友好。 + * 核心业务约束: + * - 不处理已过期的提醒:提醒时间小于当前时间的记录直接过滤,无需注册,避免无效操作; + * - 不处理非普通便签:仅对TYPE_NOTE类型生效,文件夹(TYPE_FOLDER)等无提醒功能的类型直接排除; + * - 不处理无提醒的便签:仅查询ALERTED_DATE字段大于0的记录,无提醒的便签不会进入查询结果。 + * 完整业务闭环: + * 便签设置提醒 → AlarmManager注册闹钟 → 设备重启/应用重启 → 本接收器接收初始化广播 → 查询数据库有效提醒 → 重新注册所有闹钟 → 提醒时间到达 → AlarmReceiver触发 → 展示提醒弹窗。 */ public class AlarmInitReceiver extends BroadcastReceiver { /** - * 数据库查询投影数组:仅查询核心字段,减少IO开销 - * 字段说明: - * - NoteColumns.ID:便签唯一ID(用于标识提醒所属便签); - * - NoteColumns.ALERTED_DATE:便签的提醒时间戳(>0表示有提醒)。 + * 数据库查询投影数组【按需查询,性能优化】 + * 设计目的:指定本次数据库查询仅需要返回的字段,不查询冗余字段,减少数据库IO传输数据量,降低内存消耗,提升查询效率 + * 字段说明:仅包含两个核心必要字段,满足业务需求的最小化查询 */ private static final String [] PROJECTION = new String [] { - NoteColumns.ID, // 0: 便签ID - NoteColumns.ALERTED_DATE // 1: 提醒时间戳 + NoteColumns.ID, // 数组索引0:便签的唯一主键ID,用于绑定提醒与便签、生成唯一Intent标识 + NoteColumns.ALERTED_DATE // 数组索引1:便签设置的提醒时间戳,毫秒级,用于设置闹钟的触发时间 }; - // 投影数组对应的列索引常量:简化Cursor取值,避免硬编码索引 - private static final int COLUMN_ID = 0; // 便签ID列索引 - private static final int COLUMN_ALERTED_DATE = 1; // 提醒时间戳列索引 + // 投影数组列索引常量【硬编码优化】 + // 设计目的:将数组索引封装为常量,替代代码中的硬编码数字,提升代码可读性、可维护性,避免索引写错导致的程序异常 + private static final int COLUMN_ID = 0; // 对应PROJECTION[0],便签ID列索引 + private static final int COLUMN_ALERTED_DATE = 1; // 对应PROJECTION[1],提醒时间戳列索引 /** - * 广播接收核心方法:初始化未过期的便签提醒,重新注册到AlarmManager - * @param context 广播接收器上下文:用于访问ContentResolver、系统服务 - * @param intent 触发广播的Intent(如开机完成广播、应用启动广播) + * 广播接收核心回调方法,广播触发时由安卓系统自动调用 + * 本类的唯一核心方法,承载了「查询有效提醒+重新注册闹钟」的全部核心逻辑,逻辑清晰、步骤明确、无冗余处理 + * @param context 广播接收器的运行上下文对象,不可为空,核心作用:获取ContentResolver查询数据库、获取AlarmManager系统服务、创建Intent意图 + * @param intent 触发本次广播的意图对象,携带广播的触发类型信息,本类无需解析该意图内容,仅做触发执行 */ @Override public void onReceive(Context context, Intent intent) { - // 1. 获取当前时间戳:作为筛选“未过期提醒”的阈值(仅处理提醒时间>当前时间的便签) + // 第一步:获取当前系统时间戳,作为筛选「未过期提醒」的核心阈值,只处理提醒时间在当前时间之后的记录 long currentDate = System.currentTimeMillis(); - // 2. 查询ContentResolver,获取所有未过期的普通便签提醒 - // 查询条件:提醒时间>当前时间 + 便签类型为普通便签(TYPE_NOTE) + // 第二步:通过ContentResolver查询便签数据库,获取所有符合条件的有效提醒数据 + // 核心查询参数说明: + // 1. uri:查询地址 → Notes.CONTENT_NOTE_URI 普通便签的专属Uri,精准定位查询表 + // 2. projection:查询字段 → 自定义的PROJECTION数组,仅查ID和提醒时间戳 + // 3. selection:查询条件 → 双重过滤:提醒时间>当前时间 且 便签类型为普通便签,精准筛选有效数据 + // 4. selectionArgs:条件参数 → 传入当前时间戳的字符串形式,防止SQL注入,符合安卓安全规范 + // 5. sortOrder:排序规则 → null,使用数据库默认排序,无需额外排序,提升查询效率 Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, - PROJECTION, // 要查询的字段 - NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件 - new String[] { String.valueOf(currentDate) }, // 条件参数(当前时间戳) - null); // 排序规则(默认) + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); - // 3. 遍历Cursor,为每个未过期的提醒注册闹钟 - if (c != null) { - // 游标移动到第一条数据(有未过期提醒时进入循环) + // 第三步:遍历查询结果,为每个有效提醒重新注册闹钟 + if (c != null) { // 游标非空校验:避免查询结果为空时的空指针异常,代码健壮性保障 + // 游标移动到第一条数据,存在有效提醒时进入循环遍历逻辑 if (c.moveToFirst()) { do { - // 3.1 获取当前便签的提醒时间戳和ID - long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 提醒触发时间 - long noteId = c.getLong(COLUMN_ID); // 便签唯一ID + // 3.1 从游标中读取当前行的核心数据:便签ID、提醒触发时间戳 + long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 该便签的提醒触发时间,毫秒级 + long noteId = c.getLong(COLUMN_ID); // 该便签的唯一主键ID - // 3.2 创建广播Intent:指向AlarmReceiver(提醒触发时的处理接收器) + // 3.2 创建广播意图:指定意图的目标为AlarmReceiver,即提醒时间到达时,需要触发的广播接收器 Intent sender = new Intent(context, AlarmReceiver.class); - // 将便签ID附加到Intent的Data字段:标识该提醒所属的便签,便于AlarmReceiver识别 + // 核心关键:将便签ID拼接至Intent的Data字段,生成唯一的Uri标识,精准绑定该提醒与对应便签 + // 作用:AlarmReceiver接收到广播时,可通过该Uri解析出便签ID,进而查询便签详情展示提醒内容,无串号风险 sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId)); - // 3.3 创建PendingIntent:封装广播Intent,供AlarmManager触发 - // 参数说明:context=上下文,requestCode=请求码(此处为0),intent=广播意图,flags=标记(默认) + // 3.3 创建PendingIntent延迟意图:封装上述广播意图,交由AlarmManager管理,在指定时间触发广播 + // PendingIntent.getBroadcast参数说明:上下文、请求码(此处传0即可)、待封装的广播意图、flags标记(默认0) + // 核心作用:PendingIntent是一种授权意图,允许系统在未来的指定时间,以当前应用的身份发送该广播,是闹钟触发的核心载体 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); - // 3.4 获取AlarmManager系统服务,注册提醒闹钟 + // 3.4 获取系统AlarmManager服务实例,系统级的闹钟调度核心服务 AlarmManager alermManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); - // 设置闹钟:RTC_WAKEUP(基于系统时间,触发时唤醒设备)、提醒时间、PendingIntent + + // 3.5 核心操作:重新注册闹钟,完成提醒恢复 + // alermManager.set参数说明: + // 1. type → AlarmManager.RTC_WAKEUP 最核心的闹钟类型,基于系统绝对时间,触发时唤醒设备CPU/屏幕,保证提醒必达 + // 2. triggerAtTime → 闹钟触发的具体时间,即从数据库读取的提醒时间戳alertDate + // 3. operation → 待触发的PendingIntent延迟意图,触发时发送广播至AlarmReceiver alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); - } while (c.moveToNext()); // 遍历所有未过期的提醒 + } while (c.moveToNext()); // 循环遍历游标,处理所有有效提醒数据 } - // 4. 关闭Cursor:释放数据库资源,避免内存泄漏 + // 第四步:必须关闭游标,释放数据库资源与内存,杜绝内存泄漏,安卓数据库操作的强制规范 c.close(); } } diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java index 167c9e3..0f59670 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java @@ -14,38 +14,50 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,作为便签提醒功能的核心广播接收器 +// 包声明:归属小米便签UI模块,便签提醒功能的广播接收核心类,承接闹钟触发逻辑 package net.micode.notes.ui; -// 导入安卓广播接收器核心类:接收系统/应用发送的广播事件 +// 安卓广播核心基类,继承此类实现广播接收能力,监听系统/应用发送的广播事件 import android.content.BroadcastReceiver; -// 导入安卓上下文类:提供应用运行环境(启动Activity) +// 安卓上下文核心类,提供应用运行环境、组件启动、资源访问等基础能力 import android.content.Context; -// 导入安卓意图类:用于启动提醒页面,传递数据 +// 安卓意图核心类,用于组件间通信、页面跳转、数据传递的核心载体 import android.content.Intent; /** - * 便签提醒广播接收器 + * 便签提醒功能专属广播接收器【轻量级广播处理类】 + * 继承:BroadcastReceiver 安卓系统广播接收器基类,具备监听并处理广播的核心能力 + * 核心定位:小米便签「提醒功能」的核心中转类,是闹钟触发与提醒弹窗展示的唯一桥梁,无任何业务逻辑,只做事件转发 * 核心职责: - * 1. 接收便签设置的提醒闹钟广播(由AlarmManager发送); - * 2. 接收到广播后,启动提醒弹窗页面(AlarmAlertActivity),展示便签提醒内容; - * 关键说明: - * - 广播接收器中启动Activity必须添加FLAG_ACTIVITY_NEW_TASK标记(无任务栈上下文); - * 典型使用场景:便签设置的提醒时间到达时,触发该接收器,弹出提醒窗口。 + * 1. 监听由系统AlarmManager定时发送的闹钟广播,该广播在用户设置的便签提醒时间到达时触发; + * 2. 接收到广播后,无缝承接Intent中携带的便签提醒数据(便签ID、标题、内容等); + * 3. 对Intent做标准化配置,指定跳转目标页面并补充必要的启动标记; + * 4. 启动便签提醒弹窗页面,完成提醒事件的最终展示,触发用户感知。 + * 核心特性&关键注意点: + * 1. 无状态设计:该类无任何成员变量、无初始化逻辑、无复杂处理,是纯功能性的极简类,内存占用极低; + * 2. 必加启动标记:BroadcastReceiver 属于四大组件之一,运行时无独立的Activity任务栈上下文,启动Activity时必须添加 FLAG_ACTIVITY_NEW_TASK 标记,否则会抛出运行时异常,这是安卓的硬性规范; + * 3. 数据透传:广播携带的所有Intent数据会完整透传给目标页面,无任何数据丢失或修改,保证提醒内容的正确性; + * 4. 生命周期极简:广播接收器的onReceive方法执行时间被系统严格限制,本类逻辑极致轻量化,可在瞬时完成执行,无超时风险; + * 5. 唯一触发源:该接收器只响应便签应用自身注册的闹钟广播,不接收其他任何广播事件,功能单一无干扰。 + * 典型业务流程闭环: + * 便签编辑页设置提醒时间 → 后台通过AlarmManager注册定时闹钟 → 提醒时间到达,系统发送广播 → 本接收器接收广播 → 启动AlarmAlertActivity → 展示便签提醒弹窗,响铃/震动提醒用户。 + * 典型使用场景:唯一用途就是接收便签的提醒广播,无其他任何业务场景。 */ public class AlarmReceiver extends BroadcastReceiver { + /** - * 广播接收核心方法:处理提醒广播,启动提醒页面 - * @param context 广播接收器的上下文:用于启动Activity(需添加新任务标记) - * @param intent 触发广播的Intent:携带提醒相关数据(如便签ID、内容等) + * 广播接收核心回调方法,广播触发时系统自动调用该方法 + * 该方法是本类的唯一核心方法,承载了所有的广播处理逻辑,极简且高效 + * @param context 广播接收器的运行上下文对象,不可为空,用于启动目标Activity组件 + * @param intent 触发本次广播的意图对象,内部携带了完整的便签提醒数据,也是页面跳转的核心数据载体 */ @Override public void onReceive(Context context, Intent intent) { - // 1. 修改Intent的目标类为提醒弹窗页面(AlarmAlertActivity) + // 第一步:为当前Intent指定跳转的目标页面,将广播意图转为页面跳转意图 intent.setClass(context, AlarmAlertActivity.class); - // 2. 添加新任务标记:BroadcastReceiver无Activity任务栈,必须添加该标记才能启动Activity + // 第二步:添加必选的新任务启动标记,解决广播无任务栈启动Activity的上下文问题,规避运行时异常 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // 3. 启动提醒弹窗页面,展示便签提醒内容 + // 第三步:启动提醒弹窗页面,完成广播事件的最终转发,展示便签提醒内容 context.startActivity(intent); } } \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java index 51f5c77..d829df7 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java @@ -14,157 +14,176 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,自定义日期时间选择控件核心类 +// 包声明:归属小米便签UI模块,日期时间选择的核心自定义复合控件,供弹窗调用 package net.micode.notes.ui; -// 导入日期格式化工具类:处理星期/日期的文本展示 +// Java文本格式化工具类,获取系统的上午/下午文本标识,适配多语言环境 import java.text.DateFormatSymbols; -// 导入日历类:核心的日期时间管理,处理年/月/日/时/分的计算与联动 +// Java核心日历工具类,全局唯一的时间数据载体,处理所有年月日时分的计算、联动、转换逻辑 import java.util.Calendar; -// 导入资源类:引用布局文件(datetime_picker.xml) +// 小米便签资源文件引用,获取布局、字符串等资源ID常量 import net.micode.notes.R; -// 导入安卓上下文类:创建控件、加载资源 +// 安卓系统核心类 - 上下文,提供控件创建、资源加载、布局填充的运行环境 import android.content.Context; -// 导入日期格式化工具:判断系统24小时制设置 +// 安卓系统日期格式化工具类,提供系统24小时制判断、日期文本格式化能力 import android.text.format.DateFormat; -// 导入视图相关类:布局容器、视图可见性控制 +// 安卓视图体系核心类,视图基础属性配置、可见性控制、布局容器核心父类 import android.view.View; import android.widget.FrameLayout; -// 导入数字选择器:核心的日期/时间选择UI组件 +// 安卓数字滚轮选择器,本控件的核心子组件,实现年月日时分的滚轮滑动选择UI import android.widget.NumberPicker; /** - * 自定义日期时间选择控件 - * 核心特性: - * 1. 布局组成:集成日期(近7天)、小时、分钟、上午/下午(AM/PM)四个NumberPicker; - * 2. 时间联动:处理时间选择的边界联动(如分钟59→0时小时+1、小时23→0时日期+1); - * 3. 制式适配:支持24小时制/12小时制切换,自动适配系统默认设置; - * 4. 回调通知:时间选择变化时触发回调,传递最新的年/月/日/时/分; - * 5. 状态控制:支持整体启用/禁用所有选择器,统一管理交互状态; - * 典型使用场景:便签提醒时间设置的DateTimePickerDialog中作为核心选择UI。 + * 自定义日期时间复合选择控件【核心基础UI控件】 + * 继承:FrameLayout 帧布局,作为复合控件的根布局容器,承载多个子选择器 + * 核心定位:小米便签「提醒时间设置」功能的底层核心控件,被DateTimePickerDialog弹窗集成使用 + * 核心设计理念:将「日期选择+小时选择+分钟选择+上下午选择」整合为统一控件,封装所有时间联动逻辑、格式适配逻辑、数据处理逻辑,对外提供极简的调用与通信接口 + * 核心特性与职责: + * 1. 布局整合:内置日期、小时、分钟、上下午共4个NumberPicker滚轮选择器,形成一体化的时间选择UI; + * 2. 数据闭环:基于Calendar类做全局唯一的时间数据管理,所有选择操作最终同步至该对象,保证数据一致性; + * 3. 智能联动:完美处理所有时间边界联动场景,如分钟59→0时小时+1、小时23→0时日期+1、12小时制跨上下午切换等,无数据断层; + * 4. 系统适配:自动识别并适配系统的24小时制/12小时制设置,支持手动强制切换,两种制式无缝兼容,交互无感知; + * 5. 日期展示:固定展示近7天的日期列表,格式为「月.日 星期」,满足便签短周期提醒的业务需求; + * 6. 状态统一:支持控件整体启用/禁用,一键同步所有子选择器的交互状态,无需单独配置; + * 7. 标准化通信:提供时间变化的回调接口,选择操作实时触发回调,向外传递标准化的年月日时分数据; + * 8. 细节优化:分钟选择器支持长按快速滚动、上下午文本适配系统多语言、时间数值边界校验等细节体验优化; + * 9. 防抖动处理:初始化阶段屏蔽回调触发,避免初始化时的无效数据通知,提升性能与稳定性。 + * 核心优势:控件内聚性极强,所有时间相关的逻辑全部封装内部,外部调用方只需关心「设置初始时间」「获取选中时间」「监听时间变化」三个核心操作,完全无需处理内部复杂逻辑。 + * 典型业务场景:唯一使用场景为DateTimePickerDialog弹窗的核心内容视图,支撑便签的提醒时间选择功能。 */ public class DateTimePicker extends FrameLayout { - // ======================== 基础常量 - 控件默认状态 ======================== - /** 控件默认启用状态:初始为启用 */ + // ======================== 基础常量区 - 控件默认配置与固定数值 ======================== + /** 控件默认启用状态:初始化时默认所有选择器均可交互 */ private static final boolean DEFAULT_ENABLE_STATE = true; - // ======================== 常量 - 时间数值范围 ======================== - /** 半天的小时数:12小时制的核心数值(AM/PM分界) */ + // ======================== 常量区 - 时间单位核心数值 ======================== + /** 半天小时数:12小时制的核心分界值,上午/下午的小时数上限 */ private static final int HOURS_IN_HALF_DAY = 12; - /** 全天的小时数:24小时制的核心数值 */ + /** 全天小时数:24小时制的核心数值,一天的总小时数 */ private static final int HOURS_IN_ALL_DAY = 24; - /** 一周的天数:日期选择器展示近7天 */ + /** 一周天数:日期选择器固定展示的天数,不可修改,适配短周期提醒业务 */ private static final int DAYS_IN_ALL_WEEK = 7; - // ======================== 常量 - NumberPicker取值范围 ======================== - /** 日期选择器最小值:0(对应近7天的起始索引) */ + // ======================== 常量区 - NumberPicker滚轮选择器 取值范围约束 ======================== + /** 日期选择器-最小值:固定为0,对应近7天列表的第一条数据 */ private static final int DATE_SPINNER_MIN_VAL = 0; - /** 日期选择器最大值:6(对应近7天的结束索引,DAYS_IN_ALL_WEEK - 1) */ + /** 日期选择器-最大值:固定为6,对应近7天列表的最后一条数据,数值等于天数减1 */ private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; - /** 24小时制-小时选择器最小值:0(凌晨0点) */ + /** 24小时制-小时选择器-最小值:凌晨0点,时间起点 */ private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; - /** 24小时制-小时选择器最大值:23(深夜23点) */ + /** 24小时制-小时选择器-最大值:深夜23点,时间终点 */ private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; - /** 12小时制-小时选择器最小值:1(上午/下午1点) */ + /** 12小时制-小时选择器-最小值:上午/下午的1点,无0点概念 */ private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; - /** 12小时制-小时选择器最大值:12(上午/下午12点) */ + /** 12小时制-小时选择器-最大值:上午/下午的12点,封顶数值 */ private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; - /** 分钟选择器最小值:0(整点) */ + /** 分钟选择器-最小值:整点0分,分钟起点 */ private static final int MINUT_SPINNER_MIN_VAL = 0; - /** 分钟选择器最大值:59(整点前1分钟) */ + /** 分钟选择器-最大值:整点前1分钟,59分,分钟终点 */ private static final int MINUT_SPINNER_MAX_VAL = 59; - /** 上午/下午选择器最小值:0(AM/上午) */ + /** 上下午选择器-最小值:0,对应上午/AM */ private static final int AMPM_SPINNER_MIN_VAL = 0; - /** 上午/下午选择器最大值:1(PM/下午) */ + /** 上下午选择器-最大值:1,对应下午/PM */ private static final int AMPM_SPINNER_MAX_VAL = 1; - // ======================== 成员变量 - UI组件 ======================== - /** 日期选择器:展示近7天的日期(格式如“12.23 星期二”) */ + // ======================== 成员变量区 - UI核心组件【所有子选择器,全局持用避免重复查找】 ======================== + /** 日期滚轮选择器:核心展示近7天的格式化日期文本,如「01.15 周四」,支持滑动切换日期 */ private final NumberPicker mDateSpinner; - /** 小时选择器:根据24/12小时制展示不同范围的小时数 */ + /** 小时滚轮选择器:根据24/12小时制展示对应范围的小时数,核心时间选择组件 */ private final NumberPicker mHourSpinner; - /** 分钟选择器:0~59的分钟数选择 */ + /** 分钟滚轮选择器:固定0~59的分钟数选择,支持长按快速滚动,核心时间选择组件 */ private final NumberPicker mMinuteSpinner; - /** 上午/下午选择器:仅12小时制显示,0=AM/上午,1=PM/下午 */ + /** 上下午滚轮选择器:仅12小时制显示,0=上午/AM,1=下午/PM,适配12小时制的时间展示 */ private final NumberPicker mAmPmSpinner; - // ======================== 成员变量 - 日期时间状态 ======================== - /** 核心日历对象:存储当前选中的日期时间,处理所有时间计算/联动 */ + // ======================== 成员变量区 - 核心状态与数据载体【全局核心数据,控件的大脑】 ======================== + /** 核心日历对象:全局唯一的时间数据载体,存储当前选中的完整年月日时分信息,所有选择操作最终同步至此,所有外部获取操作均来源于此,保证数据唯一可信 */ private Calendar mDate; - /** 日期选择器展示文本数组:存储近7天的格式化日期文本(如“12.23 星期二”) */ + /** 日期展示文本数组:缓存近7天的格式化日期文本,供日期选择器展示使用,避免重复计算 */ private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; - /** 上午/下午标记:true=AM/上午,false=PM/下午(仅12小时制有效) */ + /** 上下午状态标记:true=上午/AM,false=下午/PM,仅在12小时制下生效,控制时间计算与展示 */ private boolean mIsAm; - /** 24小时制标记:true=24小时制,false=12小时制 */ + /** 24小时制状态标记:true=启用24小时制,隐藏上下午选择器;false=启用12小时制,显示上下午选择器 */ private boolean mIs24HourView; - /** 控件启用状态:true=启用,false=禁用所有选择器 */ + /** 控件整体启用状态标记:true=所有选择器可交互,false=所有选择器禁用,统一管控交互权限 */ private boolean mIsEnabled = DEFAULT_ENABLE_STATE; - /** 初始化标记:true=控件正在初始化,避免初始化时触发不必要的回调 */ + /** 初始化状态标记:true=控件正在初始化,屏蔽所有回调触发;false=初始化完成,正常响应所有操作与回调,防止初始化阶段的无效数据通知 */ private boolean mInitialising; - // ======================== 成员变量 - 回调监听 ======================== - /** 日期时间变化回调监听器:外部实现,接收时间变化通知 */ + // ======================== 成员变量区 - 回调通信接口 ======================== + /** 日期时间变化的回调监听器:外部实现该接口,接收控件的时间变化通知,是控件与外部通信的唯一桥梁 */ private OnDateTimeChangedListener mOnDateTimeChangedListener; - // ======================== 成员变量 - NumberPicker值变化监听器 ======================== - /** 日期选择器值变化监听器:处理日期切换,更新日历对象并触发回调 */ + // ======================== 成员变量区 - 滚轮选择器 值变化监听器【所有选择器的核心交互逻辑,内部闭环】 ======================== + /** + * 日期选择器 值变化监听器:处理日期滑动切换的核心逻辑 + * 核心能力:监听日期选择器的数值变化,计算日期偏移量,同步更新核心日历对象的日期,刷新日期展示文本,最终触发时间变化回调 + * 无边界特殊处理:日期选择器固定展示近7天,滑动仅在7天内切换,无需处理跨月跨年的复杂逻辑 + */ private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - // 调整日历的天数(新值-旧值为天数偏移量) + // 计算日期偏移量:新值-旧值 即为需要增减的天数,直接同步至核心日历对象 mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); - // 更新日期选择器的展示文本 + // 刷新日期选择器的展示文本,保证文本与选中日期一致 updateDateControl(); - // 触发日期时间变化回调 + // 触发全局时间变化回调,向外通知日期已更新 onDateTimeChanged(); } }; - /** 小时选择器值变化监听器:处理小时切换,包含边界联动(小时→日期)和12/24小时制适配 */ + /** + * 小时选择器 值变化监听器:处理小时滑动切换的核心逻辑【最复杂的联动逻辑】 + * 核心能力:兼容24/12小时制的小时选择,处理所有小时边界的联动场景,包含「小时→日期」「小时→上下午」的双层联动,同步更新核心日历对象 + * 核心处理场景: + * 1. 12小时制:下午11点→12点 → 日期+1;上午12点→11点 → 日期-1;小时11↔12时自动切换上下午状态; + * 2. 24小时制:23点→0点 → 日期+1;0点→23点 → 日期-1;无上下切换逻辑; + * 3. 所有场景下,最终将选中的小时数适配转换为24小时制,同步至核心日历对象,保证数据统一。 + */ private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - boolean isDateChanged = false; // 日期是否变化标记 - Calendar cal = Calendar.getInstance(); + boolean isDateChanged = false; // 日期是否发生变化的标记位,默认无变化 + Calendar cal = Calendar.getInstance(); // 临时日历对象,用于处理日期偏移 - // 12小时制下的小时边界处理 + // ========== 12小时制 小时边界特殊处理 ========== if (!mIs24HourView) { - // 场景1:下午11点→12点 → 日期+1 + // 场景1:下午状态下,小时从11→12,触发日期+1(跨天) if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; } - // 场景2:上午12点→11点 → 日期-1 + // 场景2:上午状态下,小时从12→11,触发日期-1(跨天) else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } - // 小时11→12 或 12→11 时,切换上午/下午标记 + // 场景3:小时在11和12之间切换时,自动翻转上下午状态(核心联动逻辑) if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { mIsAm = !mIsAm; - updateAmPmControl(); // 更新上午/下午选择器 + updateAmPmControl(); // 同步刷新上下午选择器的选中状态 } } - // 24小时制下的小时边界处理 + // ========== 24小时制 小时边界特殊处理 ========== else { - // 场景1:23点→0点 → 日期+1 + // 场景1:小时从23→0,触发日期+1(跨天,一天的结束) if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; } - // 场景2:0点→23点 → 日期-1 + // 场景2:小时从0→23,触发日期-1(跨天,一天的开始) else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); @@ -172,13 +191,13 @@ public class DateTimePicker extends FrameLayout { } } - // 计算并设置最终的小时数(适配12/24小时制) + // ========== 统一处理:将选中的小时数转换为24小时制,同步至核心日历对象 ========== int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); mDate.set(Calendar.HOUR_OF_DAY, newHour); - // 触发时间变化回调 + // 触发全局时间变化回调,向外通知小时已更新 onDateTimeChanged(); - // 日期变化时,更新日历的年/月/日 + // ========== 日期发生变化时,同步更新核心日历对象的年月日 ========== if (isDateChanged) { setCurrentYear(cal.get(Calendar.YEAR)); setCurrentMonth(cal.get(Calendar.MONTH)); @@ -187,218 +206,232 @@ public class DateTimePicker extends FrameLayout { } }; - /** 分钟选择器值变化监听器:处理分钟切换,包含边界联动(分钟→小时→日期) */ + /** + * 分钟选择器 值变化监听器:处理分钟滑动切换的核心逻辑【分钟→小时→日期 三层联动】 + * 核心能力:监听分钟选择器的数值变化,处理分钟的边界联动场景,是最基础也是最核心的时间联动逻辑 + * 核心处理场景:分钟从59→0 触发小时+1;分钟从0→59 触发小时-1;小时变化后可能触发日期变化,自动联动处理 + * 附加能力:小时变化后,自动同步更新上下午状态与选择器,保证12小时制的展示一致性 + */ private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { int minValue = mMinuteSpinner.getMinValue(); int maxValue = mMinuteSpinner.getMaxValue(); - int offset = 0; // 小时偏移量 + int offset = 0; // 小时偏移量,默认无偏移 - // 场景1:59分→0分 → 小时+1 + // 场景1:分钟从59→0,触发小时+1(分钟到顶,进位到小时) if (oldVal == maxValue && newVal == minValue) { offset += 1; } - // 场景2:0分→59分 → 小时-1 + // 场景2:分钟从0→59,触发小时-1(分钟到底,退位到小时) else if (oldVal == minValue && newVal == maxValue) { offset -= 1; } - // 小时需要偏移时,调整日历并更新相关选择器 + // ========== 小时需要偏移时,执行联动逻辑 ========== if (offset != 0) { - mDate.add(Calendar.HOUR_OF_DAY, offset); // 调整小时 - mHourSpinner.setValue(getCurrentHour()); // 更新小时选择器 - updateDateControl(); // 更新日期选择器(小时偏移可能导致日期变化) + mDate.add(Calendar.HOUR_OF_DAY, offset); // 同步更新核心日历对象的小时数 + mHourSpinner.setValue(getCurrentHour()); // 刷新小时选择器的选中值 + updateDateControl(); // 刷新日期选择器,小时偏移可能导致日期变化 - // 根据新小时数更新上午/下午标记 + // 根据新的小时数,更新上下午状态标记 int newHour = getCurrentHourOfDay(); - if (newHour >= HOURS_IN_HALF_DAY) { - mIsAm = false; - } else { - mIsAm = true; - } - updateAmPmControl(); // 更新上午/下午选择器 + mIsAm = newHour < HOURS_IN_HALF_DAY; + updateAmPmControl(); // 同步刷新上下午选择器的选中状态 } - // 设置最终的分钟数 + // ========== 统一处理:将选中的分钟数同步至核心日历对象 ========== mDate.set(Calendar.MINUTE, newVal); - // 触发时间变化回调 + // 触发全局时间变化回调,向外通知分钟已更新 onDateTimeChanged(); } }; - /** 上午/下午选择器值变化监听器:切换上午/下午,调整小时数(±12小时) */ + /** + * 上下午选择器 值变化监听器:处理上下午切换的核心逻辑【仅12小时制生效】 + * 核心能力:监听上下午选择器的切换操作,翻转上下午状态标记,同步调整核心日历对象的小时数(±12小时) + * 核心逻辑:上午→下午,小时+12;下午→上午,小时-12,保证时间数值的正确性,无数据误差 + */ private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - // 切换上午/下午标记 + // 翻转上下午状态标记 mIsAm = !mIsAm; - // 调整小时数:上午→下午 +12小时,下午→上午 -12小时 + // 根据新状态,调整核心日历对象的小时数,保证时间正确 if (mIsAm) { mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); } else { mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); } - // 更新上午/下午选择器展示 + // 刷新上下午选择器的展示状态 updateAmPmControl(); - // 触发时间变化回调 + // 触发全局时间变化回调,向外通知上下午已切换 onDateTimeChanged(); } }; - // ======================== 回调接口 - 日期时间变化通知 ======================== + // ======================== 内部回调接口 - 日期时间变化通知【标准化通信协议】 ======================== /** - * 日期时间变化回调接口 - * 由外部(如DateTimePickerDialog)实现,接收控件选中的最新日期时间 + * 日期时间变化回调接口:控件对外提供的唯一通信接口,所有时间选择操作的最终数据出口 + * 设计原则:解耦控件内部逻辑与外部业务逻辑,控件只负责提供时间选择能力,不处理任何业务逻辑 + * 调用时机:日期、小时、分钟、上下午任一选择器发生变化时,均会触发该接口的回调方法,实时传递最新的时间数据 */ public interface OnDateTimeChangedListener { /** - * 日期时间变化回调方法 - * @param view 当前的DateTimePicker控件实例 - * @param year 选中的年份 - * @param month 选中的月份(Calendar.MONTH,0=1月,11=12月) - * @param dayOfMonth 选中的日期(当月的第几天) - * @param hourOfDay 选中的小时(24小时制,0~23) - * @param minute 选中的分钟(0~59) + * 日期时间变化的回调方法,传递标准化的时间数据 + * @param view 当前的DateTimePicker控件实例,外部可通过该实例获取更多信息或执行操作 + * @param year 选中的年份,如 2026 + * @param month 选中的月份,遵循Calendar规范,0代表1月,11代表12月 + * @param dayOfMonth 选中的日期,当月的第几天,如 15 + * @param hourOfDay 选中的小时,固定为24小时制数值,0~23,外部无需做格式转换,直接使用 + * @param minute 选中的分钟,0~59,标准化数值 */ void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute); } - // ======================== 构造方法 ======================== + // ======================== 构造方法区 - 三级重载构造,满足不同初始化需求【核心初始化入口】 ======================== /** - * 构造方法1:默认初始化(使用当前系统时间,适配系统24小时制) - * @param context 应用上下文 + * 构造方法1:最简初始化,无参重载 + * 核心能力:使用系统当前时间作为初始值,自动适配系统的24小时制设置,一键创建控件 + * @param context 应用上下文对象,不可为空 */ public DateTimePicker(Context context) { this(context, System.currentTimeMillis()); } /** - * 构造方法2:指定初始时间,适配系统24小时制 - * @param context 应用上下文 - * @param date 初始时间戳(毫秒级) + * 构造方法2:指定初始时间初始化 + * 核心能力:传入指定的毫秒级时间戳作为初始值,自动适配系统的24小时制设置,灵活适配业务需求 + * @param context 应用上下文对象,不可为空 + * @param date 初始选中的时间戳,单位:毫秒,支持任意合法的时间戳 */ public DateTimePicker(Context context, long date) { this(context, date, DateFormat.is24HourFormat(context)); } /** - * 构造方法3:指定初始时间和24小时制状态,核心初始化逻辑 - * @param context 应用上下文 - * @param date 初始时间戳(毫秒级) - * @param is24HourView 是否使用24小时制 + * 构造方法3:全参数核心初始化,控件的最终初始化入口,所有构造方法最终均调用此方法 + * 核心能力:一站式完成「上下文初始化+核心数据初始化+布局填充+子选择器初始化+监听器绑定+状态配置+初始时间设置」,无需外部执行任何额外配置,开箱即用 + * @param context 应用上下文对象,用于加载布局、创建控件、获取系统配置 + * @param date 初始选中的时间戳,单位:毫秒,控件打开时默认展示的时间 + * @param is24HourView 是否启用24小时制,true=启用,false=启用12小时制 */ public DateTimePicker(Context context, long date, boolean is24HourView) { super(context); - mDate = Calendar.getInstance(); // 初始化核心日历对象 - mInitialising = true; // 标记进入初始化阶段(避免触发不必要的回调) - // 初始化上午/下午标记:根据当前小时数判断(≥12为下午) + mDate = Calendar.getInstance(); // 初始化核心日历对象,默认加载系统当前时间 + mInitialising = true; // 标记进入初始化阶段,屏蔽所有回调触发,防止无效数据通知 + // 初始化上下午状态标记:根据当前小时数判断,≥12为下午,<12为上午 mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; - // 加载控件布局(datetime_picker.xml) + // 第一步:填充控件的核心布局,将xml布局文件加载至当前FrameLayout根容器 inflate(context, R.layout.datetime_picker, this); - // 初始化日期选择器 + // 第二步:初始化所有子滚轮选择器,绑定控件ID,配置基础属性与监听器 + // 初始化日期选择器:配置取值范围+绑定值变化监听器 mDateSpinner = (NumberPicker) findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); - // 初始化小时选择器 + // 初始化小时选择器:仅绑定值变化监听器,取值范围在24/12小时制设置时动态配置 mHourSpinner = (NumberPicker) findViewById(R.id.hour); mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); - // 初始化分钟选择器 + // 初始化分钟选择器:配置取值范围+设置长按快速滚动间隔+绑定值变化监听器,细节体验优化 mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); - mMinuteSpinner.setLongPressUpdateInterval(100); // 长按快速调整的间隔(100ms) + mMinuteSpinner.setLongPressUpdateInterval(100); // 长按滚动间隔100ms,滚动更快更流畅 mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); - // 初始化上午/下午选择器 - String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); // 获取系统AM/PM文本(适配多语言) + // 初始化上下午选择器:配置取值范围+加载系统原生的AM/PM文本(适配多语言)+绑定值变化监听器 + String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); - mAmPmSpinner.setDisplayedValues(stringsForAmPm); // 设置AM/PM展示文本 + mAmPmSpinner.setDisplayedValues(stringsForAmPm); mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); - // 更新控件到初始状态 + // 第三步:刷新所有子选择器的初始展示状态,保证UI与初始数据一致 updateDateControl(); updateHourControl(); updateAmPmControl(); - // 设置24小时制状态 + // 第四步:配置24/12小时制状态,完成制式适配 set24HourView(is24HourView); - // 设置初始时间 + // 第五步:设置控件的初始选中时间,同步至所有选择器与核心日历对象 setCurrentDate(date); - // 设置控件启用状态 + // 第六步:配置控件的整体启用状态,默认启用 setEnabled(isEnabled()); - // 初始化完成,取消标记 + // 第七步:初始化完成,解除初始化标记,控件进入正常工作状态,可响应所有操作与回调 mInitialising = false; } - // ======================== 重写方法 - 控件启用/禁用 ======================== + // ======================== 重写父类方法 - 控件整体启用/禁用【统一状态管控】 ======================== /** - * 设置控件整体启用/禁用状态 - * @param enabled true=启用(所有选择器可交互),false=禁用(所有选择器不可交互) + * 重写FrameLayout的setEnabled方法,实现控件整体的启用/禁用控制 + * 核心能力:一键同步所有子滚轮选择器的交互状态,无需单独配置每个选择器,保证状态一致性 + * 优化点:状态未变化时直接返回,避免重复执行无效操作,提升性能 + * @param enabled true=启用,所有选择器可滑动选择;false=禁用,所有选择器不可交互,灰显 */ @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { - return; // 状态未变化,直接返回 + return; } super.setEnabled(enabled); - // 同步所有选择器的启用状态 + // 同步所有子选择器的启用状态 mDateSpinner.setEnabled(enabled); mMinuteSpinner.setEnabled(enabled); mHourSpinner.setEnabled(enabled); mAmPmSpinner.setEnabled(enabled); - // 更新启用状态标记 + // 更新全局启用状态标记 mIsEnabled = enabled; } /** - * 获取控件整体启用状态 - * @return true=启用,false=禁用 + * 重写FrameLayout的isEnabled方法,获取控件的整体启用状态 + * @return true=控件已启用,false=控件已禁用 */ @Override public boolean isEnabled() { return mIsEnabled; } - // ======================== 公共方法 - 日期时间获取/设置 ======================== + // ======================== 公共方法区 - 日期时间 取值/赋值 标准化API【外部核心调用接口,最全】 ======================== /** - * 获取当前选中的日期时间戳(毫秒级) - * @return 选中时间的毫秒数 + * 获取当前选中的完整时间戳,外部最常用的取值方法 + * @return 选中时间的毫秒级时间戳,可直接用于存储、传输、转换,标准化输出 */ public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } /** - * 设置当前选中的日期时间(通过时间戳) - * @param date 要设置的时间戳(毫秒级) + * 设置当前选中的时间,通过毫秒级时间戳赋值,外部最常用的赋值方法 + * 核心能力:自动解析时间戳为年月日时分,同步至核心日历对象与所有子选择器,无需手动拆分 + * @param date 要设置的时间戳,单位:毫秒,支持任意合法的时间戳 */ public void setCurrentDate(long date) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(date); - // 解析时间戳为年/月/日/时/分,调用重载方法设置 + // 解析时间戳为标准化的年月日时分,调用重载方法完成赋值 setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); } /** - * 设置当前选中的日期时间(通过年/月/日/时/分) - * @param year 年份 - * @param month 月份(Calendar.MONTH,0=1月) - * @param dayOfMonth 日期(当月的第几天) - * @param hourOfDay 小时(24小时制,0~23) - * @param minute 分钟(0~59) + * 设置当前选中的时间,通过标准化的年月日时分赋值,最底层的赋值方法 + * 核心能力:精细化控制每个时间维度的数值,同步至核心日历对象与所有子选择器,数据完全可控 + * @param year 年份,如 2026 + * @param month 月份,Calendar规范,0=1月 + * @param dayOfMonth 日期,当月的第几天,1~31 + * @param hourOfDay 小时,24小时制,0~23 + * @param minute 分钟,0~59 */ public void setCurrentDate(int year, int month, int dayOfMonth, int hourOfDay, int minute) { @@ -411,7 +444,7 @@ public class DateTimePicker extends FrameLayout { /** * 获取当前选中的年份 - * @return 年份(如2025) + * @return 年份数值,如 2026 */ public int getCurrentYear() { return mDate.get(Calendar.YEAR); @@ -419,21 +452,21 @@ public class DateTimePicker extends FrameLayout { /** * 设置当前选中的年份 - * @param year 要设置的年份(如2025) + * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发 + * @param year 要设置的年份,如 2026 */ public void setCurrentYear(int year) { - // 初始化中或年份未变化时,直接返回 if (!mInitialising && year == getCurrentYear()) { return; } mDate.set(Calendar.YEAR, year); - updateDateControl(); // 更新日期选择器展示 - onDateTimeChanged(); // 触发时间变化回调 + updateDateControl(); + onDateTimeChanged(); } /** * 获取当前选中的月份 - * @return 月份(Calendar.MONTH,0=1月,11=12月) + * @return 月份数值,遵循Calendar规范,0代表1月,11代表12月 */ public int getCurrentMonth() { return mDate.get(Calendar.MONTH); @@ -441,216 +474,212 @@ public class DateTimePicker extends FrameLayout { /** * 设置当前选中的月份 - * @param month 要设置的月份(Calendar.MONTH,0=1月) + * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发 + * @param month 要设置的月份,Calendar规范,0=1月 */ public void setCurrentMonth(int month) { - // 初始化中或月份未变化时,直接返回 if (!mInitialising && month == getCurrentMonth()) { return; } mDate.set(Calendar.MONTH, month); - updateDateControl(); // 更新日期选择器展示 - onDateTimeChanged(); // 触发时间变化回调 + updateDateControl(); + onDateTimeChanged(); } /** - * 获取当前选中的日期(当月的第几天) - * @return 日期(1~31) + * 获取当前选中的日期 + * @return 日期数值,当月的第几天,1~31 */ public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); } /** - * 设置当前选中的日期(当月的第几天) - * @param dayOfMonth 要设置的日期(1~31) + * 设置当前选中的日期 + * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发 + * @param dayOfMonth 要设置的日期,1~31 */ public void setCurrentDay(int dayOfMonth) { - // 初始化中或日期未变化时,直接返回 if (!mInitialising && dayOfMonth == getCurrentDay()) { return; } mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); - updateDateControl(); // 更新日期选择器展示 - onDateTimeChanged(); // 触发时间变化回调 + updateDateControl(); + onDateTimeChanged(); } /** - * 获取当前选中的小时(24小时制,0~23) - * @return 小时数(0~23) + * 获取当前选中的小时数,固定返回24小时制数值,标准化输出,外部无需转换 + * @return 小时数值,0~23 */ public int getCurrentHourOfDay() { return mDate.get(Calendar.HOUR_OF_DAY); } /** - * 内部方法:获取适配当前制式的小时数(24/12小时制) - * @return 24小时制返回0~23,12小时制返回1~12 + * 内部私有方法:获取适配当前制式的小时数 + * 核心能力:根据24/12小时制,返回对应范围的小时数,供内部选择器赋值使用,不对外暴露 + * @return 24小时制返回0~23,12小时制返回1~12,适配选择器的取值范围 */ private int getCurrentHour() { if (mIs24HourView){ - return getCurrentHourOfDay(); // 24小时制直接返回 + return getCurrentHourOfDay(); } else { - // 12小时制转换:0点→12点,13点→1点,依此类推 int hour = getCurrentHourOfDay(); - if (hour > HOURS_IN_HALF_DAY) { - return hour - HOURS_IN_HALF_DAY; - } else { - return hour == 0 ? HOURS_IN_HALF_DAY : hour; - } + // 12小时制特殊转换:0点→12点,13点→1点,保证选择器展示正确 + return hour > HOURS_IN_HALF_DAY ? hour - HOURS_IN_HALF_DAY : (hour == 0 ? HOURS_IN_HALF_DAY : hour); } } /** - * 设置当前选中的小时(24小时制,0~23) - * @param hourOfDay 要设置的小时数(0~23) + * 设置当前选中的小时数,接收24小时制数值,标准化输入,内部自动适配转换 + * 核心能力:自动处理24/12小时制的转换逻辑,同步更新上下午状态与选择器,数据无误差 + * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发 + * @param hourOfDay 要设置的小时数,24小时制,0~23 */ public void setCurrentHour(int hourOfDay) { - // 初始化中或小时未变化时,直接返回 if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { return; } mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); - // 12小时制下,更新上午/下午标记和选择器 + // 12小时制下的特殊处理:转换小时数+更新上下午状态 if (!mIs24HourView) { if (hourOfDay >= HOURS_IN_HALF_DAY) { - mIsAm = false; // ≥12为下午 - if (hourOfDay > HOURS_IN_HALF_DAY) { - hourOfDay -= HOURS_IN_HALF_DAY; // 转换为12小时制(13→1,14→2...) - } + mIsAm = false; + hourOfDay = hourOfDay > HOURS_IN_HALF_DAY ? hourOfDay - HOURS_IN_HALF_DAY : hourOfDay; } else { - mIsAm = true; // <12为上午 - if (hourOfDay == 0) { - hourOfDay = HOURS_IN_HALF_DAY; // 0点→12点 - } + mIsAm = true; + hourOfDay = hourOfDay == 0 ? HOURS_IN_HALF_DAY : hourOfDay; } - updateAmPmControl(); // 更新上午/下午选择器 + updateAmPmControl(); } - // 设置小时选择器的值 mHourSpinner.setValue(hourOfDay); - // 触发时间变化回调 onDateTimeChanged(); } /** - * 获取当前选中的分钟 - * @return 分钟数(0~59) + * 获取当前选中的分钟数 + * @return 分钟数值,0~59 */ public int getCurrentMinute() { return mDate.get(Calendar.MINUTE); } /** - * 设置当前选中的分钟 - * @param minute 要设置的分钟数(0~59) + * 设置当前选中的分钟数 + * 优化点:初始化阶段或数值未变化时直接返回,避免无效操作与回调触发 + * @param minute 要设置的分钟数,0~59 */ public void setCurrentMinute(int minute) { - // 初始化中或分钟未变化时,直接返回 if (!mInitialising && minute == getCurrentMinute()) { return; } - mMinuteSpinner.setValue(minute); // 设置分钟选择器的值 - mDate.set(Calendar.MINUTE, minute); // 更新日历对象 - onDateTimeChanged(); // 触发时间变化回调 + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); } - // ======================== 公共方法 - 24小时制适配 ======================== + // ======================== 公共方法区 - 24小时制适配【系统级适配能力】 ======================== /** - * 判断当前是否为24小时制 + * 判断当前控件是否启用24小时制 * @return true=24小时制,false=12小时制 */ - public boolean is24HourView () { - return mIs24HourView; - } + public boolean is24HourView () { + return mIs24HourView; + } /** - * 设置24小时制/12小时制 - * @param is24HourView true=24小时制(隐藏AM/PM选择器),false=12小时制(显示AM/PM选择器) + * 设置控件的时间展示制式,手动强制切换24/12小时制,优先级高于系统配置 + * 核心能力:切换制式时,自动刷新小时选择器的取值范围、上下午选择器的可见性、当前小时数的展示值,无缝切换无感知 + * 优化点:状态未变化时直接返回,避免无效操作 + * @param is24HourView true=启用24小时制,隐藏上下午选择器;false=启用12小时制,显示上下午选择器 */ - public void set24HourView(boolean is24HourView) { - if (mIs24HourView == is24HourView) { - return; // 状态未变化,直接返回 - } - mIs24HourView = is24HourView; - // 显示/隐藏上午/下午选择器 + public void set24HourView(boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + // 控制上下午选择器的可见性 mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); - // 获取当前小时数(用于重置选择器) int hour = getCurrentHourOfDay(); - // 更新小时选择器的取值范围 + // 刷新小时选择器的取值范围 updateHourControl(); - // 重新设置小时数(适配新的制式) + // 重新适配并设置小时数,保证展示正确 setCurrentHour(hour); - // 更新上午/下午选择器状态 + // 刷新上下午选择器的状态 updateAmPmControl(); - } + } - // ======================== 内部方法 - 控件更新 ======================== + // ======================== 内部私有方法区 - 控件UI刷新【核心UI更新逻辑,内部闭环】 ======================== /** - * 更新日期选择器的展示文本:生成近7天的格式化日期(如“12.23 星期二”) + * 刷新日期选择器的展示文本与选中状态 + * 核心能力:重新计算并生成近7天的格式化日期文本,更新至日期选择器,保证展示的日期与核心日历对象一致 + * 展示格式:固定为「MM.dd EEEE」,即「月.日 星期」,如「01.15 星期四」 */ private void updateDateControl() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(mDate.getTimeInMillis()); - // 计算近7天的起始日期(当前日期 - 4天,确保中间为当前日期) + // 计算近7天的起始日期:当前日期向前推4天,保证选中日期在列表中间位置,交互更友好 cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); - mDateSpinner.setDisplayedValues(null); // 清空原有展示文本 - // 生成近7天的格式化日期文本 + mDateSpinner.setDisplayedValues(null); // 清空原有文本,避免残留 + // 循环生成7天的格式化日期文本 for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { cal.add(Calendar.DAY_OF_YEAR, 1); - // 格式化:月.日 星期(如“12.23 星期二”) mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); } - // 设置日期选择器的展示文本 + // 将生成的文本数组设置到日期选择器 mDateSpinner.setDisplayedValues(mDateDisplayValues); - // 设置默认选中中间项(当前日期) + // 默认选中列表中间项,即当前日期 mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); - mDateSpinner.invalidate(); // 刷新选择器UI + mDateSpinner.invalidate(); // 强制刷新UI,保证展示生效 } /** - * 更新上午/下午选择器的状态:设置选中项、控制可见性 + * 刷新上下午选择器的选中状态与可见性 + * 核心能力:根据当前的24小时制状态与上下午标记,同步更新上下午选择器的展示,保证UI与数据一致 */ private void updateAmPmControl() { if (mIs24HourView) { - mAmPmSpinner.setVisibility(View.GONE); // 24小时制隐藏 + mAmPmSpinner.setVisibility(View.GONE); } else { - // 设置选中项:0=AM/上午,1=PM/下午 + // 设置选中项:0=上午/AM,1=下午/PM int index = mIsAm ? Calendar.AM : Calendar.PM; mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); // 12小时制显示 + mAmPmSpinner.setVisibility(View.VISIBLE); } } /** - * 更新小时选择器的取值范围:适配24/12小时制 + * 刷新小时选择器的取值范围 + * 核心能力:根据当前的24小时制状态,动态配置小时选择器的最小值与最大值,适配不同制式的展示需求 */ - private void updateHourControl() { - if (mIs24HourView) { - // 24小时制:0~23 - mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); - mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); - } else { - // 12小时制:1~12 - mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); - mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); - } - } - - // ======================== 公共方法 - 回调设置 ======================== + private void updateHourControl() { + if (mIs24HourView) { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); + } else { + mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); + mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); + } + } + + // ======================== 公共方法区 - 回调监听器绑定【通信入口】 ======================== /** - * 设置日期时间变化回调监听器 - * @param callback 外部实现的监听器(null则不触发回调) + * 设置日期时间变化的回调监听器,外部通过此方法绑定业务逻辑 + * @param callback 外部实现的OnDateTimeChangedListener接口实例,传null则取消监听 */ public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { mOnDateTimeChangedListener = callback; } - // ======================== 内部方法 - 回调触发 ======================== + // ======================== 内部私有方法区 - 回调触发【核心通信逻辑,内部闭环】 ======================== /** - * 触发日期时间变化回调:传递最新的年/月/日/时/分 + * 触发日期时间变化的回调方法,所有选择操作的最终数据出口 + * 核心能力:从核心日历对象中读取标准化的年月日时分数据,调用外部绑定的监听器,完成数据传递 + * 优化点:监听器为null时直接返回,避免空指针异常 */ private void onDateTimeChanged() { if (mOnDateTimeChangedListener != null) { diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java index 6cc7119..019694a 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -14,144 +14,160 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,封装日期时间选择对话框,集成DateTimePicker控件 +// 包声明:归属小米便签UI模块,日期时间选择弹窗的核心封装类,集成自定义时间选择控件 package net.micode.notes.ui; -// 导入日历类:管理选中的日期时间,处理年/月/日/时/分的设置 +// Java日历工具类,核心时间处理载体,存储用户选中的年月日时分秒,提供时间的赋值与转换能力 import java.util.Calendar; -// 导入小米便签资源类:引用对话框按钮文本、布局等资源 +// 小米便签资源文件引用,获取字符串、布局等资源ID常量 import net.micode.notes.R; -// 导入自定义日期时间选择控件:核心的日期时间选择UI +// 自定义日期时间选择核心控件,本弹窗的核心内容视图,提供年月日时分滚轮选择UI import net.micode.notes.ui.DateTimePicker; import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; -// 导入安卓对话框相关类:基础AlertDialog、对话框点击事件 +// 安卓系统弹窗核心类,本类的父类,提供弹窗的基础展示与按钮配置能力 import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; -// 导入安卓日期格式化工具类:判断24小时制、格式化日期时间文本 +// 安卓系统日期格式化工具类,提供24小时制判断、时间戳转友好文本的格式化能力 import android.text.format.DateFormat; import android.text.format.DateUtils; /** - * 日期时间选择对话框封装类 - * 继承自AlertDialog,核心职责: - * 1. 集成自定义DateTimePicker控件,提供统一的日期时间选择UI; - * 2. 管理选中的日期时间(Calendar对象),处理时间变化的实时更新; - * 3. 适配系统24小时制设置,动态更新对话框标题(格式化显示选中的日期时间); - * 4. 提供选择完成的回调接口,返回最终选中的时间戳; - * 典型使用场景:便签添加/编辑页面的“设置提醒时间”功能。 + * 日期时间选择弹窗封装类【功能型弹窗核心类】 + * 继承:AlertDialog 安卓原生弹窗基类,具备系统弹窗的所有基础特性 + * 核心定位:小米便签「设置便签提醒时间」功能的专属弹窗,整合自定义DateTimePicker控件实现完整的时间选择能力 + * 核心设计思想:将「时间选择控件+弹窗载体+时间数据处理+回调通信」全部封装,对外提供极简调用API + * 核心职责: + * 1. 集成自定义DateTimePicker滚轮选择控件,作为弹窗的核心内容视图,提供年月日时分的可视化选择; + * 2. 统一管理选中的时间数据,基于Calendar类存储完整时间信息,自动处理时间联动更新逻辑; + * 3. 适配系统全局的24小时制/12小时制显示规则,自动切换时间展示格式,保证与系统行为一致; + * 4. 实时更新弹窗标题为当前选中的格式化时间文本,给用户直观的选择反馈; + * 5. 封装标准化的回调接口,选择完成后向外传递最终的毫秒级时间戳,解耦业务逻辑; + * 6. 固化弹窗的按钮交互逻辑:确定按钮触发回调、取消按钮关闭弹窗无操作,交互统一; + * 7. 时间精度控制:仅保留到分钟级,自动置空秒数,符合便签提醒的业务使用场景。 + * 典型业务场景:便签编辑页的「添加提醒」「修改提醒时间」功能弹窗,是便签日程提醒的核心交互载体。 */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { - // 选中的日期时间对象:存储用户选择的年/月/日/时/分,秒固定为0 + // 核心时间数据载体:存储用户当前选中的完整日期时间,初始化时获取系统当前时间,秒数固定置0 private Calendar mDate = Calendar.getInstance(); - // 是否使用24小时制:适配系统设置,影响时间显示格式 + // 24小时制标记位:适配系统设置,决定时间的展示格式(24小时/12小时带上午下午) private boolean mIs24HourView; - // 日期时间选择完成的回调监听器:外部实现,接收最终选中的时间戳 + // 时间选择完成的回调监听器:外部实现该接口接收最终选中的时间戳,核心通信桥梁 private OnDateTimeSetListener mOnDateTimeSetListener; - // 核心日期时间选择控件:承载年/月/日/时/分的选择UI + // 自定义日期时间选择控件:本弹窗的核心内容视图,提供滚轮式年月日时分选择UI,业务核心控件 private DateTimePicker mDateTimePicker; /** - * 日期时间选择完成的回调接口 - * 由外部(如NoteEditActivity)实现,接收对话框返回的选中时间戳 + * 日期时间选择完成的回调接口【标准化通信接口】 + * 设计原则:解耦弹窗内部逻辑与外部业务逻辑,弹窗只负责提供选择能力,不处理业务逻辑 + * 调用时机:用户点击弹窗的「确定」按钮后,触发该接口的回调方法,传递选中的时间数据 */ public interface OnDateTimeSetListener { /** - * 选择完成回调方法 - * @param dialog 当前的日期时间选择对话框(可用于关闭/操作对话框) - * @param date 选中的日期时间戳(毫秒级,秒已置为0) + * 时间选择完成的回调方法 + * @param dialog 当前的日期时间选择弹窗实例,外部可通过该实例做弹窗关闭等操作 + * @param date 用户最终选中的时间戳,单位:毫秒,秒数已固定置为0,精度到分钟级 */ void OnDateTimeSet(AlertDialog dialog, long date); } /** - * 构造方法:初始化日期时间选择对话框 - * @param context 应用上下文:用于创建对话框、加载资源、适配系统设置 - * @param date 初始时间戳:对话框打开时默认选中的时间(毫秒级) + * 构造方法:初始化日期时间选择弹窗的全部核心配置【一站式初始化】 + * 核心能力:传入上下文和初始时间戳后,自动完成「控件创建+视图绑定+数据初始化+事件绑定+按钮配置+标题更新」 + * 无需外部执行任何额外配置,开箱即用,调用简洁 + * @param context 应用上下文对象,用于创建弹窗、加载资源、适配系统设置,不可为空 + * @param date 弹窗初始化时的默认选中时间戳,单位:毫秒,支持传入指定时间做默认值展示 */ public DateTimePickerDialog(Context context, long date) { super(context); - // 1. 创建自定义日期时间选择控件,设置为对话框的核心视图 + // 第一步:创建自定义的日期时间选择控件,作为弹窗的核心内容视图,替换原生弹窗的默认布局 mDateTimePicker = new DateTimePicker(context); setView(mDateTimePicker); - // 2. 绑定DateTimePicker的时间变化监听:实时更新选中的Calendar对象 + // 第二步:为时间选择控件绑定实时变化监听器,核心联动逻辑 + // 监听用户在滚轮上的每一次选择操作,实时同步选中的时间数据到Calendar对象中 mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { - // 更新Calendar对象的年/月/日/时/分 + // 实时更新核心时间载体:将滚轮选中的年月日时分赋值到Calendar对象 mDate.set(Calendar.YEAR, year); mDate.set(Calendar.MONTH, month); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); mDate.set(Calendar.MINUTE, minute); - // 实时更新对话框标题(格式化显示当前选中的时间) + // 实时更新弹窗标题:将最新选中的时间格式化后展示在弹窗顶部,给用户即时反馈 updateTitle(mDate.getTimeInMillis()); } }); - // 3. 初始化选中的日期时间:设置初始时间戳,秒固定为0(仅保留到分钟级) + // 第三步:初始化选中的时间数据,设置默认选中的时间戳,统一时间精度 mDate.setTimeInMillis(date); - mDate.set(Calendar.SECOND, 0); - // 将初始时间设置到DateTimePicker控件 + mDate.set(Calendar.SECOND, 0); // 强制置空秒数,仅保留到分钟级,符合业务使用场景 + // 将初始化的时间数据同步到时间选择控件,保证弹窗打开时展示正确的默认时间 mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); - // 4. 设置对话框按钮:确认(触发回调)、取消(无操作) - setButton(context.getString(R.string.datetime_dialog_ok), this); // 确认按钮绑定当前类的onClick - setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 取消按钮无操作 + // 第四步:配置弹窗的底部按钮,固化交互逻辑,符合用户操作习惯 + setButton(context.getString(R.string.datetime_dialog_ok), this); // 确定按钮,绑定当前类的点击事件 + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 取消按钮,无业务逻辑,点击仅关闭弹窗 - // 5. 适配系统24小时制设置:获取系统是否启用24小时制,设置到对话框 + // 第五步:适配系统全局的时间显示规则,自动获取系统是否开启24小时制,保证体验一致性 set24HourView(DateFormat.is24HourFormat(this.getContext())); - // 6. 初始化对话框标题:格式化显示初始时间 + // 第六步:初始化弹窗标题,将默认时间格式化后展示,完成弹窗的最终初始化 updateTitle(mDate.getTimeInMillis()); } /** - * 设置是否使用24小时制显示时间 - * @param is24HourView true=24小时制,false=12小时制(上午/下午) + * 公有配置方法:手动设置弹窗的时间显示格式 + * 补充能力:支持外部手动覆盖系统的24小时制设置,按需指定显示规则,适配特殊业务场景 + * @param is24HourView true=强制使用24小时制展示时间,false=强制使用12小时制展示时间 */ public void set24HourView(boolean is24HourView) { mIs24HourView = is24HourView; } /** - * 设置日期时间选择完成的回调监听器 - * @param callBack 外部实现的OnDateTimeSetListener(如NoteEditActivity) + * 公有绑定方法:设置时间选择完成的回调监听器 + * 核心作用:为弹窗绑定外部的业务逻辑处理类,是弹窗向外传递数据的唯一入口 + * @param callBack 外部实现的OnDateTimeSetListener接口实例,接收最终选中的时间戳 */ public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { mOnDateTimeSetListener = callBack; } /** - * 私有方法:更新对话框标题,格式化显示选中的日期时间 - * @param date 要显示的时间戳(毫秒级) + * 私有工具方法:更新弹窗标题文本【核心格式化方法】 + * 核心职责:将毫秒级的时间戳,按照指定的格式规则,转换为用户友好的日期时间文本 + * 设计亮点:通过格式化标记位组合,灵活配置展示内容,无需手写格式化字符串,适配性更强 + * @param date 需要格式化展示的时间戳,单位:毫秒 */ private void updateTitle(long date) { - // 定义日期时间格式化标记:显示年、日期、时间 + // 定义时间格式化的标记位组合,指定需要展示的时间维度:年 + 月日 + 时分 int flag = - DateUtils.FORMAT_SHOW_YEAR | // 显示年份 - DateUtils.FORMAT_SHOW_DATE | // 显示日期(月/日) - DateUtils.FORMAT_SHOW_TIME; // 显示时间(时/分) - // 适配24小时制:覆盖标记(FORMAT_24HOUR无论true/false,仅控制显示格式) + DateUtils.FORMAT_SHOW_YEAR | // 强制展示年份,如「2026年」 + DateUtils.FORMAT_SHOW_DATE | // 强制展示日期,如「1月15日」 + DateUtils.FORMAT_SHOW_TIME; // 强制展示时间,如「15:30」或「下午3:30」 + // 根据24小时制标记位,追加对应的格式化规则,自动切换显示格式 flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; - // 格式化时间戳为文本,设置为对话框标题 + // 将时间戳格式化后,设置为弹窗的标题文本,完成UI更新 setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } /** - * 对话框确认按钮点击事件处理 - * 触发日期时间选择完成的回调,返回最终选中的时间戳 - * @param arg0 对话框实例(DialogInterface) - * @param arg1 按钮索引(确认按钮为DialogInterface.BUTTON_POSITIVE) + * 弹窗确定按钮的点击事件处理【核心交互方法】 + * 实现OnClickListener接口的核心方法,仅响应确定按钮的点击行为 + * 核心逻辑:判断是否绑定了回调监听器,若绑定则触发回调,传递选中的时间戳,完成数据通信 + * @param arg0 触发点击事件的弹窗对话框实例 + * @param arg1 被点击按钮的索引标识,对应确定/取消等按钮类型 */ public void onClick(DialogInterface arg0, int arg1) { - // 有回调监听器时,触发回调并传递选中的时间戳 + // 空值安全校验:避免未绑定回调监听器时触发空指针异常 if (mOnDateTimeSetListener != null) { + // 触发外部回调,传递弹窗实例和最终选中的时间戳,完成时间选择的业务闭环 mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); } } diff --git a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java index 9316cf4..849b09f 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java @@ -14,95 +14,109 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,封装下拉菜单的核心逻辑,简化PopupMenu的使用 +// 包声明:归属小米便签UI模块,下拉菜单功能的统一封装工具类,全局复用 package net.micode.notes.ui; -// 导入安卓上下文类:提供应用运行环境(创建PopupMenu、加载资源) +// 安卓系统核心类 - 上下文,提供资源加载、控件创建的运行环境,必须依赖项 import android.content.Context; -// 导入安卓菜单相关类:管理下拉菜单的选项和菜单项 +// 安卓菜单体系核心类,Menu管理菜单整体结构,MenuItem表示单个菜单项 import android.view.Menu; import android.view.MenuItem; -// 导入安卓视图相关类:处理按钮点击事件 +// 安卓视图体系核心类,处理视图点击事件、视图基础属性配置 import android.view.View; import android.view.View.OnClickListener; -// 导入安卓按钮控件类:作为下拉菜单的触发按钮 +// 安卓按钮控件,作为下拉菜单的触发载体,本类核心绑定控件 import android.widget.Button; -// 导入安卓PopupMenu类:核心的下拉菜单控件 +// 安卓原生下拉菜单核心控件,实现弹窗式菜单展示,本类封装的核心原生控件 import android.widget.PopupMenu; +// 安卓下拉菜单的菜单项点击事件监听器,监听菜单选项的点击行为 import android.widget.PopupMenu.OnMenuItemClickListener; -// 导入小米便签资源类:引用下拉按钮的背景资源 +// 小米便签资源文件引用,获取图标、布局等资源ID常量 import net.micode.notes.R; /** - * 下拉菜单封装类 - * 核心职责: - * 1. 封装安卓PopupMenu和触发按钮(Button),简化下拉菜单的创建与使用; - * 2. 统一设置下拉按钮的样式(背景图标); - * 3. 封装菜单加载、点击事件绑定、菜单项查找、按钮文本设置等核心逻辑; - * 典型使用场景:便签应用中排序、筛选、操作选项等下拉菜单场景(如列表页的更多操作)。 + * 下拉菜单功能封装工具类【全局复用型UI工具类】 + * 核心设计模式:封装模式,对Android原生PopupMenu+触发Button进行一站式封装 + * 核心定位:小米便签内所有下拉菜单场景的统一实现方案,抽离通用逻辑,避免重复开发 + * 核心价值: + * 1. 屏蔽原生PopupMenu的复杂创建流程,对外提供极简的调用API,一行代码即可创建下拉菜单; + * 2. 统一应用内所有下拉菜单的视觉样式(触发按钮背景图标、菜单展示位置),保证UI一致性; + * 3. 封装菜单资源加载、点击事件绑定、菜单项查找、按钮文本更新等全套核心逻辑; + * 4. 解耦下拉菜单的「创建-展示-事件处理」逻辑,外部无需关心内部实现细节; + * 5. 轻量化封装,无冗余逻辑,仅做功能整合,不侵入业务代码,适配所有下拉菜单使用场景。 + * 典型业务场景:便签列表页的排序方式选择、文件夹操作菜单、更多功能选项、筛选条件下拉框等。 */ public class DropdownMenu { - // 下拉菜单的触发按钮:点击该按钮显示下拉菜单,统一设置背景样式 + // 下拉菜单的触发按钮,全局持用引用:点击该按钮即可弹出下拉菜单,统一配置背景样式 private Button mButton; - // 核心下拉菜单控件:承载菜单选项,控制菜单的显示/隐藏 + // Android原生下拉菜单核心控件,本类封装的核心对象:负责菜单的弹出、收起、承载菜单项 private PopupMenu mPopupMenu; - // PopupMenu对应的Menu对象:用于查找菜单项、管理菜单内容 + // PopupMenu对应的菜单容器对象:用于动态查找、修改、管理菜单项的状态(显示/隐藏/禁用等) private Menu mMenu; /** - * 构造方法:初始化下拉菜单(绑定触发按钮+加载菜单布局) - * @param context 应用上下文:用于创建PopupMenu、加载菜单资源 - * @param button 触发下拉菜单的按钮控件:统一设置背景为下拉图标 - * @param menuId 菜单布局资源ID:如R.menu.xxx,定义下拉菜单的选项列表 + * 构造方法:初始化下拉菜单的全部核心配置【一站式初始化】 + * 核心能力:传入必要参数后,自动完成「按钮样式配置+下拉菜单创建+菜单资源加载+点击触发绑定」 + * 无需外部执行任何额外初始化操作,极简调用,开箱即用 + * @param context 上下文对象,用于创建PopupMenu、加载菜单资源、获取应用运行环境,不可为空 + * @param button 触发下拉菜单的按钮控件,菜单会锚定该按钮在下方弹出,与菜单强绑定 + * @param menuId 菜单布局的资源ID(如R.menu.menu_sort),定义下拉菜单的所有选项列表 */ public DropdownMenu(Context context, Button button, int menuId) { - // 保存触发按钮引用 + // 保存触发按钮的全局引用,供后续设置文本、复用控件使用 mButton = button; - // 设置按钮背景为下拉图标(统一样式) + // 统一设置触发按钮的背景样式:加载内置的下拉箭头图标,保证所有下拉按钮视觉统一 mButton.setBackgroundResource(R.drawable.dropdown_icon); - // 创建PopupMenu:关联上下文和触发按钮(菜单显示在按钮下方) + // 创建原生PopupMenu对象,绑定上下文和触发按钮,指定菜单弹出的锚点位置 mPopupMenu = new PopupMenu(context, mButton); - // 获取PopupMenu对应的Menu对象,用于后续菜单管理 + // 获取PopupMenu内部的Menu容器对象,缓存引用,供后续菜单项查找使用,避免重复获取 mMenu = mPopupMenu.getMenu(); - // 加载菜单布局资源到Menu中(初始化下拉菜单的选项) + // 通过菜单解析器,将指定的菜单布局资源加载到Menu容器中,完成菜单项的初始化展示 mPopupMenu.getMenuInflater().inflate(menuId, mMenu); - // 绑定按钮点击事件:点击按钮显示下拉菜单 + // 为触发按钮绑定点击事件监听器:点击按钮时,自动弹出下拉菜单,核心触发逻辑 mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { - mPopupMenu.show(); // 显示下拉菜单 + // 弹出下拉菜单,菜单默认展示在触发按钮的下方,对齐方式由系统原生适配 + mPopupMenu.show(); } }); } /** - * 设置下拉菜单项的点击监听器 - * 监听器由外部实现,处理不同菜单项的点击逻辑(如排序、删除、移动等) - * @param listener 菜单项点击监听器(OnMenuItemClickListener) + * 对外提供的事件绑定方法:设置下拉菜单项的点击事件监听器 + * 核心设计:事件逻辑完全交由外部实现,本类只做事件转发,无任何业务逻辑侵入,高度解耦 + * 外部可根据菜单项的ID,分别处理不同选项的点击业务(如排序、筛选、删除、移动等) + * @param listener 菜单项点击事件监听器,外部实现该接口的onMenuItemClick方法处理具体逻辑 */ public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { - // 仅当PopupMenu初始化完成时,绑定监听器 + // 空值安全校验:避免PopupMenu未初始化完成时绑定监听器导致空指针异常 if (mPopupMenu != null) { + // 将外部传入的监听器绑定到PopupMenu,所有菜单项的点击事件都会回调该监听器 mPopupMenu.setOnMenuItemClickListener(listener); } } /** - * 根据菜单项ID查找对应的MenuItem - * 用于外部动态修改菜单项状态(如隐藏/禁用/设置图标) - * @param id 菜单项的资源ID(如R.id.menu_sort_by_time) - * @return MenuItem:找到的菜单项(未找到返回null) + * 工具方法:根据菜单项的资源ID查找对应的MenuItem对象 + * 核心用途:支持外部动态修改菜单项的状态,如「隐藏/显示菜单项」「禁用/启用菜单项」「修改菜单项文本/图标」等 + * 是实现菜单动态化配置的核心入口,满足复杂业务场景的菜单定制需求 + * @param id 目标菜单项的资源ID(如R.id.menu_sort_by_time、R.id.menu_move_folder) + * @return MenuItem 找到的菜单项对象,未找到时返回null */ public MenuItem findItem(int id) { + // 直接通过缓存的Menu对象查找菜单项,高效无冗余 return mMenu.findItem(id); } /** - * 设置下拉触发按钮的显示文本 - * 用于动态更新按钮文本(如“排序方式”“当前文件夹”等) - * @param title 按钮要显示的文本内容 + * 工具方法:动态设置下拉菜单触发按钮的显示文本 + * 核心业务价值:支持按钮文本的动态更新,适配「当前选中状态展示」场景 + * 比如:排序菜单按钮显示「按时间排序」、文件夹菜单按钮显示「当前文件夹:工作」等 + * @param title 按钮需要展示的文本内容,支持字符串常量、动态拼接字符串 */ public void setTitle(CharSequence title) { + // 直接为触发按钮设置文本内容,更新UI展示 mButton.setText(title); } } \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java index 332c975..340650f 100644 --- a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java @@ -14,137 +14,162 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,作为文件夹列表(如“移动便签”弹窗)的核心适配器 +// 包声明:归属小米便签UI模块,是文件夹列表展示的核心数据适配器 package net.micode.notes.ui; -// 导入安卓上下文类:提供应用运行环境(创建列表项、加载资源) +// 安卓系统核心类 - 上下文,提供资源加载、视图创建的运行环境 import android.content.Context; -// 导入安卓数据库游标类:存储文件夹数据库查询结果 +// 安卓数据库游标类,承载文件夹的数据库查询结果集,适配器的核心数据源 import android.database.Cursor; -// 导入安卓视图相关类:创建/绑定列表项视图 +// 安卓视图体系核心类,用于创建和装载列表子项视图 import android.view.View; import android.view.ViewGroup; -// 导入安卓Cursor适配器类:适配Cursor数据的列表适配器基类 +// 安卓游标适配器基类,适配Cursor数据源的列表专用适配器,本类核心父类 import android.widget.CursorAdapter; -// 导入安卓线性布局类:作为文件夹列表项的根布局 +// 安卓线性布局,作为自定义列表项的根布局容器 import android.widget.LinearLayout; -// 导入安卓文本视图类:展示文件夹名称 +// 安卓文本控件,用于展示文件夹名称文本内容 import android.widget.TextView; -// 导入小米便签资源类:引用布局、字符串资源(根文件夹显示文本) +// 小米便签资源文件引用,获取布局、字符串等资源id import net.micode.notes.R; -// 导入便签数据常量类:定义文件夹ID(根文件夹)、字段等常量 +// 小米便签核心数据常量,定义文件夹id、数据库字段名等全局常量 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; /** - * 文件夹列表核心适配器类 - * 继承自安卓CursorAdapter,核心职责: - * 1. 定义文件夹数据库查询的投影字段(仅ID和名称),减少查询冗余; - * 2. 将文件夹Cursor数据绑定到自定义列表项(FolderListItem); - * 3. 特殊适配根文件夹:将根文件夹的名称替换为“移动到上级文件夹”(而非原始名称); - * 4. 提供工具方法,根据列表位置获取文件夹名称(适配根文件夹规则)。 - * 典型使用场景:便签“移动到文件夹”弹窗的文件夹列表展示。 + * 文件夹列表专用数据适配器【核心功能适配器】 + * 继承:CursorAdapter 安卓原生游标适配器,完美适配数据库查询的Cursor结果集 + * 核心定位:小米便签中「移动便签到文件夹」弹窗的核心适配器,负责文件夹列表的数据绑定与视图渲染 + * 核心职责: + * 1. 定义文件夹查询的精简投影字段,只查必要数据,减少数据库IO开销,提升列表加载效率; + * 2. 实现Cursor数据与列表子项视图的绑定逻辑,规范数据渲染流程; + * 3. 对「根文件夹」做特殊文本适配,将系统根文件夹名称替换为业务友好的文本提示; + * 4. 封装文件夹名称获取工具方法,对外提供统一的名称读取接口,解耦外部调用逻辑; + * 5. 内置自定义列表项布局,封装视图创建与控件绑定,避免外部多次 findViewById 性能损耗。 + * 核心特点:轻量高效、职责单一,仅处理文件夹列表的「数据-视图」映射关系,无业务逻辑侵入。 */ public class FoldersListAdapter extends CursorAdapter { /** - * 文件夹数据库查询投影数组:仅包含核心字段,减少IO开销 - * 字段说明: - * - NoteColumns.ID:文件夹唯一ID(根文件夹为Notes.ID_ROOT_FOLDER) - * - NoteColumns.SNIPPET:文件夹名称(根文件夹的SNIPPET无业务意义,需特殊替换) + * 文件夹数据库查询投影数组【核心常量】 + * 设计原则:按需查询,只获取列表展示所需的核心字段,不查冗余字段,降低内存占用和查询耗时 + * 字段组成:仅包含文件夹的唯一标识和展示名称,满足列表所有业务需求 */ public static final String [] PROJECTION = { - NoteColumns.ID, // 0: 文件夹唯一ID - NoteColumns.SNIPPET // 1: 文件夹名称 + NoteColumns.ID, // 数组索引0 - 文件夹的唯一ID,数据库主键 + NoteColumns.SNIPPET // 数组索引1 - 文件夹的名称,用于列表展示 }; - // 投影数组对应的列索引常量:简化Cursor取值,避免硬编码索引 - public static final int ID_COLUMN = 0; // 文件夹ID列索引 - public static final int NAME_COLUMN = 1; // 文件夹名称列索引 + /** + * 投影字段对应的列索引常量 + * 设计目的:封装索引数值,避免代码中出现硬编码数字,提升代码可读性和可维护性 + * 作用:通过常量直接取值,Cursor.getInt(ID_COLUMN) 比 Cursor.getInt(0) 语义更清晰 + */ + public static final int ID_COLUMN = 0; // 文件夹ID对应的游标列索引 + public static final int NAME_COLUMN = 1; // 文件夹名称对应的游标列索引 /** * 构造方法:初始化文件夹列表适配器 - * @param context 应用上下文,传递给父类(用于创建列表项、加载资源) - * @param c 包含文件夹数据的Cursor(已查询完成,包含ID和名称字段) + * 父类传参:将上下文和游标数据源传递给CursorAdapter基类,完成适配器初始化 + * @param context 上下文对象,用于加载资源、创建视图,不可为空 + * @param c 数据库查询返回的游标,封装了所有文件夹的ID和名称数据,游标已完成查询定位 */ public FoldersListAdapter(Context context, Cursor c) { super(context, c); - // TODO Auto-generated constructor stub } /** - * 重写创建新视图方法:创建文件夹列表项视图 - * @param context 应用上下文 - * @param cursor 当前位置的Cursor(未使用,仅遵循父类接口) - * @param parent 列表项的父容器(ListView) - * @return View:新创建的FolderListItem自定义列表项 + * 重写父类方法:创建列表子项的空白视图 + * 生命周期:列表滚动时,为屏幕外新进入的位置创建全新的视图对象,复用性低 + * 核心逻辑:仅创建视图容器,不做任何数据绑定,数据绑定由bindView方法完成 + * @param context 上下文对象,用于创建视图 + * @param cursor 当前位置对应的游标数据,本方法中暂未使用,仅遵循父类接口规范 + * @param parent 列表子项的父容器,即承载所有文件夹项的ListView + * @return View 新建的、未绑定数据的文件夹列表子项视图 */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - // 创建自定义文件夹列表项视图(FolderListItem) + // 创建自定义的文件夹列表项视图,内部已完成布局加载和控件绑定 return new FolderListItem(context); } /** - * 重写绑定视图方法:将文件夹Cursor数据绑定到列表项UI - * 核心逻辑: - * - 根文件夹:名称替换为“移动到上级文件夹”; - * - 普通文件夹:使用Cursor中的原始名称; - * - 将处理后的名称绑定到列表项的文本控件。 - * @param view 要绑定的列表项视图(FolderListItem) - * @param context 应用上下文(加载根文件夹的显示文本) - * @param cursor 当前位置的Cursor,包含文件夹ID和名称 + * 重写父类核心方法:为列表子项视图绑定对应的数据【核心业务方法】 + * 生命周期:列表首次加载/滚动复用视图时调用,所有数据渲染逻辑均在此实现 + * 核心业务规则: + * 1. 根文件夹(ID=Notes.ID_ROOT_FOLDER):不展示原始名称,替换为业务文本「移动到上级文件夹」; + * 2. 普通文件夹:直接展示数据库中存储的文件夹名称; + * 3. 类型安全校验:只处理自定义的FolderListItem视图,防止视图类型异常导致崩溃。 + * @param view 待绑定数据的列表子项视图,可为newView创建的新视图,也可为复用的旧视图 + * @param context 上下文对象,用于加载字符串资源 + * @param cursor 当前列表位置对应的游标,已定位到对应行,可直接读取字段值 */ @Override public void bindView(View view, Context context, Cursor cursor) { - // 仅处理FolderListItem类型的视图(防止类型错误) + // 视图类型校验,保证类型安全,避免视图强转异常 if (view instanceof FolderListItem) { - // 处理文件夹名称:根文件夹替换为“移动到上级文件夹”,普通文件夹用原始名称 - String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context - .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); - // 调用列表项的bind方法,设置文件夹名称 + // 读取当前游标中的文件夹ID,判断是否为根文件夹 + long folderId = cursor.getLong(ID_COLUMN); + String folderName; + + // 根文件夹特殊处理:替换展示文本;普通文件夹使用原始名称 + if (folderId == Notes.ID_ROOT_FOLDER) { + folderName = context.getString(R.string.menu_move_parent_folder); + } else { + folderName = cursor.getString(NAME_COLUMN); + } + + // 调用自定义视图的绑定方法,将处理后的文件夹名称设置到文本控件 ((FolderListItem) view).bind(folderName); } } /** - * 工具方法:根据列表位置获取文件夹名称(适配根文件夹规则) - * 用于外部(如弹窗逻辑)快速获取指定位置的文件夹名称,无需手动解析Cursor - * @param context 应用上下文(加载根文件夹的显示文本) - * @param position 列表项位置 - * @return String:处理后的文件夹名称(根文件夹为“移动到上级文件夹”) + * 对外提供的工具方法:根据列表位置获取对应的文件夹名称 + * 封装价值:外部调用方无需操作游标,直接传入位置即可获取名称,隐藏游标操作细节,解耦调用逻辑 + * 业务规则:与bindView保持完全一致,根文件夹返回「移动到上级文件夹」,保证数据一致性 + * @param context 上下文对象,用于加载根文件夹的业务文本 + * @param position 列表中的目标位置索引,从0开始 + * @return String 处理后的文件夹展示名称 */ public String getFolderName(Context context, int position) { - // 获取指定位置的Cursor + // 根据位置获取对应的游标对象,游标已自动定位到对应行 Cursor cursor = (Cursor) getItem(position); - // 同bindView逻辑:根文件夹替换名称,普通文件夹用原始名称 - return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context - .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + long folderId = cursor.getLong(ID_COLUMN); + + // 根文件夹判断与名称适配,逻辑与bindView完全一致 + return folderId == Notes.ID_ROOT_FOLDER ? context.getString(R.string.menu_move_parent_folder) + : cursor.getString(NAME_COLUMN); } /** - * 内部类:文件夹列表项自定义布局 - * 继承自LinearLayout,核心职责:初始化列表项布局和文本控件,提供名称绑定方法。 + * 私有内部类:文件夹列表的自定义子项视图【视图封装核心类】 + * 继承:LinearLayout 线性布局,作为子项的根布局 + * 设计思想:视图控件封装化,将列表项的布局加载、控件绑定、数据绑定全部封装在内部 + * 核心优势:外部无需关心视图内部结构,只需调用bind方法即可完成数据渲染,符合封装原则 + * 访问权限:private 私有,仅当前适配器可创建使用,不对外暴露,保证视图安全性 */ private class FolderListItem extends LinearLayout { - // 文件夹名称文本控件:展示处理后的文件夹名称 + // 列表项核心控件:展示文件夹名称的文本控件,全局缓存避免重复查找 private TextView mName; /** - * 构造方法:初始化列表项布局和控件 - * @param context 应用上下文,用于加载布局和查找控件 + * 构造方法:初始化自定义列表项视图 + * 核心逻辑:加载布局文件 + 绑定内部控件,只执行一次,视图创建时完成初始化 + * @param context 上下文对象,用于加载布局资源和查找控件 */ public FolderListItem(Context context) { super(context); - // 加载文件夹列表项布局(res/layout/folder_list_item.xml)到当前LinearLayout + // 将文件夹列表项的布局文件,填充到当前的LinearLayout根布局中 inflate(context, R.layout.folder_list_item, this); - // 初始化文件夹名称文本控件 + // 查找并缓存文件夹名称文本控件,后续直接复用,提升性能 mName = (TextView) findViewById(R.id.tv_folder_name); } /** - * 绑定方法:设置文件夹名称到文本控件 - * @param name 处理后的文件夹名称(根文件夹为“移动到上级文件夹”) + * 视图数据绑定方法:为文本控件设置文件夹名称 + * 核心作用:作为适配器与自定义视图的通信桥梁,只做单一的文本赋值操作 + * @param name 处理后的文件夹展示名称(根文件夹/普通文件夹) */ public void bind(String name) { mName.setText(name); diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java index b334f54..355f794 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java @@ -14,80 +14,148 @@ * limitations under the License. */ +// 包声明:归属小米便签UI模块,核心编辑页面,承载所有便签的新建/编辑/查看核心逻辑 package net.micode.notes.ui; +// Android系统服务-闹钟:实现便签提醒功能的核心系统服务 import android.app.Activity; import android.app.AlarmManager; +// Android弹窗:实现删除确认、时间选择等核心弹窗交互 import android.app.AlertDialog; +// Android延迟意图:绑定闹钟事件,触发提醒广播 import android.app.PendingIntent; +// Android搜索服务:接收搜索跳转参数,实现搜索关键词高亮 import android.app.SearchManager; +// Android小组件管理:实现便签与桌面小组件的绑定和更新 import android.appwidget.AppWidgetManager; +// Android内容URI工具:拼接便签ID生成唯一URI,用于闹钟广播标识 import android.content.ContentUris; +// Android上下文:页面运行环境核心类 import android.content.Context; +// Android对话框监听:处理弹窗的确认/取消点击事件 import android.content.DialogInterface; import android.content.Intent; +// Android轻量级存储:持久化保存字体大小等用户偏好设置 import android.content.SharedPreferences; +// Android图形画笔:实现清单模式勾选后的删除线文本样式 import android.graphics.Paint; +// Android页面状态存储:屏幕旋转/内存不足重建时保存便签ID import android.os.Bundle; +// Android偏好设置工具:获取全局的SharedPreferences实例 import android.preference.PreferenceManager; +// Android富文本核心类:实现搜索关键词的背景高亮效果 import android.text.Spannable; import android.text.SpannableString; +// Android文本工具类:判空、文本截取等安全高效操作 import android.text.TextUtils; +// Android时间格式化工具:格式化便签修改时间、提醒相对时间 import android.text.format.DateUtils; +// Android富文本样式:为搜索关键词添加背景色高亮 import android.text.style.BackgroundColorSpan; +// Android日志工具:输出调试日志,便于问题排查 import android.util.Log; +// Android布局加载:加载菜单布局、清单模式子项布局 import android.view.LayoutInflater; +// Android菜单核心类:创建页面右上角的功能菜单 import android.view.Menu; import android.view.MenuItem; +// Android触摸事件:处理点击外部关闭弹窗的核心事件分发 import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; +// Android窗口管理:控制软键盘的显示/隐藏模式 import android.view.WindowManager; +// Android复选框:清单模式的核心选择控件 import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; +// Android编辑框:普通文本模式的核心输入控件 import android.widget.EditText; +// Android图片控件:背景色选择、选中态展示、功能图标 import android.widget.ImageView; +// Android线性布局:清单模式的列表容器,承载多个编辑项 import android.widget.LinearLayout; +// Android文本控件:展示修改时间、提醒信息等静态文本 import android.widget.TextView; +// Android轻提示:操作成功/失败的吐司提示 import android.widget.Toast; +// 小米便签资源类:引用字符串、颜色、布局、图片等项目资源 import net.micode.notes.R; +// 小米便签数据常量:定义便签类型、文件夹ID、Intent参数等核心常量 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.TextNote; +// 小米便签核心数据模型:封装便签的所有数据、状态和业务操作,解耦UI与数据层 import net.micode.notes.model.WorkingNote; import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +// 小米便签数据工具类:数据库操作、数据校验、通话记录匹配等通用工具 import net.micode.notes.tool.DataUtils; +// 小米便签样式解析工具:解析背景色、字体大小等样式资源,统一管理样式规范 import net.micode.notes.tool.ResourceParser; import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +// 小米便签自定义时间选择器:年月日时分一体化选择弹窗 import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +// 小米便签自定义编辑框回调:处理清单模式的增删、回车事件 import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; +// Java集合框架:HashMap实现键值对映射,解耦控件ID与业务标识;HashSet存储批量操作ID import java.util.HashMap; import java.util.HashSet; import java.util.Map; +// Java正则表达式:实现搜索关键词的文本匹配,支撑高亮逻辑 import java.util.regex.Matcher; import java.util.regex.Pattern; +// Android异步查询:实现文件夹列表的异步加载 +import android.content.AsyncQueryHandler; +// Android数据库游标:存储文件夹列表查询结果 +import android.database.Cursor; +// Android列表适配器:展示文件夹列表 +import android.widget.ListAdapter; + /** - * 便签编辑核心页面 - * 核心职责:处理便签的新建/编辑/查看/删除,支持样式定制、清单模式、提醒设置、分享/桌面快捷方式等扩展能力 - * 核心依赖:WorkingNote(便签数据模型)、ResourceParser(样式资源解析)、AlarmManager(提醒功能) + * 小米便签 核心编辑页面【整个应用的核心页面】 + * 页面定位:Activity基类,承载便签的「新建、编辑、查看、删除」全生命周期操作 + * 核心业务能力(十大核心职责): + * 1. 多场景页面初始化:区分「查看已有便签、新建普通便签、新建通话记录便签、搜索跳转」4种启动场景; + * 2. 双编辑模式无缝切换:支持「普通文本模式」与「清单勾选模式」互相切换,数据无损转换; + * 3. 样式个性化定制:支持5种背景色切换、4种字体大小选择,样式偏好持久化存储; + * 4. 提醒功能完整实现:设置/取消便签提醒、注册系统闹钟、过期提醒展示、相对时间显示; + * 5. 丰富的功能菜单:新建、删除、分享、创建桌面快捷方式、字体设置、模式切换、提醒管理; + * 6. 搜索关键词高亮:从搜索列表跳转时,自动高亮匹配的搜索关键词,提升浏览体验; + * 7. 通话记录深度适配:自动匹配通话记录生成便签,避免重复创建,一键记录通话信息; + * 8. 桌面小组件联动:编辑便签后自动同步更新绑定的桌面小组件内容; + * 9. 防数据丢失机制:页面暂停/销毁/返回时自动保存便签,屏幕旋转时恢复编辑状态; + * 10. 完善的异常处理:所有关键操作均做数据校验,异常场景友好提示,杜绝崩溃闪退。 + * 核心设计模式: + * - 数据与UI解耦:通过WorkingNote数据模型封装所有数据操作,页面仅做UI展示和事件分发; + * - 回调解耦业务逻辑:通过多个回调接口处理子控件事件、数据状态变化,符合单一职责原则; + * - 常量映射解耦:通过HashMap映射控件ID与业务标识,避免硬编码,提升代码可维护性。 */ public class NoteEditActivity extends Activity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { - // 标题栏控件持有者:封装便签头部的核心UI控件,避免多次findViewById + /** + * 标题栏控件持有者 内部类【控件缓存设计】 + * 设计目的:将标题栏的所有核心控件统一缓存到该类,避免多次调用findViewById造成性能损耗 + * 核心价值:控件只查找一次,复用全局,提升页面初始化和刷新的效率 + */ private class HeadViewHolder { - public TextView tvModified; // 便签最后修改时间展示控件 - public ImageView ivAlertIcon; // 提醒功能的图标(有提醒时显示) - public TextView tvAlertDate; // 提醒时间/过期提示文本 - public ImageView ibSetBgColor; // 背景色设置按钮 + public TextView tvModified; // 展示便签的「最后修改时间」文本控件 + public ImageView ivAlertIcon; // 提醒功能的标识图标,有提醒时显示,无则隐藏 + public TextView tvAlertDate; // 展示提醒「剩余时间/已过期」文本,联动提醒图标 + public ImageView ibSetBgColor; // 背景色设置的功能按钮,点击弹出背景色选择面板 } - // ===================== 核心映射表:解耦UI控件ID与业务标识 ===================== - // 背景色选择按钮ID → ResourceParser中的颜色常量,统一管理背景色选择逻辑 + // ===================== 静态常量映射表区 - 核心解耦设计【重中之重】===================== + // 映射原则:所有映射表均为静态不可变,静态代码块初始化,全局唯一,避免内存浪费 + // 核心价值:彻底解耦「控件ID」与「业务常量标识」,修改控件ID或业务标识时互不影响,提升可维护性 + + /** + * 背景色选择按钮ID → ResourceParser背景色常量 映射表 + * 作用:点击不同背景色按钮时,快速匹配对应的背景色业务标识,无需多个if判断 + */ private static final Map sBgSelectorBtnsMap = new HashMap(); static { sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); @@ -97,7 +165,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); } - // ResourceParser颜色常量 → 背景色选中态控件ID,控制选择器的选中效果展示 + /** + * ResourceParser背景色常量 → 背景色选中态控件ID 映射表 + * 作用:选中某背景色后,快速显示对应的选中标识,未选中则隐藏,控制选择器的视觉反馈 + */ private static final Map sBgSelectorSelectionMap = new HashMap(); static { sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); @@ -107,7 +178,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); } - // 字体大小选择按钮ID → ResourceParser中的字体常量,统一管理字体大小选择逻辑 + /** + * 字体大小选择按钮ID → ResourceParser字体常量 映射表 + * 作用:点击不同字体按钮时,快速匹配对应的字体大小业务标识,统一管理字体样式 + */ private static final Map sFontSizeBtnsMap = new HashMap(); static { sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); @@ -116,7 +190,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); } - // ResourceParser字体常量 → 字体选中态控件ID,控制字体选择器的选中效果展示 + /** + * ResourceParser字体常量 → 字体选中态控件ID 映射表 + * 作用:选中某字体后,快速显示对应的选中标识,控制字体选择器的视觉反馈 + */ private static final Map sFontSelectorSelectionMap = new HashMap(); static { sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); @@ -125,48 +202,57 @@ public class NoteEditActivity extends Activity implements OnClickListener, sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); } - // ===================== 核心常量:日志/存储/业务规则 ===================== - private static final String TAG = "NoteEditActivity"; // 日志标签 - private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好设置存储Key - private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 桌面快捷方式标题最大长度 - public static final String TAG_CHECKED = String.valueOf('\u221A'); // 清单模式-已勾选标记符 - public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 清单模式-未勾选标记符 - - // ===================== 核心成员变量 ===================== - private HeadViewHolder mNoteHeaderHolder; // 标题栏控件持有者实例 - private View mHeadViewPanel; // 标题栏根布局 - private View mNoteBgColorSelector; // 背景色选择器面板(弹窗式) - private View mFontSizeSelector; // 字体大小选择器面板(弹窗式) - private EditText mNoteEditor; // 普通文本模式的编辑框 - private View mNoteEditorPanel; // 编辑区域根布局(用于设置背景色) - private WorkingNote mWorkingNote; // 核心数据模型:封装便签的所有数据和操作(加载/保存/修改) - private SharedPreferences mSharedPrefs; // 应用偏好设置:持久化存储字体大小等配置 - private int mFontSizeId; // 当前选中的字体大小标识(关联ResourceParser) - private LinearLayout mEditTextList; // 清单模式下的编辑列表(包含多个带复选框的编辑项) - private String mUserQuery; // 搜索关键词(从搜索结果跳转时,用于文本高亮) - private Pattern mPattern; // 搜索关键词的正则匹配器(用于文本高亮) - - // ===================== 生命周期核心方法 ===================== + // ===================== 全局常量区 - 日志/存储/业务规则 ===================== + private static final String TAG = "NoteEditActivity"; // 日志过滤TAG,固定值 + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好存储Key,持久化保存 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 桌面快捷方式标题最大长度,避免标题过长 + public static final String TAG_CHECKED = String.valueOf('\u221A'); // 清单模式-已勾选标记符 ✔ + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 清单模式-未勾选标记符 □ + + // ===================== 核心成员变量区 - 页面所有核心数据/控件/状态 ===================== + private HeadViewHolder mNoteHeaderHolder; // 标题栏控件持有者实例,缓存标题栏所有控件 + private View mHeadViewPanel; // 标题栏根布局,用于整体设置背景色 + private View mNoteBgColorSelector; // 背景色选择器面板,弹窗式展示,点击外部关闭 + private View mFontSizeSelector; // 字体大小选择器面板,弹窗式展示,点击外部关闭 + private EditText mNoteEditor; // 普通文本模式的核心编辑框,单行/多行输入均可 + private View mNoteEditorPanel; // 编辑区域根布局,承载普通编辑框/清单列表,用于设置整体背景色 + private WorkingNote mWorkingNote; // 核心数据模型【页面的核心】,所有便签数据/操作均通过该类完成 + private SharedPreferences mSharedPrefs; // 应用全局偏好设置,持久化存储用户的字体大小选择 + private int mFontSizeId; // 当前选中的字体大小业务标识,关联ResourceParser常量 + private LinearLayout mEditTextList; // 清单模式的核心列表容器,承载所有带复选框的编辑项 + private String mUserQuery; // 搜索关键词,从搜索列表跳转时传入,用于文本高亮 + private Pattern mPattern; // 搜索关键词的正则匹配器,编译一次复用多次,提升匹配效率 + private BackgroundQueryHandler mBackgroundQueryHandler; // 异步查询处理器,用于加载文件夹列表 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 文件夹列表查询的token标识 + + // ===================== Activity 生命周期核心方法 ===================== + /** + * 页面创建入口:初始化布局、读取启动参数、初始化页面核心状态 + * 生命周期:页面第一次创建时调用,只执行一次 + * @param savedInstanceState 页面重建时的状态缓存,屏幕旋转/内存不足时保存数据 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.note_edit); + setContentView(R.layout.note_edit); // 加载核心编辑页面布局 - // 初始化页面状态(区分查看/新建/通话记录便签),初始化失败则关闭页面 + // 核心初始化逻辑:无重建状态时,通过启动Intent初始化页面;初始化失败则直接关闭页面 if (savedInstanceState == null && !initActivityState(getIntent())) { finish(); return; } - initResources(); // 初始化控件、监听器、偏好设置等资源 + initResources(); // 初始化控件、监听器、偏好设置等所有页面资源 } /** - * 恢复Activity状态:当Activity因内存不足被销毁后重建时,恢复之前编辑的便签状态 + * 页面状态恢复:Activity因内存不足被销毁后重建时,恢复之前的编辑状态 + * 生命周期:页面重建时调用,仅在savedInstanceState不为空时执行 + * @param savedInstanceState 保存的页面状态,核心存储便签ID */ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); - // 从Bundle中读取之前保存的便签ID,重新初始化页面状态 + // 从缓存中读取便签ID,重新构建Intent并初始化页面状态 if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); @@ -179,26 +265,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * 初始化Activity核心状态:根据Intent的Action区分不同的启动场景 - * @param intent 启动Activity的Intent - * @return true-初始化成功,false-初始化失败(如便签不存在) - * 场景1:ACTION_VIEW - 查看已有便签(含从搜索结果跳转) - * 场景2:ACTION_INSERT_OR_EDIT - 新建便签(含通话记录便签) + * 核心初始化方法:根据启动Intent的Action,区分不同的业务场景,初始化页面核心状态 + * 四大核心场景全覆盖,是页面的入口逻辑,所有数据初始化均从此处开始 + * @param intent 启动当前页面的Intent对象,携带所有启动参数 + * @return true-初始化成功,false-初始化失败(如便签不存在、参数异常) */ private boolean initActivityState(Intent intent) { - mWorkingNote = null; - // 场景1:查看已有便签 + mWorkingNote = null; // 初始化数据模型为空,避免脏数据 + // 场景一:ACTION_VIEW → 查看/编辑【已有便签】(从便签列表点击进入) if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { - long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); - mUserQuery = ""; + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); // 获取要查看的便签ID + mUserQuery = ""; // 默认无搜索关键词 - // 处理从搜索结果跳转的场景:获取搜索关键词(用于文本高亮) + // 子场景:从【搜索结果列表】跳转 → 携带搜索关键词,需要文本高亮 if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); } - // 校验便签是否存在,不存在则跳转到便签列表页并提示 + // 数据校验:校验便签是否存在于数据库且为有效便签类型,不存在则跳转列表并提示 if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { Intent jump = new Intent(this, NotesListActivity.class); startActivity(jump); @@ -206,31 +291,28 @@ public class NoteEditActivity extends Activity implements OnClickListener, finish(); return false; } else { - // 加载便签数据,加载失败则关闭页面 + // 加载便签数据:通过WorkingNote从数据库加载便签的所有信息 mWorkingNote = WorkingNote.load(this, noteId); - if (mWorkingNote == null) { + if (mWorkingNote == null) { // 加载失败则关闭页面,避免空指针 Log.e(TAG, "load note failed with note id" + noteId); finish(); return false; } } - // 查看模式:隐藏软键盘,适配布局 + // 查看模式软键盘策略:隐藏软键盘,布局自适应,提升浏览体验 getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); } - // 场景2:新建/编辑便签 + // 场景二:ACTION_INSERT_OR_EDIT → 【新建便签】(包含普通新建/通话记录新建) else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { - // 解析新建便签的参数:文件夹ID、小组件ID、小组件类型、默认背景色 + // 解析新建便签的核心参数:文件夹ID、小组件ID、小组件类型、默认背景色 long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); - int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); - int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, - Notes.TYPE_WIDGET_INVALIDE); - int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, - ResourceParser.getDefaultBgId(this)); - - // 处理通话记录便签:从通话记录跳转时,优先加载已有记录,无则创建新便签 + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, ResourceParser.getDefaultBgId(this)); + + // 子场景一:从【通话记录】跳转 → 新建/编辑通话记录便签,避免重复创建 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); if (callDate != 0 && phoneNumber != null) { @@ -238,9 +320,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, Log.w(TAG, "The call record number is null"); } long noteId = 0; - // 根据手机号和通话时间查询已有便签 - if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), - phoneNumber, callDate)) > 0) { + // 先查询是否已有该通话记录的便签,有则加载,无则新建 + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), phoneNumber, callDate)) > 0) { mWorkingNote = WorkingNote.load(this, noteId); if (mWorkingNote == null) { Log.e(TAG, "load call note failed with note id" + noteId); @@ -248,212 +329,239 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } } else { - // 创建空便签并转换为通话记录便签 - mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, - widgetType, bgResId); + // 创建空便签并转换为通话记录专属便签,自动填充手机号和通话时间 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); mWorkingNote.convertToCallNote(phoneNumber, callDate); } } else { - // 普通新建便签:创建空便签 - mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, - bgResId); + // 子场景二:普通新建便签 → 创建空白便签,使用默认参数 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); } - // 新建模式:显示软键盘,适配布局 + // 新建模式软键盘策略:自动弹出软键盘,布局自适应,提升输入体验 getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } else { - // 不支持的Intent Action,关闭页面 + // 异常场景:Intent无合法Action,直接关闭页面,避免未知错误 Log.e(TAG, "Intent not specified action, should not support"); finish(); return false; } - // 注册便签设置变化监听器:监听背景色、提醒、模式等变化 + // 注册数据模型监听器:监听便签的背景色、提醒、模式、小组件等状态变化,实时更新UI mWorkingNote.setOnSettingStatusChangedListener(this); return true; } + /** + * 页面恢复可见:页面从后台切回前台、锁屏解锁后调用 + * 生命周期:每次页面恢复可见时调用,可执行多次 + */ @Override protected void onResume() { super.onResume(); - initNoteScreen(); // 恢复/初始化便签展示(适配模式、样式、高亮等) + initNoteScreen(); // 恢复/初始化便签的展示样式、模式、高亮等UI状态 } /** - * 初始化便签展示界面:适配普通/清单模式、应用字体样式、设置背景色、展示修改时间/提醒信息 + * 初始化便签展示界面:页面核心UI渲染方法,适配所有样式和模式 + * 核心逻辑:根据WorkingNote的状态,应用字体样式、切换编辑模式、设置背景色、展示时间和提醒信息 */ private void initNoteScreen() { - // 应用字体大小样式 - mNoteEditor.setTextAppearance(this, TextAppearanceResources - .getTexAppearanceResource(mFontSizeId)); - // 清单模式:切换到带复选框的列表布局 + // 第一步:为普通编辑框应用当前选中的字体大小样式 + mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + + // 第二步:根据便签模式,切换对应的编辑布局 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { - switchToListMode(mWorkingNote.getContent()); - } - // 普通模式:显示编辑框,高亮搜索关键词 - else { + switchToListMode(mWorkingNote.getContent()); // 清单模式:加载清单列表 + } else { + // 普通模式:设置文本并高亮搜索关键词,光标定位到文本末尾,提升编辑体验 mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); - mNoteEditor.setSelection(mNoteEditor.getText().length()); // 光标定位到文本末尾 + mNoteEditor.setSelection(mNoteEditor.getText().length()); } - // 初始化背景色选择器的选中态(默认全部隐藏) + + // 第三步:初始化背景色选择器的选中态,默认全部隐藏,只显示当前选中的背景色标识 for (Integer id : sBgSelectorSelectionMap.keySet()) { findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); } - // 应用便签背景色样式 + + // 第四步:为标题栏和编辑区域应用当前选中的背景色样式 mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); - // 展示便签最后修改时间(格式化显示:年月日时分) + // 第五步:格式化展示便签的最后修改时间,包含年月日时分,样式统一 mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR)); - // 展示提醒信息(过期/剩余时间) + // 第六步:展示提醒信息,区分过期/未过期状态,更新对应的文本和图标 showAlertHeader(); } /** - * 更新提醒信息展示:有提醒时显示提醒图标和时间/过期提示,无提醒时隐藏 + * 更新提醒信息展示:根据便签的提醒状态,动态显示/隐藏提醒相关控件 + * 核心逻辑:有提醒则显示图标+时间/过期提示,无提醒则隐藏所有相关控件,界面整洁 */ private void showAlertHeader() { if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); - // 提醒已过期:显示过期提示 + // 提醒已过期:显示「已过期」文本,提示用户及时处理 if (time > mWorkingNote.getAlertDate()) { mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); } else { - // 提醒未过期:显示相对时间(如“10分钟后”) + // 提醒未过期:显示相对时间(如「10分钟后」「1小时后」),更符合用户阅读习惯 mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); } mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); } else { - // 无提醒:隐藏提醒相关控件 + // 无提醒:隐藏所有提醒相关控件,节省界面空间 mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); }; } + /** + * 处理新的启动意图:页面已存在时,接收到新的Intent调用该方法(如桌面快捷方式再次打开) + * 核心逻辑:复用当前页面,重新初始化状态,避免创建多个页面实例 + * @param intent 新的启动意图 + */ @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - initActivityState(intent); // 处理新Intent(如从桌面快捷方式再次打开) + initActivityState(intent); // 重新初始化页面状态,适配新的Intent参数 } + /** + * 页面状态保存:页面即将被销毁时调用,保存核心状态数据,用于重建时恢复 + * 生命周期:屏幕旋转、内存不足、页面退到后台时调用 + * @param outState 状态缓存容器,存储需要恢复的数据 + */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - // 新便签(未持久化到数据库)先保存生成ID,避免Activity销毁后数据丢失 + // 关键逻辑:未保存到数据库的新便签,先执行保存生成ID,避免数据丢失 if (!mWorkingNote.existInDatabase()) { saveNote(); } - // 保存便签ID到Bundle,用于Activity重建时恢复状态 + // 将核心的便签ID保存到缓存,页面重建时通过该ID恢复数据 outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); } /** - * 触摸事件分发:处理背景色/字体大小选择器的点击外部关闭逻辑 + * 触摸事件分发:页面的核心触摸事件处理入口,优先级高于所有子控件的触摸事件 + * 核心业务:实现「点击外部关闭弹窗」的交互逻辑,处理背景色/字体选择器的关闭 + * @param ev 触摸事件对象,包含触摸坐标、动作类型等信息 + * @return true-事件已消费,false-事件继续分发 */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { - // 背景色选择器显示时,点击外部则关闭 + // 背景色选择器显示时,点击外部区域则关闭面板,消费事件避免穿透 if (mNoteBgColorSelector.getVisibility() == View.VISIBLE && !inRangeOfView(mNoteBgColorSelector, ev)) { mNoteBgColorSelector.setVisibility(View.GONE); return true; } - // 字体大小选择器显示时,点击外部则关闭 + // 字体大小选择器显示时,点击外部区域则关闭面板,消费事件避免穿透 if (mFontSizeSelector.getVisibility() == View.VISIBLE && !inRangeOfView(mFontSizeSelector, ev)) { mFontSizeSelector.setVisibility(View.GONE); return true; } - return super.dispatchTouchEvent(ev); + return super.dispatchTouchEvent(ev); // 未消费则交给父类处理,不影响其他触摸逻辑 } /** - * 判断触摸事件是否在指定View范围内 - * @param view 目标View - * @param ev 触摸事件 - * @return true-在范围内,false-不在范围内 + * 坐标范围校验工具:判断触摸坐标是否在指定View的矩形范围内 + * 核心作用:支撑「点击外部关闭弹窗」的逻辑,精准判断触摸位置 + * @param view 目标校验的View + * @param ev 触摸事件对象 + * @return true-触摸在View范围内,false-触摸在View范围外 */ private boolean inRangeOfView(View view, MotionEvent ev) { int []location = new int[2]; - view.getLocationOnScreen(location); + view.getLocationOnScreen(location); // 获取View在屏幕上的绝对坐标 int x = location[0]; int y = location[1]; - // 校验触摸坐标是否在View的矩形范围内 - if (ev.getX() < x - || ev.getX() > (x + view.getWidth()) - || ev.getY() < y - || ev.getY() > (y + view.getHeight())) { - return false; - } + // 校验触摸坐标是否超出View的上下左右边界 + if (ev.getX() < x || ev.getX() > (x + view.getWidth()) || ev.getY() < y || ev.getY() > (y + view.getHeight())) { + return false; + } return true; } /** - * 初始化资源:绑定UI控件、设置监听器、读取偏好设置(字体大小) + * 页面资源初始化:绑定所有UI控件、设置点击监听器、读取用户偏好设置 + * 核心逻辑:所有控件只查找一次,监听器一次性绑定,偏好设置一次性读取,提升页面性能 */ private void initResources() { - // 绑定标题栏根布局和控件持有者 + // 第一步:绑定标题栏根布局和控件持有者,缓存标题栏所有控件 mHeadViewPanel = findViewById(R.id.note_title); mNoteHeaderHolder = new HeadViewHolder(); mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); - mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); // 背景色按钮点击监听器 + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); // 绑定背景色按钮点击监听 - // 绑定编辑区域控件 + // 初始化异步查询处理器,用于加载文件夹列表 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + + // 第二步:绑定编辑区域核心控件 mNoteEditor = (EditText) findViewById(R.id.note_edit_view); mNoteEditorPanel = findViewById(R.id.sv_note_edit); mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); - // 绑定背景色选择器的所有按钮,设置点击监听器 + // 第三步:为所有背景色选择按钮绑定点击监听器,统一处理背景色切换逻辑 for (int id : sBgSelectorBtnsMap.keySet()) { ImageView iv = (ImageView) findViewById(id); iv.setOnClickListener(this); } - // 绑定字体大小选择器,设置点击监听器 + // 第四步:绑定字体大小选择器,并为所有字体按钮绑定点击监听器 mFontSizeSelector = findViewById(R.id.font_size_selector); for (int id : sFontSizeBtnsMap.keySet()) { View view = findViewById(id); view.setOnClickListener(this); }; - // 读取字体大小偏好设置(持久化存储),兼容异常值(重置为默认) + // 第五步:读取用户的字体大小偏好设置,持久化存储,兼容异常值(重置为默认) mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; } - // 绑定清单模式的编辑列表 + + // 第六步:绑定清单模式的核心列表容器 mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); } + /** + * 页面暂停:页面退到后台、锁屏、启动新页面时调用 + * 生命周期:每次页面失去焦点时调用,可执行多次 + * 核心逻辑:自动保存便签数据,避免数据丢失;清理弹窗状态,保证界面整洁 + */ @Override protected void onPause() { super.onPause(); - // 页面暂停时自动保存便签,避免数据丢失 + // 自动保存便签:页面暂停时必保存,所有编辑操作不会丢失,核心防丢机制 if(saveNote()) { Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); } - clearSettingState(); // 关闭背景色/字体大小选择器面板 + clearSettingState(); // 关闭所有弹窗面板,避免重建时残留显示 } /** - * 更新桌面小组件:当便签关联小组件时,同步更新小组件内容 + * 更新桌面小组件:当便签关联了桌面小组件时,编辑后同步更新小组件内容 + * 核心逻辑:发送广播通知对应的小组件刷新,保证便签与小组件数据一致 */ private void updateWidget() { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - // 区分2x/4x小组件类型,指定对应的Provider + // 根据便签绑定的小组件类型,选择对应的小组件广播接收者 if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { intent.setClass(this, NoteWidgetProvider_2x.class); } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { @@ -463,133 +571,185 @@ public class NoteEditActivity extends Activity implements OnClickListener, return; } - // 传入小组件ID,发送更新广播 - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { - mWorkingNote.getWidgetId() - }); - + // 传入小组件ID,发送精准的更新广播 + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { mWorkingNote.getWidgetId() }); sendBroadcast(intent); - setResult(RESULT_OK, intent); + setResult(RESULT_OK, intent); // 设置返回结果,告知调用方更新成功 } /** - * 点击事件处理:处理背景色/字体大小选择器的核心交互 - * @param v 被点击的View + * 点击事件统一处理:所有控件的点击事件均在此处分发,统一管理,避免分散处理 + * 核心处理:背景色选择、字体大小选择、背景色面板打开,所有点击逻辑集中管理 + * @param v 被点击的View控件 */ public void onClick(View v) { int id = v.getId(); - // 打开背景色选择器,显示当前选中的背景色 + // 点击背景色按钮:打开背景色选择面板,显示当前选中的背景色标识 if (id == R.id.btn_set_bg_color) { mNoteBgColorSelector.setVisibility(View.VISIBLE); - findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.VISIBLE); + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE); } - // 选择背景色:更新便签背景色,关闭选择器 + // 点击背景色选择按钮:切换便签背景色,隐藏选择面板,触发背景色变化回调 else if (sBgSelectorBtnsMap.containsKey(id)) { - findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.GONE); + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.GONE); mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); mNoteBgColorSelector.setVisibility(View.GONE); } - // 选择字体大小:持久化设置,更新UI样式,关闭选择器 + // 点击字体大小选择按钮:持久化保存字体选择、更新UI样式、隐藏选择面板 else if (sFontSizeBtnsMap.containsKey(id)) { findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); mFontSizeId = sFontSizeBtnsMap.get(id); - // 持久化字体大小到偏好设置 + // 持久化存储用户的字体选择,下次打开页面自动应用 mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); - // 适配清单/普通模式的字体样式更新 + // 根据当前编辑模式,分别更新字体样式,保证样式统一 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { getWorkingText(); switchToListMode(mWorkingNote.getContent()); } else { - mNoteEditor.setTextAppearance(this, - TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); } mFontSizeSelector.setVisibility(View.GONE); } } - /** - * 返回键事件处理:优先关闭背景色/字体大小选择器面板,再保存便签并执行默认返回逻辑 - * 设计意图:避免用户误触返回键时丢失设置面板的操作,同时保证便签数据不丢失 + /** + * 返回键事件处理:重写系统返回键,增加弹窗关闭逻辑,提升用户体验 + * 设计意图:用户点击返回键时,优先关闭打开的弹窗面板,而非直接退出页面,避免误操作 */ @Override public void onBackPressed() { - // 若关闭了设置面板(背景色/字体选择器),则直接返回,不执行后续逻辑 + // 若有弹窗面板打开,则先关闭面板,不执行返回逻辑 if(clearSettingState()) { return; } - // 保存当前便签内容,避免数据丢失 - saveNote(); - // 执行系统默认的返回逻辑 - super.onBackPressed(); + saveNote(); // 保存当前编辑内容,避免数据丢失 + super.onBackPressed(); // 执行系统默认的返回逻辑,退出当前页面 } /** - * 清理设置面板状态:关闭背景色/字体大小选择器 - * @return true-关闭了某个面板,false-无面板需要关闭 + * 清理弹窗面板状态:关闭背景色/字体大小选择器,统一的弹窗关闭方法 + * @return true-关闭了某一个面板,false-无面板需要关闭 */ private boolean clearSettingState() { - // 关闭背景色选择器 if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { mNoteBgColorSelector.setVisibility(View.GONE); return true; - } - // 关闭字体大小选择器 - else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { mFontSizeSelector.setVisibility(View.GONE); return true; } return false; } + // ===================== WorkingNote 状态变化回调接口实现 ===================== + // 该接口由WorkingNote定义,当便签的背景色、提醒、模式、小组件等状态变化时,自动回调以下方法 + // 核心价值:解耦数据模型与UI层,数据变化自动更新UI,无需手动调用,符合观察者模式 + /** - * 背景色变化回调:当便签背景色修改后,更新UI的背景色展示和选择器选中态 - * (WorkingNote背景色变更后触发此回调) + * 背景色变化回调:便签背景色修改后,自动更新页面的背景色样式和选择器选中态 */ public void onBackgroundColorChanged() { - // 更新背景色选择器的选中态 - findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - View.VISIBLE); - // 更新编辑区域和标题栏的背景色 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE); mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); } /** - * 准备选项菜单:根据便签类型/状态动态加载菜单,更新菜单显示内容 - * @param menu 要初始化的菜单 - * @return true-菜单初始化成功 + * 提醒时间变化回调:便签的提醒时间设置/取消后,注册/取消系统闹钟,更新提醒UI展示 + * @param date 新的提醒时间戳,0表示取消提醒 + * @param set true-设置提醒,false-取消提醒 + */ + public void onClockAlertChanged(long date, boolean set) { + // 关键逻辑:未保存的新便签,设置提醒前必须先保存,否则无唯一ID绑定闹钟 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + // 仅处理有效便签ID的提醒设置,避免无效操作 + if (mWorkingNote.getNoteId() > 0) { + // 构建提醒广播Intent,通过URI绑定唯一的便签ID,确保闹钟触发时能找到对应的便签 + Intent intent = new Intent(this, AlarmReceiver.class); + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + + showAlertHeader(); // 更新提醒信息的UI展示 + + // 取消提醒:移除已注册的闹钟,清理提醒状态 + if(!set) { + alarmManager.cancel(pendingIntent); + } else { + // 设置提醒:注册系统闹钟,RTC_WAKEUP模式保证即使设备休眠也能触发提醒 + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + // 异常提示:便签无内容未保存,无法设置提醒,引导用户输入内容 + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } + } + + /** + * 小组件关联变化回调:便签绑定的小组件信息变更后,自动更新桌面小组件内容 + */ + public void onWidgetChanged() { + updateWidget(); + } + + /** + * 编辑模式变化回调:便签在普通/清单模式间切换时,更新对应的编辑布局和数据 + * @param oldMode 切换前的模式,0=普通模式,MODE_CHECK_LIST=清单模式 + * @param newMode 切换后的模式 + */ + public void onCheckListModeChanged(int oldMode, int newMode) { + if (newMode == TextNote.MODE_CHECK_LIST) { + switchToListMode(mNoteEditor.getText().toString()); // 切换到清单模式 + } else { + // 切换到普通模式:先获取清单模式的所有内容,合并为文本后展示 + if (!getWorkingText()) { + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", "")); + } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mEditTextList.setVisibility(View.GONE); + mNoteEditor.setVisibility(View.VISIBLE); + } + } + + // ===================== 选项菜单核心逻辑 ===================== + /** + * 准备选项菜单:页面右上角的功能菜单,动态加载菜单项,适配不同的便签状态 + * 核心逻辑:根据便签类型(普通/通话记录)、模式(清单/普通)、提醒状态,动态显示/隐藏菜单项 + * @param menu 菜单容器,用于加载和展示菜单项 + * @return true-菜单初始化成功,false-初始化失败 */ @Override public boolean onPrepareOptionsMenu(Menu menu) { - // 页面正在关闭时,无需初始化菜单 - if (isFinishing()) { + if (isFinishing()) { // 页面正在关闭时,无需初始化菜单 return true; } - // 先关闭设置面板,避免菜单和面板叠加显示 - clearSettingState(); - // 清空原有菜单,避免重复加载 - menu.clear(); + clearSettingState(); // 关闭所有弹窗面板,避免菜单与面板叠加显示 + menu.clear(); // 清空原有菜单,避免重复加载 - // 区分菜单类型:通话记录便签加载专属菜单,普通便签加载默认菜单 + // 根据便签所属文件夹,加载对应的菜单布局:通话记录便签有专属菜单 if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { getMenuInflater().inflate(R.menu.call_note_edit, menu); } else { getMenuInflater().inflate(R.menu.note_edit, menu); + // 动态添加"移动到文件夹"菜单项 + menu.add(Menu.NONE, R.id.menu_move_to_folder, Menu.CATEGORY_CONTAINER, R.string.menu_move); } - // 更新模式切换菜单的标题:清单模式→显示“普通模式”,普通模式→显示“清单模式” + // 动态更新模式切换菜单的标题:清单模式显示「普通模式」,普通模式显示「清单模式」 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); } else { menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); } - // 控制提醒相关菜单显隐:已有提醒→隐藏“设置提醒”,无提醒→隐藏“取消提醒” + // 动态控制提醒菜单的显隐:有提醒则隐藏「设置提醒」,无提醒则隐藏「取消提醒」 if (mWorkingNote.hasClockAlert()) { menu.findItem(R.id.menu_alert).setVisible(false); } else { @@ -599,59 +759,57 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * 选项菜单点击事件处理:处理所有菜单项的核心业务逻辑 + * 选项菜单点击事件处理:所有菜单项的核心业务逻辑入口,页面的核心功能集合 + * 包含:新建便签、删除便签、字体设置、模式切换、分享、桌面快捷方式、提醒设置/取消 * @param item 被点击的菜单项 - * @return true-事件已处理 + * @return true-事件已处理,false-事件未处理 */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_new_note: - // 新建便签:先保存当前便签,再启动新的编辑页面 - createNewNote(); + createNewNote(); // 新建便签:保存当前便签后,启动新的编辑页面 break; case R.id.menu_delete: - // 删除便签:弹窗确认,确认后执行删除并关闭页面 + // 删除便签:弹出确认弹窗,用户确认后执行删除逻辑,避免误删 AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.alert_title_delete)); - builder.setIcon(android.R.drawable.ic_dialog_alert); // 警告图标 + builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(getString(R.string.alert_message_delete_note)); - builder.setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - deleteCurrentNote(); // 执行删除逻辑 - finish(); // 关闭编辑页面 - } - }); - builder.setNegativeButton(android.R.string.cancel, null); // 取消按钮无操作 + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteCurrentNote(); + finish(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); builder.show(); break; case R.id.menu_font_size: - // 打开字体大小选择器,显示当前选中的字体样式 + // 打开字体大小选择面板,显示当前选中的字体标识 mFontSizeSelector.setVisibility(View.VISIBLE); findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); break; case R.id.menu_list_mode: - // 切换便签模式:普通↔清单(0=普通模式,MODE_CHECK_LIST=清单模式) - mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? - TextNote.MODE_CHECK_LIST : 0); + // 切换便签编辑模式:普通 ↔ 清单,数据自动转换,无损切换 + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? TextNote.MODE_CHECK_LIST : 0); break; case R.id.menu_share: - // 分享便签:先获取最新编辑内容,再调用系统分享接口 + // 分享便签:获取最新编辑内容,调用系统分享接口,支持所有分享渠道 getWorkingText(); sendTo(this, mWorkingNote.getContent()); break; case R.id.menu_send_to_desktop: - // 创建桌面快捷方式:点击后生成指向当前便签的桌面图标 - sendToDesktop(); + sendToDesktop(); // 创建桌面快捷方式:一键生成便签的桌面图标,快速访问 break; case R.id.menu_alert: - // 设置提醒:弹出时间选择对话框,选择后绑定提醒时间 - setReminder(); + setReminder(); // 设置提醒:弹出时间选择弹窗,选择后绑定闹钟 break; case R.id.menu_delete_remind: - // 取消提醒:清空便签的提醒时间 - mWorkingNote.setAlertDate(0, false); + mWorkingNote.setAlertDate(0, false); // 取消提醒:清空便签的提醒时间 + break; + case R.id.menu_move_to_folder: + startQueryDestinationFolders(); // 查询文件夹列表,让用户选择要移动到的文件夹 break; default: break; @@ -660,226 +818,209 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * 设置便签提醒:弹出日期时间选择对话框,选择后更新便签的提醒时间 + * 设置便签提醒:弹出自定义的时间选择弹窗,支持年月日时分的精准选择 */ private void setReminder() { - // 创建时间选择对话框,默认显示当前时间 DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); - // 设置时间选择回调:选择时间后更新便签的提醒时间 d.setOnDateTimeSetListener(new OnDateTimeSetListener() { public void OnDateTimeSet(AlertDialog dialog, long date) { - mWorkingNote.setAlertDate(date , true); + mWorkingNote.setAlertDate(date, true); } }); - d.show(); // 显示对话框 + d.show(); } /** - * 分享便签内容:调用系统的ACTION_SEND接口,分享纯文本格式的便签内容 - * @param context 上下文 + * 系统分享功能:调用Android原生的分享接口,分享便签的纯文本内容 + * @param context 上下文对象 * @param info 要分享的便签文本内容 */ private void sendTo(Context context, String info) { Intent intent = new Intent(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, info); // 分享的文本内容 - intent.setType("text/plain"); // 分享类型:纯文本 - context.startActivity(intent); // 启动系统分享界面 + intent.putExtra(Intent.EXTRA_TEXT, info); + intent.setType("text/plain"); + context.startActivity(intent); } /** - * 新建便签:先保存当前编辑的便签,再启动新的编辑页面(同文件夹) - * 设计意图:避免新建时丢失当前便签的修改,同时保持新便签和当前便签同文件夹 + * 新建便签:保存当前便签后,启动新的编辑页面,继承当前便签的文件夹属性 + * 设计意图:用户新建便签时,保留当前的编辑上下文,提升操作效率 */ private void createNewNote() { - // 第一步:保存当前正在编辑的便签 - saveNote(); - - // 第二步:关闭当前页面,启动新的编辑页面 - finish(); + saveNote(); // 先保存当前编辑的便签,避免数据丢失 + finish(); // 关闭当前页面,避免页面栈过深 Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_INSERT_OR_EDIT); // 标记为新建/编辑模式 - intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); // 继承当前文件夹ID + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); startActivity(intent); } /** - * 删除当前便签:区分同步模式和非同步模式,执行不同的删除逻辑 - * 同步模式:移到回收站;非同步模式:直接从数据库删除 + * 删除当前便签:核心删除逻辑,区分同步模式和非同步模式,执行不同的删除策略 + * 同步模式:有绑定同步账号,便签移至回收站,可恢复;非同步模式:直接从数据库删除,不可恢复 */ - private void deleteCurrentNote() { - // 仅处理已持久化到数据库的便签 - if (mWorkingNote.existInDatabase()) { + private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { // 仅处理已保存到数据库的便签 HashSet ids = new HashSet(); long id = mWorkingNote.getNoteId(); - // 校验便签ID有效性(避免根文件夹ID) - if (id != Notes.ID_ROOT_FOLDER) { + if (id != Notes.ID_ROOT_FOLDER) { // 校验便签ID有效性,避免误删根文件夹 ids.add(id); } else { Log.d(TAG, "Wrong note id, should not happen"); } - // 非同步模式:直接删除便签 + // 非同步模式:直接批量删除便签 if (!isSyncMode()) { if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { Log.e(TAG, "Delete Note error"); } - } - // 同步模式:将便签移到回收站(而非直接删除) - else { + } else { + // 同步模式:将便签移至回收站文件夹,而非直接删除,支持恢复 if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { Log.e(TAG, "Move notes to trash folder error, should not happens"); } } } - // 标记便签为已删除(更新WorkingNote状态) - mWorkingNote.markDeleted(true); - } + mWorkingNote.markDeleted(true); // 标记便签为已删除,更新数据模型状态 + } /** - * 判断是否为同步模式:检测是否配置了同步账号(有账号则为同步模式) - * @return true-同步模式,false-非同步模式 + * 判断是否为同步模式:检测用户是否配置了同步账号,决定删除策略 + * @return true-已配置同步账号(同步模式),false-未配置(非同步模式) */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } - + /** - * 提醒时间变化回调:当便签提醒时间修改后,注册/取消系统闹钟 - * @param date 新的提醒时间戳 - * @param set true-设置提醒,false-取消提醒 + * 异步查询处理器:用于加载文件夹列表 */ - public void onClockAlertChanged(long date, boolean set) { - /** - * 处理未保存的便签:设置提醒前必须先保存(否则无NoteID,无法绑定闹钟) - */ - if (!mWorkingNote.existInDatabase()) { - saveNote(); + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver cr) { + super(cr); } - // 仅处理有效NoteID的便签 - if (mWorkingNote.getNoteId() > 0) { - // 构建提醒广播Intent(指向AlarmReceiver,用于接收闹钟触发事件) - Intent intent = new Intent(this, AlarmReceiver.class); - intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); - // 创建PendingIntent:广播类型,用于AlarmManager触发 - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); - AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); - - // 更新提醒UI展示 - showAlertHeader(); - - // 取消提醒:取消已注册的闹钟 - if(!set) { - alarmManager.cancel(pendingIntent); - } - // 设置提醒:注册新的闹钟(RTC_WAKEUP模式:唤醒设备触发) - else { - alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (token == FOLDER_LIST_QUERY_TOKEN) { + showFolderListMenu(cursor); } - } else { - /** - * 异常场景:便签无有效ID(未输入内容/未保存),提示用户输入内容 - */ - Log.e(TAG, "Clock alert setting error"); - showToast(R.string.error_note_empty_for_clock); } } - + /** - * 小组件关联变化回调:当便签关联的小组件信息变更时,更新小组件内容 + * 查询文件夹列表:异步加载所有可用的文件夹 */ - public void onWidgetChanged() { - updateWidget(); + private void startQueryDestinationFolders() { + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, + Notes.NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER, null, + Notes.NoteColumns.SNIPPET + " COLLATE LOCALIZED ASC"); + } + + /** + * 显示文件夹选择菜单:让用户选择要移动到的文件夹 + * @param cursor 文件夹列表的查询结果 + */ + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(NoteEditActivity.this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 保存当前编辑的内容 + getWorkingText(); + // 更新便签的文件夹ID + mWorkingNote.setFolderId(adapter.getItemId(which)); + // 保存便签 + saveNote(); + // 提示用户操作成功 + Toast.makeText(NoteEditActivity.this, + getString(R.string.format_move_notes_to_folder, 1, + adapter.getFolderName(NoteEditActivity.this, which)), + Toast.LENGTH_SHORT).show(); + } + }); + builder.show(); } + // ===================== NoteEditText 回调接口实现(清单模式核心) ===================== + // 该接口由自定义NoteEditText定义,处理清单模式下的「删除项、回车新增项」核心交互 + // 是清单模式能够正常增删、换行的核心支撑,所有清单的动态操作均通过该接口回调实现 + /** - * 清单模式-删除项回调:删除指定索引的清单项,调整剩余项的索引并合并文本 - * @param index 要删除的项索引 - * @param text 被删除项的文本内容(合并到前一项) + * 清单模式-删除项回调:删除指定索引的清单项,合并文本到前一项,调整后续项的索引 + * @param index 要删除的清单项索引 + * @param text 被删除项的文本内容,需要合并到前一项 */ public void onEditTextDelete(int index, String text) { int childCount = mEditTextList.getChildCount(); - // 仅保留最后一项时,不执行删除(避免清单列表为空) - if (childCount == 1) { + if (childCount == 1) { // 仅保留最后一项时,禁止删除,避免清单为空 return; } - // 调整删除项之后的所有项的索引(索引-1) + // 调整删除项之后的所有项索引,保证索引连续 for (int i = index + 1; i < childCount; i++) { - ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) - .setIndex(i - 1); + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)).setIndex(i - 1); } - // 移除指定索引的清单项 - mEditTextList.removeViewAt(index); + mEditTextList.removeViewAt(index); // 移除指定索引的清单项 - // 获取合并目标项(删除项的前一项,索引0则取第一项) + // 获取合并目标项,将被删除的文本合并到目标项中 NoteEditText edit = null; if(index == 0) { - edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( - R.id.et_edit_text); + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(R.id.et_edit_text); } else { - edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( - R.id.et_edit_text); + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(R.id.et_edit_text); } - // 合并被删除项的文本到目标项,光标定位到合并后的末尾 int length = edit.length(); edit.append(text); edit.requestFocus(); - edit.setSelection(length); + edit.setSelection(length); // 光标定位到合并后的文本末尾,提升编辑体验 } /** - * 清单模式-回车回调:在指定索引位置添加新的清单项,调整后续项的索引 - * @param index 回车的项索引 - * @param text 原项中回车后的文本内容(拆分到新项) + * 清单模式-回车回调:在指定索引位置新增清单项,拆分文本,调整后续项的索引 + * @param index 触发回车的清单项索引 + * @param text 原项中回车后的文本内容,拆分到新项中 */ public void onEditTextEnter(int index, String text) { - /** - * 异常校验:索引超出列表长度(理论上不会触发,仅调试用) - */ - if(index > mEditTextList.getChildCount()) { + if(index > mEditTextList.getChildCount()) { // 异常校验,避免索引越界 Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); } - // 创建新的清单项View,插入到指定索引位置 - View view = getListItem(text, index); - mEditTextList.addView(view, index); + View view = getListItem(text, index); // 创建新的清单项View + mEditTextList.addView(view, index); // 插入到指定索引位置 - // 新项获取焦点,光标定位到开头 + // 新项获取焦点,光标定位到开头,便于用户继续输入 NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); edit.requestFocus(); edit.setSelection(0); - // 调整新项之后的所有项的索引(索引+1) + // 调整新项之后的所有项索引,保证索引连续 for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { - ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) - .setIndex(i); + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)).setIndex(i); } } /** - * 切换到清单模式:将便签文本按换行拆分,生成对应的清单项列表 - * @param text 便签的原始文本内容 + * 切换到清单模式:将普通文本按换行拆分,生成对应的带复选框的清单项列表 + * @param text 普通模式的文本内容,按换行符拆分为多个清单项 */ private void switchToListMode(String text) { - // 清空原有清单项 - mEditTextList.removeAllViews(); - // 按换行拆分文本为单个清单项 - String[] items = text.split("\n"); + mEditTextList.removeAllViews(); // 清空原有列表,避免重复加载 + String[] items = text.split("\n"); // 按换行拆分文本 int index = 0; - // 遍历拆分后的文本,创建并添加清单项 + // 遍历拆分后的文本,创建并添加清单项,仅处理非空文本 for (String item : items) { if(!TextUtils.isEmpty(item)) { mEditTextList.addView(getListItem(item, index)); index++; } } - // 添加空的清单项(用于继续编辑) - mEditTextList.addView(getListItem("", index)); - // 空项获取焦点,便于用户继续输入 - mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); + mEditTextList.addView(getListItem("", index)); // 添加空项,便于用户继续输入 + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); // 空项获取焦点 // 切换UI显示:隐藏普通编辑框,显示清单列表 mNoteEditor.setVisibility(View.GONE); @@ -887,239 +1028,178 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * 搜索关键词高亮:为匹配搜索关键词的文本添加背景色Span - * @param fullText 便签完整文本 - * @param userQuery 搜索关键词 - * @return 带高亮效果的Spannable文本 + * 搜索关键词高亮:为匹配的搜索关键词添加背景色高亮效果,提升搜索体验 + * 核心实现:使用Spannable富文本,为匹配的文本段添加BackgroundColorSpan样式 + * @param fullText 便签的完整文本内容 + * @param userQuery 搜索关键词,为空则直接返回原文本 + * @return 带高亮效果的富文本,无匹配则返回原文本 */ private Spannable getHighlightQueryResult(String fullText, String userQuery) { - // 初始化Spannable(兼容null文本) SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); - // 仅当关键词非空时,执行高亮逻辑 if (!TextUtils.isEmpty(userQuery)) { - mPattern = Pattern.compile(userQuery); // 编译关键词正则 - Matcher m = mPattern.matcher(fullText); // 匹配文本 + mPattern = Pattern.compile(userQuery); // 编译正则表达式,提升匹配效率 + Matcher m = mPattern.matcher(fullText); int start = 0; - // 遍历所有匹配项,添加背景色Span + // 遍历所有匹配项,为每个匹配段添加高亮样式 while (m.find(start)) { - spannable.setSpan( - new BackgroundColorSpan(this.getResources().getColor( - R.color.user_query_highlight)), // 高亮背景色 - m.start(), m.end(), // 匹配文本的起止索引 - Spannable.SPAN_INCLUSIVE_EXCLUSIVE); // Span模式:包含起始,不包含结束 - start = m.end(); // 更新起始位置,避免重复匹配 + spannable.setSpan(new BackgroundColorSpan(this.getResources().getColor(R.color.user_query_highlight)), + m.start(), m.end(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + start = m.end(); // 更新起始位置,避免重复匹配同一文本 } } return spannable; } /** - * 创建清单项View:加载布局,绑定复选框和编辑框,处理勾选状态和文本高亮 + * 创建清单模式的子项View:加载布局、绑定控件、设置样式、处理勾选状态和文本高亮 + * 清单模式的核心子项构建方法,每个清单项均通过该方法创建 * @param item 清单项的文本内容 - * @param index 清单项的索引 - * @return 组装好的清单项View + * @param index 清单项的索引,用于绑定回调和定位 + * @return 组装完成的清单项View */ private View getListItem(String item, int index) { - // 加载清单项布局 - View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); // 加载子项布局 final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); - // 应用当前字体大小样式 - edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); // 应用字体样式 - // 绑定复选框,设置勾选状态变化监听 + // 绑定复选框,设置勾选状态变化监听,控制删除线样式 CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - // 勾选:添加删除线效果 if (isChecked) { - edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - } - // 取消勾选:移除删除线,恢复默认样式 - else { - edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); // 勾选:添加删除线 + } else { + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); // 取消勾选:移除删除线 } } }); - // 处理文本中的勾选标记,初始化复选框状态和文本 + // 处理文本中的勾选标记符,初始化复选框状态和文本内容 if (item.startsWith(TAG_CHECKED)) { - // 已勾选标记:复选框选中,添加删除线 cb.setChecked(true); edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); - item = item.substring(TAG_CHECKED.length(), item.length()).trim(); // 移除标记符 + item = item.substring(TAG_CHECKED.length(), item.length()).trim(); } else if (item.startsWith(TAG_UNCHECKED)) { - // 未勾选标记:复选框未选中,无删除线 cb.setChecked(false); edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); - item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); // 移除标记符 + item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); } - // 绑定文本变化监听(用于控制复选框显隐) + // 绑定回调和索引,设置文本并高亮搜索关键词 edit.setOnTextViewChangeListener(this); - // 设置清单项索引 edit.setIndex(index); - // 设置文本(含搜索高亮) edit.setText(getHighlightQueryResult(item, mUserQuery)); return view; } /** - * 清单项文本变化回调:根据文本是否为空,控制复选框的显隐 - * @param index 清单项索引 - * @param hasText 文本是否非空 + * 清单项文本变化回调:根据文本是否为空,控制复选框的显隐,保证界面整洁 + * @param index 清单项的索引 + * @param hasText 文本是否非空,true-显示复选框,false-隐藏复选框 */ public void onTextChange(int index, boolean hasText) { - // 异常校验:索引超出列表长度 - if (index >= mEditTextList.getChildCount()) { + if (index >= mEditTextList.getChildCount()) { // 异常校验,避免索引越界 Log.e(TAG, "Wrong index, should not happen"); return; } - // 文本非空:显示复选框 if(hasText) { mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); - } - // 文本为空:隐藏复选框 - else { + } else { mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); } } /** - * 模式切换回调:当便签在普通/清单模式间切换时,更新UI和数据 - * @param oldMode 旧模式(0=普通,MODE_CHECK_LIST=清单) - * @param newMode 新模式 - */ - public void onCheckListModeChanged(int oldMode, int newMode) { - // 切换到清单模式:将普通编辑框的文本转为清单项 - if (newMode == TextNote.MODE_CHECK_LIST) { - switchToListMode(mNoteEditor.getText().toString()); - } - // 切换到普通模式:将清单项合并为文本,显示到普通编辑框 - else { - // 获取清单项内容,合并为文本 - if (!getWorkingText()) { - // 清理未勾选标记符(避免普通模式显示冗余标记) - mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", - "")); - } - // 显示合并后的文本(含搜索高亮),切换UI显示 - mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); - mEditTextList.setVisibility(View.GONE); - mNoteEditor.setVisibility(View.VISIBLE); - } - } - - /** - * 获取编辑内容:根据当前模式(普通/清单),读取编辑框/清单项的文本,更新到WorkingNote - * @return true-清单模式下存在已勾选项,false-无勾选项/普通模式 + * 获取当前编辑的文本内容:根据当前的编辑模式,读取对应的文本并更新到WorkingNote + * 核心数据同步方法,保存便签、切换模式、分享等操作前均需调用该方法 + * @return true-清单模式下存在已勾选的项,false-无勾选/普通模式 */ private boolean getWorkingText() { boolean hasChecked = false; - // 清单模式:遍历所有清单项,拼接带标记符的文本 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { StringBuilder sb = new StringBuilder(); + // 遍历所有清单项,拼接带标记符的文本,已勾选加✔,未勾选加□ for (int i = 0; i < mEditTextList.getChildCount(); i++) { View view = mEditTextList.getChildAt(i); NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); - // 仅处理非空文本的清单项 if (!TextUtils.isEmpty(edit.getText())) { - // 已勾选:添加已勾选标记符 if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); hasChecked = true; - } - // 未勾选:添加未勾选标记符 - else { + } else { sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); } } } - // 更新WorkingNote的内容 - mWorkingNote.setWorkingText(sb.toString()); - } - // 普通模式:直接读取编辑框文本 - else { - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + mWorkingNote.setWorkingText(sb.toString()); // 更新数据模型的内容 + } else { + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); // 普通模式直接读取编辑框文本 } return hasChecked; } /** - * 保存便签:先获取最新编辑内容,再通过WorkingNote持久化到数据库 + * 保存便签核心方法:页面的核心数据持久化入口,所有保存操作均通过该方法完成 + * 核心逻辑:先同步编辑内容到WorkingNote,再调用WorkingNote的saveNote方法持久化到数据库 * @return true-保存成功,false-保存失败 */ private boolean saveNote() { - // 第一步:获取最新的编辑内容(普通/清单模式) - getWorkingText(); - // 第二步:持久化到数据库 - boolean saved = mWorkingNote.saveNote(); + getWorkingText(); // 同步当前编辑的内容到数据模型 + boolean saved = mWorkingNote.saveNote(); // 持久化到数据库 if (saved) { - /** - * 设置返回结果:区分“查看便签返回”和“新建/编辑便签返回” - * RESULT_OK:告知列表页,需要刷新数据(新建/编辑后) - */ - setResult(RESULT_OK); + setResult(RESULT_OK); // 设置返回结果,告知调用方数据已更新,需要刷新列表 } return saved; } /** - * 创建桌面快捷方式:生成指向当前便签的桌面图标,点击可直接打开便签 - * 设计意图:方便用户快速访问常用便签 + * 创建桌面快捷方式:生成指向当前便签的桌面图标,点击可直接打开该便签 + * 核心逻辑:发送系统广播通知桌面启动器创建快捷方式,绑定便签的唯一ID */ private void sendToDesktop() { - /** - * 前置校验:新便签必须先保存(否则无NoteID,无法创建快捷方式) - */ + // 关键逻辑:未保存的新便签,创建快捷方式前必须先保存,否则无唯一ID if (!mWorkingNote.existInDatabase()) { saveNote(); } - // 仅处理有效NoteID的便签 if (mWorkingNote.getNoteId() > 0) { - // 构建快捷方式的Intent(指向当前便签的查看页面) + // 构建快捷方式的Intent,指向当前便签的查看页面 Intent sender = new Intent(); Intent shortcutIntent = new Intent(this, NoteEditActivity.class); - shortcutIntent.setAction(Intent.ACTION_VIEW); // 标记为查看模式 - shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); // 绑定便签ID + shortcutIntent.setAction(Intent.ACTION_VIEW); + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); - // 设置快捷方式参数 - sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); // 点击快捷方式触发的Intent - sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, makeShortcutIconTitle(mWorkingNote.getContent())); // 快捷方式标题 - sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); // 快捷方式图标 - sender.putExtra("duplicate", true); // 允许重复创建(同一便签可创建多个快捷方式) - sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); // 桌面快捷方式创建广播 + // 设置快捷方式的核心参数:意图、标题、图标、允许重复创建 + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, makeShortcutIconTitle(mWorkingNote.getContent())); + sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); + sender.putExtra("duplicate", true); + sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); - // 提示用户创建成功 - showToast(R.string.info_note_enter_desktop); - // 发送广播,通知桌面启动器创建快捷方式 - sendBroadcast(sender); + showToast(R.string.info_note_enter_desktop); // 提示创建成功 + sendBroadcast(sender); // 发送广播,通知桌面创建快捷方式 } else { - /** - * 异常场景:便签无有效ID(未输入内容/未保存),提示用户输入内容 - */ + // 异常提示:便签无内容未保存,无法创建快捷方式 Log.e(TAG, "Send to desktop error"); showToast(R.string.error_note_empty_for_send_to_desktop); } } /** - * 生成桌面快捷方式标题:清理清单标记符,截断超长文本(最多10个字符) - * @param content 便签原始内容 - * @return 处理后的快捷方式标题 + * 生成桌面快捷方式标题:清理清单标记符,截断超长文本,保证标题简洁美观 + * @param content 便签的原始内容 + * @return 处理后的快捷方式标题,最长10个字符 */ private String makeShortcutIconTitle(String content) { - // 移除清单模式的勾选/未勾选标记符 content = content.replace(TAG_CHECKED, ""); content = content.replace(TAG_UNCHECKED, ""); - // 截断超长文本,最多保留10个字符 - return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, - SHORTCUT_ICON_TITLE_MAX_LEN) : content; + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, SHORTCUT_ICON_TITLE_MAX_LEN) : content; } + // ===================== 工具方法封装 - 简化重复调用 ===================== /** - * 显示短时长Toast提示(封装方法,简化调用) + * 显示短时长Toast提示:封装系统Toast,简化调用,统一提示样式 * @param resId 提示文本的资源ID */ private void showToast(int resId) { @@ -1127,11 +1207,11 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * 显示指定时长的Toast提示 + * 显示指定时长的Toast提示:重载方法,支持长短两种时长 * @param resId 提示文本的资源ID - * @param duration 时长(Toast.LENGTH_SHORT/Toast.LENGTH_LONG) + * @param duration 提示时长,Toast.LENGTH_SHORT/Toast.LENGTH_LONG */ private void showToast(int resId, int duration) { Toast.makeText(this, resId, duration).show(); } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java index 5235dc5..65401cb 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java @@ -14,309 +14,324 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,自定义EditText适配便签编辑的特殊交互逻辑 +// 包声明:归属小米便签的UI模块,该类是便签编辑页的核心自定义输入控件 package net.micode.notes.ui; -// 导入安卓上下文类:提供应用运行环境 +// 安卓系统上下文:提供应用运行环境与系统服务访问能力 import android.content.Context; -// 导入安卓图形矩形类:用于焦点变化时的区域计算 +// 安卓图形矩形类:用于焦点切换时的焦点区域坐标计算与传递 import android.graphics.Rect; -// 导入安卓文本布局类:处理文本的行/列定位(触摸光标定位) +// 安卓文本布局核心类:管理文本的排版、行高、行列定位,核心支撑触摸光标精准定位 import android.text.Layout; -// 导入安卓文本选择类:控制文本光标位置 +// 安卓文本选择工具类:用于手动设置文本光标位置、选中文本区域 import android.text.Selection; -// 导入安卓富文本类:处理包含URLSpan的富文本 +// 安卓富文本标记接口:标识带格式的文本(如包含链接、颜色的文本),本类核心处理该类型文本 import android.text.Spanned; -// 导入安卓文本工具类:判空等文本操作 +// 安卓文本工具类:提供字符串判空、文本处理等安全高效的工具方法 import android.text.TextUtils; -// 导入安卓URLSpan类:处理文本中的链接(电话/网页/邮箱) +// 安卓文本链接样式类:封装文本中的超链接数据,包含链接地址与点击事件 import android.text.style.URLSpan; -// 导入安卓属性集类:自定义View构造参数 +// 安卓属性集类:承载布局xml中配置的控件属性,自定义View必备构造参数 import android.util.AttributeSet; -// 导入安卓日志类:输出编辑框相关日志 +// 安卓日志工具类:输出编辑框相关的调试日志,便于问题排查 import android.util.Log; -// 导入安卓上下文菜单类:自定义链接的上下文菜单 +// 安卓上下文菜单类:构建长按文本弹出的菜单容器 import android.view.ContextMenu; -// 导入安卓按键事件类:处理回车/删除等按键交互 +// 安卓按键事件类:封装物理按键/软键盘按键的事件信息,用于监听回车、删除键 import android.view.KeyEvent; -// 导入安卓菜单项类:构建上下文菜单的选项 +// 安卓菜单项类:上下文菜单中的单个选项对象 import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; -// 导入安卓触摸事件类:处理触摸定位光标 +// 安卓触摸事件类:封装屏幕触摸的坐标、动作类型,用于重写触摸定位光标逻辑 import android.view.MotionEvent; -// 导入安卓编辑框类:自定义EditText的父类 +// 安卓原生编辑框:本自定义控件的父类,继承所有原生EditText基础能力 import android.widget.EditText; -// 导入小米便签资源类:引用字符串资源(链接菜单文本) +// 小米便签资源类:引用项目中的字符串、图片等资源文件,此处核心用链接菜单的文本资源 import net.micode.notes.R; -// 导入集合类:存储scheme与菜单文本的映射关系 +// Java集合类:HashMap存储链接协议与菜单文本的映射关系,实现协议与文案解耦 import java.util.HashMap; import java.util.Map; /** - * 便签编辑页自定义EditText类 - * 继承自安卓EditText,核心扩展职责: - * 1. 适配便签编辑的按键交互: - * - 删除键:光标在起始位置且非首个编辑框时,回调删除当前编辑框; - * - 回车键:分割文本并回调新增编辑框; - * 2. 自定义触摸事件:精准定位触摸位置的光标,优化富文本编辑体验; - * 3. 处理富文本链接(电话/网页/邮箱):长按链接弹出自定义上下文菜单,支持跳转; - * 4. 焦点变化回调:编辑框失焦且为空时,回调隐藏相关选项; - * 5. 管理编辑框索引:支持多编辑框的增删交互。 + * 小米便签 编辑页核心自定义EditText控件 + * 核心定位:继承原生Android EditText,基于便签编辑业务做深度定制扩展,无侵入式修改原生逻辑 + * 核心设计原则:重写系统回调方法实现定制化需求,所有扩展逻辑均通过回调接口解耦给上层Activity处理 + * 核心扩展能力(六大核心职责): + * 1. 定制化按键交互:重写回车/删除键逻辑,适配多编辑框联动的核心业务; + * 2. 精准触摸光标定位:修复原生触摸光标偏移问题,优化富文本编辑的触摸体验; + * 3. 富文本链接专属处理:长按识别文本中的链接,弹出自定义上下文菜单并支持一键跳转; + * 4. 焦点状态联动回调:编辑框获焦/失焦时,根据文本是否为空回调上层控制功能按钮显隐; + * 5. 多编辑框索引管理:维护自身在编辑框列表中的索引,支撑增删联动逻辑; + * 6. 完整兼容原生能力:所有重写方法最终都会调用父类实现,保留原生EditText全部功能。 */ public class NoteEditText extends EditText { - // 日志标签:用于编辑框相关日志输出 + // 日志打印的TAG标识:固定值,便于过滤该控件的所有日志信息 private static final String TAG = "NoteEditText"; - // 当前编辑框在多编辑框列表中的索引(用于增删回调) + + // 核心成员变量:当前编辑框在【多编辑框列表】中的索引值 + // 作用:用于向Activity回调增删操作时,标识当前操作的编辑框位置,核心支撑多框联动 private int mIndex; - // 删除键按下前的光标起始位置(用于判断是否触发编辑框删除) + + // 核心成员变量:删除键按下瞬间的光标起始位置 + // 作用:在onKeyDown中记录、onKeyUp中校验,判断是否触发「删除当前编辑框」的业务逻辑 private int mSelectionStartBeforeDelete; - // 链接Scheme常量:匹配文本中的不同类型链接 - private static final String SCHEME_TEL = "tel:" ; // 电话链接前缀 - private static final String SCHEME_HTTP = "http:" ; // 网页链接前缀 - private static final String SCHEME_EMAIL = "mailto:" ;// 邮箱链接前缀 + // ========== 常量区:文本链接的协议头(Scheme)常量 ========== + // 定义安卓原生支持的三大核心链接协议前缀,用于匹配文本中的URLSpan链接类型 + private static final String SCHEME_TEL = "tel:"; // 电话链接协议头 → 匹配电话号码链接 + private static final String SCHEME_HTTP = "http:"; // 网页链接协议头 → 匹配http/https网页链接 + private static final String SCHEME_EMAIL = "mailto:";// 邮箱链接协议头 → 匹配邮件发送链接 /** - * Scheme与上下文菜单文本资源的映射表 - * 用于根据链接类型(电话/网页/邮箱)显示对应的菜单文本 + * 静态不可变映射表:链接协议头(Scheme) → 上下文菜单文本的资源ID + * 设计模式:静态代码块初始化的常量映射,全局唯一,避免多次创建对象造成内存浪费 + * 核心作用:根据识别到的链接协议,自动匹配对应的菜单文本,实现协议与文案的解耦管理 */ private static final Map sSchemaActionResMap = new HashMap(); static { - // 初始化映射关系:Scheme -> 菜单文本资源ID - sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接 → “拨打电话” - sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页链接 → “打开网页” - sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);// 邮箱链接 → “发送邮件” + // 初始化映射关系:协议头 → 对应的菜单文本资源ID + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接 → 展示「拨打电话」 + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页链接 → 展示「打开网页」 + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 邮箱链接 → 展示「发送邮件」 } /** - * 编辑框状态变化回调接口 - * 由NoteEditActivity实现,处理编辑框的增删、文本变化交互 + * 编辑框状态变化 回调接口【核心解耦设计】 + * 设计思想:将所有业务逻辑(增删编辑框、控制显隐)完全抽离到接口中,由上层NoteEditActivity实现 + * 核心价值:该控件只负责「触发事件」,不负责「处理事件」,彻底解耦UI控件与业务逻辑,符合单一职责 */ public interface OnTextViewChangeListener { /** - * 删除当前编辑框的回调 - * 触发条件:按下删除键 + 光标在起始位置 + 非首个编辑框 - * @param index 当前编辑框的索引 - * @param text 当前编辑框的文本内容 + * 删除当前编辑框的回调方法 + * 触发时机:按下删除键 + 光标在文本起始位 + 当前编辑框不是第一个编辑框 + * @param index 当前触发删除的编辑框索引 + * @param text 当前编辑框中的文本内容,供上层做数据保存 */ void onEditTextDelete(int index, String text); /** - * 新增编辑框的回调 - * 触发条件:按下回车键 - * @param index 新增编辑框的目标索引(当前索引+1) - * @param text 分割后的文本(回车后的内容) + * 新增编辑框的回调方法 + * 触发时机:按下回车键时,自动分割文本并回调上层新增编辑框 + * @param index 新增编辑框应该插入的目标索引(当前索引+1) + * @param text 回车光标后的分割文本,作为新增编辑框的初始化内容 */ void onEditTextEnter(int index, String text); /** - * 文本变化/焦点变化的回调 - * 用于控制编辑框相关选项的显示/隐藏 + * 文本/焦点状态变化的回调方法 + * 触发时机:编辑框获焦/失焦、文本内容变化时 * @param index 当前编辑框的索引 - * @param hasText 是否有文本(true=显示选项,false=隐藏选项) + * @param hasText 当前编辑框是否有文本内容 → true显示功能按钮,false隐藏功能按钮 */ void onTextChange(int index, boolean hasText); } - // 编辑框状态变化监听器(由Activity实现) + // 成员变量:回调接口的实例对象,由上层Activity通过set方法注入 private OnTextViewChangeListener mOnTextViewChangeListener; /** - * 构造方法:初始化单个编辑框(无属性集) - * @param context 应用上下文 + * 构造方法一:代码中手动创建控件时调用(无布局属性) + * @param context 应用上下文对象,必传参数 */ public NoteEditText(Context context) { super(context, null); - // 默认索引为0(首个编辑框) - mIndex = 0; + mIndex = 0; // 默认索引为0,代表首个编辑框,后续可通过setIndex重新赋值 } /** - * 设置当前编辑框的索引 - * @param index 多编辑框列表中的索引 + * 对外提供的设置方法:为当前编辑框绑定在列表中的索引值 + * @param index 多编辑框列表中的位置索引 */ public void setIndex(int index) { mIndex = index; } /** - * 设置编辑框状态变化监听器 - * @param listener 实现OnTextViewChangeListener的监听器(通常为NoteEditActivity) + * 对外提供的设置方法:注入状态变化的回调监听器 + * @param listener 实现了OnTextViewChangeListener接口的实例(上层Activity) */ public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } /** - * 构造方法:带属性集的初始化(布局文件引用时调用) + * 构造方法二:布局xml中引用该控件时自动调用(带布局属性) + * 安卓自定义View的标准构造方法,适配布局文件中的属性配置 * @param context 应用上下文 - * @param attrs 布局属性集 + * @param attrs 布局xml中配置的控件属性集 */ public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } /** - * 构造方法:带属性集和默认样式的初始化 + * 构造方法三:带默认样式的构造方法,适配主题样式定制场景 + * 安卓自定义View的完整构造方法,满足所有创建场景,无业务逻辑,仅做父类调用 * @param context 应用上下文 * @param attrs 布局属性集 - * @param defStyle 默认样式 + * @param defStyle 默认样式资源ID */ public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub } /** - * 重写触摸事件:精准定位触摸位置的光标 - * 核心逻辑:将触摸坐标转换为文本的行/列偏移,设置光标位置 - * @param event 触摸事件 - * @return boolean:是否消费触摸事件(此处返回父类处理结果) + * 重写触摸事件处理方法:核心优化【触摸光标精准定位】 + * 原生EditText痛点:触摸文本时光标位置容易偏移,尤其富文本编辑时体验差 + * 本方法核心逻辑:将触摸的屏幕坐标,精准转换为文本的行列偏移量,手动设置光标位置 + * @param event 触摸事件对象,包含触摸坐标、动作类型等信息 + * @return boolean 事件消费标记:返回父类处理结果,不拦截原生触摸逻辑 */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - // 1. 计算触摸的相对坐标(扣除内边距,加上滚动偏移) + case MotionEvent.ACTION_DOWN: // 只处理「按下」动作,是触摸的起始事件 + // 步骤1:计算触摸点的【文本相对坐标】- 修正内边距与滚动偏移的影响 int x = (int) event.getX(); int y = (int) event.getY(); - x -= getTotalPaddingLeft(); - y -= getTotalPaddingTop(); - x += getScrollX(); - y += getScrollY(); + x -= getTotalPaddingLeft(); // 减去左侧总内边距,得到文本区域内的X坐标 + y -= getTotalPaddingTop(); // 减去顶部总内边距,得到文本区域内的Y坐标 + x += getScrollX(); // 加上横向滚动偏移,适配文本横向滚动的场景 + y += getScrollY(); // 加上纵向滚动偏移,适配文本纵向滚动的场景 - // 2. 根据坐标获取文本的行和偏移量 - Layout layout = getLayout(); - int line = layout.getLineForVertical(y); // 触摸位置的文本行 - int off = layout.getOffsetForHorizontal(line, x); // 该行的水平偏移量 + // 步骤2:通过文本布局对象,将坐标转换为文本的行列信息 + Layout layout = getLayout(); // 获取当前编辑框的文本布局管理器 + int line = layout.getLineForVertical(y); // 根据Y坐标获取触摸到的文本行号 + int off = layout.getOffsetForHorizontal(line, x); // 根据行号+X坐标获取该行的字符偏移量 - // 3. 设置光标到触摸位置 + // 步骤3:手动设置光标到精准的触摸位置,核心修复原生偏移问题 Selection.setSelection(getText(), off); break; } - - // 交给父类处理剩余触摸逻辑(如滑动、长按) + // 必须调用父类方法:保留原生的滑动、长按、双击等所有触摸逻辑,只做增量优化 return super.onTouchEvent(event); } /** - * 重写按键按下事件:预处理回车/删除键 - * @param keyCode 按键码(KEYCODE_ENTER/KEYCODE_DEL等) - * @param event 按键事件 - * @return boolean:是否消费按键事件 + * 重写按键按下事件:预处理核心按键(回车/删除),做事件标记,不做业务逻辑处理 + * 核心设计:按键按下时「只记录状态」,按键抬起时「执行业务逻辑」,符合安卓按键事件的处理规范 + * 原因:按下事件可能有长按重复触发,抬起事件只会触发一次,保证业务逻辑只执行一次 + * @param keyCode 按键码:标识按下的是哪个按键(回车/删除/字母等) + * @param event 按键事件对象,包含按键动作、重复次数等信息 + * @return boolean 事件消费标记:false=不消费,交给onKeyUp处理;true=消费,拦截后续处理 */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: - // 有监听器时返回false,交给onKeyUp处理(分割文本+新增编辑框) + // 回车键按下:有监听器则返回false,交给onKeyUp处理分割+新增逻辑,无则走原生逻辑 if (mOnTextViewChangeListener != null) { return false; } break; case KeyEvent.KEYCODE_DEL: - // 记录删除键按下前的光标位置,用于onKeyUp判断是否触发删除编辑框 + // 删除键按下:记录此时的光标起始位置,供onKeyUp校验是否触发删除编辑框逻辑 mSelectionStartBeforeDelete = getSelectionStart(); break; default: break; } - // 交给父类处理其他按键逻辑 + // 调用父类方法:处理所有其他按键的原生逻辑,无侵入式修改 return super.onKeyDown(keyCode, event); } /** - * 重写按键抬起事件:处理回车/删除键的核心交互逻辑 + * 重写按键抬起事件:核心业务逻辑处理入口【重中之重】 + * 该方法是本控件的核心,集中处理「回车新增编辑框」「删除当前编辑框」两大核心业务逻辑 + * 所有业务逻辑执行完毕后,均调用父类方法,保证原生按键功能不受影响 * @param keyCode 按键码 - * @param event 按键事件 - * @return boolean:是否消费按键事件 + * @param event 按键事件对象 + * @return boolean 事件消费标记:true=已处理业务逻辑,拦截原生行为;false=未处理,走原生逻辑 */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { case KeyEvent.KEYCODE_DEL: - // 有监听器时处理删除逻辑 + // ========== 删除键抬起:处理「删除编辑框」核心逻辑 ========== if (mOnTextViewChangeListener != null) { - // 触发条件:光标在起始位置 + 非首个编辑框(索引≠0) + // 触发条件【三重校验,缺一不可】: + // 1. 光标在文本的起始位置(0);2. 当前编辑框不是第一个编辑框(索引≠0);3. 有回调监听器 if (0 == mSelectionStartBeforeDelete && mIndex != 0) { - // 回调删除当前编辑框 + // 回调上层Activity:执行删除当前编辑框的逻辑,并传递当前文本内容 mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); - return true; // 消费事件,避免父类处理 + return true; // 消费事件,避免原生删除逻辑执行,防止文本内容错乱 } } else { - // 无监听器时输出日志提示 + // 无监听器时打印日志,便于调试排查问题 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; case KeyEvent.KEYCODE_ENTER: - // 有监听器时处理回车逻辑 + // ========== 回车键抬起:处理「分割文本+新增编辑框」核心逻辑 ========== if (mOnTextViewChangeListener != null) { - // 获取光标起始位置 - int selectionStart = getSelectionStart(); - // 分割文本:光标后的内容作为新增编辑框的文本 + int selectionStart = getSelectionStart(); // 获取当前光标位置 + // 步骤1:分割文本 → 光标后的内容作为新增编辑框的初始化文本 String text = getText().subSequence(selectionStart, length()).toString(); - // 保留光标前的内容在当前编辑框 + // 步骤2:更新当前编辑框文本 → 只保留光标前的内容,完成文本分割 setText(getText().subSequence(0, selectionStart)); - // 回调新增编辑框(索引为当前+1) + // 步骤3:回调上层Activity → 在当前索引+1的位置新增编辑框,并传递分割后的文本 mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); } else { - // 无监听器时输出日志提示 Log.d(TAG, "OnTextViewChangeListener was not seted"); } break; default: break; } - // 交给父类处理其他按键逻辑 + // 调用父类方法:处理其他按键的原生抬起逻辑,兼容所有原生功能 return super.onKeyUp(keyCode, event); } /** - * 重写焦点变化事件:回调文本状态(控制选项显示/隐藏) - * @param focused 是否获取焦点 - * @param direction 焦点变化方向 - * @param previouslyFocusedRect 之前的焦点区域 + * 重写焦点变化事件:处理「焦点联动功能按钮显隐」的业务逻辑 + * 触发时机:编辑框获取焦点/失去焦点时自动调用 + * 核心逻辑:失焦时如果文本为空,回调隐藏功能按钮;其他情况回调显示功能按钮 + * 设计亮点:只关心「失焦+空文本」的特殊场景,其他场景统一回调显示,逻辑简洁高效 + * @param focused 当前是否获取到焦点:true=获焦,false=失焦 + * @param direction 焦点变化的方向,系统传递的参数,无实际业务用途 + * @param previouslyFocusedRect 上一个焦点控件的矩形区域,系统传递的参数 */ @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (mOnTextViewChangeListener != null) { - // 失焦且文本为空:回调隐藏选项;否则回调显示选项 + // 核心判断:失焦 并且 文本为空 → 回调隐藏按钮;否则回调显示按钮 if (!focused && TextUtils.isEmpty(getText())) { mOnTextViewChangeListener.onTextChange(mIndex, false); } else { mOnTextViewChangeListener.onTextChange(mIndex, true); } } - // 交给父类处理焦点变化 + // 调用父类方法:执行原生的焦点变化逻辑,如光标显示/隐藏、背景色变化等 super.onFocusChanged(focused, direction, previouslyFocusedRect); } /** - * 重写上下文菜单创建方法:自定义链接的上下文菜单 - * 核心逻辑:识别选中区域的URLSpan,根据Scheme显示对应的菜单选项,点击触发链接跳转 - * @param menu 上下文菜单对象 + * 重写上下文菜单创建方法:核心实现「富文本链接的自定义菜单」功能 + * 触发时机:长按编辑框中的文本时,系统自动调用该方法创建上下文菜单 + * 核心业务逻辑:识别长按区域是否包含链接 → 是则添加自定义链接菜单 → 点击菜单触发链接跳转 + * 原生兼容:识别不到链接时,自动调用父类方法创建默认菜单(复制/粘贴/全选等) + * @param menu 系统传入的上下文菜单容器,用于添加自定义菜单项 */ @Override protected void onCreateContextMenu(ContextMenu menu) { - // 仅处理富文本(Spanned)类型的文本 + // 前置判断:只有文本是【富文本(Spanned)】类型时,才处理链接识别逻辑 if (getText() instanceof Spanned) { - // 获取光标选中的起始/结束位置 + // 步骤1:获取当前光标选中的文本区域,兼容正选/反选两种情况 int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); + int min = Math.min(selStart, selEnd); // 选中区域的起始位置 + int max = Math.max(selStart, selEnd); // 选中区域的结束位置 - // 修正选中范围(确保min≤max) - int min = Math.min(selStart, selEnd); - int max = Math.max(selStart, selEnd); - - // 获取选中区域内的所有URLSpan(链接) + // 步骤2:从选中区域中提取所有的URLSpan链接对象 final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); - // 仅处理单个链接的情况(避免多链接冲突) + + // 步骤3:仅处理「单个链接」的场景,避免多链接冲突,保证菜单的唯一性 if (urls.length == 1) { - // 默认菜单文本资源ID(未知链接) - int defaultResId = 0; - // 匹配链接的Scheme,获取对应的菜单文本 + int defaultResId = 0; // 初始化菜单文本资源ID + + // 步骤4:遍历协议映射表,匹配当前链接的协议类型,获取对应的菜单文本 for(String schema: sSchemaActionResMap.keySet()) { if(urls[0].getURL().indexOf(schema) >= 0) { defaultResId = sSchemaActionResMap.get(schema); @@ -324,23 +339,23 @@ public class NoteEditText extends EditText { } } - // 未匹配到已知Scheme:使用“其他链接”文本 + // 兜底处理:未匹配到已知协议时,显示「其他链接」的默认文本 if (defaultResId == 0) { defaultResId = R.string.note_link_other; } - // 添加菜单选项并设置点击事件 + // 步骤5:向菜单中添加自定义选项,并绑定点击事件 menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - // 触发URLSpan的点击事件(跳转链接:拨打电话/打开网页/发送邮件) + // 核心逻辑:触发URLSpan原生的点击事件 → 自动跳转对应链接(拨打电话/打开网页/发送邮件) urls[0].onClick(NoteEditText.this); - return true; + return true; // 消费菜单点击事件,避免后续处理 } }); } } - // 交给父类创建默认上下文菜单 + // 必须调用父类方法:无链接时创建原生默认菜单(复制、粘贴、剪切、选择全部等),完整兼容原生功能 super.onCreateContextMenu(menu); } } \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java index 82cc2fe..653b7fc 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java @@ -14,174 +14,187 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,作为便签列表项的核心数据模型,解析并封装Cursor中的便签数据 +// 包声明:归属小米便签UI模块,为列表页提供标准化的便签/文件夹数据模型,封装数据库游标解析逻辑 package net.micode.notes.ui; -// 导入安卓上下文类:用于获取ContentResolver、查询联系人信息 +// 安卓上下文:提供系统服务访问能力,用于联系人查询、内容解析器获取 import android.content.Context; -// 导入安卓数据库游标类:存储便签数据库查询结果,作为数据解析源 +// 安卓数据库游标:承载数据库查询结果集,是本类核心的数据解析源 import android.database.Cursor; -// 导入安卓文本工具类:处理字符串判空、替换等操作 +// 安卓文本工具类:提供字符串判空、文本处理等安全操作方法 import android.text.TextUtils; -// 导入联系人数据类:根据手机号查询联系人名称 +// 联系人数据工具类:根据手机号匹配系统通讯录中的联系人姓名 import net.micode.notes.data.Contact; -// 导入便签数据常量类:定义便签类型、特殊文件夹ID(通话记录文件夹)等常量 +// 便签核心常量类:定义便签类型、文件夹ID、字段名等全局常量 import net.micode.notes.data.Notes; +// 便签数据库列名常量:数据库表的字段名称枚举,避免硬编码字符串 import net.micode.notes.data.Notes.NoteColumns; -// 导入数据工具类:获取通话记录手机号、格式化数据等 +// 便签数据工具类:提供通话记录手机号查询、数据格式化等通用能力 import net.micode.notes.tool.DataUtils; /** - * 便签列表项核心数据模型类 + * 便签列表项核心数据模型类【小米便签核心类】 + * 核心定位:MVC架构中的Model层,纯数据载体,无任何UI渲染逻辑 * 核心职责: - * 1. 定义便签数据库查询的投影字段(PROJECTION),指定需要解析的核心字段; - * 2. 从Cursor中解析便签/文件夹的所有核心数据(ID、类型、背景色、时间、摘要等); - * 3. 适配通话记录项:解析手机号并查询对应的联系人名称; - * 4. 判断当前项在列表中的位置状态(首/尾/唯一项、文件夹后的便签项类型); - * 5. 提供数据访问方法(getter),封装数据逻辑(如是否有提醒、是否为通话记录)。 + * 1. 定义数据库查询的投影字段,精准指定查询列,规避全表查询的性能损耗; + * 2. 从数据库Cursor中解析并封装所有便签/文件夹的业务数据; + * 3. 对通话记录类便签做专属适配,自动关联手机号和联系人名称; + * 4. 智能判断列表项的位置状态,为UI背景样式渲染提供数据支撑; + * 5. 提供只读的getter方法和业务判断方法,对外屏蔽数据细节,保证数据安全; + * 设计原则:单一职责 + 完全封装 + 只读数据 + 业务内聚,解耦性极强 */ public class NoteItemData { /** - * 数据库查询投影数组:指定需要解析的便签核心字段,减少查询冗余 - * 覆盖便签/文件夹的基础属性、时间属性、关联属性(小部件、文件夹)、扩展属性(附件、提醒) + * 数据库查询投影数组:指定本次查询需要返回的数据库列 + * 投影设计原则:按需查询,只获取业务需要的字段,减少内存占用和IO开销 + * 字段覆盖:便签/文件夹的基础属性、时间属性、关联属性、扩展属性四大类 */ - static final String [] PROJECTION = new String [] { - NoteColumns.ID, // 0: 便签/文件夹唯一ID - NoteColumns.ALERTED_DATE, // 1: 提醒时间戳(0表示无提醒) - NoteColumns.BG_COLOR_ID, // 2: 背景色ID - NoteColumns.CREATED_DATE, // 3: 创建时间戳 - NoteColumns.HAS_ATTACHMENT, // 4: 是否有附件(0=无,1=有) - NoteColumns.MODIFIED_DATE, // 5: 最后修改时间戳 - NoteColumns.NOTES_COUNT, // 6: 文件夹内便签数量(仅文件夹有效) - NoteColumns.PARENT_ID, // 7: 父文件夹ID(根文件夹为Notes.ID_ROOT_FOLDER) - NoteColumns.SNIPPET, // 8: 便签摘要/文件夹名称 - NoteColumns.TYPE, // 9: 类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) - NoteColumns.WIDGET_ID, // 10: 关联的小部件ID(无效为AppWidgetManager.INVALID_APPWIDGET_ID) - NoteColumns.WIDGET_TYPE, // 11: 关联的小部件类型(2x/4x,对应Notes.TYPE_WIDGET_2X/4X) + static final String[] PROJECTION = new String[]{ + NoteColumns.ID, // 0: 便签/文件夹的唯一主键ID + NoteColumns.ALERTED_DATE, // 1: 提醒时间戳(0代表无提醒) + NoteColumns.BG_COLOR_ID, // 2: 背景色ID,用于列表项背景着色 + NoteColumns.CREATED_DATE, // 3: 创建时间戳,UTC毫秒值 + NoteColumns.HAS_ATTACHMENT, // 4: 是否包含附件 0-无 1-有(图片/音频等) + NoteColumns.MODIFIED_DATE, // 5: 最后修改时间戳,列表页优先展示该时间 + NoteColumns.NOTES_COUNT, // 6: 文件夹内包含的便签数量,仅文件夹类型有效 + NoteColumns.PARENT_ID, // 7: 父文件夹ID,根目录为Notes.ID_ROOT_FOLDER + NoteColumns.SNIPPET, // 8: 便签内容摘要/文件夹名称,短文本展示用 + NoteColumns.TYPE, // 9: 数据类型,区分便签/文件夹/系统项 + NoteColumns.WIDGET_ID, // 10: 关联的桌面小部件ID,无效则为默认值 + NoteColumns.WIDGET_TYPE // 11: 关联小部件尺寸类型 2x/4x }; - // 投影数组对应的列索引常量:简化Cursor取值,避免硬编码索引 - private static final int ID_COLUMN = 0; // 便签/文件夹ID列索引 - private static final int ALERTED_DATE_COLUMN = 1; // 提醒时间列索引 - private static final int BG_COLOR_ID_COLUMN = 2; // 背景色ID列索引 - private static final int CREATED_DATE_COLUMN = 3; // 创建时间列索引 - private static final int HAS_ATTACHMENT_COLUMN = 4; // 是否有附件列索引 - private static final int MODIFIED_DATE_COLUMN = 5; // 最后修改时间列索引 - private static final int NOTES_COUNT_COLUMN = 6; // 文件夹内便签数量列索引 - private static final int PARENT_ID_COLUMN = 7; // 父文件夹ID列索引 - private static final int SNIPPET_COLUMN = 8; // 摘要/文件夹名称列索引 - private static final int TYPE_COLUMN = 9; // 类型列索引 - private static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引 - private static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列索引 - - // 核心数据字段:与投影数组一一对应 - private long mId; // 便签/文件夹唯一ID - private long mAlertDate; // 提醒时间戳(>0表示有提醒) - private int mBgColorId; // 背景色ID(用于列表项背景渲染) - private long mCreatedDate; // 创建时间戳 - private boolean mHasAttachment; // 是否有附件(图片/音频等) - private long mModifiedDate; // 最后修改时间戳(用于列表项时间展示) - private int mNotesCount; // 文件夹内便签数量(仅TYPE_FOLDER有效) - private long mParentId; // 父文件夹ID(通话记录项为Notes.ID_CALL_RECORD_FOLDER) - private String mSnippet; // 便签摘要/文件夹名称(清理了勾选标记后的纯文本) - private int mType; // 类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) - private int mWidgetId; // 关联的小部件ID(无效为INVALID_APPWIDGET_ID) - private int mWidgetType; // 关联的小部件类型(2x/4x) - private String mName; // 通话记录项的联系人名称(无则显示手机号) - private String mPhoneNumber; // 通话记录项的手机号(仅父ID为通话记录文件夹有效) - - // 列表位置状态字段:用于适配列表项背景渲染 - private boolean mIsLastItem; // 是否为列表最后一项 - private boolean mIsFirstItem; // 是否为列表第一项 - private boolean mIsOnlyOneItem; // 是否为列表唯一一项 - private boolean mIsOneNoteFollowingFolder; // 是否为文件夹后的唯一便签项 - private boolean mIsMultiNotesFollowingFolder;// 是否为文件夹后的多个便签项的第一项 + /** + * 投影数组列索引常量:与PROJECTION数组字段一一对应 + * 设计目的:彻底避免硬编码数字索引,提升代码可读性+可维护性 + * 核心价值:修改投影数组字段顺序时,仅需同步修改此处索引,无需改动业务解析代码 + */ + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + // ===================== 核心业务数据字段 - 与投影数组一一映射 ===================== + // 访问规则:全部私有,仅通过getter方法访问,无setter,数据只读,保证一致性 + private long mId; // 便签/文件夹唯一ID,数据库主键 + private long mAlertDate; // 提醒时间戳,>0表示该便签设置了提醒 + private int mBgColorId; // 背景色ID,对应预设的颜色值,列表项渲染用 + private long mCreatedDate; // 便签创建时间戳 + private boolean mHasAttachment; // 是否包含附件,数据库int转Java布尔值,贴合业务语义 + private long mModifiedDate; // 最后修改时间戳,列表页展示的核心时间字段 + private int mNotesCount; // 文件夹内便签数量,仅TYPE_FOLDER类型有效 + private long mParentId; // 父文件夹ID,通话记录便签固定为通话记录文件夹ID + private String mSnippet; // 便签纯文本摘要/文件夹名称,已清理勾选标记 + private int mType; // 数据类型:Notes.TYPE_NOTE/文件夹/系统项 + private int mWidgetId; // 关联桌面小部件ID,无效则为INVALID_APPWIDGET_ID + private int mWidgetType; // 关联小部件尺寸类型,2x/4x两种规格 + + // ===================== 通话记录专属扩展字段 ===================== + private String mName; // 通话记录联系人姓名,无则显示手机号 + private String mPhoneNumber; // 通话记录对应的手机号,仅通话便签有值 + + // ===================== 列表位置状态字段 - 纯UI渲染支撑 ===================== + // 作用:标记当前项在列表中的位置,用于适配不同的背景样式(圆角/分割线/边距等) + private boolean mIsLastItem; // 是否为列表最后一条数据 + private boolean mIsFirstItem; // 是否为列表第一条数据 + private boolean mIsOnlyOneItem; // 是否为列表中唯一的一条数据 + private boolean mIsOneNoteFollowingFolder; // 文件夹后的唯一一条便签项 + private boolean mIsMultiNotesFollowingFolder;// 文件夹后的第一条便签(后续还有更多项) /** - * 构造方法:从Cursor解析便签/文件夹数据,初始化所有字段 - * @param context 应用上下文:用于查询ContentResolver、获取联系人信息 - * @param cursor 包含便签数据的Cursor(已移动到目标位置) + * 唯一构造方法:私有化核心初始化逻辑,从Cursor解析所有数据并完成对象初始化 + * 设计特点:全参构造,一次性完成所有数据赋值,无空构造,保证对象完整性 + * @param context 应用上下文,用于获取内容解析器、查询联系人信息,不可为空 + * @param cursor 数据库查询结果游标,已移动到目标行,不可为空/已关闭 */ public NoteItemData(Context context, Cursor cursor) { - // 1. 解析Cursor核心字段:与投影数组索引一一对应 + // 第一步:解析数据库核心字段,与投影数组索引一一对应,基础数据初始化 mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); - // 转换int为boolean:0=无附件,>0=有附件 - mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; + // 数据库存储为int(0/1),转换为业务语义更清晰的boolean类型 + mHasAttachment = cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0; mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); + // 解析摘要文本,并清理勾选框标记标签,仅保留纯文本内容,优化展示效果 mSnippet = cursor.getString(SNIPPET_COLUMN); - // 清理摘要中的勾选标记(TAG_CHECKED/TAG_UNCHECKED),仅保留纯文本 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); mType = cursor.getInt(TYPE_COLUMN); mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); - // 2. 初始化通话记录相关字段:仅父ID为通话记录文件夹时解析 + // 第二步:通话记录便签专属解析逻辑,仅父文件夹为通话记录文件夹时触发 mPhoneNumber = ""; if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { - // 根据便签ID查询对应的通话记录手机号 + // 根据便签ID查询关联的通话手机号 mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); - // 有手机号时,查询对应的联系人名称 + // 手机号非空时,查询系统通讯录匹配联系人姓名 if (!TextUtils.isEmpty(mPhoneNumber)) { mName = Contact.getContact(context, mPhoneNumber); - // 无联系人名称时,使用手机号作为名称 + // 无匹配联系人时,手机号作为联系人名称兜底展示 if (mName == null) { mName = mPhoneNumber; } } } - // 3. 兜底:联系人名称为空时设为空字符串,避免空指针 + // 第三步:空值兜底处理,防止空指针异常,生产级代码必备容错逻辑 if (mName == null) { mName = ""; } - // 4. 判断当前项在列表中的位置状态(用于背景渲染) + // 第四步:自动判断当前项在列表中的位置状态,为UI渲染提供数据支撑 checkPostion(cursor); } /** - * 私有方法:判断当前项在Cursor中的位置状态,初始化位置相关字段 - * 核心逻辑: - * 1. 判断是否为首/尾/唯一项; - * 2. 判断普通便签是否为文件夹后的第一项(单个/多个)。 - * @param cursor 包含便签数据的Cursor(已移动到目标位置) + * 私有核心方法:解析当前Cursor所在行的列表位置状态,初始化位置标记字段 + * 核心作用:为列表项的背景样式提供精准的状态标记,不同位置展示不同样式 + * 设计亮点:Cursor位置移动后必回位,防止游标位置错乱导致后续解析异常 + * @param cursor 数据库游标,已定位到当前数据行 */ private void checkPostion(Cursor cursor) { - // 初始化基础位置状态 - mIsLastItem = cursor.isLast() ? true : false; // 是否为最后一项 - mIsFirstItem = cursor.isFirst() ? true : false; // 是否为第一项 - mIsOnlyOneItem = (cursor.getCount() == 1); // 是否为唯一一项 - // 初始化文件夹后便签项状态为false + // 初始化基础位置状态:首项、尾项、唯一项 + mIsLastItem = cursor.isLast(); + mIsFirstItem = cursor.isFirst(); + mIsOnlyOneItem = cursor.getCount() == 1; + + // 初始化文件夹子项状态为默认值false mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; - // 仅处理普通便签且非第一项的情况:判断是否为文件夹后的便签项 + // 核心业务判断:仅处理【普通便签】且【非列表首项】的场景 if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { - // 记录当前位置 - int position = cursor.getPosition(); - // 移动Cursor到上一项,判断上一项是否为文件夹/系统项 + // 记录当前游标位置,用于后续回位,防止位置丢失 + int currentPosition = cursor.getPosition(); + // 游标上移一行,判断上一项是否为【文件夹】或【系统项】 if (cursor.moveToPrevious()) { - if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER - || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { - // 上一项是文件夹/系统项:判断当前项后是否还有更多项 - if (cursor.getCount() > (position + 1)) { - // 有更多项:标记为“文件夹后的多个便签项的第一项” + int prevItemType = cursor.getInt(TYPE_COLUMN); + if (prevItemType == Notes.TYPE_FOLDER || prevItemType == Notes.TYPE_SYSTEM) { + // 上一项是文件夹/系统项 → 当前项是文件夹的子项第一条 + if (cursor.getCount() > currentPosition + 1) { + // 后续还有更多数据 → 标记为多子项的第一条 mIsMultiNotesFollowingFolder = true; } else { - // 无更多项:标记为“文件夹后的唯一便签项” + // 后续无数据 → 标记为文件夹后的唯一子项 mIsOneNoteFollowingFolder = true; } } - // 移动Cursor回原位置,避免影响后续操作 + // 游标回位到原位置,必须执行,否则会导致后续数据解析错位 if (!cursor.moveToNext()) { - // 移动失败时抛出异常,防止Cursor位置错乱 + // 回位失败时主动抛异常,快速定位问题,避免隐性bug throw new IllegalStateException("cursor move to previous but can't move back"); } } @@ -189,178 +202,182 @@ public class NoteItemData { } /** - * 判断是否为文件夹后的唯一便签项 - * @return boolean:true=是,false=否 + * 业务判断方法:当前项是否为【文件夹后的唯一便签项】 + * @return true-是 false-否 */ public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } /** - * 判断是否为文件夹后的多个便签项的第一项 - * @return boolean:true=是,false=否 + * 业务判断方法:当前项是否为【文件夹后的第一条便签(后续还有项)】 + * @return true-是 false-否 */ public boolean isMultiFollowingFolder() { return mIsMultiNotesFollowingFolder; } /** - * 判断是否为列表最后一项 - * @return boolean:true=是,false=否 + * 位置判断方法:当前项是否为列表最后一项 + * @return true-是 false-否 */ public boolean isLast() { return mIsLastItem; } /** - * 获取通话记录项的联系人名称(无则返回手机号) - * @return String:联系人名称/手机号 + * 数据访问方法:获取通话记录的联系人名称(无则返回手机号) + * @return 联系人姓名/手机号 空则返回空字符串 */ public String getCallName() { return mName; } /** - * 判断是否为列表第一项 - * @return boolean:true=是,false=否 + * 位置判断方法:当前项是否为列表第一项 + * @return true-是 false-否 */ public boolean isFirst() { return mIsFirstItem; } /** - * 判断是否为列表唯一一项 - * @return boolean:true=是,false=否 + * 位置判断方法:当前项是否为列表中唯一的一项 + * @return true-是 false-否 */ public boolean isSingle() { return mIsOnlyOneItem; } /** - * 获取便签/文件夹唯一ID - * @return long:ID值 + * 基础数据访问:获取便签/文件夹唯一ID + * @return 数据库主键ID,long类型 */ public long getId() { return mId; } /** - * 获取提醒时间戳 - * @return long:提醒时间戳(>0表示有提醒) + * 基础数据访问:获取提醒时间戳 + * @return 提醒时间UTC毫秒值,0表示无提醒 */ public long getAlertDate() { return mAlertDate; } /** - * 获取创建时间戳 - * @return long:创建时间戳 + * 基础数据访问:获取创建时间戳 + * @return 创建时间UTC毫秒值 */ public long getCreatedDate() { return mCreatedDate; } /** - * 判断是否有附件 - * @return boolean:true=有,false=无 + * 业务判断方法:当前便签是否包含附件(图片/音频等) + * @return true-有附件 false-无附件 */ public boolean hasAttachment() { return mHasAttachment; } /** - * 获取最后修改时间戳 - * @return long:最后修改时间戳 + * 基础数据访问:获取最后修改时间戳(列表页优先展示) + * @return 修改时间UTC毫秒值 */ public long getModifiedDate() { return mModifiedDate; } /** - * 获取背景色ID - * @return int:背景色ID(用于列表项背景渲染) + * 基础数据访问:获取背景色ID + * @return 预设的颜色ID值,用于列表项背景渲染 */ public int getBgColorId() { return mBgColorId; } /** - * 获取父文件夹ID - * @return long:父文件夹ID(通话记录项为Notes.ID_CALL_RECORD_FOLDER) + * 基础数据访问:获取父文件夹ID + * @return 父文件夹主键ID,通话记录为固定常量ID */ public long getParentId() { return mParentId; } /** - * 获取文件夹内便签数量(仅TYPE_FOLDER有效) - * @return int:便签数量 + * 基础数据访问:获取文件夹内便签数量 + * @return 数量值,仅文件夹类型有效,便签类型返回无意义值 */ public int getNotesCount() { return mNotesCount; } /** - * 兼容方法:获取父文件夹ID(与getParentId逻辑一致,适配外部调用) - * @return long:父文件夹ID + * 兼容适配方法:等价于getParentId(),适配外部旧调用逻辑 + * 设计目的:向下兼容,无侵入式修改原有业务代码 + * @return 父文件夹主键ID */ - public long getFolderId () { + public long getFolderId() { return mParentId; } /** - * 获取便签/文件夹类型 - * @return int:类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) + * 基础数据访问:获取数据类型 + * @return 类型值:Notes.TYPE_NOTE/文件夹/系统项 */ public int getType() { return mType; } /** - * 获取关联的小部件类型 - * @return int:小部件类型(2x/4x,对应Notes.TYPE_WIDGET_2X/4X) + * 基础数据访问:获取关联小部件类型 + * @return 小部件尺寸类型 2x/4x */ - public int getWidgetType() { - return mWidgetType; - } + public int getWidgetType() { + return mWidgetType; + } /** - * 获取关联的小部件ID - * @return int:小部件ID(无效为AppWidgetManager.INVALID_APPWIDGET_ID) + * 基础数据访问:获取关联小部件ID + * @return 小部件ID,无效则为INVALID_APPWIDGET_ID */ - public int getWidgetId() { - return mWidgetId; - } + public int getWidgetId() { + return mWidgetId; + } /** - * 获取清理后的便签摘要/文件夹名称 - * @return String:纯文本摘要/名称 + * 基础数据访问:获取清理后的纯文本摘要/文件夹名称 + * @return 无勾选标记的纯文本短内容 */ public String getSnippet() { return mSnippet; } /** - * 判断是否有提醒 - * @return boolean:true=有提醒(mAlertDate>0),false=无提醒 + * 核心业务判断方法:当前便签是否设置了提醒 + * 封装细节:屏蔽「时间戳判0」的技术细节,对外提供业务语义 + * @return true-有提醒 false-无提醒 */ public boolean hasAlert() { - return (mAlertDate > 0); + return mAlertDate > 0; } /** - * 判断是否为通话记录项 - * @return boolean:true=父ID为通话记录文件夹且有手机号,false=否 + * 核心业务判断方法:当前项是否为【通话记录便签】 + * 判断条件:父文件夹是通话记录文件夹 + 手机号非空,双重校验保证准确性 + * @return true-通话记录便签 false-普通便签/文件夹 */ public boolean isCallRecord() { - return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + return mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber); } /** - * 静态工具方法:从Cursor中获取便签类型 - * 适配外部(如NotesListAdapter)快速获取类型,无需创建NoteItemData实例 - * @param cursor 包含便签数据的Cursor(已移动到目标位置) - * @return int:便签类型(Notes.TYPE_NOTE/TYPE_FOLDER/TYPE_SYSTEM) + * 静态工具方法:无需创建对象,快速从Cursor获取便签类型 + * 性能优化点:避免为了单个字段创建完整对象实例,适配列表适配器高频调用场景 + * 对外赋能:给外部适配器提供轻量级的类型查询能力 + * @param cursor 数据库游标,已定位到目标行 + * @return 便签类型值 */ public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java index 00df199..462e30a 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java @@ -79,85 +79,131 @@ import java.io.InputStreamReader; import java.util.HashSet; /** - * 便签列表核心页面 - * 核心职责:展示便签/文件夹列表、处理便签的新建/批量删除/批量移动、文件夹管理(重命名/删除/查看)、 - * 首次使用引导、同步模式适配、桌面小组件关联更新等 - * 核心依赖:AsyncQueryHandler(异步数据库查询)、NotesListAdapter(列表数据适配)、DataUtils(数据操作工具) + * 小米便签 核心主页面 - 便签列表页 + *

+ * 继承安卓原生{Activity},是小米便签应用的**核心门户页面**,隶属于MVC架构的UI层业务页面,承载应用所有核心交互能力; + * 核心设计定位:作为应用的入口与中枢,统一管理便签/文件夹的所有核心业务操作,封装完整的页面状态管理、事件分发、异步数据处理、交互反馈逻辑, + * 是连接便签编辑页、设置页、小组件、数据工具类的核心桥梁; + * 核心业务职责: + * 1. 数据展示:分页展示便签列表、文件夹列表、通话记录专属文件夹及内容,适配不同文件夹层级的视图切换; + * 2. 便签操作:新建普通便签、打开已有便签编辑、批量删除便签、批量移动便签到指定文件夹,适配同步/非同步模式的差异化删除逻辑; + * 3. 文件夹管理:创建文件夹、重命名文件夹、删除文件夹、进入文件夹查看子项,校验文件夹名称唯一性,维护文件夹层级关系; + * 4. 多选模式:完整支撑便签的批量操作,包含全选/取消全选、选中状态维护、选中数量统计、多选菜单适配; + * 5. 初始化引导:首次打开应用自动创建引导便签,展示应用核心功能与使用说明; + * 6. 数据交互:异步执行数据库查询/更新/删除,避免主线程阻塞,保证页面流畅性;导出便签为文本文件,支持本地备份; + * 7. 同步适配:区分同步模式/非同步模式,同步模式下删除便签移至回收站,支持手动触发/取消同步,跳转同步设置页; + * 8. 小组件联动:便签数据变更后,自动发送广播更新关联的2x/4x桌面小组件内容,保证数据一致性; + * 9. 交互优化:处理新建按钮透明区域的事件透传、长按震动反馈、软键盘的显隐控制、上下文菜单的创建与销毁; + * 技术实现特点: + * - 基于{AsyncQueryHandler}实现数据库异步操作,解决主线程阻塞问题,提升页面滑动与操作流畅度; + * - 基于{NotesListAdapter}实现列表数据与视图的解耦,统一管理列表项的渲染与选中状态; + * - 通过枚举{ListEditState}维护页面状态,精准控制不同状态下的视图展示与功能开放; + * - 大量使用{AsyncTask}处理耗时操作(删除/导出),保证UI线程不被阻塞; + * - 完整的异常边界处理,包含空数据校验、无效ID过滤、日志输出,提升应用稳定性; + * - 精细化的交互体验优化,包含触摸事件分发、震动反馈、Toast提示、对话框确认,兼顾功能完整性与用户体验。 + *

*/ public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { - // ===================== 异步查询Token:区分不同类型的异步查询任务 ===================== - private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 文件夹下便签列表查询标识 - private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 文件夹列表查询标识(用于移动便签时选择目标文件夹) - - // ===================== 文件夹上下文菜单ID:区分文件夹操作类型 ===================== - private static final int MENU_FOLDER_DELETE = 0; // 删除文件夹 - private static final int MENU_FOLDER_VIEW = 1; // 查看文件夹下的便签 - private static final int MENU_FOLDER_CHANGE_NAME = 2;// 重命名文件夹 - - // ===================== 偏好设置Key:标记是否已展示首次使用引导 ===================== + // ===================== 异步查询任务标识常量 - 核心区分不同类型的异步数据库查询 ===================== + /** 异步查询Token:查询指定文件夹下的所有便签/子文件夹数据,是页面最核心的查询任务 */ + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + /** 异步查询Token:查询可用于移动便签的目标文件夹列表,仅在批量移动便签时触发 */ + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + // ===================== 文件夹上下文菜单操作ID - 区分文件夹的右键菜单功能 ===================== + /** 上下文菜单ID:删除当前文件夹 */ + private static final int MENU_FOLDER_DELETE = 0; + /** 上下文菜单ID:进入当前文件夹查看子项 */ + private static final int MENU_FOLDER_VIEW = 1; + /** 上下文菜单ID:修改当前文件夹的名称 */ + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + // ===================== 偏好设置存储键值 - 持久化标记应用初始化状态 ===================== + /** SharedPreference存储Key:标记是否已为新用户创建首次使用引导便签,避免重复创建 */ private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; - // ===================== 列表编辑状态枚举:区分当前列表展示的内容类型 ===================== + // ===================== 页面核心状态枚举 - 控制当前页面展示的内容类型与功能边界 ===================== + /** + * 列表编辑状态枚举:完整定义页面的三种核心状态,不同状态对应不同的视图展示、菜单加载、功能开放 + */ private enum ListEditState { - NOTE_LIST, // 普通便签列表状态(默认) - SUB_FOLDER, // 子文件夹列表状态 - CALL_RECORD_FOLDER// 通话记录文件夹状态(专属) + NOTE_LIST, // 根目录普通便签列表状态【默认】:展示所有文件夹+根目录便签+通话记录文件夹 + SUB_FOLDER, // 子文件夹状态:展示指定文件夹下的便签,显示标题栏,隐藏部分根目录功能 + CALL_RECORD_FOLDER// 通话记录文件夹专属状态:展示通话记录便签,隐藏新建按钮,专属菜单适配 }; - // ===================== 核心成员变量 ===================== - private ListEditState mState; // 当前列表编辑状态(枚举) - private BackgroundQueryHandler mBackgroundQueryHandler; // 异步数据库查询处理器(避免主线程阻塞) - private NotesListAdapter mNotesListAdapter; // 便签列表适配器(绑定数据与UI) - private ListView mNotesListView; // 便签列表展示控件 - private Button mAddNewNote; // 新建便签按钮 - private boolean mDispatch; // 新建按钮触摸事件分发标记(处理透明区域事件透传) - private int mOriginY; // 新建按钮触摸事件原始Y坐标(用于事件分发计算) - private int mDispatchY; // 新建按钮触摸事件分发后的Y坐标 - private TextView mTitleBar; // 标题栏文本控件(显示当前文件夹名称) - private long mCurrentFolderId; // 当前选中的文件夹ID(默认根文件夹) - private ContentResolver mContentResolver; // 内容解析器(操作ContentProvider) - private ModeCallback mModeCallBack; // 列表多选模式回调(处理批量操作) - private static final String TAG = "NotesListActivity"; // 日志标签 - public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; // 列表滚动速率(自定义滚动行为) - private NoteItemData mFocusNoteDataItem; // 当前聚焦的便签数据项(用于多选模式初始化) - - // ===================== 数据库查询条件:区分根文件夹和普通文件夹的查询逻辑 ===================== - // 普通文件夹查询条件:仅查询指定父文件夹下的便签 + // ===================== 页面核心成员变量 ===================== + /** 当前页面的状态标识:控制视图展示与功能逻辑分支,默认值为NOTE_LIST根目录状态 */ + private ListEditState mState; + /** 异步数据库查询处理器:核心工具类,所有数据库的增删改查均通过该类异步执行,避免主线程阻塞 */ + private BackgroundQueryHandler mBackgroundQueryHandler; + /** 列表核心适配器:绑定数据库Cursor数据与ListView,管理列表项渲染、选中状态、数据统计,页面核心数据桥梁 */ + private NotesListAdapter mNotesListAdapter; + /** 核心列表控件:承载所有便签/文件夹的展示,是页面的核心视图容器 */ + private ListView mNotesListView; + /** 新建便签按钮:页面底部悬浮按钮,点击快速创建新便签,包含透明区域事件透传逻辑 */ + private Button mAddNewNote; + /** 事件分发标记:标记是否需要将新建按钮的触摸事件透传给下方的ListView,处理透明区域点击 */ + private boolean mDispatch; + /** 触摸原始坐标Y:记录新建按钮触摸事件的初始Y坐标,用于事件透传时的坐标校准 */ + private int mOriginY; + /** 触摸分发坐标Y:记录事件透传后的目标Y坐标,保证ListView接收的坐标准确性 */ + private int mDispatchY; + /** 标题栏文本控件:子文件夹/通话记录文件夹状态下显示,展示当前文件夹名称,根目录状态下隐藏 */ + private TextView mTitleBar; + /** 当前选中的文件夹ID:标记用户当前浏览的文件夹层级,默认值为根文件夹ID,用于数据库查询条件拼接 */ + private long mCurrentFolderId; + /** 内容解析器:应用与ContentProvider的核心交互工具,所有数据库操作均通过该类执行 */ + private ContentResolver mContentResolver; + /** 多选模式回调处理器:封装多选模式的创建、菜单适配、选中状态变更、批量操作逻辑,是多选功能的核心 */ + private ModeCallback mModeCallBack; + /** 日志输出标签:页面所有日志的统一标识,便于日志过滤与问题定位 */ + private static final String TAG = "NotesListActivity"; + /** 列表滚动速率常量:自定义列表滚动行为的速率参数,优化滚动流畅度 */ + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + /** 当前聚焦的数据项:记录长按选中的便签/文件夹数据,用于多选模式初始化、文件夹上下文菜单操作 */ + private NoteItemData mFocusNoteDataItem; + + // ===================== 数据库查询条件常量 - 区分根文件夹与普通文件夹的查询逻辑 ===================== + /** 普通文件夹查询条件:精准查询指定父文件夹下的所有子项,适用于子文件夹层级的数据加载 */ private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; - // 根文件夹查询条件:排除系统文件夹 + 包含有内容的通话记录文件夹 + /** 根文件夹查询条件:特殊复合条件,排除系统文件夹 + 仅展示有内容的通话记录文件夹,保证根目录视图整洁 */ private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)"; - // ===================== Activity请求码:区分不同场景的返回结果 ===================== - private final static int REQUEST_CODE_OPEN_NODE = 102; // 打开已有便签的请求码 - private final static int REQUEST_CODE_NEW_NODE = 103; // 新建便签的请求码 + // ===================== Activity跳转请求码 - 区分不同场景的页面跳转与结果回调 ===================== + /** 请求码:打开已有便签编辑的跳转标识,用于onActivityResult区分回调场景 */ + private final static int REQUEST_CODE_OPEN_NODE = 102; + /** 请求码:新建便签的跳转标识,用于onActivityResult区分回调场景 */ + private final static int REQUEST_CODE_NEW_NODE = 103; /** - * 生命周期-创建:初始化资源、加载布局、执行首次使用引导逻辑 + * Activity生命周期 - 页面创建阶段【核心初始化】 + * 执行时机:页面首次启动时调用,仅执行一次 + * 核心逻辑:加载页面布局、初始化所有核心资源与控件、绑定适配器与监听器、执行首次使用引导逻辑,为页面就绪做准备 + * @param savedInstanceState 页面重建时的状态数据,当前页面未使用该参数 */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.note_list); // 加载列表布局 - initResources(); // 初始化控件、适配器、查询处理器等资源 - - /** - * 首次使用引导:首次打开应用时,从raw资源读取引导文本,创建引导便签 - */ - setAppInfoFromRawRes(); + setContentView(R.layout.note_list); // 加载页面核心布局 + initResources(); // 初始化所有控件、适配器、监听器、状态变量,核心初始化方法 + setAppInfoFromRawRes(); // 执行首次使用引导,创建引导便签(仅首次打开应用触发) } /** - * 生命周期-Activity返回结果处理:处理从NoteEditActivity返回的结果 - * @param requestCode 请求码(区分打开/新建便签) - * @param resultCode 结果码(RESULT_OK表示操作成功) - * @param data 返回的Intent数据 + * Activity生命周期 - 页面返回结果回调 + * 执行时机:从{NoteEditActivity}编辑/新建便签返回当前页面时触发 + * 核心逻辑:判断返回结果为成功时,清空列表适配器的Cursor数据,触发重新查询,保证列表数据与最新编辑结果一致 + * @param requestCode 页面跳转时传入的请求码,区分新建/打开便签场景 + * @param resultCode 目标页面返回的结果码,RESULT_OK表示操作成功 + * @param data 目标页面返回的Intent数据,当前页面未使用该参数 */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - // 打开/新建便签返回且操作成功:清空列表游标,触发重新查询(刷新列表) + // 新建/打开便签操作成功返回,刷新列表数据 if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { mNotesListAdapter.changeCursor(null); @@ -167,20 +213,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 首次使用引导:从raw资源读取引导文本,创建引导便签(仅首次打开应用时执行) - * 设计意图:降低用户使用门槛,展示应用核心功能 + * 核心初始化引导逻辑 - 首次使用应用创建引导便签 + * 设计意图:降低新用户使用门槛,自动展示应用核心功能与使用说明,提升用户体验 + * 核心规则:通过SharedPreference持久化标记,仅在应用首次打开时执行一次,避免重复创建引导便签 + * 实现逻辑:读取raw目录下的引导文本文件 → 封装为WorkingNote数据模型 → 保存到数据库 → 标记已创建引导 */ private void setAppInfoFromRawRes() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - // 已展示过引导,直接返回 + // 已创建过引导便签,直接返回,避免重复执行 if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { StringBuilder sb = new StringBuilder(); InputStream in = null; try { - // 打开raw目录下的introduction.txt资源(引导文本) - in = getResources().openRawResource(R.raw.introduction); + // 打开raw资源目录下的引导文本文件,该文件存储应用使用说明 + in = getResources().openRawResource(R.raw.introduction); if (in != null) { - // 读取引导文本内容 + // 流式读取文本内容,拼接为完整的引导文案 InputStreamReader isr = new InputStreamReader(in); BufferedReader br = new BufferedReader(isr); char [] buf = new char[1024]; @@ -196,7 +244,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt e.printStackTrace(); return; } finally { - // 关闭输入流,避免资源泄漏 + // 关闭输入流,释放系统资源,避免内存泄漏 if(in != null) { try { in.close(); @@ -206,12 +254,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } - // 创建引导便签:根文件夹、红色背景、无小组件关联 + // 创建空的工作便签模型:归属根文件夹、无小组件关联、红色背景(醒目) WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, ResourceParser.RED); - note.setWorkingText(sb.toString()); // 设置引导文本 - // 保存引导便签成功后,标记已展示引导(持久化到偏好设置) + note.setWorkingText(sb.toString()); // 为便签赋值引导文本内容 + // 保存便签到数据库成功后,标记已创建引导,持久化到偏好设置 if (note.saveNote()) { sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); } else { @@ -222,78 +270,81 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 生命周期-启动:启动异步查询,加载当前文件夹下的便签列表 + * Activity生命周期 - 页面启动阶段 + * 执行时机:页面创建完成后、用户可见前调用,每次页面恢复可见时都会执行 + * 核心逻辑:触发异步数据库查询,加载当前文件夹下的便签/文件夹列表数据,保证页面展示最新数据 */ @Override protected void onStart() { super.onStart(); - startAsyncNotesListQuery(); + startAsyncNotesListQuery(); // 启动异步查询,加载列表核心数据 } /** - * 初始化资源:绑定UI控件、初始化适配器/查询处理器、设置监听器、初始化状态 - * 设计意图:集中管理资源初始化,避免分散在多个生命周期方法中 + * 页面核心初始化方法 - 集中管理所有资源与控件的初始化 + * 设计意图:将所有初始化逻辑集中到该方法,避免分散在多个生命周期方法中,便于维护与扩展 + * 核心逻辑:获取内容解析器、初始化异步查询处理器、绑定核心控件、设置列表适配器与监听器、初始化状态变量 */ private void initResources() { - mContentResolver = this.getContentResolver(); // 获取内容解析器 - // 初始化异步查询处理器(用于数据库异步查询) - mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); - mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 默认选中根文件夹 - mNotesListView = (ListView) findViewById(R.id.notes_list); // 绑定列表控件 + mContentResolver = this.getContentResolver(); // 获取应用的内容解析器,用于数据库操作 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); // 初始化异步查询处理器 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 默认选中根文件夹,展示根目录数据 + mNotesListView = (ListView) findViewById(R.id.notes_list); // 绑定核心列表控件 - // 为列表添加底部视图(footer),避免列表为空时UI塌陷 + // 为列表添加底部空白视图,避免列表数据为空时ListView塌陷,保证页面布局完整性 mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); - // 设置列表项点击监听器(打开便签/进入文件夹) - mNotesListView.setOnItemClickListener(new OnListItemClickListener()); - // 设置列表项长按监听器(触发多选模式) - mNotesListView.setOnItemLongClickListener(this); - // 初始化列表适配器 - mNotesListAdapter = new NotesListAdapter(this); - mNotesListView.setAdapter(mNotesListAdapter); // 绑定适配器到列表 - - // 绑定新建便签按钮,设置点击/触摸监听器 + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); // 设置列表项点击监听器 + mNotesListView.setOnItemLongClickListener(this); // 设置列表项长按监听器 + + mNotesListAdapter = new NotesListAdapter(this); // 初始化列表核心适配器 + mNotesListView.setAdapter(mNotesListAdapter); // 将适配器绑定到列表控件 + + // 绑定新建便签按钮,设置点击与触摸监听器 mAddNewNote = (Button) findViewById(R.id.btn_new_note); - mAddNewNote.setOnClickListener(this); // 点击:新建便签 - mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 触摸:处理透明区域事件透传 + mAddNewNote.setOnClickListener(this); // 点击事件:创建新便签 + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 触摸事件:处理透明区域事件透传 - // 初始化事件分发相关变量 + // 初始化事件分发相关变量,默认关闭事件透传 mDispatch = false; mDispatchY = 0; mOriginY = 0; - // 绑定标题栏控件 - mTitleBar = (TextView) findViewById(R.id.tv_title_bar); - // 初始化列表状态为普通便签列表 - mState = ListEditState.NOTE_LIST; - // 初始化多选模式回调 - mModeCallBack = new ModeCallback(); + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); // 绑定标题栏控件 + mState = ListEditState.NOTE_LIST; // 初始化页面状态为根目录普通便签列表 + mModeCallBack = new ModeCallback(); // 初始化多选模式回调处理器 } /** - * 列表多选模式回调:处理便签批量操作(删除/移动)、多选UI状态管理 - * 实现MultiChoiceModeListener:监听多选模式的创建/准备/销毁/项选中状态变化 - * 实现OnMenuItemClickListener:监听批量操作菜单点击 + * 内部核心回调类 - 列表多选模式完整处理器 + * 实现{ListView.MultiChoiceModeListener}:监听多选模式的创建、准备、销毁、选中状态变更; + * 实现{OnMenuItemClickListener}:监听多选菜单的点击事件,处理批量删除/移动操作; + * 核心职责:封装多选模式的所有逻辑,包括菜单加载、选中状态维护、全选/取消全选、批量操作执行、视图状态切换, + * 是页面批量操作功能的核心实现类,与列表适配器联动完成所有多选相关逻辑。 */ private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { - private DropdownMenu mDropDownMenu; // 多选模式下的下拉菜单(全选/取消全选) - private ActionMode mActionMode; // 多选模式的ActionMode(顶部操作栏) - private MenuItem mMoveMenu; // 移动便签菜单项(根据场景控制显隐) + /** 多选模式顶部下拉菜单:封装全选/取消全选功能,展示当前选中数量,提升交互便捷性 */ + private DropdownMenu mDropDownMenu; + /** 系统多选模式ActionMode:控制顶部操作栏的创建与销毁,是多选模式的核心载体 */ + private ActionMode mActionMode; + /** 批量移动菜单项:根据当前场景控制显隐,通话记录文件夹下/无用户文件夹时隐藏该菜单 */ + private MenuItem mMoveMenu; /** - * 创建多选模式:初始化菜单、设置菜单点击监听、适配移动菜单显隐、创建自定义下拉菜单 - * @param mode 多选模式的ActionMode - * @param menu 要初始化的菜单 - * @return true-创建成功 + * 多选模式创建回调 - 多选功能初始化入口 + * 执行时机:用户长按便签触发多选模式时调用,仅执行一次 + * 核心逻辑:加载多选操作菜单、设置菜单点击监听、适配移动菜单显隐、创建自定义顶部视图、初始化下拉菜单、 + * 切换适配器多选状态、隐藏新建按钮,完成多选模式的所有初始化配置 + * @param mode 当前创建的多选模式ActionMode对象 + * @param menu 多选模式的操作菜单对象 + * @return boolean true表示创建成功,展示多选菜单 */ public boolean onCreateActionMode(ActionMode mode, Menu menu) { - // 加载批量操作菜单(删除/移动) + // 加载多选模式的核心菜单资源,包含删除、移动两个核心操作 getMenuInflater().inflate(R.menu.note_list_options, menu); - // 设置删除菜单点击监听 - menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 设置删除菜单点击监听 mMoveMenu = menu.findItem(R.id.move); - // 控制移动菜单显隐: - // 1. 通话记录文件夹下的便签不可移动;2. 无用户文件夹时不可移动 + // 移动菜单显隐规则:通话记录文件夹下的便签不可移动 + 无用户文件夹时无需移动,两种场景均隐藏 if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER || DataUtils.getUserFolderCount(mContentResolver) == 0) { mMoveMenu.setVisible(false); @@ -303,24 +354,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } mActionMode = mode; - mNotesListAdapter.setChoiceMode(true); // 适配器切换到多选模式 - mNotesListView.setLongClickable(false); // 多选模式下禁用长按(避免重复触发) - mAddNewNote.setVisibility(View.GONE); // 隐藏新建便签按钮(避免干扰) + mNotesListAdapter.setChoiceMode(true); // 通知适配器切换到多选模式,展示勾选框 + mNotesListView.setLongClickable(false); // 多选模式下禁用长按,避免重复触发多选逻辑 + mAddNewNote.setVisibility(View.GONE); // 隐藏新建按钮,避免干扰多选操作 - // 创建自定义多选顶部视图(包含全选/取消全选下拉菜单) + // 加载自定义多选模式顶部视图,包含全选/取消全选的下拉菜单,提升交互体验 View customView = LayoutInflater.from(NotesListActivity.this).inflate( R.layout.note_list_dropdown_menu, null); mode.setCustomView(customView); - // 初始化下拉菜单(绑定按钮与菜单资源) + // 初始化下拉菜单,绑定按钮与菜单资源 mDropDownMenu = new DropdownMenu(NotesListActivity.this, (Button) customView.findViewById(R.id.selection_menu), R.menu.note_list_dropdown); - // 设置下拉菜单点击监听(全选/取消全选) + // 设置下拉菜单点击监听,处理全选/取消全选逻辑 mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ public boolean onMenuItemClick(MenuItem item) { - // 切换全选状态:已全选→取消全选,未全选→全选 + // 切换全选状态:已全选则取消全选,未全选则执行全选 mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); - updateMenu(); // 更新菜单标题(显示选中数量) + updateMenu(); // 同步更新菜单标题与按钮文本 return true; } }); @@ -328,76 +379,88 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 更新多选菜单:同步选中数量到下拉菜单标题,更新全选/取消全选按钮文本 + * 多选菜单更新方法 - 同步选中状态与菜单展示内容 + * 核心逻辑:获取当前选中的便签数量,更新下拉菜单标题为「已选择 X 项」;根据全选状态,切换按钮文本为「全选」/「取消全选」, + * 保证菜单展示内容与实际选中状态一致,提升交互准确性。 */ private void updateMenu() { int selectedCount = mNotesListAdapter.getSelectedCount(); - // 更新下拉菜单标题(如“已选择 3 项”) + // 格式化选中数量文本,更新下拉菜单标题 String format = getResources().getString(R.string.menu_select_title, selectedCount); mDropDownMenu.setTitle(format); - // 更新全选/取消全选按钮文本 + // 根据全选状态切换按钮文本 MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); if (item != null) { if (mNotesListAdapter.isAllSelected()) { item.setChecked(true); - item.setTitle(R.string.menu_deselect_all); // 已全选→显示“取消全选” + item.setTitle(R.string.menu_deselect_all); // 已全选 → 显示取消全选 } else { item.setChecked(false); - item.setTitle(R.string.menu_select_all); // 未全选→显示“全选” + item.setTitle(R.string.menu_select_all); // 未全选 → 显示全选 } } } /** - * 准备多选模式:预留扩展接口(当前无逻辑) + * 多选模式准备回调 - 预留扩展接口 + * 执行时机:多选模式创建后、每次菜单刷新前调用,当前页面无额外逻辑,返回false即可 */ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } /** - * 多选模式菜单项点击:预留扩展接口(当前无逻辑,实际点击监听在onMenuItemClick) + * 多选菜单项点击回调 - 预留扩展接口 + * 注:实际的菜单点击逻辑在{onMenuItemClickListener}中实现,当前方法仅返回false */ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { return false; } /** - * 销毁多选模式:恢复列表默认状态、显示新建便签按钮 + * 多选模式销毁回调 - 恢复页面默认状态 + * 执行时机:用户退出多选模式(点击返回键/完成按钮)时调用 + * 核心逻辑:通知适配器退出多选模式、恢复列表项长按功能、重新显示新建便签按钮,将页面恢复到普通浏览状态 */ public void onDestroyActionMode(ActionMode mode) { - mNotesListAdapter.setChoiceMode(false); // 适配器退出多选模式 - mNotesListView.setLongClickable(true); // 恢复列表项长按 - mAddNewNote.setVisibility(View.VISIBLE); // 显示新建便签按钮 + mNotesListAdapter.setChoiceMode(false); // 适配器退出多选模式,隐藏勾选框 + mNotesListView.setLongClickable(true); // 恢复列表项长按功能,允许再次触发多选 + mAddNewNote.setVisibility(View.VISIBLE); // 重新显示新建便签按钮 } /** - * 结束多选模式:主动关闭ActionMode + * 主动结束多选模式方法 - 对外提供的退出接口 + * 核心作用:批量操作完成后(删除/移动),主动关闭多选模式,恢复页面默认状态,提升交互连贯性 */ public void finishActionMode() { mActionMode.finish(); } /** - * 列表项选中状态变化:同步适配器的选中状态,更新菜单 - * @param mode 多选模式ActionMode - * @param position 列表项位置 - * @param id 列表项ID - * @param checked 是否选中 + * 列表项选中状态变更回调 - 多选模式核心状态同步 + * 执行时机:用户勾选/取消勾选列表项时调用 + * 核心逻辑:将选中状态同步到列表适配器,更新适配器的选中状态映射,然后刷新菜单展示内容, + * 保证视图展示的选中状态与实际数据一致。 + * @param mode 当前的多选模式ActionMode对象 + * @param position 发生状态变更的列表项位置 + * @param id 列表项对应的便签ID + * @param checked 变更后的选中状态:true=选中,false=取消选中 */ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { - mNotesListAdapter.setCheckedItem(position, checked); // 更新适配器选中状态 - updateMenu(); // 同步更新菜单标题 + mNotesListAdapter.setCheckedItem(position, checked); // 同步选中状态到适配器 + updateMenu(); // 刷新菜单标题与按钮文本 } /** - * 多选模式菜单点击处理:处理删除/移动操作 - * @param item 被点击的菜单项 - * @return true-事件已处理 + * 多选菜单点击事件处理 - 批量操作核心执行逻辑 + * 核心职责:处理「删除」「移动」两个核心批量操作,包含前置校验、用户确认、业务逻辑执行, + * 是多选模式的核心功能出口。 + * @param item 被点击的菜单项对象 + * @return boolean true表示事件已处理完成 */ public boolean onMenuItemClick(MenuItem item) { - // 无选中项时,提示用户选择便签 + // 前置校验:无选中项时,弹出Toast提示用户选择便签,避免无效操作 if (mNotesListAdapter.getSelectedCount() == 0) { Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), Toast.LENGTH_SHORT).show(); @@ -406,24 +469,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt switch (item.getItemId()) { case R.id.delete: - // 批量删除:弹窗确认,确认后执行批量删除逻辑 + // 批量删除:弹出确认对话框,提示删除数量,用户确认后执行删除逻辑,防止误删 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(getString(R.string.alert_title_delete)); - builder.setIcon(android.R.drawable.ic_dialog_alert); // 警告图标 + builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(getString(R.string.alert_message_delete_notes, - mNotesListAdapter.getSelectedCount())); // 提示删除数量 + mNotesListAdapter.getSelectedCount())); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - batchDelete(); // 执行批量删除 + batchDelete(); // 执行异步批量删除逻辑 } }); - builder.setNegativeButton(android.R.string.cancel, null); // 取消无操作 + builder.setNegativeButton(android.R.string.cancel, null); builder.show(); break; case R.id.move: - // 批量移动:查询目标文件夹列表,展示文件夹选择菜单 + // 批量移动:触发目标文件夹查询,展示文件夹选择对话框,用户选择后执行移动逻辑 startQueryDestinationFolders(); break; default: @@ -434,50 +497,51 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 新建便签按钮触摸事件监听器:处理按钮透明区域的事件透传 - * 设计意图:新建按钮有透明区域,点击透明区域时,事件需透传到下方的列表控件(滚动/点击列表项) + * 内部触摸事件监听器 - 新建按钮透明区域事件透传处理器 + * 核心设计意图:新建便签按钮为悬浮半透明样式,包含大量透明区域,用户点击透明区域时,希望事件能透传到下方的ListView, + * 支持列表滚动/点击列表项,提升交互体验,解决「透明区域点击无响应」的问题。 + * 核心逻辑:根据触摸坐标判断是否点击了透明区域 → 若是则校准坐标并将事件分发给ListView → 否则交由按钮默认处理, + * 完美兼容按钮点击与列表交互。 */ private class NewNoteOnTouchListener implements OnTouchListener { public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { - // 获取屏幕/按钮尺寸,计算事件坐标 + // 获取屏幕与按钮尺寸,计算触摸事件的绝对坐标,用于透明区域判断 Display display = getWindowManager().getDefaultDisplay(); int screenHeight = display.getHeight(); int newNoteViewHeight = mAddNewNote.getHeight(); int start = screenHeight - newNoteViewHeight; int eventY = start + (int) event.getY(); - // 子文件夹状态下,扣除标题栏高度(坐标校准) + // 子文件夹状态下,扣除标题栏高度,校准触摸坐标,保证判断准确性 if (mState == ListEditState.SUB_FOLDER) { eventY -= mTitleBar.getHeight(); start -= mTitleBar.getHeight(); } /** - * 核心逻辑:判断是否点击了新建按钮的透明区域 - * 透明区域公式:y=-0.12x+94(像素),基于按钮左侧坐标 - * (UI设计要求,若按钮背景变更,公式需同步调整) + * 核心透明区域判断逻辑:基于UI设计的像素公式 y=-0.12x+94,判断触摸点是否在按钮的透明区域内 + * 该公式由UI设计稿确定,若按钮背景样式变更,需同步调整该公式 */ if (event.getY() < (event.getX() * (-0.12) + 94)) { - // 获取列表最后一个可见项(排除footer) + // 获取列表最后一个可见项(排除底部footer),判断是否在透明区域范围内 View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - mNotesListView.getFooterViewsCount()); - // 列表项在透明区域范围内:触发事件分发 if (view != null && view.getBottom() > start && (view.getTop() < (start + 94))) { - mOriginY = (int) event.getY(); // 记录原始Y坐标 - mDispatchY = eventY; // 初始化分发Y坐标 + mOriginY = (int) event.getY(); // 记录原始触摸坐标 + mDispatchY = eventY; // 初始化分发坐标 event.setLocation(event.getX(), mDispatchY); // 校准事件坐标 - mDispatch = true; // 标记为分发状态 - return mNotesListView.dispatchTouchEvent(event); // 分发事件到列表 + mDispatch = true; // 标记为需要分发事件 + return mNotesListView.dispatchTouchEvent(event); // 将事件分发给ListView } } break; } case MotionEvent.ACTION_MOVE: { - // 分发状态下:同步移动坐标,继续分发事件到列表 + // 事件分发状态下,同步移动坐标,继续将触摸事件分发给ListView,支持列表滑动 if (mDispatch) { mDispatchY += (int) event.getY() - mOriginY; event.setLocation(event.getX(), mDispatchY); @@ -486,7 +550,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt break; } default: { - // 触摸结束:分发最后一次事件,重置分发标记 + // 触摸事件结束(抬起/取消),分发最后一次事件,重置分发标记,恢复默认状态 if (mDispatch) { event.setLocation(event.getX(), mDispatchY); mDispatch = false; @@ -495,25 +559,20 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt break; } } - return false; // 非透明区域:不分发,交由按钮默认处理 + return false; // 非透明区域点击,交由按钮默认处理(触发新建便签) } }; /** - * 启动异步查询:查询当前文件夹下的便签列表(区分根文件夹/普通文件夹查询条件) - * 设计意图:异步查询避免主线程阻塞,提升UI流畅度 + * 核心异步查询方法 - 加载当前文件夹下的便签/文件夹列表 + * 设计意图:所有列表数据均通过该方法异步查询,避免主线程阻塞,保证页面滑动流畅,是页面数据加载的核心入口 + * 核心逻辑:根据当前文件夹是否为根目录,选择对应的查询条件 → 调用异步查询处理器执行查询 → 查询结果在回调中更新到适配器 */ private void startAsyncNotesListQuery() { - // 选择查询条件:根文件夹使用特殊条件,普通文件夹使用默认条件 + // 拼接查询条件:根目录使用特殊复合条件,普通文件夹使用精准匹配条件 String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; - // 启动异步查询: - // Token:FOLDER_NOTE_LIST_QUERY_TOKEN(标识为便签列表查询) - // Uri:Notes.CONTENT_NOTE_URI(便签内容URI) - // Projection:NoteItemData.PROJECTION(查询字段) - // Selection:上述查询条件 - // SelectionArgs:当前文件夹ID - // SortOrder:按类型降序、修改时间降序(系统文件夹/便签区分,最新修改在前) + // 启动异步数据库查询:指定查询标识、查询URI、查询字段、查询条件、条件参数、排序规则 mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { String.valueOf(mCurrentFolderId) @@ -521,8 +580,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 异步查询处理器:处理数据库异步查询的结果回调 - * 继承AsyncQueryHandler:封装异步查询逻辑,简化主线程与子线程通信 + * 内部核心异步处理器 - 数据库异步操作完整封装 + * 继承安卓原生{AsyncQueryHandler},核心职责:封装数据库的异步查询、插入、更新、删除操作, + * 将耗时的数据库操作放到子线程执行,查询结果通过回调返回主线程,避免主线程阻塞,是页面流畅性的核心保障。 + * 所有数据库操作均通过该类执行,包含列表数据查询、文件夹列表查询。 */ private final class BackgroundQueryHandler extends AsyncQueryHandler { public BackgroundQueryHandler(ContentResolver contentResolver) { @@ -530,20 +591,21 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 查询完成回调:根据Token区分查询类型,处理查询结果 - * @param token 查询标识(区分便签列表/文件夹列表) - * @param cookie 自定义参数(当前未使用) - * @param cursor 查询结果游标 + * 异步查询完成回调 - 数据库查询结果处理核心方法 + * 执行时机:异步查询任务完成后,由系统自动调用,运行在主线程 + * 核心逻辑:根据查询标识(Token)区分不同的查询任务 → 列表数据查询:更新适配器Cursor → 文件夹列表查询:展示文件夹选择菜单 + * @param token 异步查询任务的标识,区分不同查询类型 + * @param cookie 异步查询的附加参数,当前页面未使用 + * @param cursor 数据库查询结果游标,封装了查询到的所有数据 */ @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { switch (token) { case FOLDER_NOTE_LIST_QUERY_TOKEN: - // 便签列表查询完成:更新适配器游标(刷新列表) - mNotesListAdapter.changeCursor(cursor); + mNotesListAdapter.changeCursor(cursor); // 更新列表适配器数据,刷新视图 break; case FOLDER_LIST_QUERY_TOKEN: - // 文件夹列表查询完成:展示文件夹选择菜单(批量移动便签时) + // 文件夹列表查询完成,展示文件夹选择对话框,供用户选择目标文件夹 if (cursor != null && cursor.getCount() > 0) { showFolderListMenu(cursor); } else { @@ -557,72 +619,69 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 展示文件夹选择菜单:用于批量移动便签时选择目标文件夹 - * @param cursor 文件夹列表游标 + * 文件夹选择对话框展示方法 - 批量移动便签的目标文件夹选择 + * 核心逻辑:将查询到的文件夹列表封装为适配器 → 展示对话框供用户选择 → 用户选择后执行批量移动逻辑 → 提示移动成功 → 退出多选模式 + * @param cursor 数据库查询到的文件夹列表游标,封装了所有可选择的文件夹数据 */ private void showFolderListMenu(Cursor cursor) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(R.string.menu_title_select_folder); // 标题:选择文件夹 - final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); // 文件夹列表适配器 + builder.setTitle(R.string.menu_title_select_folder); // 设置对话框标题 + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); // 初始化文件夹列表适配器 builder.setAdapter(adapter, new DialogInterface.OnClickListener() { - /** - * 选择文件夹回调:执行批量移动操作,提示用户,结束多选模式 - * @param dialog 对话框 - * @param which 选中的文件夹位置 - */ public void onClick(DialogInterface dialog, int which) { - // 批量移动便签到选中的文件夹 + // 执行批量移动逻辑:将选中的便签移至目标文件夹 DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); - // 提示移动成功(显示移动数量和目标文件夹名称) + // 弹出Toast提示,展示移动数量与目标文件夹名称,提升用户感知 Toast.makeText( NotesListActivity.this, getString(R.string.format_move_notes_to_folder, mNotesListAdapter.getSelectedCount(), adapter.getFolderName(NotesListActivity.this, which)), Toast.LENGTH_SHORT).show(); - // 结束多选模式 - mModeCallBack.finishActionMode(); + mModeCallBack.finishActionMode(); // 移动完成,退出多选模式 } }); - builder.show(); // 显示对话框 + builder.show(); // 显示文件夹选择对话框 } /** - * 新建便签:启动NoteEditActivity,标记为新建模式,传递当前文件夹ID - * 设计意图:新建便签默认归属当前文件夹,保持文件夹上下文一致性 + * 新建便签核心方法 - 跳转编辑页创建新便签 + * 核心逻辑:构建跳转Intent,标记为「新建/编辑」模式 → 传递当前文件夹ID,保证新便签归属当前文件夹 → 启动编辑页并等待返回结果 + * 设计意图:新便签默认归属用户当前浏览的文件夹,保持文件夹上下文一致性,提升用户体验 */ private void createNewNote() { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); // 标记为新建/编辑模式 intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); // 传递当前文件夹ID - this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); // 启动Activity,接收返回结果 + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); // 启动编辑页,接收返回结果 } /** - * 批量删除便签:区分同步/非同步模式,执行不同的删除逻辑 - * 同步模式:移到回收站;非同步模式:直接删除;删除后更新关联的桌面小组件 - * 设计意图:异步执行删除操作,避免主线程阻塞;适配同步模式,防止数据丢失 + * 批量删除便签核心方法 - 异步执行差异化删除逻辑 + * 核心设计:通过{AsyncTask}异步执行删除操作,避免主线程阻塞;区分同步/非同步模式,执行差异化删除逻辑,保证数据安全; + * 删除完成后自动更新关联的桌面小组件,保证数据一致性。 + * 核心规则:非同步模式 → 直接物理删除便签;同步模式 → 将便签移至回收站,支持恢复,防止误删导致数据丢失。 */ private void batchDelete() { new AsyncTask>() { /** - * 后台执行:处理批量删除逻辑,获取关联的小组件信息 - * @param unused 无参数 - * @return 关联的小组件属性集合(用于后续更新小组件) + * 后台执行方法 - 耗时删除逻辑处理,运行在子线程 + * 核心逻辑:获取选中便签关联的小组件信息 → 执行差异化删除逻辑 → 返回小组件信息供后续更新 + * @param unused 无传入参数 + * @return HashSet 选中便签关联的所有小组件属性集合 */ protected HashSet doInBackground(Void... unused) { - HashSet widgets = mNotesListAdapter.getSelectedWidget(); // 获取选中项关联的小组件 - // 非同步模式:直接删除便签 + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 非同步模式:直接物理删除选中的便签 if (!isSyncMode()) { if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter .getSelectedItemIds())) { } else { Log.e(TAG, "Delete notes error, should not happens"); } - } - // 同步模式:将便签移到回收站(而非直接删除) - else { + } else { + // 同步模式:将便签移至回收站,而非直接删除,适配同步逻辑,支持数据恢复 if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { Log.e(TAG, "Move notes to trash folder error, should not happens"); @@ -632,54 +691,54 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 主线程回调:更新关联的桌面小组件,结束多选模式 - * @param widgets 关联的小组件属性集合 + * 主线程回调方法 - 删除完成后的UI更新与联动,运行在主线程 + * 核心逻辑:遍历关联的小组件,发送广播更新小组件内容 → 退出多选模式,恢复页面默认状态 + * @param widgets 后台方法返回的小组件属性集合 */ @Override protected void onPostExecute(HashSet widgets) { - // 遍历小组件,更新内容 if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { - updateWidget(widget.widgetId, widget.widgetType); + updateWidget(widget.widgetId, widget.widgetType); // 更新桌面小组件 } } } - // 结束多选模式 - mModeCallBack.finishActionMode(); + mModeCallBack.finishActionMode(); // 退出多选模式 } }.execute(); } - /** - * 删除文件夹:校验文件夹ID有效性,区分同步/非同步模式执行删除逻辑,更新关联的桌面小组件 + /** + * 删除文件夹核心方法 - 适配同步模式的文件夹删除逻辑 + * 核心规则:根文件夹不可删除(系统保护);非同步模式 → 直接删除文件夹及子项;同步模式 → 移至回收站; + * 删除后自动更新关联的桌面小组件,保证数据一致性。 * @param folderId 要删除的文件夹ID */ private void deleteFolder(long folderId) { - // 校验:根文件夹不可删除(异常场景,理论上不会触发) + // 系统保护:根文件夹为核心目录,禁止删除,防止应用数据异常 if (folderId == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Wrong folder id, should not happen " + folderId); return; } - // 构建要删除的文件夹ID集合 + // 构建待删除的文件夹ID集合 HashSet ids = new HashSet(); ids.add(folderId); - // 获取该文件夹下便签关联的桌面小组件(用于后续更新) + // 获取该文件夹下所有便签关联的桌面小组件信息,用于后续更新 HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); - // 非同步模式:直接删除文件夹 + // 非同步模式:直接物理删除文件夹 if (!isSyncMode()) { DataUtils.batchDeleteNotes(mContentResolver, ids); - } - // 同步模式:将文件夹移到回收站(而非直接删除,适配同步逻辑) - else { + } else { + // 同步模式:将文件夹移至回收站,适配同步逻辑 DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); } - // 更新关联的桌面小组件(文件夹删除后,同步更新小组件内容) + // 更新关联的桌面小组件,保证小组件数据与页面一致 if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -691,49 +750,54 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 打开便签:启动NoteEditActivity,标记为查看模式,传递便签ID - * @param data 要打开的便签数据项 + * 打开便签核心方法 - 跳转编辑页查看/编辑已有便签 + * 核心逻辑:构建跳转Intent,标记为「查看」模式 → 传递便签唯一ID → 启动编辑页并等待返回结果, + * 编辑完成后列表会自动刷新,保证数据一致性。 + * @param data 要打开的便签数据模型,包含便签ID等核心信息 */ private void openNode(NoteItemData data) { Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); // 标记为查看模式(区别于新建/编辑) - intent.putExtra(Intent.EXTRA_UID, data.getId()); // 传递便签ID - this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); // 启动Activity,接收返回结果 + intent.setAction(Intent.ACTION_VIEW); // 标记为查看模式,区别于新建便签 + intent.putExtra(Intent.EXTRA_UID, data.getId()); // 传递便签唯一ID + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); // 启动编辑页 } /** - * 打开文件夹:更新当前文件夹ID,重新查询便签列表,切换列表状态,更新标题栏 - * @param data 要打开的文件夹数据项 + * 打开文件夹核心方法 - 进入文件夹层级,加载子项数据 + * 核心逻辑:更新当前文件夹ID → 异步查询该文件夹下的子项 → 切换页面状态 → 适配视图展示(标题栏、新建按钮)→ 更新标题栏文本, + * 完成文件夹层级的切换,是页面层级导航的核心方法。 + * @param data 要打开的文件夹数据模型,包含文件夹ID、名称等核心信息 */ private void openFolder(NoteItemData data) { - mCurrentFolderId = data.getId(); // 更新当前文件夹ID - startAsyncNotesListQuery(); // 重新查询该文件夹下的便签列表 + mCurrentFolderId = data.getId(); // 更新当前文件夹ID,标记用户浏览层级 + startAsyncNotesListQuery(); // 异步查询该文件夹下的所有子项 - // 切换列表状态:通话记录文件夹为专属状态,其他为子文件夹状态 + // 切换页面状态:通话记录文件夹为专属状态,其他文件夹为子文件夹状态 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mState = ListEditState.CALL_RECORD_FOLDER; - mAddNewNote.setVisibility(View.GONE); // 通话记录文件夹下隐藏新建便签按钮 + mAddNewNote.setVisibility(View.GONE); // 通话记录文件夹下隐藏新建按钮,禁止创建普通便签 } else { mState = ListEditState.SUB_FOLDER; } - // 更新标题栏文本:通话记录文件夹显示固定名称,其他显示文件夹名称 + // 更新标题栏展示:通话记录文件夹显示固定名称,其他文件夹显示自定义名称 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mTitleBar.setText(R.string.call_record_folder_name); } else { mTitleBar.setText(data.getSnippet()); } - mTitleBar.setVisibility(View.VISIBLE); // 显示标题栏(根文件夹下隐藏) + mTitleBar.setVisibility(View.VISIBLE); // 显示标题栏,展示当前文件夹名称 } /** - * 点击事件处理:仅处理新建便签按钮的点击 - * @param v 被点击的视图 + * 页面点击事件统一处理 - 实现{OnClickListener}接口 + * 核心职责:处理页面所有控件的点击事件,当前仅处理新建便签按钮的点击,触发新建便签逻辑 + * @param v 被点击的视图控件对象 */ public void onClick(View v) { switch (v.getId()) { case R.id.btn_new_note: - createNewNote(); // 点击新建按钮→创建新便签 + createNewNote(); // 点击新建按钮,创建新便签 break; default: break; @@ -741,19 +805,20 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 显示软键盘:强制弹出软键盘(用于文件夹名称编辑) + * 软键盘强制显示方法 - 用于文件夹名称编辑场景 + * 核心逻辑:通过系统输入法服务,强制弹出软键盘,无需用户手动点击输入框,提升交互便捷性,适用于对话框打开后自动聚焦输入的场景。 */ private void showSoftInput() { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { - // SHOW_FORCED:强制显示软键盘,0:无额外标志 inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } } /** - * 隐藏软键盘:根据指定视图的WindowToken隐藏软键盘 - * @param view 用于获取WindowToken的视图(通常是输入框) + * 软键盘隐藏方法 - 适配指定视图的软键盘关闭 + * 核心逻辑:通过视图的WindowToken,精准关闭当前页面的软键盘,避免影响其他应用,适用于输入完成后自动关闭软键盘的场景。 + * @param view 用于获取WindowToken的视图,通常为输入框控件 */ private void hideSoftInput(View view) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); @@ -761,17 +826,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 显示创建/修改文件夹对话框:处理文件夹名称输入、校验唯一性、执行创建/修改逻辑 - * @param create true-创建新文件夹,false-修改已有文件夹名称 + * 文件夹创建/重命名对话框展示方法 - 核心文件夹编辑逻辑 + * 核心职责:统一处理文件夹的创建与重命名,包含输入框初始化、名称唯一性校验、数据库操作、软键盘控制, + * 是文件夹管理的核心交互入口。 + * @param create boolean true=创建新文件夹,false=修改已有文件夹名称 */ private void showCreateOrModifyFolderDialog(final boolean create) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); - // 加载对话框布局(包含输入框) + // 加载对话框布局,包含输入框用于输入文件夹名称 View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); - showSoftInput(); // 弹出软键盘,便于输入 + showSoftInput(); // 打开对话框后,强制弹出软键盘,提升输入便捷性 - // 修改文件夹:初始化输入框为当前文件夹名称,设置标题为“重命名文件夹” + // 重命名文件夹场景:初始化输入框为当前文件夹名称,设置对话框标题为「重命名文件夹」 if (!create) { if (mFocusNoteDataItem != null) { etName.setText(mFocusNoteDataItem.getSnippet()); @@ -780,69 +847,67 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt Log.e(TAG, "The long click data item is null"); return; } - } - // 创建文件夹:清空输入框,设置标题为“创建文件夹” - else { + } else { + // 创建文件夹场景:清空输入框,设置对话框标题为「创建文件夹」 etName.setText(""); builder.setTitle(this.getString(R.string.menu_create_folder)); } - // 设置对话框按钮:确定(先不绑定点击,后续自定义)、取消(隐藏软键盘) + // 设置对话框按钮:确定按钮先不绑定点击事件(自定义逻辑),取消按钮点击时隐藏软键盘 builder.setPositiveButton(android.R.string.ok, null); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - hideSoftInput(etName); // 取消时隐藏软键盘 + hideSoftInput(etName); // 取消操作,隐藏软键盘 } }); - // 显示对话框,自定义确定按钮的点击逻辑(避免默认点击关闭对话框) + // 显示对话框,并自定义确定按钮的点击逻辑,避免默认点击直接关闭对话框 final Dialog dialog = builder.setView(view).show(); final Button positive = (Button)dialog.findViewById(android.R.id.button1); positive.setOnClickListener(new OnClickListener() { public void onClick(View v) { - hideSoftInput(etName); // 隐藏软键盘 + hideSoftInput(etName); // 输入完成,隐藏软键盘 String name = etName.getText().toString(); - // 校验文件夹名称唯一性:已存在则提示,聚焦输入框 + // 核心校验:文件夹名称唯一性校验,已存在则提示用户并选中输入框文本,避免重复创建同名文件夹 if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), Toast.LENGTH_LONG).show(); - etName.setSelection(0, etName.length()); // 选中输入框文本,便于修改 + etName.setSelection(0, etName.length()); // 选中输入框文本,便于用户修改 return; } - // 修改文件夹名称:非空则更新数据库 + // 重命名文件夹逻辑:名称非空时,更新数据库中该文件夹的名称与本地修改标记 if (!create) { if (!TextUtils.isEmpty(name)) { ContentValues values = new ContentValues(); - values.put(NoteColumns.SNIPPET, name); // 文件夹名称 - values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 类型为文件夹 - values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地已修改(适配同步) - // 更新指定ID的文件夹 + values.put(NoteColumns.SNIPPET, name); // 更新文件夹名称 + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 标记为文件夹类型 + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记本地修改,适配同步逻辑 + // 执行数据库更新操作,精准更新指定ID的文件夹 mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] { String.valueOf(mFocusNoteDataItem.getId()) }); } - } - // 创建新文件夹:非空则插入数据库 - else if (!TextUtils.isEmpty(name)) { + } else if (!TextUtils.isEmpty(name)) { + // 创建文件夹逻辑:名称非空时,插入新文件夹到数据库,归属根目录 ContentValues values = new ContentValues(); - values.put(NoteColumns.SNIPPET, name); // 文件夹名称 - values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 类型为文件夹 + values.put(NoteColumns.SNIPPET, name); // 设置文件夹名称 + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 标记为文件夹类型 mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); // 插入新文件夹 } - dialog.dismiss(); // 关闭对话框 + dialog.dismiss(); // 操作完成,关闭对话框 } }); - // 初始化确定按钮状态:输入框为空时禁用 + // 初始化确定按钮状态:输入框为空时禁用,避免创建空名称的文件夹 if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } /** - * 输入框文本变化监听:为空时禁用确定按钮,非空时启用 - * 设计意图:避免创建/修改空名称的文件夹 + * 输入框文本变化监听:实时校验输入内容,为空时禁用确定按钮,非空时启用, + * 从交互层面避免用户输入空名称,提升数据有效性。 */ etName.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @@ -860,21 +925,23 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 返回键事件处理:根据当前列表状态,执行不同的返回逻辑 - * 设计意图:子文件夹/通话记录文件夹返回根文件夹,普通列表执行默认返回 + * 返回键事件重写 - 适配文件夹层级的返回逻辑 + * 核心设计意图:用户点击返回键时,根据当前页面状态执行差异化逻辑,子文件夹返回根目录,根目录退出应用, + * 实现「层级导航」的交互逻辑,符合用户使用习惯。 + * 核心规则:子文件夹状态 → 返回根目录;通话记录文件夹 → 返回根目录并恢复新建按钮;根目录 → 执行系统默认返回逻辑。 */ @Override public void onBackPressed() { switch (mState) { case SUB_FOLDER: - // 子文件夹状态:返回根文件夹,重置状态,重新查询列表,隐藏标题栏 + // 子文件夹状态:返回根目录,重置状态,刷新列表,隐藏标题栏 mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; startAsyncNotesListQuery(); mTitleBar.setVisibility(View.GONE); break; case CALL_RECORD_FOLDER: - // 通话记录文件夹状态:返回根文件夹,重置状态,显示新建按钮,隐藏标题栏,重新查询列表 + // 通话记录文件夹状态:返回根目录,重置状态,恢复新建按钮,隐藏标题栏,刷新列表 mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; mAddNewNote.setVisibility(View.VISIBLE); @@ -882,7 +949,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt startAsyncNotesListQuery(); break; case NOTE_LIST: - // 普通便签列表状态:执行系统默认返回逻辑 + // 根目录状态:执行系统默认返回逻辑,退出应用 super.onBackPressed(); break; default: @@ -891,13 +958,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 更新桌面小组件:发送广播通知小组件刷新内容 - * @param appWidgetId 小组件ID - * @param appWidgetType 小组件类型(2x/4x) + * 桌面小组件更新核心方法 - 发送广播同步小组件数据 + * 核心设计意图:便签数据变更后,必须同步更新关联的桌面小组件,保证应用内数据与桌面展示数据一致,是小组件联动的核心方法。 + * 核心逻辑:根据小组件类型,指定对应的广播接收者 → 封装要更新的小组件ID → 发送广播通知小组件刷新内容。 + * @param appWidgetId 要更新的小组件系统唯一标识ID + * @param appWidgetType 小组件类型:2x/4x,对应不同的广播接收者 */ private void updateWidget(int appWidgetId, int appWidgetType) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - // 根据小组件类型,指定对应的广播接收者 + // 根据小组件类型,绑定对应的广播接收者,保证精准更新 if (appWidgetType == Notes.TYPE_WIDGET_2X) { intent.setClass(this, NoteWidgetProvider_2x.class); } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { @@ -907,17 +976,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return; } - // 传递要更新的小组件ID + // 传递要更新的小组件ID数组,支持批量更新 intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { appWidgetId }); - sendBroadcast(intent); // 发送广播,通知小组件更新 - setResult(RESULT_OK, intent); // 设置返回结果,告知上层页面更新成功 + sendBroadcast(intent); // 发送广播,通知小组件执行刷新 + setResult(RESULT_OK, intent); // 设置返回结果,标记更新成功 } /** - * 文件夹上下文菜单创建监听器:长按文件夹时,创建包含“查看/删除/重命名”的上下文菜单 + * 文件夹上下文菜单创建监听器 - 长按文件夹时展示右键菜单 + * 核心职责:创建文件夹的右键菜单,包含「查看」「删除」「重命名」三个核心操作,菜单标题为文件夹名称, + * 是文件夹的核心右键交互入口。 */ private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { @@ -931,8 +1002,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }; /** - * 上下文菜单关闭回调:清空列表的上下文菜单创建监听器,避免内存泄漏 - * @param menu 关闭的菜单 + * 上下文菜单关闭回调 - 内存泄漏防护处理 + * 执行时机:上下文菜单关闭时调用 + * 核心逻辑:清空列表的上下文菜单创建监听器,避免监听器持有页面引用导致内存泄漏,提升应用稳定性。 + * @param menu 被关闭的上下文菜单对象 */ @Override public void onContextMenuClosed(Menu menu) { @@ -943,23 +1016,25 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 上下文菜单项选择处理:处理文件夹的“查看/删除/重命名”操作 - * @param item 被选中的菜单项 - * @return true-事件已处理 + * 上下文菜单项选择处理 - 文件夹右键菜单的核心逻辑执行 + * 核心职责:处理文件夹右键菜单的「查看」「删除」「重命名」操作,包含前置校验、用户确认、业务逻辑执行, + * 是文件夹右键交互的核心出口。 + * @param item 被选中的菜单项对象 + * @return boolean true表示事件已处理完成 */ @Override public boolean onContextItemSelected(MenuItem item) { - // 异常校验:无聚焦的文件夹数据项,直接返回 + // 前置校验:无聚焦的文件夹数据,直接返回,避免空指针异常 if (mFocusNoteDataItem == null) { Log.e(TAG, "The long click data item is null"); return false; } switch (item.getItemId()) { case MENU_FOLDER_VIEW: - openFolder(mFocusNoteDataItem); // 查看文件夹→打开该文件夹 + openFolder(mFocusNoteDataItem); // 查看文件夹 → 进入该文件夹层级 break; case MENU_FOLDER_DELETE: - // 删除文件夹:弹窗确认,确认后执行删除逻辑 + // 删除文件夹:弹出确认对话框,防止误删,用户确认后执行删除逻辑 AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.alert_title_delete)); builder.setIcon(android.R.drawable.ic_dialog_alert); @@ -974,7 +1049,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt builder.show(); break; case MENU_FOLDER_CHANGE_NAME: - showCreateOrModifyFolderDialog(false); // 重命名文件夹→显示修改对话框 + showCreateOrModifyFolderDialog(false); // 重命名文件夹 → 打开修改对话框 break; default: break; @@ -983,23 +1058,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 准备选项菜单:根据当前列表状态加载不同的菜单资源,更新同步按钮文本 - * @param menu 要初始化的菜单 - * @return true-菜单初始化成功 + * 选项菜单准备回调 - 动态加载不同状态的菜单资源 + * 执行时机:页面顶部菜单展示前调用,每次菜单刷新都会执行 + * 核心逻辑:清空原有菜单 → 根据当前页面状态加载对应的菜单资源 → 更新同步按钮的文本(同步/取消同步), + * 保证不同页面状态下展示的菜单功能与当前场景匹配。 + * @param menu 要初始化的选项菜单对象 + * @return boolean true表示菜单初始化成功,展示菜单 */ @Override public boolean onPrepareOptionsMenu(Menu menu) { - menu.clear(); // 清空原有菜单,避免重复加载 - // 根据列表状态加载对应菜单 + menu.clear(); // 清空原有菜单,避免重复加载导致菜单异常 + // 根据页面状态加载对应的菜单资源 if (mState == ListEditState.NOTE_LIST) { - getMenuInflater().inflate(R.menu.note_list, menu); // 普通列表→加载便签列表菜单 - // 更新同步按钮文本:同步中→显示“取消同步”,未同步→显示“同步” + getMenuInflater().inflate(R.menu.note_list, menu); // 根目录加载完整菜单 + // 更新同步按钮文本:同步中显示「取消同步」,未同步显示「同步」 menu.findItem(R.id.menu_sync).setTitle( GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); } else if (mState == ListEditState.SUB_FOLDER) { - getMenuInflater().inflate(R.menu.sub_folder, menu); // 子文件夹→加载子文件夹菜单 + getMenuInflater().inflate(R.menu.sub_folder, menu); // 子文件夹加载精简菜单 } else if (mState == ListEditState.CALL_RECORD_FOLDER) { - getMenuInflater().inflate(R.menu.call_record_folder, menu); // 通话记录文件夹→加载专属菜单 + getMenuInflater().inflate(R.menu.call_record_folder, menu); // 通话记录文件夹加载专属菜单 } else { Log.e(TAG, "Wrong state:" + mState); } @@ -1007,41 +1085,43 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 选项菜单点击处理:处理“新建文件夹/导出文本/同步/设置/新建便签/搜索”操作 - * @param item 被选中的菜单项 - * @return true-事件已处理 + * 选项菜单点击事件处理 - 页面顶部菜单的核心交互逻辑 + * 核心职责:处理顶部菜单的所有点击事件,包含「新建文件夹」「导出文本」「同步」「设置」「新建便签」「搜索」, + * 是页面顶部功能的核心出口。 + * @param item 被选中的菜单项对象 + * @return boolean true表示事件已处理完成 */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_new_folder: - showCreateOrModifyFolderDialog(true); // 新建文件夹→显示创建对话框 + showCreateOrModifyFolderDialog(true); // 新建文件夹 → 打开创建对话框 break; case R.id.menu_export_text: - exportNoteToText(); // 导出文本→执行便签导出逻辑 + exportNoteToText(); // 导出文本 → 执行便签导出逻辑,保存为本地文件 break; case R.id.menu_sync: - // 同步操作:区分同步模式/非同步模式 + // 同步操作:区分同步模式与非同步模式,执行差异化逻辑 if (isSyncMode()) { - // 同步模式:点击“同步”→启动同步,点击“取消同步”→取消同步 + // 同步模式下:点击「同步」启动同步,点击「取消同步」终止同步 if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { GTaskSyncService.startSync(this); } else { GTaskSyncService.cancelSync(this); } } else { - // 非同步模式:跳转到设置页面(配置同步账号) + // 非同步模式下:跳转至设置页面,引导用户配置同步账号 startPreferenceActivity(); } break; case R.id.menu_setting: - startPreferenceActivity(); // 设置→跳转到偏好设置页面 + startPreferenceActivity(); // 设置 → 跳转至应用偏好设置页面 break; case R.id.menu_new_note: - createNewNote(); // 新建便签→执行新建逻辑 + createNewNote(); // 新建便签 → 执行新建逻辑 break; case R.id.menu_search: - onSearchRequested(); // 搜索→触发系统搜索 + onSearchRequested(); // 搜索 → 触发系统全局搜索功能 break; default: break; @@ -1050,117 +1130,116 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 触发搜索:调用系统搜索接口,启动搜索页面 - * @return true-搜索请求已触发 + * 系统搜索触发方法 - 重写原生方法 + * 核心逻辑:调用系统搜索接口,启动应用内搜索页面,支持便签内容的全局搜索,提升数据查找效率。 + * @return boolean true表示搜索请求已成功触发 */ @Override public boolean onSearchRequested() { - // 启动搜索:无初始查询词、不包含应用数据、不强制显示搜索界面 - startSearch(null, false, null /* appData */, false); + startSearch(null, false, null, false); return true; } /** - * 导出便签到文本文件:异步执行导出操作,根据导出结果展示不同的提示弹窗 - * 设计意图:异步执行避免主线程阻塞,适配SD卡状态、系统异常等场景 + * 便签导出核心方法 - 异步导出便签为文本文件 + * 核心设计:通过{AsyncTask}异步执行导出操作,避免主线程阻塞;适配SD卡状态、导出结果,展示对应的提示对话框, + * 支持本地备份,提升数据安全性。 + * 核心逻辑:调用备份工具类执行导出 → 根据导出状态码展示不同提示 → 成功则显示文件路径,失败则提示原因。 */ - private void exportNoteToText() { - final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); - new AsyncTask() { - - /** - * 后台执行:调用BackupUtils执行导出逻辑,返回导出状态码 - */ - @Override - protected Integer doInBackground(Void... unused) { - return backup.exportToText(); - } - - /** - * 主线程回调:根据导出状态码展示对应的提示弹窗 - * @param result 导出状态码(SD卡未挂载/成功/系统异常) - */ - @Override - protected void onPostExecute(Integer result) { - // SD卡未挂载:提示SD卡未挂载 - if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.failed_sdcard_export)); - builder.setMessage(NotesListActivity.this - .getString(R.string.error_sdcard_unmounted)); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } - // 导出成功:提示导出成功,显示文件路径和名称 - else if (result == BackupUtils.STATE_SUCCESS) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.success_sdcard_export)); - builder.setMessage(NotesListActivity.this.getString( - R.string.format_exported_file_location, backup - .getExportedTextFileName(), backup.getExportedTextFileDir())); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } - // 系统异常:提示导出失败 - else if (result == BackupUtils.STATE_SYSTEM_ERROR) { - AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); - builder.setTitle(NotesListActivity.this - .getString(R.string.failed_sdcard_export)); - builder.setMessage(NotesListActivity.this - .getString(R.string.error_sdcard_export)); - builder.setPositiveButton(android.R.string.ok, null); - builder.show(); - } - } - }.execute(); - } + private void exportNoteToText() { + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + new AsyncTask() { + + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); // 后台执行导出逻辑,返回状态码 + } + + @Override + protected void onPostExecute(Integer result) { + // 根据导出状态码,展示对应的提示对话框 + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + // SD卡未挂载,导出失败,提示用户挂载SD卡 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_unmounted)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + // 导出成功,提示用户文件路径与名称 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.success_sdcard_export)); + builder.setMessage(NotesListActivity.this.getString( + R.string.format_exported_file_location, backup + .getExportedTextFileName(), backup.getExportedTextFileDir())); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + // 系统异常,导出失败,提示用户重试 + alertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_export)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + } + }.execute(); + } /** - * 判断是否为同步模式:检测是否配置了同步账号(有账号则为同步模式) - * @return true-同步模式,false-非同步模式 + * 同步模式判断方法 - 核心模式区分依据 + * 核心规则:通过判断是否配置了同步账号,区分同步模式与非同步模式,同步模式下所有删除操作均移至回收站,非同步模式直接物理删除, + * 是页面差异化业务逻辑的核心判断依据。 + * @return boolean true=已配置同步账号(同步模式),false=未配置同步账号(非同步模式) */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } /** - * 启动偏好设置页面:适配嵌套Activity场景,使用父Activity或当前Activity启动 + * 偏好设置页面启动方法 - 适配嵌套Activity场景 + * 核心逻辑:判断当前页面是否有父Activity,有则通过父Activity启动,无则直接启动,适配应用的嵌套页面结构, + * 保证设置页面正常打开。 */ private void startPreferenceActivity() { - Activity from = getParent() != null ? getParent() : this; // 适配嵌套Activity + Activity from = getParent() != null ? getParent() : this; Intent intent = new Intent(from, NotesPreferenceActivity.class); - from.startActivityIfNeeded(intent, -1); // 启动设置页面,-1表示无返回结果 + from.startActivityIfNeeded(intent, -1); } /** - * 列表项点击监听器:区分多选/普通模式,处理便签/文件夹的点击逻辑 - * 核心逻辑:多选模式下切换选中状态,普通模式下打开便签/文件夹 + * 内部列表项点击监听器 - 列表项核心交互逻辑 + * 实现{OnItemClickListener}接口,核心职责:区分多选模式与普通模式,处理列表项的点击事件, + * 多选模式下切换选中状态,普通模式下打开便签/文件夹,是列表项点击交互的核心实现类。 */ private class OnListItemClickListener implements OnItemClickListener { public void onItemClick(AdapterView parent, View view, int position, long id) { - // 仅处理NotesListItem类型的列表项 + // 仅处理自定义的NotesListItem列表项,避免类型错误 if (view instanceof NotesListItem) { NoteItemData item = ((NotesListItem) view).getItemData(); - // 多选模式下:切换便签的选中状态(文件夹不可选) + // 多选模式下的点击逻辑:仅处理普通便签,切换选中状态,文件夹不可选 if (mNotesListAdapter.isInChoiceMode()) { if (item.getType() == Notes.TYPE_NOTE) { - // 校准位置(排除header) + // 校准列表项位置,排除头部视图的影响 position = position - mNotesListView.getHeaderViewsCount(); - // 切换选中状态 + // 切换当前项的选中状态:选中→取消,取消→选中 mModeCallBack.onItemCheckedStateChanged(null, position, id, !mNotesListAdapter.isSelectedItem(position)); } return; } - // 普通模式下:根据当前列表状态,处理不同类型的列表项 + // 普通模式下的点击逻辑:根据当前页面状态,执行差异化操作 switch (mState) { case NOTE_LIST: - // 普通列表:文件夹/系统文件夹→打开文件夹,便签→打开便签 + // 根目录状态:文件夹/系统文件夹→打开文件夹,普通便签→打开便签编辑 if (item.getType() == Notes.TYPE_FOLDER || item.getType() == Notes.TYPE_SYSTEM) { openFolder(item); @@ -1172,7 +1251,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt break; case SUB_FOLDER: case CALL_RECORD_FOLDER: - // 子文件夹/通话记录文件夹:仅处理便签(无文件夹) + // 子文件夹/通话记录文件夹状态:仅处理普通便签,打开编辑,无文件夹层级 if (item.getType() == Notes.TYPE_NOTE) { openNode(item); } else { @@ -1187,20 +1266,18 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 启动目标文件夹查询:查询可移动便签的目标文件夹,构建差异化的查询条件 - * 设计意图:排除回收站、当前文件夹,子文件夹状态下包含根文件夹 + * 目标文件夹查询启动方法 - 批量移动便签的前置查询 + * 核心设计意图:查询所有可用于移动便签的目标文件夹,构建差异化的查询条件,排除无效文件夹,保证移动逻辑的准确性。 + * 核心查询规则:包含所有用户文件夹 → 排除回收站、当前文件夹 → 子文件夹状态下额外包含根文件夹(允许移回根目录)。 */ private void startQueryDestinationFolders() { - // 基础查询条件:类型为文件夹 + 父文件夹不是回收站 + ID不是当前文件夹 + // 基础查询条件:类型为文件夹 + 父文件夹不是回收站 + 文件夹ID不是当前文件夹 String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; - // 子文件夹状态下:额外包含根文件夹(允许移到根文件夹) + // 子文件夹状态下,额外包含根文件夹,允许用户将便签移回根目录 selection = (mState == ListEditState.NOTE_LIST) ? selection: "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; - // 启动异步查询: - // Token:FOLDER_LIST_QUERY_TOKEN(标识为文件夹列表查询) - // SelectionArgs:文件夹类型、回收站ID、当前文件夹ID - // SortOrder:按修改时间降序(最新修改的文件夹在前) + // 启动异步查询,获取目标文件夹列表,用于展示选择对话框 mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, @@ -1215,28 +1292,29 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } /** - * 列表项长按监听:触发便签多选模式或文件夹上下文菜单 - * @param parent 列表控件 - * @param view 被长按的列表项 + * 列表项长按事件处理 - 实现{OnItemLongClickListener}接口,页面核心长按交互逻辑 + * 核心职责:区分长按的是便签还是文件夹,执行差异化逻辑 → 长按便签:触发多选模式,选中当前项并震动反馈; + * 长按文件夹:绑定上下文菜单监听器,展示右键菜单,是页面长按交互的核心入口。 + * @param parent 列表控件对象 + * @param view 被长按的列表项视图 * @param position 列表项位置 - * @param id 列表项ID - * @return false-不消费事件(允许后续处理) + * @param id 列表项对应的便签/文件夹ID + * @return boolean false表示不消费事件,允许后续逻辑执行 */ public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { mFocusNoteDataItem = ((NotesListItem) view).getItemData(); - // 便签且非多选模式:启动多选模式,选中当前项,触发长按震动反馈 + // 长按普通便签且非多选模式:启动多选模式,选中当前项,触发震动反馈,提升交互感知 if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { if (mNotesListView.startActionMode(mModeCallBack) != null) { mModeCallBack.onItemCheckedStateChanged(null, position, id, true); - // 长按震动反馈(提升交互体验) + // 执行长按震动反馈,符合安卓交互规范,提升用户体验 mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } else { Log.e(TAG, "startActionMode fails"); } - } - // 文件夹:设置上下文菜单创建监听器(长按显示菜单) - else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 长按文件夹:绑定上下文菜单创建监听器,准备展示右键菜单 mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); } } diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java index 2f4444a..d8720c9 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java @@ -14,151 +14,170 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,作为便签列表页的核心适配器,绑定Cursor数据到列表项 +// 包声明:小米便签 核心UI模块,该包承载应用所有可视化交互页面及配套适配器,本类为列表页核心数据适配桥梁 package net.micode.notes.ui; -// 导入安卓上下文类:提供应用运行环境 +// -------------------------- 安卓系统核心依赖包 - 上下文/数据库/日志/视图/适配器能力 -------------------------- +// 安卓应用全局上下文:提供资源访问、视图创建、数据解析等基础能力,适配器必备依赖 import android.content.Context; -// 导入安卓数据库游标类:存储便签数据库查询结果 +// 安卓数据库游标核心类:封装数据库查询结果集,承载便签数据表的查询数据,列表展示的核心数据来源 import android.database.Cursor; -// 导入安卓日志类:输出适配器相关日志(异常/调试) +// 安卓系统日志工具类:输出适配器运行的调试/错误日志,便于问题定位与线上排查 import android.util.Log; -// 导入安卓视图相关类:创建/绑定列表项视图 +// 安卓视图体系核心类:视图创建、视图容器的核心父类,适配列表项的创建与挂载 import android.view.View; import android.view.ViewGroup; -// 导入安卓Cursor适配器类:适配Cursor数据的列表适配器基类 +// 安卓游标适配器基类:专为Cursor数据设计的列表适配器,封装数据绑定、视图复用、数据变化监听等核心能力,本类的核心父类 import android.widget.CursorAdapter; -// 导入便签数据常量类:定义便签类型、特殊ID(根文件夹)等常量 +// -------------------------- 小米便签业务层核心依赖 - 数据常量/集合工具 -------------------------- +// 小米便签数据层核心常量类:定义便签/文件夹/小部件的类型、特殊ID、业务状态等全局核心常量 import net.micode.notes.data.Notes; -// 导入集合相关类:管理选中项的状态、统计选中数量 +// Java集合框架相关类:封装选中项状态的存储、遍历、统计,支撑批量选择模式的核心逻辑 import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; /** - * 便签列表页核心适配器类 - * 继承自安卓CursorAdapter,核心职责: - * 1. 将便签数据库的Cursor数据绑定到自定义列表项(NotesListItem); - * 2. 管理列表的选择模式(批量操作):维护选中项状态、全选/取消全选、统计选中数量; - * 3. 提取选中项的核心信息(便签ID、关联的小部件属性); - * 4. 监听数据变化,实时更新普通便签的总数。 + * 小米便签 列表页核心数据适配器 + *

+ * 继承安卓系统{CursorAdapter}游标适配器,隶属于MVC架构的UI层适配桥梁,是{NotesListActivity}与数据库/Cursor数据、{NotesListItem}列表项之间的核心纽带; + * 核心设计定位:封装Cursor数据库数据与列表项视图的绑定逻辑,统一管理列表的展示规则、选择模式、数据变化监听,解耦列表页的业务逻辑与数据渲染逻辑; + * 核心业务职责:创建并复用自定义列表项视图、将Cursor数据映射为业务模型并绑定至列表项、完整支撑批量选择模式的所有操作(单选/全选/取消全选/选中状态维护)、 + * 统计选中项数据与普通便签总数、提取选中项的小部件关联属性、监听数据库数据变化并实时刷新视图; + * 技术实现特点:基于安卓原生CursorAdapter实现视图复用,提升列表滑动性能;通过HashMap维护选中项状态,保证选择操作的高效性; + * 封装数据统计与业务属性提取逻辑,对外提供简洁的调用接口;自动监听数据库数据变化,保证视图与数据的一致性。 + *

*/ public class NotesListAdapter extends CursorAdapter { - // 日志标签:用于适配器相关日志输出,便于问题定位 + /** 日志常量标签:适配器相关日志的统一标识,便于日志过滤与问题定位 */ private static final String TAG = "NotesListAdapter"; - // 应用上下文:用于创建列表项、解析Cursor数据 + /** 应用上下文对象:用于创建列表项视图、解析业务数据,全局复用避免多次创建 */ private Context mContext; - // 选中项状态映射:Key=列表项位置,Value=是否选中(选择模式下) + /** 选中项状态映射容器:核心数据结构,Key=列表项的索引位置,Value=该位置的选中状态,支撑选择模式的核心存储 */ private HashMap mSelectedIndex; - // 普通便签总数:仅统计TYPE_NOTE类型的项,用于判断是否全选 + /** 普通便签数量统计:仅统计{Notes.TYPE_NOTE}类型的项,排除文件夹/通话记录等类型,用于全选状态的判定依据 */ private int mNotesCount; - // 选择模式标记:true=批量选择模式,false=普通浏览模式 + /** 选择模式状态标记:true=开启批量选择模式(批量操作),false=关闭选择模式(普通浏览),控制列表项勾选框的显隐 */ private boolean mChoiceMode; /** - * 内部静态类:存储便签关联的小部件属性 - * 用于批量操作时,提取选中便签绑定的小部件ID和类型 + * 内部静态数据载体类:便签关联的桌面小部件属性封装 + * 核心设计目的:批量操作场景下,统一封装选中便签所绑定的桌面小部件核心属性,便于后续同步更新小部件数据, + * 静态类设计减少内存开销,无上下文引用避免内存泄漏 */ public static class AppWidgetAttribute { - public int widgetId; // 小部件唯一标识ID - public int widgetType; // 小部件类型(2x/4x,对应Notes.TYPE_WIDGET_2X/4X) + /** 小部件系统唯一标识ID:桌面小部件的注册ID,用于精准定位目标小部件 */ + public int widgetId; + /** 小部件类型标识:区分2x/4x两种尺寸的便签小部件,对应{Notes.TYPE_WIDGET_2X}/{Notes.TYPE_WIDGET_4X} */ + public int widgetType; }; /** - * 构造方法:初始化适配器核心参数 - * @param context 应用上下文,传递给父类并保存 + * 构造方法:适配器的初始化入口,完成核心成员变量的初始化配置 + * 核心初始化逻辑:调用父类构造方法完成基础配置、初始化选中状态映射容器、保存上下文引用、重置普通便签统计数, + * 初始无绑定Cursor,后续通过{changeCursor}方法动态绑定数据库查询结果 + * @param context 应用上下文对象,传递至父类并全局保存 */ public NotesListAdapter(Context context) { - // 父类构造:上下文+初始Cursor(null,后续通过changeCursor设置) + // 父类构造:传入上下文+空Cursor,Cursor数据后续动态绑定,保证初始化灵活性 super(context, null); - // 初始化选中项状态映射(空HashMap) + // 初始化选中项状态映射容器,空HashMap保证初始无选中项 mSelectedIndex = new HashMap(); - // 保存上下文引用 + // 保存应用上下文引用,供后续视图创建与数据解析使用 mContext = context; - // 初始化普通便签总数为0 + // 重置普通便签统计数量为0,初始无数据状态 mNotesCount = 0; } /** - * 重写创建新视图方法:创建便签列表项视图 - * @param context 应用上下文 - * @param cursor 当前位置的Cursor(未使用,仅遵循父类接口) - * @param parent 列表项的父容器(ListView) - * @return View:新创建的NotesListItem自定义列表项 + * 重写父类核心方法:创建新的列表项视图 + * 执行时机:列表首次加载、滑动时需要创建新视图的场景,遵循安卓视图复用机制 + * 核心逻辑:创建自定义的{NotesListItem}列表项视图并返回,该方法仅负责视图创建,不负责数据绑定 + * @param context 应用上下文对象 + * @param cursor 当前位置的数据库游标(本方法未使用,仅遵循父类接口规范) + * @param parent 列表项的父容器,即承载所有列表项的ListView + * @return View 创建完成的、未绑定数据的自定义列表项视图 */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - // 创建自定义列表项视图(NotesListItem) + // 创建小米便签自定义列表项视图,作为列表的最小展示单元 return new NotesListItem(context); } /** - * 重写绑定视图方法:将Cursor数据绑定到列表项UI - * @param view 要绑定的列表项视图(NotesListItem) - * @param context 应用上下文 - * @param cursor 当前位置的Cursor,包含便签数据 + * 重写父类核心方法:数据与视图的绑定核心实现 + * 执行时机:列表首次加载、视图复用、数据变化刷新时调用,是适配器的核心业务方法 + * 核心逻辑:将指定位置的Cursor数据库数据,解析为业务数据模型{NoteItemData},并调用列表项的绑定方法, + * 完成数据渲染、选择模式适配、选中状态赋值,实现「数据→模型→视图」的完整映射 + * @param view 待绑定数据的列表项视图,为{NotesListItem}类型,支持视图复用 + * @param context 应用上下文对象 + * @param cursor 当前列表位置的数据库游标,封装了该位置的完整便签数据 */ @Override public void bindView(View view, Context context, Cursor cursor) { - // 仅处理NotesListItem类型的视图(防止类型错误) + // 类型安全校验:仅处理自定义的NotesListItem视图,防止视图类型错误导致崩溃 if (view instanceof NotesListItem) { - // 根据Cursor创建便签数据模型(NoteItemData) + // 将Cursor数据库数据解析为业务数据模型,封装所有展示所需的业务字段 NoteItemData itemData = new NoteItemData(context, cursor); - // 调用列表项的bind方法:绑定数据+适配选择模式+设置选中状态 + // 调用列表项的核心绑定方法,完成数据渲染+选择模式适配+选中状态赋值 ((NotesListItem) view).bind(context, itemData, mChoiceMode, isSelectedItem(cursor.getPosition())); } } /** - * 设置指定位置列表项的选中状态 - * @param position 列表项位置 - * @param checked 是否选中 + * 公开业务方法:设置指定位置列表项的选中状态 + * 核心作用:单选操作的核心接口,支持选择模式下的单个列表项勾选/取消勾选, + * 更新状态后主动通知列表刷新视图,保证选中状态的实时展示 + * @param position 待设置状态的列表项索引位置 + * @param checked 目标选中状态,true=勾选,false=取消勾选 */ public void setCheckedItem(final int position, final boolean checked) { - // 更新选中项状态映射 + // 更新选中状态映射容器,保存当前位置的选中状态 mSelectedIndex.put(position, checked); - // 通知ListView数据变化,刷新UI + // 通知列表数据发生变化,触发视图刷新,展示最新的选中状态 notifyDataSetChanged(); } /** - * 判断当前是否为选择模式 - * @return boolean:true=选择模式,false=普通模式 + * 公开查询方法:获取当前列表的选择模式状态 + * @return boolean true=处于批量选择模式,false=处于普通浏览模式 */ public boolean isInChoiceMode() { return mChoiceMode; } /** - * 设置选择模式 + * 公开业务方法:开启/关闭列表的批量选择模式 + * 核心逻辑:切换模式时自动清空所有选中状态,避免模式切换后残留选中标记,保证视图展示的一致性, + * 是选择模式的总开关接口 * @param mode true=开启选择模式,false=关闭选择模式 */ public void setChoiceMode(boolean mode) { - // 清空原有选中项状态(切换模式时重置选择) + // 清空所有选中项状态,重置选择容器为初始状态 mSelectedIndex.clear(); - // 更新选择模式标记 + // 更新选择模式标记,控制后续视图绑定的逻辑分支 mChoiceMode = mode; } /** - * 全选/取消全选操作 - * 仅对普通便签(TYPE_NOTE)生效,文件夹不参与选择 - * @param checked true=全选,false=取消全选 + * 公开业务方法:全选/取消全选的批量操作 + * 核心规则:仅对{Notes.TYPE_NOTE}类型的普通便签生效,文件夹/通话记录等特殊类型不参与选择, + * 避免用户误操作系统特殊项,保证业务数据的安全性 + * @param checked true=执行全选操作,false=执行取消全选操作 */ public void selectAll(boolean checked) { - // 获取当前绑定的Cursor + // 获取当前绑定的数据库游标,遍历所有列表项数据 Cursor cursor = getCursor(); - // 遍历所有列表项 for (int i = 0; i < getCount(); i++) { - // 移动Cursor到当前位置 + // 移动游标到当前列表项的位置,匹配对应数据 if (cursor.moveToPosition(i)) { - // 仅处理普通便签类型的项 + // 仅处理普通便签类型,过滤文件夹等非选择项 if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { - // 设置当前位置的选中状态 + // 批量设置当前位置的选中状态 setCheckedItem(i, checked); } } @@ -166,22 +185,24 @@ public class NotesListAdapter extends CursorAdapter { } /** - * 获取选中项的便签ID集合 - * @return HashSet:选中的便签ID集合(过滤根文件夹ID) + * 公开业务方法:获取所有选中项的便签ID集合 + * 核心作用:批量删除/批量移动等业务操作的核心数据来源,返回去重的选中便签ID, + * 自动过滤根文件夹等无效ID并输出日志提示,保证业务数据的有效性 + * @return HashSet 所有选中便签的ID集合,无选中项则返回空集合 */ public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); - // 遍历所有选中的位置 + // 遍历所有已记录的选中状态位置 for (Integer position : mSelectedIndex.keySet()) { - // 仅处理选中状态为true的项 + // 仅处理选中状态为true的有效项 if (mSelectedIndex.get(position) == true) { - // 获取当前位置的便签ID + // 获取当前位置对应的便签ID Long id = getItemId(position); - // 过滤根文件夹ID(无效项,日志提示) + // 过滤系统根文件夹的无效ID,避免业务操作异常 if (id == Notes.ID_ROOT_FOLDER) { Log.d(TAG, "Wrong item id, should not happen"); } else { - // 添加有效ID到集合 + // 将有效ID添加至结果集合 itemSet.add(id); } } @@ -190,32 +211,34 @@ public class NotesListAdapter extends CursorAdapter { } /** - * 获取选中项关联的小部件属性集合 - * @return HashSet:选中项的小部件属性集合(无效时返回null) + * 公开业务方法:获取所有选中项关联的小部件属性集合 + * 核心作用:批量操作后同步更新桌面小部件的核心数据来源,提取选中便签所绑定的小部件ID与类型, + * 自动校验游标有效性,异常时输出错误日志并返回null,保证数据安全性 + * @return HashSet 选中项的小部件属性集合,无效数据时返回null */ public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); - // 遍历所有选中的位置 + // 遍历所有已记录的选中状态位置 for (Integer position : mSelectedIndex.keySet()) { - // 仅处理选中状态为true的项 + // 仅处理选中状态为true的有效项 if (mSelectedIndex.get(position) == true) { - // 获取当前位置的Cursor + // 获取当前位置对应的数据库游标 Cursor c = (Cursor) getItem(position); if (c != null) { - // 创建小部件属性对象 + // 创建小部件属性对象,封装核心数据 AppWidgetAttribute widget = new AppWidgetAttribute(); - // 根据Cursor创建便签数据模型 + // 将游标数据解析为业务模型,提取小部件关联属性 NoteItemData item = new NoteItemData(mContext, c); - // 提取小部件ID和类型 widget.widgetId = item.getWidgetId(); widget.widgetType = item.getWidgetType(); - // 添加到集合 + // 添加至结果集合 itemSet.add(widget); /** - * 此处不关闭Cursor:Cursor由适配器统一管理,外部关闭会导致数据异常 + * 重要说明:此处不主动关闭Cursor + * Cursor由CursorAdapter统一管理生命周期,外部关闭会导致列表数据异常、游标越界等崩溃问题 */ } else { - // 无效Cursor,输出错误日志并返回null + // 游标无效时输出错误日志,返回null标记异常状态 Log.e(TAG, "Invalid cursor"); return null; } @@ -225,17 +248,17 @@ public class NotesListAdapter extends CursorAdapter { } /** - * 统计选中项的数量 - * @return int:选中的普通便签数量 + * 公开业务方法:统计当前选中的普通便签数量 + * 核心作用:列表页展示选中数量、判定全选状态的核心依据,仅统计选中状态为true的有效项,保证计数准确性 + * @return int 选中的普通便签数量,无选中项则返回0 */ public int getSelectedCount() { - // 获取所有选中状态值 + // 获取所有已记录的选中状态值集合 Collection values = mSelectedIndex.values(); - // 无状态值时返回0 if (null == values) { return 0; } - // 遍历状态值,统计选中(true)的数量 + // 遍历状态值,统计选中状态为true的项数 Iterator iter = values.iterator(); int count = 0; while (iter.hasNext()) { @@ -247,61 +270,68 @@ public class NotesListAdapter extends CursorAdapter { } /** - * 判断是否全选 - * @return boolean:true=全选(选中数>0且等于普通便签总数),false=未全选 + * 公开业务方法:判断当前是否处于「全选」状态 + * 核心判定规则:选中数量大于0 且 选中数量等于列表中普通便签的总数,双条件保证判定准确性, + * 避免无数据时误判为全选、选中部分项时误判为全选 + * @return boolean true=已全选所有普通便签,false=未全选/无选中项/无普通便签 */ public boolean isAllSelected() { int checkedCount = getSelectedCount(); - // 选中数非0且等于普通便签总数,判定为全选 return (checkedCount != 0 && checkedCount == mNotesCount); } /** - * 判断指定位置的列表项是否选中 - * @param position 列表项位置 - * @return boolean:true=选中,false=未选中(包括状态为null的情况) + * 公开查询方法:判断指定位置的列表项是否被选中 + * 核心规则:未记录状态的位置默认视为「未选中」,避免空指针异常,保证业务逻辑的健壮性 + * @param position 待查询的列表项索引位置 + * @return boolean true=该位置已选中,false=该位置未选中/无状态记录 */ public boolean isSelectedItem(final int position) { - // 状态为null时视为未选中 + // 状态容器中无该位置记录时,默认返回未选中 if (null == mSelectedIndex.get(position)) { return false; } - // 返回当前位置的选中状态 + // 返回该位置的实际选中状态 return mSelectedIndex.get(position); } /** - * 重写内容变化回调方法:数据变化时更新普通便签总数 - * 当Cursor数据(便签数据库)发生变化时触发 + * 重写父类回调方法:监听数据库内容变化的核心回调 + * 触发时机:当适配器绑定的数据库表数据发生增/删/改操作时,系统自动调用该方法 + * 核心逻辑:执行父类默认刷新逻辑后,重新统计普通便签数量,保证全选状态判定的准确性, + * 是视图与数据库数据一致性的核心保障 */ @Override protected void onContentChanged() { super.onContentChanged(); - // 重新计算普通便签总数 + // 数据变化后重新统计普通便签数量 calcNotesCount(); } /** - * 重写更换Cursor方法:更换Cursor后更新普通便签总数 - * @param cursor 新的Cursor + * 重写父类核心方法:更换适配器绑定的游标数据 + * 触发时机:列表页切换文件夹、刷新数据、查询条件变更时主动调用 + * 核心逻辑:执行父类游标更换逻辑后,重新统计普通便签数量,适配新数据集的全选判定规则 + * @param cursor 新的数据库游标,封装了新的查询结果集 */ @Override public void changeCursor(Cursor cursor) { super.changeCursor(cursor); - // 重新计算普通便签总数 + // 更换游标后重新统计普通便签数量 calcNotesCount(); } /** - * 私有方法:计算列表中普通便签(TYPE_NOTE)的总数 - * 用于判断全选状态,仅统计有效Cursor的普通便签项 + * 私有核心方法:统计列表中普通便签的总数 + * 核心业务规则:仅遍历统计{Notes.TYPE_NOTE}类型的项,排除文件夹、通话记录等非便签类型, + * 自动校验游标有效性,异常时终止统计并输出日志,保证计数准确性,是全选功能的核心支撑方法 */ private void calcNotesCount() { - // 重置总数为0 + // 重置计数为0,避免累计统计错误 mNotesCount = 0; - // 遍历所有列表项 + // 遍历适配器绑定的所有列表项数据 for (int i = 0; i < getCount(); i++) { - // 获取当前位置的Cursor + // 获取当前位置对应的数据库游标 Cursor c = (Cursor) getItem(i); if (c != null) { // 仅统计普通便签类型的项 @@ -309,7 +339,7 @@ public class NotesListAdapter extends CursorAdapter { mNotesCount++; } } else { - // 无效Cursor,输出错误日志并终止计算 + // 游标无效时输出错误日志,终止统计避免数据异常 Log.e(TAG, "Invalid cursor"); return; } diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java index f605539..d890f6c 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java @@ -14,64 +14,70 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,负责便签列表页的单个列表项渲染 +// 包声明:小米便签 核心UI模块,该包承载应用所有可视化交互页面及自定义UI组件,本类为列表页核心子项组件 package net.micode.notes.ui; -// 导入安卓上下文类:提供应用运行环境(资源访问、样式加载) +// -------------------------- 安卓系统核心依赖包 - 上下文/视图/布局/基础控件能力 -------------------------- +// 安卓应用全局上下文:提供资源访问、样式加载、布局渲染等基础能力,自定义控件必备依赖 import android.content.Context; -// 导入安卓日期工具类:格式化相对时间(如“1分钟前”“昨天”) +// 安卓系统时间格式化工具类:提供相对时间格式化能力,将时间戳转为「几分钟前/昨天/上周」等友好展示格式 import android.text.format.DateUtils; -// 导入安卓视图类:控制控件可见性、状态 +// 安卓视图体系核心父类:控制控件的显示/隐藏、可见性状态、视图属性等核心操作 import android.view.View; -// 导入安卓复选框类:选择模式下展示的勾选框 +// 安卓复选框控件:列表选择模式下的核心勾选组件,用于便签的批量操作场景 import android.widget.CheckBox; -// 导入安卓图片视图类:展示提醒图标、通话记录图标 +// 安卓图片展示控件:承载提醒图标、通话记录图标等所有图片类展示内容 import android.widget.ImageView; -// 导入安卓线性布局类:作为列表项的根布局 +// 安卓线性布局容器:本类的父布局,提供横向/纵向的线性控件排列能力,作为列表项的根布局 import android.widget.LinearLayout; -// 导入安卓文本视图类:展示标题、时间、通话名称等文本 +// 安卓文本展示控件:承载所有文本类展示内容,如标题、时间、通话名称等 import android.widget.TextView; -// 导入小米便签资源类:引用布局、字符串、样式、图片等资源 +// -------------------------- 小米便签业务层核心依赖 - 资源/数据/工具适配 -------------------------- +// 小米便签资源常量类:统一管理布局、字符串、样式、图片等所有本地资源ID引用 import net.micode.notes.R; -// 导入便签数据常量类:定义便签/文件夹类型、特殊ID(通话记录文件夹)等常量 +// 小米便签数据层核心常量类:定义便签/文件夹的类型、特殊ID、业务状态等全局核心常量 import net.micode.notes.data.Notes; -// 导入数据工具类:格式化便签摘要文本 +// 小米便签数据格式化工具类:封装便签摘要文本的格式化处理逻辑,统一文本展示规则 import net.micode.notes.tool.DataUtils; -// 导入资源解析工具类:获取便签/文件夹的背景资源ID +// 小米便签资源解析工具类:封装便签/文件夹的背景资源适配逻辑,提供不同状态的背景资源获取能力 import net.micode.notes.tool.ResourceParser.NoteItemBgResources; /** - * 便签列表项自定义布局类 - * 继承自LinearLayout,作为便签列表页(NotesListActivity)的单个列表项容器,核心职责: - * 1. 初始化列表项的UI控件(提醒图标、标题、时间、通话名称、勾选框); - * 2. 根据便签数据类型(普通便签/文件夹/通话记录文件夹/通话记录项)渲染差异化UI; - * 3. 适配选择模式(显示/隐藏勾选框); - * 4. 根据便签状态(是否有提醒、背景色、列表位置)设置背景和图标。 + * 小米便签 列表页核心自定义列表项组件 + *

+ * 继承安卓系统{LinearLayout}线性布局,隶属于MVC架构的UI层视图组件,是{NotesListActivity}列表页的最小展示单元; + * 核心设计定位:作为便签列表的标准化子项容器,封装所有列表项的UI渲染逻辑、数据绑定逻辑、样式适配逻辑,解耦列表页与子项展示逻辑; + * 核心业务职责:统一承载所有类型便签/文件夹的差异化展示、选择模式的UI适配、提醒状态的图标展示、背景样式的精准适配、通话记录的专属布局; + * 技术实现特点:基于数据驱动视图的设计思想,通过统一的bind方法完成数据与视图的绑定,内置多分支逻辑适配不同业务类型, + * 封装背景设置的复杂逻辑,对外提供简洁的调用与数据获取接口,是列表页高性能展示的核心基础组件。 + *

*/ public class NotesListItem extends LinearLayout { - // 提醒/类型图标:展示闹钟提醒、通话记录等图标 + /** 功能图标控件:展示闹钟提醒、通话记录等业务类型图标,不同场景展示对应功能标识 */ private ImageView mAlert; - // 标题文本:展示便签摘要、文件夹名称(含数量)、通话记录标题 + /** 主标题文本控件:核心文本展示区,适配展示便签摘要、文件夹名称+数量、通话记录内容等核心信息 */ private TextView mTitle; - // 时间文本:展示便签最后修改的相对时间(如“5分钟前”) + /** 时间文本控件:展示便签/文件夹的最后修改时间,统一格式化为相对友好时间格式 */ private TextView mTime; - // 通话名称文本:仅通话记录项展示来电/去电联系人名称 + /** 通话名称文本控件:通话记录专属展示区,仅通话记录项展示来电/去电的联系人名称,其他场景隐藏 */ private TextView mCallName; - // 列表项绑定的便签数据模型 + /** 数据模型载体:保存当前列表项绑定的业务数据,用于后续视图刷新与数据获取 */ private NoteItemData mItemData; - // 选择模式下的勾选框:批量操作时展示 + /** 选择复选框控件:批量操作模式专属控件,用于勾选待操作的便签,非选择模式下默认隐藏 */ private CheckBox mCheckBox; /** - * 构造方法:初始化列表项布局和控件 - * @param context 应用上下文,用于加载布局和查找控件 + * 构造方法:自定义控件的初始化入口 + * 核心初始化逻辑:加载列表项的基础布局文件,完成所有子控件的视图绑定,为后续数据绑定做准备; + * 该方法仅在列表项创建时执行一次,保证控件初始化的性能最优 + * @param context 应用上下文对象,用于加载布局资源与查找子控件 */ public NotesListItem(Context context) { super(context); - // 加载列表项布局(res/layout/note_item.xml)到当前LinearLayout + // 加载列表项的基础布局文件,将xml布局解析为当前线性布局的子视图 inflate(context, R.layout.note_item, this); - // 初始化各UI控件 + // 绑定布局内所有子控件,通过ID精准获取并赋值给成员变量 mAlert = (ImageView) findViewById(R.id.iv_alert_icon); mTitle = (TextView) findViewById(R.id.tv_title); mTime = (TextView) findViewById(R.id.tv_time); @@ -80,14 +86,16 @@ public class NotesListItem extends LinearLayout { } /** - * 核心绑定方法:将便签数据绑定到列表项UI,适配不同类型/状态的展示逻辑 - * @param context 应用上下文,用于资源/样式加载 - * @param data 列表项对应的便签数据模型(NoteItemData) - * @param choiceMode 是否为选择模式(批量操作) - * @param checked 选择模式下是否勾选当前项 + * 核心公开绑定方法:数据与视图的统一绑定入口,列表项所有展示逻辑的核心处理方法 + * 核心业务能力:接收业务数据模型,根据数据类型/状态/模式完成所有视图的差异化渲染,包括控件显隐、文本赋值、图标切换、样式适配、勾选状态设置; + * 该方法为列表项的核心对外接口,列表页通过调用此方法完成所有子项的内容展示 + * @param context 应用上下文,用于资源加载、样式设置、字符串格式化 + * @param data 当前列表项对应的业务数据模型,承载所有展示所需的业务字段 + * @param choiceMode 是否开启列表选择模式:true=批量操作模式,false=普通浏览模式 + * @param checked 选择模式下当前项的勾选状态:true=已勾选,false=未勾选 */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { - // 选择模式且为普通便签:显示勾选框并设置勾选状态;否则隐藏勾选框 + // 选择模式适配逻辑:仅普通便签在选择模式下展示勾选框,其他类型/模式均隐藏,防止误操作文件夹 if (choiceMode && data.getType() == Notes.TYPE_NOTE) { mCheckBox.setVisibility(View.VISIBLE); mCheckBox.setChecked(checked); @@ -95,26 +103,28 @@ public class NotesListItem extends LinearLayout { mCheckBox.setVisibility(View.GONE); } - // 保存当前绑定的数据模型 + // 缓存当前绑定的业务数据模型,供后续背景设置与外部数据获取使用 mItemData = data; - // 分支1:通话记录文件夹(特殊ID) + // ===== 分支一:通话记录专属文件夹(系统特殊固定ID),独立的展示样式 ===== if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mCallName.setVisibility(View.GONE); // 隐藏通话名称 - mAlert.setVisibility(View.VISIBLE); // 显示类型图标 - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题主样式 - // 标题:通话记录文件夹名称 + 文件夹内文件数量(格式化) + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + // 标题展示规则:固定文件夹名称 + 文件夹内的通话记录数量,格式化展示 mTitle.setText(context.getString(R.string.call_record_folder_name) + context.getString(R.string.format_folder_files_count, data.getNotesCount())); - mAlert.setImageResource(R.drawable.call_record); // 设置通话记录图标 + // 展示通话记录专属图标,明确业务类型标识 + mAlert.setImageResource(R.drawable.call_record); } - // 分支2:通话记录项(父ID为通话记录文件夹) + // ===== 分支二:通话记录子项(归属通话记录文件夹),通话类专属展示样式 ===== else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { - mCallName.setVisibility(View.VISIBLE); // 显示通话名称 - mCallName.setText(data.getCallName()); // 设置通话联系人名称 - mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); // 设置标题次要样式 - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 格式化显示便签摘要 - // 有提醒:显示闹钟图标;无提醒:隐藏图标 + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); + // 标题展示格式化后的通话记录摘要内容,保证文本展示的规范性 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 提醒状态适配:有闹钟提醒则展示闹钟图标,无则隐藏 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); @@ -122,23 +132,22 @@ public class NotesListItem extends LinearLayout { mAlert.setVisibility(View.GONE); } } - // 分支3:普通文件夹/普通便签(非通话记录相关) + // ===== 分支三:普通业务类型(普通文件夹/普通便签),通用展示样式 ===== else { - mCallName.setVisibility(View.GONE); // 隐藏通话名称 - mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题主样式 + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); - // 子分支3.1:普通文件夹 + // 子分支1:普通文件夹类型,展示文件夹名称+包含便签数量 if (data.getType() == Notes.TYPE_FOLDER) { - // 标题:文件夹名称 + 文件夹内文件数量(格式化) mTitle.setText(data.getSnippet() + context.getString(R.string.format_folder_files_count, data.getNotesCount())); - mAlert.setVisibility(View.GONE); // 隐藏图标 + mAlert.setVisibility(View.GONE); } - // 子分支3.2:普通便签 + // 子分支2:普通便签类型,展示便签核心摘要内容 else { - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 格式化显示便签摘要 - // 有提醒:显示闹钟图标;无提醒:隐藏图标 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 提醒状态适配:有闹钟提醒展示闹钟图标,无则隐藏 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); @@ -147,47 +156,51 @@ public class NotesListItem extends LinearLayout { } } } - // 设置最后修改时间:格式化为相对时间(如“1小时前”) + // 统一设置最后修改时间:将时间戳转为「几分钟前/昨天」等相对友好的时间格式,提升用户体验 mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); - // 根据数据模型设置列表项背景 + // 根据当前绑定的数据模型,完成列表项背景样式的精准适配 setBackground(data); } /** - * 私有方法:根据便签数据设置列表项背景 - * 核心逻辑:区分普通便签/文件夹,普通便签再根据列表位置(首/中/尾/单独项)适配不同背景 - * @param data 列表项对应的便签数据模型 + * 私有核心方法:列表项背景样式的统一适配处理 + * 核心业务逻辑:封装复杂的背景适配规则,区分普通便签与文件夹的不同背景策略; + * 普通便签根据列表中的位置(首项/中项/尾项/单独项)+ 背景色ID,匹配对应的背景资源; + * 文件夹统一使用固定背景,简化适配逻辑,保证列表样式的统一性与美观性 + * @param data 当前列表项绑定的业务数据模型,提供背景色ID、类型、列表位置等适配所需字段 */ private void setBackground(NoteItemData data) { - // 获取便签背景色ID + // 获取当前数据模型的背景色标识ID,作为背景资源匹配的核心依据 int id = data.getBgColorId(); - // 普通便签:根据列表位置适配不同背景资源 + // 普通便签的背景适配逻辑:多场景精准匹配,保证列表连贯的视觉效果 if (data.getType() == Notes.TYPE_NOTE) { if (data.isSingle() || data.isOneFollowingFolder()) { - // 单独项/文件夹下唯一项:使用“单独项”背景 + // 场景1:列表中唯一项 / 文件夹下的唯一项 → 使用独立完整背景 setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); } else if (data.isLast()) { - // 列表最后一项:使用“最后项”背景 + // 场景2:列表中的最后一项 → 使用底部收尾样式背景 setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); } else if (data.isFirst() || data.isMultiFollowingFolder()) { - // 列表第一项/文件夹下多项的第一项:使用“第一项”背景 + // 场景3:列表中的第一项 / 文件夹下的多项首项 → 使用顶部起始样式背景 setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); } else { - // 列表中间项:使用“普通项”背景 + // 场景4:列表中的中间项 → 使用标准连贯样式背景 setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); } } - // 文件夹(含通话记录文件夹):使用文件夹统一背景 + // 文件夹类型统一适配逻辑:所有文件夹(含通话记录文件夹)使用固定背景样式 else { setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } } /** - * 获取当前列表项绑定的便签数据模型 - * @return NoteItemData:当前项的数据模型 + * 公开数据获取方法:获取当前列表项绑定的业务数据模型 + * 核心作用:为列表页提供数据访问接口,列表页可通过该方法获取选中项、点击项的业务数据,完成跳转/删除/编辑等后续操作; + * 是视图组件与业务逻辑之间的核心数据桥梁 + * @return NoteItemData 当前列表项绑定的完整业务数据模型 */ public NoteItemData getItemData() { return mItemData; diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java index a924928..0151e15 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -14,149 +14,176 @@ * limitations under the License. */ -// 包声明:归属小米便签的UI模块,负责便签应用的设置页面逻辑 +// 包声明:小米便签 核心UI业务模块,承载应用所有可视化交互页面,本类为应用设置页核心实现 package net.micode.notes.ui; -// 导入安卓账号相关类:用于Google账号的获取与管理 +// -------------------------- 安卓系统核心依赖包 - 账号/页面/弹窗/广播/数据存储能力 -------------------------- +// 安卓账号体系核心类:封装Google账号的账号名、账号类型等基础信息载体 import android.accounts.Account; +// 安卓账号管理核心服务类:系统级账号管理器,负责设备上所有账号的查询、管理、鉴权等操作 import android.accounts.AccountManager; -// 导入安卓ActionBar类:设置顶部导航栏(返回按钮) +// 安卓顶部导航栏核心类:配置页面导航样式、返回按钮、标题等ActionBar相关属性 import android.app.ActionBar; -// 导入安卓对话框类:用于账号选择、更改确认等弹窗 +// 安卓系统弹窗核心类:构建标准化的对话框,承载账号选择、确认提示等交互弹窗 import android.app.AlertDialog; -// 导入安卓广播接收器类:接收同步服务的状态广播 +// 安卓广播核心组件:监听系统/应用内部的广播消息,本类用于监听同步服务状态变更广播 import android.content.BroadcastReceiver; -// 导入安卓内容值类:用于更新便签数据库的同步相关字段 +// 安卓数据封装类:封装键值对数据,用于ContentProvider执行数据库字段更新操作 import android.content.ContentValues; -// 导入安卓上下文类:提供应用运行环境 +// 安卓应用全局上下文:提供资源访问、组件通信、偏好设置读写等核心基础能力 import android.content.Context; -// 导入安卓对话框点击监听类:处理弹窗选项的点击事件 +// 安卓对话框交互核心接口:监听弹窗选项的点击事件,处理用户选择逻辑 import android.content.DialogInterface; -// 导入安卓意图类:用于页面跳转、广播过滤 +// 安卓组件通信核心类:封装页面跳转指令、广播指令、参数传递,实现跨组件通信 import android.content.Intent; -// 导入安卓意图过滤器类:筛选需要接收的广播 +// 安卓广播过滤核心类:筛选需要监听的广播动作,精准匹配目标广播消息 import android.content.IntentFilter; -// 导入安卓共享偏好设置类:存储便签的偏好配置(同步账号、最后同步时间等) +// 安卓轻量级存储核心类:键值对持久化存储,用于保存应用偏好配置,非数据库存储 import android.content.SharedPreferences; -// 导入安卓Bundle类:保存Activity状态 +// 安卓页面状态存储类:保存页面销毁重建时的临时数据,保证页面状态不丢失 import android.os.Bundle; -// 导入安卓偏好设置相关类:构建设置页面的UI组件 +// 安卓系统偏好设置组件:构建设置页面的标准化UI控件,封装设置项的展示与交互逻辑 import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceActivity; import android.preference.PreferenceCategory; -// 导入安卓文本工具类:处理字符串判空、相等判断 +// 安卓文本工具类:封装字符串判空、内容对比等常用操作,避免空指针与硬编码判断 import android.text.TextUtils; -// 导入安卓日期格式化类:格式化最后同步时间 +// 安卓系统日期格式化工具类:标准化格式化时间戳为指定样式的字符串,适配多语言展示 import android.text.format.DateFormat; -// 导入安卓布局填充类:加载自定义布局(设置页面header、对话框标题等) +// 安卓布局加载核心类:将xml布局文件解析为Java视图对象,加载自定义布局与弹窗样式 import android.view.LayoutInflater; -// 导入安卓菜单相关类:处理ActionBar的菜单点击 +// 安卓页面菜单核心类:配置ActionBar右侧菜单的创建与点击事件处理 import android.view.Menu; import android.view.MenuItem; -// 导入安卓视图相关类:操作按钮、文本框等UI控件 +// 安卓视图体系核心类:操作所有可视化控件的父类,实现控件的点击、赋值、显隐等操作 import android.view.View; import android.widget.Button; import android.widget.TextView; -// 导入安卓吐司类:显示操作结果提示 +// 安卓轻量级提示组件:展示短时操作结果提示,无焦点不阻塞用户交互 import android.widget.Toast; -// 导入小米便签资源类:引用字符串、布局、偏好设置xml等资源 +// -------------------------- 小米便签业务层核心依赖 - 资源/数据/同步服务 -------------------------- +// 小米便签资源常量类:统一管理布局、字符串、颜色、样式等所有本地资源ID引用 import net.micode.notes.R; -// 导入便签数据常量类:定义便签URI、字段等核心常量 +// 小米便签数据层核心常量类:定义便签ContentProvider URI、数据表字段、业务常量等核心配置 import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; -// 导入GTask同步服务类:处理便签与Google Task的同步逻辑 +// 小米便签核心同步服务类:封装便签与Google Task的双向同步逻辑,提供同步启停、状态查询等核心能力 import net.micode.notes.gtask.remote.GTaskSyncService; /** - * 便签应用设置页面Activity - * 继承自安卓PreferenceActivity,核心职责: - * 1. 管理Google账号的绑定/切换/移除,支持便签与Google Task同步; - * 2. 提供手动触发同步、取消同步的按钮,展示同步状态和最后同步时间; - * 3. 存储/读取偏好设置(同步账号、最后同步时间、背景色设置等); - * 4. 接收同步服务的广播,实时刷新同步状态UI; - * 5. 处理设置页面的导航(返回便签列表页)。 + * 小米便签 应用核心设置页面Activity + *

+ * 继承安卓系统{PreferenceActivity},隶属于MVC架构的UI层核心业务页面,是便签应用的全局配置中心; + * 核心设计定位:统一承载应用所有用户可配置项,聚焦「Google账号绑定与GTask云同步」核心能力,兼顾基础偏好配置; + * 核心业务职责:Google账号的绑定/切换/移除管理、手动同步/取消同步操作、同步状态实时展示、同步时间持久化、 + * 偏好配置读写、同步服务广播监听、页面导航与交互反馈,是便签应用云同步能力的唯一配置入口; + * 技术实现特点:通过SharedPreferences实现轻量级配置持久化,通过广播接收器实现同步状态实时刷新, + * 通过异步线程处理数据库更新避免主线程阻塞,通过系统账号管理器实现Google账号的标准化管理。 + *

*/ public class NotesPreferenceActivity extends PreferenceActivity { - /** 偏好设置文件名:所有便签偏好配置存储在该文件下 */ + /** + * 全局常量:SharedPreferences偏好设置存储文件名,应用所有配置项均持久化存储在该文件中,单文件统一管理 + */ public static final String PREFERENCE_NAME = "notes_preferences"; - /** 偏好设置Key:存储当前绑定的Google同步账号名 */ + /** + * 全局常量:偏好配置存储键 - 当前绑定的Google同步账号名,核心云同步关联标识 + */ public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; - /** 偏好设置Key:存储最后一次同步的时间戳 */ + /** + * 全局常量:偏好配置存储键 - 最后一次同步成功的时间戳,用于展示同步历史记录 + */ public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; - /** 偏好设置Key:控制便签背景色是否随机显示 */ + /** + * 全局常量:偏好配置存储键 - 便签背景色随机展示开关,控制新建便签的背景色展示策略 + */ public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; - /** 私有偏好设置Key:同步账号相关的偏好分类标识 */ + /** + * 私有常量:偏好设置分类标识 - 账号同步相关配置项的分类容器Key,用于页面UI组件定位 + */ private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; - /** 私有常量:添加账号时的权限过滤Key */ + /** + * 私有常量:系统账号过滤标识 - 添加新账号时的权限过滤关键字,用于精准筛选Google账号类型 + */ private static final String AUTHORITIES_FILTER_KEY = "authorities"; - /** 账号相关的偏好分类组件:承载账号选择的偏好项 */ + /** + * 页面核心控件:账号同步相关的偏好设置分类容器,承载账号选择配置项的父组件 + */ private PreferenceCategory mAccountCategory; - /** 广播接收器:接收GTask同步服务的状态广播(同步中/同步完成) */ + /** + * 核心广播接收器:监听GTaskSyncService的同步状态广播,实现同步中/同步完成的UI实时刷新 + */ private GTaskReceiver mReceiver; - /** 原始Google账号数组:用于对比是否新增了账号 */ + /** + * 账号缓存数组:存储操作前的设备Google账号列表,用于对比判断是否新增账号 + */ private Account[] mOriAccounts; - /** 标记位:是否触发了添加新账号的操作 */ + /** + * 状态标记位:标记是否触发了系统添加账号的操作,用于页面恢复时的账号自动绑定逻辑 + */ private boolean mHasAddedAccount; /** - * Activity创建时的初始化逻辑 - * 核心操作:设置ActionBar、加载偏好设置布局、初始化账号偏好组件、注册同步广播接收器、添加页面header - * @param icicle 保存Activity状态的Bundle + * 重写页面生命周期:页面创建初始化入口方法 + * 执行时机:页面第一次被创建时调用,仅执行一次 + * 核心初始化逻辑:配置ActionBar导航样式、加载偏好设置页面布局、初始化核心控件、注册同步状态广播接收器、 + * 添加页面自定义头部布局,完成页面所有基础初始化工作,为后续交互做准备 + * @param icicle 页面状态存储Bundle,恢复重建时的临时数据载体 */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - /* 使用应用图标作为导航:开启ActionBar的返回按钮 */ + /* 配置ActionBar导航:启用左上角返回按钮,跳转回便签列表页 */ getActionBar().setDisplayHomeAsUpEnabled(true); - // 从xml资源加载偏好设置页面的UI结构(res/xml/preferences.xml) + // 从xml资源加载设置页面的标准化偏好配置UI结构,页面主体内容初始化 addPreferencesFromResource(R.xml.preferences); - // 获取账号相关的偏好分类组件 + // 根据标识获取账号同步分类容器控件,完成核心控件绑定 mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); - // 初始化同步广播接收器 + // 初始化同步状态广播接收器,注册监听同步服务的状态变更广播 mReceiver = new GTaskReceiver(); IntentFilter filter = new IntentFilter(); - // 过滤GTask同步服务的广播动作 filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); - // 注册广播接收器,监听同步状态变化 registerReceiver(mReceiver, filter); - // 初始化原始账号数组为null + // 初始化账号缓存数组与状态标记位,默认无账号无新增操作 mOriAccounts = null; - // 加载设置页面的header布局并添加到列表顶部 + mHasAddedAccount = false; + // 加载自定义页面头部布局并添加至列表顶部,丰富页面视觉层级 View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); getListView().addHeaderView(header, null, true); } /** - * Activity恢复可见时的逻辑 - * 核心操作:检查是否新增了Google账号,自动绑定新账号;刷新设置页面UI + * 重写页面生命周期:页面恢复可见状态回调方法 + * 执行时机:页面从后台切回前台、弹窗关闭后、页面创建后首次展示时调用 + * 核心业务逻辑:检测是否触发过添加账号操作,若有则自动绑定新增的Google账号;刷新页面所有UI控件状态, + * 保证页面展示的账号信息、同步按钮、同步状态均为最新数据,是页面数据一致性的核心保障 */ @Override protected void onResume() { super.onResume(); - // 若触发了添加账号操作,检查是否有新账号并自动绑定 + // 检测添加账号标记位,处理新增账号的自动绑定逻辑 if (mHasAddedAccount) { - // 获取当前设备的Google账号列表 Account[] accounts = getGoogleAccounts(); - // 对比原始账号数组,判断是否新增了账号 + // 对比操作前后的账号数量,判断是否有新账号添加成功 if (mOriAccounts != null && accounts.length > mOriAccounts.length) { for (Account accountNew : accounts) { boolean found = false; - // 遍历原始账号,判断新账号是否已存在 + // 遍历原始账号列表,过滤已存在的账号 for (Account accountOld : mOriAccounts) { if (TextUtils.equals(accountOld.name, accountNew.name)) { found = true; break; } } - // 找到新增账号,自动绑定为同步账号 + // 匹配到新增账号,自动完成绑定并终止遍历 if (!found) { setSyncAccount(accountNew.name); break; @@ -165,17 +192,18 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } - // 刷新设置页面的UI(账号偏好、同步按钮、同步状态) + // 统一刷新页面所有UI组件状态,保证数据与视图一致 refreshUI(); } /** - * Activity销毁时的清理逻辑 - * 核心操作:注销广播接收器,防止内存泄漏 + * 重写页面生命周期:页面销毁回收资源回调方法 + * 执行时机:页面退出、被销毁时调用,仅执行一次 + * 核心优化逻辑:注销已注册的广播接收器,释放系统资源,防止内存泄漏,是安卓组件开发的必做优化项 */ @Override protected void onDestroy() { - // 注销同步广播接收器 + // 安全注销广播接收器,判空避免空指针异常 if (mReceiver != null) { unregisterReceiver(mReceiver); } @@ -183,34 +211,34 @@ public class NotesPreferenceActivity extends PreferenceActivity { } /** - * 加载账号偏好项 - * 核心逻辑:构建账号选择的偏好组件,设置点击事件(选择/更改账号) + * 私有核心方法:加载并初始化账号同步偏好配置项 + * 核心业务逻辑:动态构建账号选择配置项,绑定点击事件,实现「未绑定账号展示选择弹窗、已绑定账号展示变更弹窗」的交互逻辑, + * 同步中禁用账号操作防止数据异常,是账号管理能力的核心实现 */ private void loadAccountPreference() { - // 清空账号分类下的原有偏好项,避免重复添加 + // 清空分类容器内原有配置项,避免重复添加导致的UI重复展示问题 mAccountCategory.removeAll(); - // 创建账号选择的偏好项 + // 新建账号选择偏好配置项,初始化展示文案与交互行为 Preference accountPref = new Preference(this); - // 获取当前绑定的默认同步账号 final String defaultAccount = getSyncAccountName(this); - // 设置偏好项标题和摘要 accountPref.setTitle(getString(R.string.preferences_account_title)); accountPref.setSummary(getString(R.string.preferences_account_summary)); - // 设置偏好项点击事件 + + // 绑定配置项点击事件,处理账号选择与变更的核心交互逻辑 accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { public boolean onPreferenceClick(Preference preference) { - // 同步中不允许更改账号 + // 同步中禁止账号操作,防止同步数据与账号信息不一致导致异常 if (!GTaskSyncService.isSyncing()) { - // 未绑定账号:显示账号选择对话框 if (TextUtils.isEmpty(defaultAccount)) { + // 未绑定账号:展示账号选择弹窗,供用户选择绑定 showSelectAccountAlertDialog(); } else { - // 已绑定账号:显示更改账号的确认对话框(提示风险) + // 已绑定账号:展示账号变更确认弹窗,提示风险并提供操作选项 showChangeAccountConfirmAlertDialog(); } } else { - // 同步中提示无法更改账号 + // 同步中操作拦截,展示友好的吐司提示 Toast.makeText(NotesPreferenceActivity.this, R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) .show(); @@ -219,25 +247,23 @@ public class NotesPreferenceActivity extends PreferenceActivity { } }); - // 将账号偏好项添加到账号分类组件中 + // 将配置项添加至分类容器,完成UI渲染 mAccountCategory.addPreference(accountPref); } /** - * 加载同步按钮与同步状态 - * 核心逻辑: - * 1. 根据同步状态设置按钮文本(立即同步/取消同步)和点击事件; - * 2. 展示最后同步时间或同步中进度; - * 3. 无绑定账号时禁用同步按钮。 + * 私有核心方法:加载并初始化同步按钮与同步状态展示控件 + * 核心业务逻辑:根据同步服务的运行状态,动态切换按钮文本与点击事件(立即同步/取消同步); + * 展示同步进度或最后同步时间;无绑定账号时禁用同步按钮,是同步能力的核心交互入口实现 */ private void loadSyncButton() { - // 获取同步按钮和最后同步时间文本框 + // 获取页面同步按钮与同步状态文本控件,完成视图绑定 Button syncButton = (Button) findViewById(R.id.preference_sync_button); TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); - // 根据同步状态设置按钮状态 + // 根据同步服务状态,动态配置按钮行为与文本 if (GTaskSyncService.isSyncing()) { - // 同步中:按钮文本设为“取消同步”,点击触发取消同步 + // 同步中状态:按钮文本为取消同步,点击触发同步终止逻辑 syncButton.setText(getString(R.string.preferences_button_sync_cancel)); syncButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { @@ -245,7 +271,7 @@ public class NotesPreferenceActivity extends PreferenceActivity { } }); } else { - // 未同步:按钮文本设为“立即同步”,点击触发同步 + // 未同步状态:按钮文本为立即同步,点击触发同步启动逻辑 syncButton.setText(getString(R.string.preferences_button_sync_immediately)); syncButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { @@ -253,33 +279,33 @@ public class NotesPreferenceActivity extends PreferenceActivity { } }); } - // 无绑定账号时禁用同步按钮 + // 无绑定账号时禁用同步按钮,避免无账号同步的无效操作 syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); - // 设置最后同步时间/同步进度 + // 配置同步状态文本展示逻辑,区分同步中与历史同步记录 if (GTaskSyncService.isSyncing()) { - // 同步中:显示同步进度文本 + // 同步中:展示实时同步进度文案 lastSyncTimeView.setText(GTaskSyncService.getProgressString()); lastSyncTimeView.setVisibility(View.VISIBLE); } else { - // 未同步:获取最后同步时间戳 + // 未同步:读取最后同步时间戳,展示格式化的历史同步记录 long lastSyncTime = getLastSyncTime(this); if (lastSyncTime != 0) { - // 有同步记录:格式化并显示最后同步时间 lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, DateFormat.format(getString(R.string.preferences_last_sync_time_format), lastSyncTime))); lastSyncTimeView.setVisibility(View.VISIBLE); } else { - // 无同步记录:隐藏时间文本 + // 无同步记录:隐藏状态文本,简化页面展示 lastSyncTimeView.setVisibility(View.GONE); } } } /** - * 刷新设置页面UI - * 核心操作:重新加载账号偏好项和同步按钮/状态 + * 私有统一刷新方法:页面UI状态刷新总入口 + * 核心职责:统一调用账号配置项与同步按钮的加载方法,实现页面所有核心控件的状态刷新, + * 简化多处刷新逻辑的调用成本,保证刷新行为的一致性 */ private void refreshUI() { loadAccountPreference(); @@ -287,203 +313,179 @@ public class NotesPreferenceActivity extends PreferenceActivity { } /** - * 显示账号选择对话框 - * 核心逻辑: - * 1. 展示当前设备的Google账号列表,支持选择绑定; - * 2. 提供“添加新账号”选项,跳转系统添加账号页面; - * 3. 选择账号后自动绑定为同步账号。 + * 私有弹窗方法:展示Google账号选择对话框 + * 核心业务逻辑:展示设备上已有的Google账号列表供用户选择绑定;提供添加新账号入口跳转系统设置; + * 选择账号后自动完成绑定并刷新页面,是账号绑定的核心交互弹窗实现 */ - private void showSelectAccountAlertDialog() { - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); - - // 加载自定义对话框标题布局 - View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); - TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); - titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); - TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); - subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); - - dialogBuilder.setCustomTitle(titleView); - // 隐藏默认的PositiveButton(通过单选列表选择账号) - dialogBuilder.setPositiveButton(null, null); - - // 获取设备的Google账号列表 - Account[] accounts = getGoogleAccounts(); - // 获取当前绑定的账号 - String defAccount = getSyncAccountName(this); - - // 记录当前账号数组,用于后续对比是否新增账号 - mOriAccounts = accounts; - // 初始化标记位:未添加新账号 - mHasAddedAccount = false; - - // 有Google账号时,构建单选列表 - if (accounts.length > 0) { - CharSequence[] items = new CharSequence[accounts.length]; - final CharSequence[] itemMapping = items; - int checkedItem = -1; - int index = 0; - // 遍历账号,构建列表项并标记当前绑定账号 - for (Account account : accounts) { - if (TextUtils.equals(account.name, defAccount)) { - checkedItem = index; - } - items[index++] = account.name; - } - // 设置单选列表,选择后绑定账号并刷新UI - dialogBuilder.setSingleChoiceItems(items, checkedItem, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - setSyncAccount(itemMapping[which].toString()); - dialog.dismiss(); - refreshUI(); - } - }); - } - - // 加载“添加新账号”的视图并添加到对话框 - View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); - dialogBuilder.setView(addAccountView); - - // 显示对话框 - final AlertDialog dialog = dialogBuilder.show(); - // 设置“添加新账号”点击事件:跳转系统添加账号页面 - addAccountView.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - // 标记触发了添加账号操作 - mHasAddedAccount = true; - Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); - // 过滤仅显示Google账号添加选项 - intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { - "gmail-ls" - }); - // 启动添加账号页面(无返回值) - startActivityForResult(intent, -1); - dialog.dismiss(); - } - }); - } + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 加载自定义弹窗标题布局,初始化弹窗头部文案 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + + // 获取设备上所有Google账号,构建账号选择列表 + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + + // 缓存当前账号列表,用于后续新增账号判断 + mOriAccounts = accounts; + mHasAddedAccount = false; + + // 存在Google账号时,构建单选列表供用户选择 + if (accounts.length > 0) { + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + // 绑定列表选择事件,选择后完成账号绑定并关闭弹窗 + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + + // 加载添加新账号的自定义视图,提供账号新增入口 + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + // 展示弹窗并绑定添加账号点击事件,跳转系统账号添加页面 + final AlertDialog dialog = dialogBuilder.show(); + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); + dialog.dismiss(); + } + }); + } /** - * 显示更改账号的确认对话框 - * 核心逻辑: - * 1. 提示更改账号的风险; - * 2. 提供“更改账号”“移除账号”“取消”三个选项; - * 3. 对应选项触发不同逻辑(选择新账号/移除当前账号)。 + * 私有弹窗方法:展示账号变更/移除确认对话框 + * 核心业务逻辑:针对已绑定账号的场景,展示风险提示与操作选项(更改账号/移除账号/取消); + * 处理账号变更与解绑逻辑,是账号管理的核心确认交互弹窗实现 */ - private void showChangeAccountConfirmAlertDialog() { - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); - - // 加载自定义对话框标题布局 - View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); - TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); - // 标题显示当前绑定的账号名 - titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, - getSyncAccountName(this))); - TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); - // 副标题提示更改账号的风险 - subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); - dialogBuilder.setCustomTitle(titleView); - - // 构建对话框选项:更改账号、移除账号、取消 - CharSequence[] menuItemArray = new CharSequence[] { - getString(R.string.preferences_menu_change_account), - getString(R.string.preferences_menu_remove_account), - getString(R.string.preferences_menu_cancel) - }; - // 设置选项点击事件 - dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - // 选择“更改账号”:显示账号选择对话框 - showSelectAccountAlertDialog(); - } else if (which == 1) { - // 选择“移除账号”:清理绑定的账号信息 - removeSyncAccount(); - refreshUI(); - } - // which==2为取消,无操作 - } - }); - dialogBuilder.show(); - } + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 加载自定义弹窗标题布局,展示当前绑定账号与风险提示 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + // 构建弹窗操作选项,绑定点击事件处理不同操作逻辑 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + // 选择更改账号:跳转账号选择弹窗 + showSelectAccountAlertDialog(); + } else if (which == 1) { + // 选择移除账号:执行账号解绑逻辑 + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); + } /** - * 获取设备上的Google账号列表 - * @return Account[]:所有类型为“com.google”的账号 + * 私有工具方法:获取设备上所有已登录的Google账号列表 + * 核心实现:通过系统账号管理器,根据Google账号类型精准筛选,返回纯净的Google账号数组, + * 是账号管理能力的基础数据来源 + * @return Account[] 设备上所有com.google类型的账号数组,无账号则返回空数组 */ private Account[] getGoogleAccounts() { AccountManager accountManager = AccountManager.get(this); - // 根据账号类型筛选Google账号 return accountManager.getAccountsByType("com.google"); } /** - * 设置同步账号 - * 核心逻辑: - * 1. 更新SharedPreferences存储的账号名; - * 2. 清理最后同步时间; - * 3. 异步清理本地便签的GTask同步相关字段(GTASK_ID、SYNC_ID); - * 4. 提示设置成功。 - * @param account 要绑定的Google账号名 + * 私有核心方法:设置当前绑定的Google同步账号 + * 核心业务逻辑:更新偏好配置中的账号信息、清空历史同步时间、异步清理本地便签的同步关联字段、 + * 展示操作成功提示;账号未变化时不执行任何操作,避免无效处理,是账号绑定的核心业务逻辑实现 + * @param account 待绑定的Google账号名 */ - private void setSyncAccount(String account) { - // 账号未变化时不执行操作 - if (!getSyncAccountName(this).equals(account)) { - // 获取偏好设置编辑器 - SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = settings.edit(); - // 存储新账号名(空账号则存空字符串) - if (account != null) { - editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); - } else { - editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); - } - editor.commit(); - - // 清理最后同步时间 - setLastSyncTime(this, 0); - - // 异步清理本地便签的GTask同步信息(避免阻塞主线程) - new Thread(new Runnable() { - public void run() { - ContentValues values = new ContentValues(); - // 清空GTASK_ID和SYNC_ID - values.put(NoteColumns.GTASK_ID, ""); - values.put(NoteColumns.SYNC_ID, 0); - // 更新所有便签的同步字段 - getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); - } - }).start(); - - // 提示账号设置成功 - Toast.makeText(NotesPreferenceActivity.this, - getString(R.string.preferences_toast_success_set_accout, account), - Toast.LENGTH_SHORT).show(); - } - } + private void setSyncAccount(String account) { + // 账号未发生变化时,直接返回避免无效操作 + if (!getSyncAccountName(this).equals(account)) { + // 写入偏好配置,持久化存储绑定的账号名 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // 账号变更后清空历史同步时间,保证同步记录的准确性 + setLastSyncTime(this, 0); + + // 异步线程清理本地便签的同步关联字段,避免主线程阻塞导致页面卡顿 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + // 展示账号绑定成功的友好提示 + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } /** - * 移除同步账号 - * 核心逻辑: - * 1. 从SharedPreferences中移除账号名和最后同步时间; - * 2. 异步清理本地便签的GTask同步相关字段; + * 私有核心方法:移除当前绑定的Google同步账号,完成账号解绑 + * 核心业务逻辑:清理偏好配置中的账号名与同步时间、异步清理本地便签的同步关联字段, + * 彻底解除本地便签与原账号的云同步关联,是账号解绑的核心业务逻辑实现 */ private void removeSyncAccount() { - // 获取偏好设置编辑器 + // 读取偏好配置并清理账号与同步时间相关配置项 SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); - // 移除同步账号名 if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); } - // 移除最后同步时间 if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { editor.remove(PREFERENCE_LAST_SYNC_TIME); } editor.commit(); - // 异步清理本地便签的GTask同步信息 + // 异步线程清理本地便签的同步关联字段,释放系统资源 new Thread(new Runnable() { public void run() { ContentValues values = new ContentValues(); @@ -495,9 +497,10 @@ public class NotesPreferenceActivity extends PreferenceActivity { } /** - * 静态工具方法:获取当前绑定的同步账号名 - * @param context 应用上下文 - * @return String:绑定的账号名(无则返回空字符串) + * 公开静态工具方法:获取当前绑定的Google同步账号名 + * 全局通用能力:供应用其他组件调用,获取同步账号信息,解耦配置读取逻辑 + * @param context 应用上下文,用于访问偏好配置 + * @return String 当前绑定的账号名,无绑定则返回空字符串 */ public static String getSyncAccountName(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, @@ -506,9 +509,10 @@ public class NotesPreferenceActivity extends PreferenceActivity { } /** - * 静态工具方法:设置最后同步时间戳 - * @param context 应用上下文 - * @param time 同步时间戳(毫秒) + * 公开静态工具方法:设置最后一次同步的时间戳 + * 全局通用能力:供同步服务调用,持久化同步完成时间,解耦配置写入逻辑 + * @param context 应用上下文,用于访问偏好配置 + * @param time 同步完成的时间戳(毫秒值) */ public static void setLastSyncTime(Context context, long time) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, @@ -519,9 +523,10 @@ public class NotesPreferenceActivity extends PreferenceActivity { } /** - * 静态工具方法:获取最后同步时间戳 - * @param context 应用上下文 - * @return long:最后同步时间戳(无则返回0) + * 公开静态工具方法:获取最后一次同步的时间戳 + * 全局通用能力:供应用其他组件调用,读取同步历史时间,解耦配置读取逻辑 + * @param context 应用上下文,用于访问偏好配置 + * @return long 最后同步时间戳,无记录则返回0 */ public static long getLastSyncTime(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, @@ -530,21 +535,24 @@ public class NotesPreferenceActivity extends PreferenceActivity { } /** - * 内部广播接收器类:接收GTask同步服务的状态广播 - * 核心职责:同步状态变化时刷新UI,展示同步进度 + * 内部私有广播接收器类:监听GTask同步服务的状态变更广播 + * 核心设计定位:页面与同步服务的通信桥梁,无耦合监听同步状态变化; + * 核心职责:接收同步中/同步完成的广播消息,触发页面UI刷新,展示实时同步进度, + * 是同步状态实时更新的核心实现,生命周期与宿主页面一致 */ private class GTaskReceiver extends BroadcastReceiver { /** - * 接收广播时的处理逻辑 - * @param context 广播上下文 - * @param intent 携带同步状态的意图 + * 重写广播接收回调方法:处理同步服务的状态广播 + * 核心逻辑:接收到广播后立即刷新页面UI,同步中时更新进度文本,保证页面状态与同步服务一致 + * @param context 广播上下文对象 + * @param intent 携带同步状态与进度的广播意图 */ @Override public void onReceive(Context context, Intent intent) { - // 刷新设置页面UI + // 刷新页面所有UI控件,同步最新状态 refreshUI(); - // 同步中:更新同步进度文本 + // 同步中状态:更新实时同步进度文本 if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); syncStatus.setText(intent @@ -555,17 +563,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { } /** - * 处理ActionBar菜单点击事件 - * 核心逻辑:点击返回按钮时,跳转回便签列表页并清除顶部Activity栈 - * @param item 被点击的菜单项 - * @return boolean:是否处理了该点击事件 + * 重写ActionBar菜单点击事件处理方法 + * 核心业务逻辑:处理左上角返回按钮的点击事件,跳转回便签列表主页面并清除顶部Activity栈, + * 避免返回时重复创建页面,优化用户导航体验,是页面导航的核心实现 + * @param item 被点击的菜单项对象 + * @return boolean 是否成功处理该点击事件 */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - // 构建跳转到便签列表页的意图 + // 构建返回主页面的意图,清除顶部栈保证页面唯一性 Intent intent = new Intent(this, NotesListActivity.class); - // 清除顶部Activity栈,避免返回时重复创建 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); return true; diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java index 2b1ba2a..fd6f02f 100644 --- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java +++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider.java @@ -14,222 +14,231 @@ * limitations under the License. */ -// 包声明:归属小米便签桌面小部件模块,定义所有尺寸便签小部件的通用基类 +// 包声明:小米便签 桌面小部件功能模块,该包统管所有小部件基类与不同规格的实现子类 package net.micode.notes.widget; -// 导入安卓延迟意图类:用于小部件点击事件的异步意图触发 +// -------------------------- 安卓系统核心依赖包 - 桌面小部件基础能力 -------------------------- +// 安卓延迟意图类:小部件跨进程点击事件核心载体,桌面进程通过该类触发应用内页面跳转,异步执行意图逻辑 import android.app.PendingIntent; -// 导入安卓小部件管理器类:管理小部件的生命周期(更新、删除、创建) +// 安卓系统桌面小部件核心管理类:负责小部件的创建、更新、销毁、状态维护等全生命周期系统调度 import android.appwidget.AppWidgetManager; -// 导入安卓小部件提供者基类:所有桌面小部件的系统基类 +// 安卓系统小部件基类:所有桌面小部件的标准父类,封装系统层小部件生命周期回调方法 import android.appwidget.AppWidgetProvider; -// 导入安卓内容值类:用于封装ContentProvider的更新数据 +// 安卓数据封装类:封装ContentProvider的更新数据键值对,用于便签数据的字段更新操作 import android.content.ContentValues; -// 导入安卓上下文类:提供应用运行环境(资源访问、ContentResolver获取等) +// 安卓应用全局上下文:提供资源访问、ContentResolver获取、系统服务调用等核心能力,小部件核心依赖 import android.content.Context; -// 导入安卓意图类:用于组件间通信(小部件点击跳转页面) +// 安卓组件通信核心类:封装页面跳转指令与传递参数,用于小部件点击后的页面跳转逻辑 import android.content.Intent; -// 导入安卓数据库游标类:存储数据库查询结果 +// 安卓数据库游标类:承载ContentProvider的查询结果集,按需读取便签数据,用完需手动释放资源 import android.database.Cursor; -// 导入安卓日志类:输出小部件相关日志(异常/调试) +// 安卓系统日志工具类:输出调试与异常日志,便于小部件功能的问题定位与线上排查 import android.util.Log; -// 导入安卓远程视图类:用于渲染桌面小部件的UI(小部件运行在桌面进程,需远程视图) +// 安卓远程视图类:小部件核心UI载体,因小部件运行在桌面系统进程,需通过该类跨进程渲染UI布局与数据 import android.widget.RemoteViews; -// 导入小米便签资源类:引用布局、字符串、图片等资源 +// -------------------------- 小米便签业务层核心依赖 - 资源/数据/页面/工具 -------------------------- +// 小米便签资源常量类:统一管理布局、字符串、图片、颜色等所有本地资源ID引用 import net.micode.notes.R; -// 导入便签数据常量类:定义便签URI、字段、意图参数等核心常量 +// 小米便签数据层核心常量类:定义便签URI、意图参数、业务状态、小部件类型等全局核心常量,数据层与UI层通用 import net.micode.notes.data.Notes; -// 导入便签列常量子类:简化便签表字段的引用 +// 小米便签数据表字段子类:简化便签数据库表的列名引用,避免硬编码,提升代码可维护性 import net.micode.notes.data.Notes.NoteColumns; -// 导入资源解析工具类:解析小部件背景资源 +// 小米便签资源解析工具类:封装多规格小部件背景资源的映射适配逻辑,统一提供不同尺寸的背景资源获取能力 import net.micode.notes.tool.ResourceParser; -// 导入便签编辑页面:小部件点击跳转的目标页面 +// 小米便签核心业务页面:便签新建/编辑页面,小部件点击跳转的核心目标页面 import net.micode.notes.ui.NoteEditActivity; -// 导入便签列表页面:隐私模式下小部件点击跳转的目标页面 +// 小米便签核心业务页面:便签列表展示页面,隐私模式下小部件点击的跳转目标页面 + import net.micode.notes.ui.NotesListActivity; /** - * 便签桌面小部件抽象基类 - * 继承自安卓系统AppWidgetProvider,封装所有尺寸便签小部件的通用核心逻辑: - * 1. 小部件删除时清理关联的便签数据; - * 2. 小部件更新时的通用UI渲染、数据查询、点击事件绑定; - * 3. 定义抽象方法,由子类(2x/4x)实现尺寸相关的布局、背景、类型适配。 + * 小米便签 桌面小部件通用抽象基类 + *

+ * 继承安卓系统标准基类{AppWidgetProvider},隶属于MVC架构的UI层核心组件,是所有尺寸规格便签小部件的父类; + * 核心设计理念:采用「模板方法设计模式」,封装所有小部件的通用核心业务逻辑与生命周期管理,下沉共性能力, + * 把与尺寸强相关的差异化适配逻辑抽离为抽象方法,交由子类{NoteWidgetProvider_2x}/{NoteWidgetProvider_4x}实现; + * 统一承载能力:小部件删除时的关联数据清理、小部件更新时的通用数据查询/UI渲染/点击事件绑定、隐私模式适配、资源释放等; + * 本类为抽象类,无法实例化,仅作为子类的标准化模板与能力基座。 + *

*/ public abstract class NoteWidgetProvider extends AppWidgetProvider { /** - * 数据库查询投影数组:指定查询便签的核心字段,减少查询冗余 - * 包含:便签ID、背景色ID、便签摘要(用于小部件展示) + * 数据库查询投影数组:指定查询便签表的核心业务字段,按需查询,减少无效字段的内存占用与查询耗时 + * 投影字段严格匹配业务需求,仅查询小部件展示所需的核心数据,无冗余字段 */ public static final String [] PROJECTION = new String [] { - NoteColumns.ID, // 便签唯一标识 - NoteColumns.BG_COLOR_ID, // 便签背景色ID - NoteColumns.SNIPPET // 便签内容摘要 + NoteColumns.ID, // 便签数据表-主键ID,唯一标识单条便签数据 + NoteColumns.BG_COLOR_ID, // 便签数据表-背景色标识ID,用于匹配小部件对应背景资源 + NoteColumns.SNIPPET // 便签数据表-内容摘要,小部件UI上展示的核心文本内容 }; - // 投影数组对应的列索引常量,简化Cursor取值 - public static final int COLUMN_ID = 0; // 便签ID列索引 - public static final int COLUMN_BG_COLOR_ID = 1; // 背景色ID列索引 - public static final int COLUMN_SNIPPET = 2; // 摘要列索引 + // 投影数组对应的列索引常量:固化查询结果集的字段下标,简化Cursor取值逻辑,避免硬编码索引值导致的错误 + public static final int COLUMN_ID = 0; // 投影数组中-便签ID的列索引 + public static final int COLUMN_BG_COLOR_ID = 1; // 投影数组中-背景色ID的列索引 + public static final int COLUMN_SNIPPET = 2; // 投影数组中-便签摘要的列索引 - // 日志标签:用于小部件相关日志输出,便于问题定位 + // 日志统一标签:小部件模块所有日志输出的固定标识,便于日志过滤与问题精准定位 private static final String TAG = "NoteWidgetProvider"; /** - * 重写小部件删除回调方法 - * 当用户删除便签小部件时触发,核心逻辑:将关联便签的WIDGET_ID置为无效,清理关联关系 - * @param context 应用上下文 - * @param appWidgetIds 被删除的小部件ID数组 + * 重写系统小部件生命周期回调:小部件删除触发方法 + * 触发时机:用户在桌面手动删除任意规格的便签小部件实例时由系统回调 + * 核心业务逻辑:清理当前小部件与便签的关联关系,将关联便签的WIDGET_ID字段置为无效值, + * 保证数据层的关联关系一致性,防止出现无效的脏数据关联 + * @param context 应用全局上下文对象,提供ContentResolver数据操作能力 + * @param appWidgetIds 被用户删除的小部件ID数组,支持批量删除处理 */ @Override public void onDeleted(Context context, int[] appWidgetIds) { - // 构建ContentValues:将WIDGET_ID设为无效值(INVALID_APPWIDGET_ID) + // 构建数据更新载体:封装需要更新的字段与对应值,将关联标识置为系统无效值 ContentValues values = new ContentValues(); values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - // 遍历所有被删除的小部件ID,更新关联便签的WIDGET_ID + // 遍历所有待删除的小部件ID,逐一对关联便签执行数据更新操作 for (int i = 0; i < appWidgetIds.length; i++) { - context.getContentResolver().update(Notes.CONTENT_NOTE_URI, // 便签ContentProvider的URI - values, // 要更新的字段和值 - NoteColumns.WIDGET_ID + "=?", // 更新条件:WIDGET_ID匹配当前删除的ID - new String[] { String.valueOf(appWidgetIds[i])}); // 条件参数(防止SQL注入) + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); } } /** - * 私有方法:根据小部件ID查询关联的便签信息 - * 过滤条件:关联当前widgetId + 非回收站便签(PARENT_ID≠回收站ID) - * @param context 应用上下文,用于获取ContentResolver - * @param widgetId 目标小部件ID - * @return Cursor:包含匹配的便签信息(ID、背景色、摘要),无匹配则返回null + * 私有核心工具方法:根据小部件ID,查询其关联的有效便签数据信息 + * 核心过滤条件:匹配当前小部件ID + 排除回收站中的便签数据,保证查询结果为有效展示的便签 + * @param context 应用上下文,用于获取ContentResolver执行数据查询 + * @param widgetId 目标小部件的唯一标识ID + * @return Cursor 匹配条件的便签数据结果集,无匹配数据时返回null;结果集需调用方手动关闭释放资源 */ private Cursor getNoteWidgetInfo(Context context, int widgetId) { - return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, // 便签查询URI - PROJECTION, // 查询的字段投影 - NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", // 查询条件 - new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, // 条件参数 - null); // 排序规则(无) + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); } /** - * 对外暴露的小部件更新方法(重载) - * 默认以非隐私模式更新小部件,调用带privacyMode的核心更新方法 - * @param context 应用上下文 - * @param appWidgetManager 小部件管理器 - * @param appWidgetIds 需要更新的小部件ID数组 + * 受保护的重载更新方法:对外暴露的小部件通用更新入口 + * 业务适配:默认以【非隐私模式】执行小部件的UI渲染与数据绑定,为子类提供极简的调用入口 + * @param context 应用全局上下文 + * @param appWidgetManager 系统小部件管理器,负责最终的UI更新调度 + * @param appWidgetIds 需要执行更新操作的小部件ID数组,支持批量更新 */ protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { update(context, appWidgetManager, appWidgetIds, false); } /** - * 核心私有更新方法:实现小部件UI渲染、数据绑定、点击事件配置的通用逻辑 - * @param context 应用上下文 - * @param appWidgetManager 小部件管理器,用于更新小部件UI - * @param appWidgetIds 需要更新的小部件ID数组 - * @param privacyMode 是否为隐私模式(隐私模式下隐藏内容,跳转列表页) + * 私有核心业务方法:小部件通用更新逻辑的总入口,封装所有规格小部件的完整更新流程 + * 核心职责:统一实现 数据查询 → 数据解析 → 视图初始化 → 内容赋值 → 点击事件绑定 → UI刷新 全链路逻辑, + * 兼容普通模式/隐私模式双场景,处理有无关联便签的分支逻辑,是整个小部件的核心业务实现方法 + * @param context 应用全局上下文,支撑资源访问与数据操作 + * @param appWidgetManager 系统小部件管理器,提供小部件UI更新的核心能力 + * @param appWidgetIds 需要更新的小部件ID数组,支持多实例批量处理 + * @param privacyMode 是否启用隐私模式:true=隐私模式(隐藏内容),false=普通模式(展示内容) */ private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, boolean privacyMode) { - // 遍历所有需要更新的小部件ID,逐个处理 + // 遍历所有待更新的小部件ID,逐个完成独立的更新逻辑处理 for (int i = 0; i < appWidgetIds.length; i++) { - // 过滤无效的小部件ID(INVALID_APPWIDGET_ID) + // 过滤无效的小部件ID,跳过无意义的更新操作,提升执行效率 if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { - // 初始化默认背景ID(从资源解析工具获取全局默认背景) + // 初始化默认背景ID:从资源工具类获取应用全局默认的便签背景标识 int bgId = ResourceParser.getDefaultBgId(context); - // 初始化便签摘要(默认空字符串) + // 初始化便签摘要:默认空字符串,防止空指针异常 String snippet = ""; - // 构建小部件点击意图:默认跳转便签编辑页 + // 构建默认跳转意图:无关联便签时,跳转至便签编辑页执行新建操作 Intent intent = new Intent(context, NoteEditActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 防止重复创建Activity - intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); // 携带小部件ID参数 - intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); // 携带小部件类型(子类实现) + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 启动模式:栈顶复用,避免重复创建页面实例 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); // 携带小部件ID参数,供编辑页关联使用 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); // 携带小部件类型,由子类实现差异化适配 - // 查询当前小部件关联的便签信息 + // 查询当前小部件关联的有效便签数据 Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); if (c != null && c.moveToFirst()) { - // 异常日志:同一个widgetId关联多个便签(数据异常) + // 异常日志埋点:同一小部件ID关联多条便签数据,属于数据异常场景,输出错误日志便于排查 if (c.getCount() > 1) { Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); c.close(); return; } - // 从Cursor中获取便签摘要和背景色ID + // 从结果集中解析业务数据,赋值给本地变量用于后续视图渲染 snippet = c.getString(COLUMN_SNIPPET); bgId = c.getInt(COLUMN_BG_COLOR_ID); - // 携带便签ID参数,跳转编辑页时定位到该便签 + // 携带便签ID参数,跳转编辑页时直接定位到当前关联的便签内容 intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); - // 设置意图动作:查看已有便签 + // 修改意图动作:由「新建」改为「查看/编辑」已有便签 intent.setAction(Intent.ACTION_VIEW); } else { - // 无关联便签时:显示默认提示文本 + // 无关联便签数据时:展示应用默认的空内容提示文本 snippet = context.getResources().getString(R.string.widget_havenot_content); - // 设置意图动作:新建便签 + // 修改意图动作:执行新建便签的业务逻辑 intent.setAction(Intent.ACTION_INSERT_OR_EDIT); } - // 关闭Cursor,释放数据库资源 + // 安全关闭游标,释放数据库连接资源,防止内存泄漏 if (c != null) { c.close(); } - // 创建远程视图:加载子类定义的布局(2x/4x) + // 创建远程视图实例:加载子类实现的规格专属布局,完成跨进程UI初始化 RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); - // 设置小部件背景图:使用子类定义的背景资源(2x/4x) + // 为远程视图设置背景资源:加载子类实现的规格专属背景,完成尺寸与背景的精准适配 rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); - // 携带背景ID参数,跳转编辑页时同步背景 + // 携带背景ID参数,跳转编辑页时同步使用当前小部件的背景样式 intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); /** - * 生成小部件点击的PendingIntent(延迟意图) - * 区分隐私模式和普通模式: - * 1. 隐私模式:显示提示文本,跳转便签列表页; - * 2. 普通模式:显示便签摘要,跳转便签编辑页。 + * 构建小部件点击的延迟意图:区分隐私模式与普通模式,实现差异化的交互逻辑 + * PendingIntent为跨进程意图载体,是小部件点击事件的核心实现方式,保证桌面进程能触发应用内逻辑 */ PendingIntent pendingIntent = null; if (privacyMode) { - // 隐私模式:设置提示文本(“访问模式下隐藏内容”) + // 隐私模式逻辑:隐藏便签真实内容,展示隐私提示文本 rv.setTextViewText(R.id.widget_text, context.getString(R.string.widget_under_visit_mode)); - // 构建跳转列表页的PendingIntent + // 隐私模式跳转:点击后跳转至便签列表页面,而非编辑页 pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); } else { - // 普通模式:设置便签摘要文本 + // 普通模式逻辑:展示解析后的便签摘要文本 rv.setTextViewText(R.id.widget_text, snippet); - // 构建跳转编辑页的PendingIntent(FLAG_UPDATE_CURRENT:更新已有Intent的参数) + // 普通模式跳转:点击后跳转至便签编辑页,携带完整业务参数 pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, PendingIntent.FLAG_UPDATE_CURRENT); } - // 绑定小部件文本区域的点击事件:触发上述PendingIntent + // 为小部件核心文本区域绑定点击事件,触发上述构建的延迟意图 rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); - // 通过小部件管理器更新当前小部件的UI + // 通过系统管理器完成最终的UI刷新,将渲染完成的远程视图同步至桌面小部件 appWidgetManager.updateAppWidget(appWidgetIds[i], rv); } } } /** - * 抽象方法:获取小部件背景资源ID - * 由子类(2x/4x)实现,返回对应尺寸的背景资源ID - * @param bgId 背景色标识ID - * @return 对应尺寸的背景资源ID + * 抽象方法:获取小部件规格专属的背景资源ID + * 由子类根据自身尺寸规格实现,完成背景资源与小部件尺寸的精准适配,避免背景拉伸/变形 + * @param bgId 便签数据表中定义的背景色标识ID,为全局统一的背景风格常量 + * @return int 对应尺寸规格的背景Drawable资源ID */ protected abstract int getBgResourceId(int bgId); /** - * 抽象方法:获取小部件布局ID - * 由子类(2x/4x)实现,返回对应尺寸的布局资源ID - * @return 对应尺寸的布局资源ID + * 抽象方法:获取小部件规格专属的布局资源ID + * 由子类根据自身尺寸规格实现,加载与桌面占位大小匹配的专属布局,保证UI展示效果 + * @return int 对应尺寸规格的布局资源ID */ protected abstract int getLayoutId(); /** - * 抽象方法:获取小部件类型标识 - * 由子类(2x/4x)实现,返回对应尺寸的类型常量(Notes.TYPE_WIDGET_2X/4X) - * @return 小部件类型标识 + * 抽象方法:获取小部件的标准化业务类型标识 + * 由子类根据自身尺寸规格实现,返回数据层统一定义的类型常量; + * 该标识是数据层识别小部件规格的核心依据,用于数据关联、筛选与同步,保证数据与视图的一致性 + * @return int 小部件业务类型常量,取值为Notes.TYPE_WIDGET_2X / Notes.TYPE_WIDGET_4X */ protected abstract int getWidgetType(); } \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java index 1273c8c..78af6c9 100644 --- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java +++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_2x.java @@ -14,70 +14,66 @@ * limitations under the License. */ -// 包声明:归属小米便签的桌面小部件模块 +// 包声明:小米便签 桌面小部件功能模块,该包下统管所有尺寸规格的便签桌面小组件实现类 package net.micode.notes.widget; -// 导入必要的安卓系统类:用于管理桌面小部件 import android.appwidget.AppWidgetManager; -// 导入安卓上下文类:提供应用运行环境的全局信息 import android.content.Context; -// 导入小米便签的资源类:用于引用布局等资源 import net.micode.notes.R; -// 导入便签数据常量类:定义小部件类型等常量 import net.micode.notes.data.Notes; -// 导入资源解析工具类:用于解析小部件背景资源 import net.micode.notes.tool.ResourceParser; /** - * 2x尺寸便签桌面小部件提供者类 - * 继承自基础的NoteWidgetProvider,专门适配2x尺寸的小部件布局、背景和类型标识 + * 小米便签 2X规格桌面小部件提供者核心实现类 + * 继承抽象基类NoteWidgetProvider,作为MVC架构中UI层的桌面可视化组件,仅负责2X规格专属适配逻辑; + * 核心职责:实现基类抽象方法,为2X尺寸小部件提供专属布局、匹配的背景资源、标准化业务类型标识; + * 设计原则:通用的小部件生命周期管理、数据更新、视图渲染逻辑完全复用父类,子类只做规格差异化实现,解耦通用逻辑与尺寸适配逻辑。 */ public class NoteWidgetProvider_2x extends NoteWidgetProvider { /** - * 重写小部件更新方法 - * 触发2x尺寸小部件的更新逻辑,调用父类的update方法完成具体更新操作 - * @param context 应用上下文 - * @param appWidgetManager 桌面小部件管理器 - * @param appWidgetIds 需要更新的小部件ID数组 + * 重写系统小部件生命周期的更新回调方法 + * 触发场景:小部件首次添加至桌面、系统触发定时刷新、便签数据变更主动同步时调用 + * 方法逻辑:无差异化业务处理,直接调用父类统一的update核心方法,完成2X小部件的视图刷新与便签数据绑定 + * @param context 全局上下文对象,提供系统服务调用、应用资源访问的基础能力 + * @param appWidgetManager 系统桌面小部件管理器,负责小部件的创建、更新、销毁等全生命周期调度 + * @param appWidgetIds 待更新的2X规格小部件ID数组,支持多实例批量更新操作 */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - // 调用父类的update方法,执行2x尺寸小部件的更新核心逻辑 super.update(context, appWidgetManager, appWidgetIds); } /** - * 重写获取布局ID的方法 - * 返回2x尺寸小部件对应的布局资源ID - * @return 2x小部件布局资源ID + * 重写父类抽象方法,获取2X规格小部件的专属布局资源ID + * 布局文件为固定尺寸适配,与2X桌面占位大小完全匹配,保证便签内容在该规格下的完整展示 + * @return int 2X小部件专属布局资源ID,对应布局文件:R.layout.widget_2x */ @Override protected int getLayoutId() { - // 返回R.layout.widget_2x:2x尺寸小部件的布局文件ID return R.layout.widget_2x; } /** - * 重写获取背景资源ID的方法 - * 根据传入的背景ID,解析并返回2x尺寸小部件对应的背景资源ID - * @param bgId 背景标识ID - * @return 2x尺寸小部件对应的背景资源ID + * 重写父类抽象方法,获取2X规格小部件对应的背景资源ID + * 核心适配说明:不同尺寸的小部件背景资源为独立资源文件,需根据背景主题ID精准匹配对应规格的背景; + * 避免因尺寸不一致导致的背景拉伸、变形等UI兼容问题,通过工具类封装所有背景资源的映射关系,统一管理。 + * @param bgId 背景主题标识ID,为Notes模块统一定义的背景风格常量,与具体尺寸解耦 + * @return int 适配2X规格的背景Drawable资源ID */ @Override protected int getBgResourceId(int bgId) { - // 调用ResourceParser工具类,获取2x尺寸小部件对应的背景资源ID return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); } /** - * 重写获取小部件类型的方法 - * 返回2x尺寸便签小部件的类型标识 - * @return 2x小部件类型常量(Notes.TYPE_WIDGET_2X) + * 重写父类抽象方法,获取当前小部件的标准化业务类型标识 + * 该标识为数据层核心常量,用于NotesProvider数据持久化、便签数据同步、小部件类型筛选的核心依据; + * 保证数据层能精准识别当前小部件的尺寸规格,返回对应规格的便签数据,实现「数据-视图」的规格一致性适配。 + * @return int 2X规格小部件的业务类型常量,定值:Notes.TYPE_WIDGET_2X */ @Override protected int getWidgetType() { - // 返回Notes.TYPE_WIDGET_2X:2x尺寸便签小部件的类型标识常量 return Notes.TYPE_WIDGET_2X; } } \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java index 1b69129..c339503 100644 --- a/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java +++ b/src/Notes-master/src/net/micode/notes/widget/NoteWidgetProvider_4x.java @@ -14,70 +14,70 @@ * limitations under the License. */ -// 包声明:归属小米便签的桌面小部件模块,统一管理不同尺寸的便签小部件 +// 包声明:小米便签 桌面小部件功能模块,该包统一承载所有尺寸规格的便签桌面小组件实现类 package net.micode.notes.widget; -// 导入安卓系统小部件管理类:用于管理桌面小部件的创建、更新、删除等生命周期 +// 安卓系统核心依赖:桌面小部件全生命周期调度的核心管理类,负责小部件的创建、更新、销毁等系统操作 import android.appwidget.AppWidgetManager; -// 导入安卓上下文类:提供应用运行时的环境信息(如资源访问、组件通信) +// 安卓系统核心依赖:应用全局上下文,提供资源访问、系统服务调用、组件通信的基础能力,小部件初始化必备 import android.content.Context; -// 导入小米便签资源类:引用布局、颜色等本地资源 +// 小米便签业务依赖:应用资源常量类,统一管理所有布局、样式等资源ID引用 import net.micode.notes.R; -// 导入便签数据常量类:定义小部件类型、便签状态等核心常量 +// 小米便签业务依赖:数据层核心常量类,定义便签及小部件的类型、状态等全局业务枚举常量 import net.micode.notes.data.Notes; -// 导入资源解析工具类:专门处理小部件背景资源的解析与匹配 +// 小米便签业务依赖:资源解析工具类,封装多规格小部件背景资源的映射适配逻辑,统一提供背景资源获取能力 import net.micode.notes.tool.ResourceParser; /** - * 4x尺寸便签桌面小部件提供者类 - * 继承自基础的NoteWidgetProvider抽象类,专门适配4x尺寸的桌面小部件, - * 核心职责是定义4x尺寸小部件的布局、背景资源和类型标识 + * 小米便签 4X规格桌面小部件提供者核心实现类 + * 继承抽象基类NoteWidgetProvider,隶属于MVC架构的UI层桌面可视化组件,专注4X规格的差异化适配; + * 核心设计职责:实现基类定义的抽象方法,为4X尺寸小部件提供专属的布局资源、匹配的背景样式资源、标准化业务类型标识; + * 设计思想遵循:父类封装所有小部件通用的生命周期、数据更新、视图渲染核心逻辑,子类仅实现当前规格的差异化配置,解耦通用逻辑与尺寸适配逻辑,提升扩展性。 */ public class NoteWidgetProvider_4x extends NoteWidgetProvider { /** - * 重写小部件更新回调方法 - * 当4x尺寸便签小部件需要更新时触发,调用父类update方法执行具体更新逻辑 - * @param context 应用上下文,用于资源访问和组件交互 - * @param appWidgetManager 小部件管理器,负责小部件的更新操作 - * @param appWidgetIds 需要更新的4x小部件ID数组(支持多实例) + * 重写系统小部件生命周期的更新回调方法 + * 触发场景:4X小部件首次添加至桌面、系统执行定时刷新任务、便签数据变更触发主动同步时调用 + * 方法核心逻辑:无4X规格的差异化业务处理,直接复用父类统一的update核心方法,完成4X小部件的视图刷新与最新便签数据绑定 + * @param context 应用全局上下文对象,支撑小部件的资源访问与系统服务调用 + * @param appWidgetManager 系统桌面小部件管理器,统一调度所有小部件实例的更新操作 + * @param appWidgetIds 待执行更新的4X规格小部件ID数组,原生支持多小部件实例的批量更新 */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - // 调用父类核心更新方法,复用通用更新逻辑,适配4x尺寸小部件 super.update(context, appWidgetManager, appWidgetIds); } /** - * 定义4x尺寸小部件对应的布局资源ID - * (注:此处未显式标注@Override,但逻辑上是重写父类抽象方法) - * @return 4x小部件专属布局文件ID(R.layout.widget_4x) + * 实现父类抽象方法:获取4X规格小部件的专属布局资源ID + * 该布局为小米便签定制化的4X尺寸UI布局,与桌面4格占位大小精准匹配,保障便签内容完整展示无适配问题 + * @return int 4X小部件专属布局资源ID,对应固定布局文件:R.layout.widget_4x */ protected int getLayoutId() { - // 返回widget_4x布局ID:该布局定义了4x尺寸小部件的UI结构 return R.layout.widget_4x; } /** - * 重写背景资源获取方法 - * 根据背景标识ID,匹配并返回4x尺寸小部件对应的背景资源ID - * @param bgId 背景样式标识(不同数值对应不同背景风格) - * @return 4x尺寸小部件的背景资源ID + * 重写父类抽象方法:根据背景主题标识ID,获取4X规格小部件的专属背景资源ID + * 核心适配逻辑:不同尺寸规格的小部件对应独立的背景资源文件,避免尺寸不匹配导致的背景拉伸、变形等UI兼容问题; + * 背景资源的映射关系由工具类统一封装管理,实现业务逻辑与资源适配的解耦,便于后续扩展新的背景样式。 + * @param bgId 背景主题枚举标识ID,为项目统一定义的小部件背景风格常量,与具体尺寸规格解耦 + * @return int 精准匹配4X规格的背景Drawable资源ID */ @Override protected int getBgResourceId(int bgId) { - // 调用工具类方法,获取4x尺寸小部件对应的背景资源ID return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); } /** - * 重写小部件类型获取方法 - * 返回4x尺寸便签小部件的专属类型标识,用于区分不同尺寸的小部件 - * @return 4x小部件类型常量(Notes.TYPE_WIDGET_4X) + * 重写父类抽象方法:获取当前4X规格小部件的标准化业务类型标识 + * 该标识为数据层核心业务常量,是NotesProvider数据持久化、便签数据同步、小部件类型筛选的核心依据; + * 作用是让数据层能够精准识别当前小部件的尺寸规格,返回对应规格的适配数据,实现「业务数据-桌面视图」的规格一致性闭环。 + * @return int 4X规格小部件的业务类型常量,定值:Notes.TYPE_WIDGET_4X */ @Override protected int getWidgetType() { - // 返回Notes中定义的4x小部件类型常量,用于业务层识别小部件尺寸 return Notes.TYPE_WIDGET_4X; } } \ No newline at end of file