From 59a4fd5716523eb62add048450cb3c1396ea44a4 Mon Sep 17 00:00:00 2001 From: whale Date: Sun, 23 Nov 2025 18:34:42 +0800 Subject: [PATCH 1/2] update --- doc/~$便签开源代码泛读报告.docx | Bin 0 -> 162 bytes doc/小米便签泛读报告.docx | Bin 91169 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/~$便签开源代码泛读报告.docx delete mode 100644 doc/小米便签泛读报告.docx diff --git a/doc/~$便签开源代码泛读报告.docx b/doc/~$便签开源代码泛读报告.docx new file mode 100644 index 0000000000000000000000000000000000000000..a6f19504e2ffc22146e3a8d1daa7deb07dad9627 GIT binary patch literal 162 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 a<)`9s4>KL$wRi@Kh6REA4T1Lr^#1_DerZhr -- 2.34.1 From 55d29271e28d93972bb9f9688bd7148eb8e065e6 Mon Sep 17 00:00:00 2001 From: whale Date: Tue, 23 Dec 2025 21:17:53 +0800 Subject: [PATCH 2/2] update --- src/net/micode/notes/gtask/data/MetaData.java | 69 ++- .../micode/notes/ui/AlarmAlertActivity.java | 58 ++- .../micode/notes/ui/AlarmInitReceiver.java | 23 + src/net/micode/notes/ui/AlarmReceiver.java | 11 + src/net/micode/notes/ui/DateTimePicker.java | 236 ++++++++-- .../micode/notes/ui/DateTimePickerDialog.java | 55 ++- src/net/micode/notes/ui/DropdownMenu.java | 35 ++ .../micode/notes/ui/FoldersListAdapter.java | 55 ++- src/net/micode/notes/ui/NoteEditActivity.java | 399 +++++++++++++--- src/net/micode/notes/ui/NoteEditText.java | 107 ++++- src/net/micode/notes/ui/NoteItemData.java | 204 +++++++++ .../micode/notes/ui/NotesListActivity.java | 433 +++++++++++++----- src/net/micode/notes/ui/NotesListAdapter.java | 156 ++++++- src/net/micode/notes/ui/NotesListItem.java | 57 ++- .../notes/ui/NotesPreferenceActivity.java | 72 +++ .../notes/widget/NoteWidgetProvider.java | 45 ++ .../notes/widget/NoteWidgetProvider_2x.java | 45 ++ .../notes/widget/NoteWidgetProvider_4x.java | 46 ++ 18 files changed, 1857 insertions(+), 249 deletions(-) diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java index 3a2050b..b4d8817 100644 --- a/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -25,11 +25,27 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * MetaData - 元数据类 + *

+ * 继承自Task类,用于存储Google Task的元数据信息 + * 主要用于关联本地便签和Google Task + *

+ */ public class MetaData extends Task { - private final static String TAG = MetaData.class.getSimpleName(); + private final static String TAG = MetaData.class.getSimpleName(); // 日志标签 - private String mRelatedGid = null; + private String mRelatedGid = null; // 关联的全局唯一标识符 + /** + * 设置元数据 + *

+ * 将元数据信息存储到Task的notes字段中,并设置名称为META_NOTE_NAME + *

+ * + * @param gid 关联的全局唯一标识符 + * @param metaInfo 元数据JSON对象 + */ public void setMeta(String gid, JSONObject metaInfo) { try { metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); @@ -40,15 +56,36 @@ public class MetaData extends Task { setName(GTaskStringUtils.META_NOTE_NAME); } + /** + * 获取关联的全局唯一标识符 + * + * @return 关联的全局唯一标识符 + */ public String getRelatedGid() { return mRelatedGid; } + /** + * 判断元数据是否值得保存 + *

+ * 只要notes字段不为null就值得保存 + *

+ * + * @return 是否值得保存 + */ @Override public boolean isWorthSaving() { return getNotes() != null; } + /** + * 从远程JSON对象设置元数据内容 + *

+ * 调用父类方法后,解析notes字段获取关联的GID + *

+ * + * @param js 远程JSON对象 + */ @Override public void setContentByRemoteJSON(JSONObject js) { super.setContentByRemoteJSON(js); @@ -63,17 +100,45 @@ public class MetaData extends Task { } } + /** + * 从本地JSON对象设置元数据内容 + *

+ * 该方法不应该被调用,会抛出IllegalAccessError + *

+ * + * @param js 本地JSON对象 + * @throws IllegalAccessError 调用该方法时抛出 + */ @Override public void setContentByLocalJSON(JSONObject js) { // this function should not be called throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); } + /** + * 从元数据内容生成本地JSON对象 + *

+ * 该方法不应该被调用,会抛出IllegalAccessError + *

+ * + * @return 本地JSON对象 + * @throws IllegalAccessError 调用该方法时抛出 + */ @Override public JSONObject getLocalJSONFromContent() { throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); } + /** + * 获取同步操作类型 + *

+ * 该方法不应该被调用,会抛出IllegalAccessError + *

+ * + * @param c 游标对象 + * @return 同步操作类型 + * @throws IllegalAccessError 调用该方法时抛出 + */ @Override public int getSyncAction(Cursor c) { throw new IllegalAccessError("MetaData:getSyncAction should not be called"); diff --git a/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..f5c5fb4 100644 --- a/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -40,20 +40,41 @@ import net.micode.notes.tool.DataUtils; import java.io.IOException; +/** + * 闹钟提醒活动,用于显示笔记的闹钟提醒 + */ public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + /** + * 笔记ID + */ private long mNoteId; + /** + * 笔记摘要 + */ private String mSnippet; + /** + * 摘要预览最大长度 + */ private static final int SNIPPET_PREW_MAX_LEN = 60; + /** + * 媒体播放器,用于播放闹钟铃声 + */ MediaPlayer mPlayer; + /** + * 活动创建时调用 + * @param savedInstanceState 保存的实例状态 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); 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 @@ -64,8 +85,11 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD Intent intent = getIntent(); try { + // 从意图数据中获取笔记ID mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 获取笔记摘要 mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 处理摘要长度,超过最大长度则截断并添加省略号 mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) : mSnippet; @@ -75,6 +99,7 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } mPlayer = new MediaPlayer(); + // 如果笔记存在于数据库中,显示提醒对话框并播放铃声 if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { showActionDialog(); playAlarmSound(); @@ -83,56 +108,73 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } } + /** + * 检查屏幕是否开启 + * @return 屏幕是否开启 + */ private boolean isScreenOn() { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); return pm.isScreenOn(); } + /** + * 播放闹钟铃声 + */ private void playAlarmSound() { + // 获取默认闹钟铃声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 { mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); } try { + // 设置媒体数据源并播放 mPlayer.setDataSource(this, url); mPlayer.prepare(); mPlayer.setLooping(true); mPlayer.start(); } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (SecurityException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalStateException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block 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); + // 如果屏幕已开启,添加"进入"按钮 if (isScreenOn()) { dialog.setNegativeButton(R.string.notealert_enter, this); } dialog.show().setOnDismissListener(this); } + /** + * 处理对话框点击事件 + * @param dialog 对话框对象 + * @param which 点击的按钮ID + */ public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_NEGATIVE: + // 点击"进入"按钮,跳转到笔记编辑界面 Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_VIEW); intent.putExtra(Intent.EXTRA_UID, mNoteId); @@ -143,11 +185,19 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD } } + /** + * 处理对话框关闭事件 + * @param dialog 对话框对象 + */ public void onDismiss(DialogInterface dialog) { + // 停止闹钟铃声并结束活动 stopAlarmSound(); finish(); } + /** + * 停止闹钟铃声 + */ private void stopAlarmSound() { if (mPlayer != null) { mPlayer.stop(); diff --git a/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/net/micode/notes/ui/AlarmInitReceiver.java index f221202..2e92b8f 100644 --- a/src/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -28,19 +28,37 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; +/** + * 闹钟初始化广播接收器,用于在系统启动时重新设置所有未过期的闹钟 + */ public class AlarmInitReceiver extends BroadcastReceiver { + /** + * 查询投影,包括笔记ID和提醒日期 + */ private static final String [] PROJECTION = new String [] { NoteColumns.ID, NoteColumns.ALERTED_DATE }; + /** + * 笔记ID列索引 + */ private static final int COLUMN_ID = 0; + /** + * 提醒日期列索引 + */ private static final int COLUMN_ALERTED_DATE = 1; + /** + * 接收广播时调用 + * @param context 上下文对象 + * @param intent 接收的意图 + */ @Override public void onReceive(Context context, Intent intent) { long currentDate = System.currentTimeMillis(); + // 查询所有未过期的笔记提醒 Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, PROJECTION, NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, @@ -50,15 +68,20 @@ public class AlarmInitReceiver extends BroadcastReceiver { if (c != null) { if (c.moveToFirst()) { do { + // 获取提醒日期和笔记ID long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 创建闹钟接收器意图 Intent sender = new Intent(context, AlarmReceiver.class); sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建PendingIntent PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取闹钟管理器并设置闹钟 AlarmManager alermManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); } while (c.moveToNext()); } + // 关闭游标 c.close(); } } diff --git a/src/net/micode/notes/ui/AlarmReceiver.java b/src/net/micode/notes/ui/AlarmReceiver.java index 54e503b..709be3f 100644 --- a/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/net/micode/notes/ui/AlarmReceiver.java @@ -20,11 +20,22 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +/** + * 闹钟接收器,用于接收闹钟触发事件并启动闹钟提醒活动 + */ public class AlarmReceiver extends BroadcastReceiver { + /** + * 接收广播时调用 + * @param context 上下文对象 + * @param intent 接收的意图 + */ @Override public void onReceive(Context context, Intent intent) { + // 将意图的目标类设置为闹钟提醒活动 intent.setClass(context, AlarmAlertActivity.class); + // 添加新任务标志,允许在非活动上下文启动活动 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 启动闹钟提醒活动 context.startActivity(intent); } } diff --git a/src/net/micode/notes/ui/DateTimePicker.java b/src/net/micode/notes/ui/DateTimePicker.java index 496b0cd..706003f 100644 --- a/src/net/micode/notes/ui/DateTimePicker.java +++ b/src/net/micode/notes/ui/DateTimePicker.java @@ -28,85 +28,182 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.NumberPicker; +/** + * 日期时间选择器,用于选择日期和时间 + */ public class DateTimePicker extends FrameLayout { + /** + * 默认启用状态 + */ private static final boolean DEFAULT_ENABLE_STATE = true; + /** + * 半天的小时数 + */ private static final int HOURS_IN_HALF_DAY = 12; + /** + * 全天的小时数 + */ private static final int HOURS_IN_ALL_DAY = 24; + /** + * 一周的天数 + */ private static final int DAYS_IN_ALL_WEEK = 7; + /** + * 日期选择器最小值 + */ private static final int DATE_SPINNER_MIN_VAL = 0; + /** + * 日期选择器最大值 + */ private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + /** + * 24小时制小时选择器最小值 + */ private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + /** + * 24小时制小时选择器最大值 + */ private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + /** + * 12小时制小时选择器最小值 + */ private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + /** + * 12小时制小时选择器最大值 + */ private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + /** + * 分钟选择器最小值 + */ private static final int MINUT_SPINNER_MIN_VAL = 0; + /** + * 分钟选择器最大值 + */ private static final int MINUT_SPINNER_MAX_VAL = 59; + /** + * AM/PM选择器最小值 + */ private static final int AMPM_SPINNER_MIN_VAL = 0; + /** + * AM/PM选择器最大值 + */ private static final int AMPM_SPINNER_MAX_VAL = 1; + /** + * 日期选择器 + */ private final NumberPicker mDateSpinner; + /** + * 小时选择器 + */ private final NumberPicker mHourSpinner; + /** + * 分钟选择器 + */ private final NumberPicker mMinuteSpinner; + /** + * AM/PM选择器 + */ private final NumberPicker mAmPmSpinner; + /** + * 当前日期时间 + */ private Calendar mDate; + /** + * 日期显示值数组 + */ private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + /** + * 是否为上午 + */ private boolean mIsAm; + /** + * 是否为24小时制 + */ private boolean mIs24HourView; + /** + * 是否启用 + */ private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + /** + * 是否正在初始化 + */ private boolean mInitialising; + /** + * 日期时间变化监听器 + */ private OnDateTimeChangedListener mOnDateTimeChangedListener; + /** + * 日期变化监听器 + */ 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(); } }; + /** + * 小时变化监听器 + */ private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { boolean isDateChanged = false; Calendar cal = Calendar.getInstance(); if (!mIs24HourView) { + // 12小时制处理 if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + // 从下午11点到上午12点,日期+1 cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + // 从上午12点到下午11点,日期-1 cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } + // 切换AM/PM 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(); } } else { + // 24小时制处理 if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + // 从23点到0点,日期+1 cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + // 从0点到23点,日期-1 cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } } + // 更新小时 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)); @@ -115,6 +212,9 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 分钟变化监听器 + */ private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { @@ -122,14 +222,19 @@ public class DateTimePicker extends FrameLayout { int maxValue = mMinuteSpinner.getMaxValue(); int offset = 0; if (oldVal == maxValue && newVal == minValue) { + // 从59分钟到0分钟,小时+1 offset += 1; } else if (oldVal == minValue && newVal == maxValue) { + // 从0分钟到59分钟,小时-1 offset -= 1; } if (offset != 0) { + // 更新小时 mDate.add(Calendar.HOUR_OF_DAY, offset); mHourSpinner.setValue(getCurrentHour()); + // 更新日期控件 updateDateControl(); + // 更新AM/PM状态 int newHour = getCurrentHourOfDay(); if (newHour >= HOURS_IN_HALF_DAY) { mIsAm = false; @@ -139,58 +244,100 @@ public class DateTimePicker extends FrameLayout { updateAmPmControl(); } } + // 更新分钟 mDate.set(Calendar.MINUTE, newVal); + // 通知日期时间变化 onDateTimeChanged(); } }; + /** + * AM/PM变化监听器 + */ private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 切换AM/PM mIsAm = !mIsAm; if (mIsAm) { + // 从PM切换到AM,小时-12 mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); } else { + // 从AM切换到PM,小时+12 mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); } + // 更新AM/PM控件 updateAmPmControl(); + // 通知日期时间变化 onDateTimeChanged(); } }; + /** + * 日期时间变化监听器接口 + */ public interface OnDateTimeChangedListener { + /** + * 日期时间变化时调用 + * @param view 日期时间选择器 + * @param year 年份 + * @param month 月份 + * @param dayOfMonth 日 + * @param hourOfDay 小时(24小时制) + * @param minute 分钟 + */ void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute); } + /** + * 使用当前时间初始化日期时间选择器 + * @param context 上下文 + */ public DateTimePicker(Context context) { this(context, System.currentTimeMillis()); } + /** + * 使用指定时间初始化日期时间选择器 + * @param context 上下文 + * @param date 时间戳 + */ public DateTimePicker(Context context, long date) { this(context, date, DateFormat.is24HourFormat(context)); } + /** + * 使用指定时间和24小时制标志初始化日期时间选择器 + * @param context 上下文 + * @param date 时间戳 + * @param is24HourView 是否为24小时制 + */ public DateTimePicker(Context context, long date, boolean is24HourView) { super(context); mDate = Calendar.getInstance(); mInitialising = true; mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + // 加载布局 inflate(context, R.layout.datetime_picker, this); + // 初始化日期选择器 mDateSpinner = (NumberPicker) findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + // 初始化小时选择器 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.setOnLongPressUpdateInterval(100); mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + // 初始化AM/PM选择器 String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); @@ -198,22 +345,28 @@ public class DateTimePicker extends FrameLayout { mAmPmSpinner.setDisplayedValues(stringsForAmPm); mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); - // update controls to initial state + // 更新控件到初始状态 updateDateControl(); updateHourControl(); updateAmPmControl(); + // 设置24小时制 set24HourView(is24HourView); - // set to current time + // 设置当前时间 setCurrentDate(date); + // 设置启用状态 setEnabled(isEnabled()); - // set the content descriptions + // 设置内容描述 mInitialising = false; } + /** + * 设置是否启用 + * @param enabled 是否启用 + */ @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { @@ -227,24 +380,28 @@ public class DateTimePicker extends FrameLayout { mIsEnabled = enabled; } + /** + * 获取是否启用 + * @return 是否启用 + */ @Override public boolean isEnabled() { return mIsEnabled; } /** - * Get the current date in millis + * 获取当前时间戳 * - * @return the current date in millis + * @return 当前时间戳 */ public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } /** - * Set the current date + * 设置当前时间 * - * @param date The current date in millis + * @param date 当前时间戳 */ public void setCurrentDate(long date) { Calendar cal = Calendar.getInstance(); @@ -254,13 +411,13 @@ public class DateTimePicker extends FrameLayout { } /** - * Set the current date + * 设置当前时间 * - * @param year The current year - * @param month The current month - * @param dayOfMonth The current dayOfMonth - * @param hourOfDay The current hourOfDay - * @param minute The current minute + * @param year 当前年份 + * @param month 当前月份(0-11) + * @param dayOfMonth 当前日期(1-31) + * @param hourOfDay 当前小时(根据24小时制或12小时制) + * @param minute 当前分钟(0-59) */ public void setCurrentDate(int year, int month, int dayOfMonth, int hourOfDay, int minute) { @@ -272,18 +429,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current year + * 获取当前年份 * - * @return The current year + * @return 当前年份 */ public int getCurrentYear() { return mDate.get(Calendar.YEAR); } /** - * Set current year + * 设置当前年份 * - * @param year The current year + * @param year 当前年份 */ public void setCurrentYear(int year) { if (!mInitialising && year == getCurrentYear()) { @@ -295,18 +452,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current month in the year + * 获取当前月份(0-11) * - * @return The current month in the year + * @return 当前月份(0-11) */ public int getCurrentMonth() { return mDate.get(Calendar.MONTH); } /** - * Set current month in the year + * 设置当前月份(0-11) * - * @param month The month in the year + * @param month 当前月份(0-11) */ public void setCurrentMonth(int month) { if (!mInitialising && month == getCurrentMonth()) { @@ -318,18 +475,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current day of the month + * 获取当前日期(1-31) * - * @return The day of the month + * @return 当前日期(1-31) */ public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); } /** - * Set current day of the month + * 设置当前日期(1-31) * - * @param dayOfMonth The day of the month + * @param dayOfMonth 当前日期(1-31) */ public void setCurrentDay(int dayOfMonth) { if (!mInitialising && dayOfMonth == getCurrentDay()) { @@ -341,13 +498,17 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current hour in 24 hour mode, in the range (0~23) - * @return The current hour in 24 hour mode + * 获取当前小时(根据24小时制或12小时制) + * @return 当前小时(根据24小时制或12小时制) */ public int getCurrentHourOfDay() { return mDate.get(Calendar.HOUR_OF_DAY); } + /** + * 获取当前小时(根据24小时制或12小时制) + * @return 当前小时(根据24小时制或12小时制) + */ private int getCurrentHour() { if (mIs24HourView){ return getCurrentHourOfDay(); @@ -434,35 +595,53 @@ public class DateTimePicker extends FrameLayout { updateAmPmControl(); } + /** + * 更新日期控件 + */ private void updateDateControl() { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(mDate.getTimeInMillis()); + // 设置为一周前的日期 cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); mDateSpinner.setDisplayedValues(null); + // 生成一周的日期显示值 for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { cal.add(Calendar.DAY_OF_YEAR, 1); mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); } + // 设置日期显示值 mDateSpinner.setDisplayedValues(mDateDisplayValues); + // 设置当前选中值为中间项 mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + // 刷新控件 mDateSpinner.invalidate(); } + /** + * 更新AM/PM控件 + */ private void updateAmPmControl() { if (mIs24HourView) { + // 24小时制下隐藏AM/PM选择器 mAmPmSpinner.setVisibility(View.GONE); } else { + // 12小时制下设置AM/PM值并显示 int index = mIsAm ? Calendar.AM : Calendar.PM; mAmPmSpinner.setValue(index); mAmPmSpinner.setVisibility(View.VISIBLE); } } + /** + * 更新小时控件 + */ private void updateHourControl() { if (mIs24HourView) { + // 设置24小时制的小时范围 mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); } else { + // 设置12小时制的小时范围 mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); } @@ -476,6 +655,9 @@ public class DateTimePicker extends FrameLayout { mOnDateTimeChangedListener = callback; } + /** + * 通知日期时间变化 + */ private void onDateTimeChanged() { if (mOnDateTimeChangedListener != null) { mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), diff --git a/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..344c16a 100644 --- a/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -29,51 +29,99 @@ import android.content.DialogInterface.OnClickListener; import android.text.format.DateFormat; import android.text.format.DateUtils; +/** + * 日期时间选择对话框 + */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + /** + * 当前日期时间 + */ private Calendar mDate = Calendar.getInstance(); + /** + * 是否为24小时制 + */ private boolean mIs24HourView; + /** + * 日期时间设置监听器 + */ private OnDateTimeSetListener mOnDateTimeSetListener; + /** + * 日期时间选择器 + */ private DateTimePicker mDateTimePicker; + /** + * 日期时间设置监听器接口 + */ public interface OnDateTimeSetListener { + /** + * 日期时间设置时调用 + * @param dialog 对话框 + * @param date 日期时间戳 + */ void OnDateTimeSet(AlertDialog dialog, long date); } + /** + * 构造函数 + * @param context 上下文 + * @param date 初始日期时间戳 + */ public DateTimePickerDialog(Context context, long date) { super(context); + // 初始化日期时间选择器 mDateTimePicker = new DateTimePicker(context); setView(mDateTimePicker); + // 设置日期时间变化监听器 mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { + // 更新日期时间 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()); } }); + // 设置初始日期时间 mDate.setTimeInMillis(date); mDate.set(Calendar.SECOND, 0); mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + // 设置按钮 setButton(context.getString(R.string.datetime_dialog_ok), this); setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + // 设置24小时制 set24HourView(DateFormat.is24HourFormat(this.getContext())); + // 更新标题 updateTitle(mDate.getTimeInMillis()); } + /** + * 设置是否为24小时制 + * @param is24HourView 是否为24小时制 + */ public void set24HourView(boolean is24HourView) { mIs24HourView = is24HourView; } + /** + * 设置日期时间设置监听器 + * @param callBack 监听器 + */ public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { mOnDateTimeSetListener = callBack; } + /** + * 更新标题 + * @param date 日期时间戳 + */ private void updateTitle(long date) { - int flag = + int flag = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME; @@ -81,6 +129,11 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } + /** + * 点击按钮时调用 + * @param arg0 对话框 + * @param arg1 按钮索引 + */ public void onClick(DialogInterface arg0, int arg1) { if (mOnDateTimeSetListener != null) { mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); diff --git a/src/net/micode/notes/ui/DropdownMenu.java b/src/net/micode/notes/ui/DropdownMenu.java index 613dc74..d7f7bff 100644 --- a/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/net/micode/notes/ui/DropdownMenu.java @@ -27,17 +27,39 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import net.micode.notes.R; +/** + * 下拉菜单类,用于创建和管理下拉菜单 + */ public class DropdownMenu { + /** + * 按钮控件,用于触发下拉菜单 + */ private Button mButton; + /** + * 弹出菜单 + */ private PopupMenu mPopupMenu; + /** + * 菜单对象 + */ private Menu mMenu; + /** + * 构造函数 + * @param context 上下文 + * @param button 触发下拉菜单的按钮 + * @param menuId 菜单资源ID + */ public DropdownMenu(Context context, Button button, int menuId) { mButton = button; + // 设置按钮背景为下拉图标 mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 初始化弹出菜单 mPopupMenu = new PopupMenu(context, mButton); mMenu = mPopupMenu.getMenu(); + // 加载菜单资源 mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 设置按钮点击事件,点击时显示弹出菜单 mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { mPopupMenu.show(); @@ -45,16 +67,29 @@ public class DropdownMenu { }); } + /** + * 设置下拉菜单项点击监听器 + * @param listener 监听器 + */ public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { if (mPopupMenu != null) { mPopupMenu.setOnMenuItemClickListener(listener); } } + /** + * 根据ID查找菜单项 + * @param id 菜单项ID + * @return 菜单项 + */ public MenuItem findItem(int id) { return mMenu.findItem(id); } + /** + * 设置按钮标题 + * @param title 标题文本 + */ public void setTitle(CharSequence title) { mButton.setText(title); } diff --git a/src/net/micode/notes/ui/FoldersListAdapter.java b/src/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..b2de8e0 100644 --- a/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/net/micode/notes/ui/FoldersListAdapter.java @@ -29,50 +29,103 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; +/** + * 文件夹列表适配器,用于显示文件夹列表 + */ public class FoldersListAdapter extends CursorAdapter { + /** + * 查询投影,包括文件夹ID和名称 + */ public static final String [] PROJECTION = { NoteColumns.ID, NoteColumns.SNIPPET }; + /** + * ID列索引 + */ public static final int ID_COLUMN = 0; + /** + * 名称列索引 + */ public static final int NAME_COLUMN = 1; + /** + * 构造函数 + * @param context 上下文 + * @param c 游标 + */ public FoldersListAdapter(Context context, Cursor c) { super(context, c); - // TODO Auto-generated constructor stub } + /** + * 创建新视图 + * @param context 上下文 + * @param cursor 游标 + * @param parent 父视图 + * @return 新创建的视图 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new FolderListItem(context); } + /** + * 绑定视图数据 + * @param view 视图 + * @param context 上下文 + * @param cursor 游标 + */ @Override public void bindView(View view, Context context, Cursor cursor) { 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); ((FolderListItem) view).bind(folderName); } } + /** + * 获取文件夹名称 + * @param context 上下文 + * @param position 位置 + * @return 文件夹名称 + */ public String getFolderName(Context context, int position) { Cursor cursor = (Cursor) getItem(position); return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); } + /** + * 文件夹列表项内部类 + */ private class FolderListItem extends LinearLayout { + /** + * 文件夹名称文本视图 + */ private TextView mName; + /** + * 构造函数 + * @param context 上下文 + */ public FolderListItem(Context context) { super(context); + // 加载布局 inflate(context, R.layout.folder_list_item, this); + // 初始化文本视图 mName = (TextView) findViewById(R.id.tv_folder_name); } + /** + * 绑定数据 + * @param name 文件夹名称 + */ public void bind(String name) { + // 设置文件夹名称 mName.setText(name); } } diff --git a/src/net/micode/notes/ui/NoteEditActivity.java b/src/net/micode/notes/ui/NoteEditActivity.java index 96a9ff8..b0325c3 100644 --- a/src/net/micode/notes/ui/NoteEditActivity.java +++ b/src/net/micode/notes/ui/NoteEditActivity.java @@ -72,18 +72,34 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * NoteEditActivity - 便签编辑活动 + *

+ * 该类负责处理便签的创建、编辑、保存等核心功能,支持普通文本模式和 checklist 模式 + * 实现了背景色切换、字体大小调整、提醒设置、分享等功能 + *

+ * + * @author MiCode Open Source Community + * @version 1.0 + */ public class NoteEditActivity extends Activity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { + /** + * HeadViewHolder - 便签头部视图持有者 + *

+ * 用于缓存便签头部的UI组件,提高性能 + *

+ */ 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映射到颜色常量 + */ private static final Map sBgSelectorBtnsMap = new HashMap(); static { sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); @@ -93,6 +109,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); } + /** + * 背景选择状态映射 - 将颜色常量映射到选中状态图标ID + */ private static final Map sBgSelectorSelectionMap = new HashMap(); static { sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); @@ -102,6 +121,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); } + /** + * 字体大小按钮映射 - 将UI按钮ID映射到字体大小常量 + */ private static final Map sFontSizeBtnsMap = new HashMap(); static { sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); @@ -110,6 +132,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); } + /** + * 字体大小选择状态映射 - 将字体大小常量映射到选中状态图标ID + */ private static final Map sFontSelectorSelectionMap = new HashMap(); static { sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); @@ -118,56 +143,62 @@ public class NoteEditActivity extends Activity implements OnClickListener, sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); } - private static final String TAG = "NoteEditActivity"; - - 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; - - private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + private static final String TAG = "NoteEditActivity"; // 日志标签 - private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + 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; // 当前字体大小ID - public static final String TAG_CHECKED = String.valueOf('\u221A'); - public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好键 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 快捷方式标题最大长度 - private LinearLayout mEditTextList; + public static final String TAG_CHECKED = String.valueOf('\u221A'); // 已勾选标记 + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 未勾选标记 - private String mUserQuery; - private Pattern mPattern; + private LinearLayout mEditTextList; // 编辑列表(用于checklist模式) + private String mUserQuery; // 用户查询(用于搜索高亮) + private Pattern mPattern; // 搜索模式(用于搜索高亮) + /** + * 初始化活动 onCreate 方法 + *

+ * 当活动创建时调用,设置布局并初始化活动状态 + *

+ * + * @param savedInstanceState 保存的实例状态,用于恢复活动 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setContentView(R.layout.note_edit); + // 如果没有保存的实例状态且初始化失败,则结束活动 if (savedInstanceState == null && !initActivityState(getIntent())) { finish(); return; } + // 初始化资源 initResources(); } /** - * Current activity may be killed when the memory is low. Once it is killed, for another time - * user load this activity, we should restore the former state + * 恢复活动状态 onRestoreInstanceState 方法 + *

+ * 当活动因内存不足被杀死后重新加载时,恢复之前的状态 + *

+ * + * @param savedInstanceState 保存的实例状态 */ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); + // 如果保存了实例状态且包含UID,则恢复活动 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,24 +210,30 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 初始化活动状态 + *

+ * 根据传入的意图初始化便签编辑活动,处理查看、创建、编辑便签的情况 + *

+ * + * @param intent 传入的意图,包含操作类型和数据 + * @return 初始化是否成功 + */ private boolean initActivityState(Intent intent) { - /** - * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id, - * then jump to the NotesListActivity - */ mWorkingNote = null; + + // 处理查看便签操作 if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); mUserQuery = ""; - /** - * Starting from the searched result - */ + // 从搜索结果启动 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); @@ -204,6 +241,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, finish(); return false; } else { + // 加载便签 mWorkingNote = WorkingNote.load(this, noteId); if (mWorkingNote == null) { Log.e(TAG, "load note failed with note id" + noteId); @@ -211,11 +249,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } } + // 设置软键盘模式为隐藏状态 getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - } else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { - // New note + } + // 处理创建或编辑便签操作 + else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { + // 获取参数 long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); @@ -224,7 +265,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, ResourceParser.getDefaultBgId(this)); - // Parse call-record note + // 处理通话记录便签 String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); if (callDate != 0 && phoneNumber != null) { @@ -232,6 +273,7 @@ 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) { mWorkingNote = WorkingNote.load(this, noteId); @@ -241,119 +283,193 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } } else { + // 创建新的通话记录便签 mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); mWorkingNote.convertToCallNote(phoneNumber, callDate); } } else { + // 创建普通新便签 mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, bgResId); } + // 设置软键盘模式为可见状态 getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - } else { + } + // 不支持的操作类型 + else { Log.e(TAG, "Intent not specified action, should not support"); finish(); return false; } + + // 设置设置状态变化监听器 mWorkingNote.setOnSettingStatusChangedListener(this); return true; } + /** + * 活动恢复 onResume 方法 + *

+ * 当活动恢复到前台时调用,初始化便签屏幕 + *

+ */ @Override protected void onResume() { super.onResume(); initNoteScreen(); } + /** + * 初始化便签屏幕 + *

+ * 根据当前便签状态初始化UI,包括字体大小、背景颜色、内容显示等 + *

+ */ private void initNoteScreen() { + // 设置编辑器字体大小 mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); + + // 根据便签模式显示内容 if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 切换到列表模式 switchToListMode(mWorkingNote.getContent()); } else { + // 普通文本模式,显示高亮搜索结果 mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + // 将光标定位到文本末尾 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)); - /** - * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker - * is not ready - */ + // 显示提醒头部 showAlertHeader(); } + /** + * 显示提醒头部 + *

+ * 根据便签是否有提醒以及提醒是否过期,显示相应的提醒信息 + *

+ */ private void showAlertHeader() { if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); if (time > mWorkingNote.getAlertDate()) { + // 提醒已过期 mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); } else { + // 显示相对时间 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); }; } + /** + * 处理新意图 onNewIntent 方法 + *

+ * 当活动已经存在且收到新意图时调用,重新初始化活动状态 + *

+ * + * @param intent 新的意图 + */ @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); initActivityState(intent); } + /** + * 保存实例状态 onSaveInstanceState 方法 + *

+ * 当活动即将被销毁时调用,保存当前便签状态 + *

+ * + * @param outState 用于保存状态的Bundle对象 + */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - /** - * For new note without note id, we should firstly save it to - * generate a id. If the editing note is not worth saving, there - * is no id which is equivalent to create new note - */ + + // 对于新便签,先保存生成ID if (!mWorkingNote.existInDatabase()) { saveNote(); } + + // 保存便签ID outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); } + /** + * 分发触摸事件 + *

+ * 处理屏幕触摸事件,点击外部区域关闭背景选择器和字体大小选择器 + *

+ * + * @param ev 触摸事件对象 + * @return 是否消耗了该事件 + */ @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); } + /** + * 检查触摸事件是否在指定视图范围内 + * + * @param view 要检查的视图 + * @param ev 触摸事件对象 + * @return 是否在视图范围内 + */ private boolean inRangeOfView(View view, MotionEvent ev) { int []location = new int[2]; view.getLocationOnScreen(location); int x = location[0]; int y = location[1]; + + // 检查触摸坐标是否在视图范围内 if (ev.getX() < x || ev.getX() > (x + view.getWidth()) || ev.getY() < y @@ -363,7 +479,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 初始化资源 + *

+ * 初始化UI组件,设置点击事件监听器 + *

+ */ private void initResources() { + // 初始化头部视图 mHeadViewPanel = findViewById(R.id.note_title); mNoteHeaderHolder = new HeadViewHolder(); mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); @@ -371,32 +494,44 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + + // 初始化编辑器 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); - /** - * HACKME: Fix bug of store the resource id in shared preference. - * The id may larger than the length of resources, in this case, - * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} - */ + + // 修复字体大小ID可能超出范围的问题 if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; } + + // 初始化编辑列表 mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); } + /** + * 活动暂停 onPause 方法 + *

+ * 当活动进入后台时调用,保存便签并清除设置状态 + *

+ */ @Override protected void onPause() { super.onPause(); @@ -406,6 +541,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, clearSettingState(); } + /** + * 更新小部件 + *

+ * 当便签更新时,更新对应的桌面小部件 + *

+ */ private void updateWidget() { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { @@ -425,6 +566,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, setResult(RESULT_OK, intent); } + /** + * 点击事件处理 + *

+ * 处理UI组件的点击事件,包括设置背景色、选择背景色、选择字体大小 + *

+ * + * @param v 被点击的视图 + */ public void onClick(View v) { int id = v.getId(); if (id == R.id.btn_set_bg_color) { @@ -452,6 +601,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 返回键处理 onBackPressed 方法 + *

+ * 当用户点击返回键时调用,先尝试清除设置状态,否则保存便签并返回 + *

+ */ @Override public void onBackPressed() { if(clearSettingState()) { @@ -462,6 +617,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, super.onBackPressed(); } + /** + * 清除设置状态 + *

+ * 关闭背景选择器和字体大小选择器 + *

+ * + * @return 是否清除了设置状态 + */ private boolean clearSettingState() { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { mNoteBgColorSelector.setVisibility(View.GONE); @@ -473,6 +636,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } + /** + * 背景颜色变化监听器 + *

+ * 当背景颜色变化时调用,更新UI显示 + *

+ */ public void onBackgroundColorChanged() { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.VISIBLE); @@ -480,23 +649,40 @@ public class NoteEditActivity extends Activity implements OnClickListener, mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); } + /** + * 准备选项菜单 + *

+ * 在显示菜单前调用,根据当前便签状态调整菜单项 + *

+ * + * @param menu 菜单对象 + * @return 是否显示菜单 + */ @Override public boolean onPrepareOptionsMenu(Menu menu) { if (isFinishing()) { return true; } + + // 清除设置状态 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); } + + // 根据便签模式调整列表模式菜单项标题 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 { @@ -505,6 +691,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 选项菜单点击处理 + *

+ * 处理菜单选项的点击事件,包括新建便签、删除、字体大小、列表模式、分享等 + *

+ * + * @param item 被点击的菜单项 + * @return 是否处理了该事件 + */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -512,6 +707,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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); @@ -527,24 +723,30 @@ public class NoteEditActivity extends Activity implements OnClickListener, 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: + // 切换便签模式 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(); break; case R.id.menu_alert: + // 设置提醒 setReminder(); break; case R.id.menu_delete_remind: + // 删除提醒 mWorkingNote.setAlertDate(0, false); break; default: @@ -553,6 +755,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 设置提醒 + *

+ * 显示日期时间选择对话框,设置便签提醒 + *

+ */ private void setReminder() { DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); d.setOnDateTimeSetListener(new OnDateTimeSetListener() { @@ -564,8 +772,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * Share note to apps that support {@link Intent#ACTION_SEND} action - * and {@text/plain} type + * 分享便签 + *

+ * 将便签内容分享给支持ACTION_SEND和text/plain类型的应用 + *

+ * + * @param context 上下文对象 + * @param info 要分享的内容 */ private void sendTo(Context context, String info) { Intent intent = new Intent(Intent.ACTION_SEND); @@ -574,11 +787,17 @@ public class NoteEditActivity extends Activity implements OnClickListener, context.startActivity(intent); } + /** + * 创建新便签 + *

+ * 保存当前便签,然后启动新的编辑活动 + *

+ */ private void createNewNote() { - // Firstly, save current editing notes + // 首先保存当前编辑的便签 saveNote(); - // For safety, start a new NoteEditActivity + // 安全起见,启动新的NoteEditActivity finish(); Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); @@ -586,6 +805,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, startActivity(intent); } + /** + * 删除当前便签 + *

+ * 根据是否同步模式,直接删除或移到回收站 + *

+ */ private void deleteCurrentNote() { if (mWorkingNote.existInDatabase()) { HashSet ids = new HashSet(); @@ -595,53 +820,81 @@ public class NoteEditActivity extends Activity implements OnClickListener, } else { Log.d(TAG, "Wrong note id, should not happen"); } + + // 根据同步模式处理删除 if (!isSyncMode()) { + // 非同步模式,直接删除 if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { Log.e(TAG, "Delete Note error"); } } else { + // 同步模式,移到回收站 if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { Log.e(TAG, "Move notes to trash folder error, should not happens"); } } } + + // 标记为已删除 mWorkingNote.markDeleted(true); } + /** + * 检查是否为同步模式 + *

+ * 判断当前是否已配置同步账号 + *

+ * + * @return 是否为同步模式 + */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + /** + * 提醒时间变化监听器 + *

+ * 当提醒时间变化时调用,设置或取消系统闹钟 + *

+ * + * @param date 提醒日期时间 + * @param set 是否设置提醒 + */ public void onClockAlertChanged(long date, boolean set) { - /** - * User could set clock to an unsaved note, so before setting the - * alert clock, we should save the note first - */ + // 先保存便签,确保有Note ID if (!mWorkingNote.existInDatabase()) { saveNote(); } + if (mWorkingNote.getNoteId() > 0) { + // 创建闹钟意图 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(); + + // 设置或取消闹钟 if(!set) { alarmManager.cancel(pendingIntent); } else { alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); } } else { - /** - * There is the condition that user has input nothing (the note is - * not worthy saving), we have no note id, remind the user that he - * should input something - */ + // 便签为空,无法设置提醒 Log.e(TAG, "Clock alert setting error"); showToast(R.string.error_note_empty_for_clock); } } + /** + * 小部件变化监听器 + *

+ * 当便签关联的小部件变化时调用,更新小部件 + *

+ */ public void onWidgetChanged() { updateWidget(); } diff --git a/src/net/micode/notes/ui/NoteEditText.java b/src/net/micode/notes/ui/NoteEditText.java index 2afe2a8..610623c 100644 --- a/src/net/micode/notes/ui/NoteEditText.java +++ b/src/net/micode/notes/ui/NoteEditText.java @@ -37,15 +37,39 @@ import net.micode.notes.R; import java.util.HashMap; import java.util.Map; +/** + * 笔记编辑文本框,用于处理笔记编辑中的各种功能 + */ public class NoteEditText extends EditText { + /** + * 日志标签 + */ private static final String TAG = "NoteEditText"; + /** + * 文本框索引 + */ private int mIndex; + /** + * 删除前的选择起始位置 + */ private int mSelectionStartBeforeDelete; + /** + * 电话链接前缀 + */ private static final String SCHEME_TEL = "tel:" ; + /** + * HTTP链接前缀 + */ private static final String SCHEME_HTTP = "http:" ; + /** + * 邮箱链接前缀 + */ private static final String SCHEME_EMAIL = "mailto:" ; + /** + * 链接前缀与操作资源ID的映射 + */ private static final Map sSchemaActionResMap = new HashMap(); static { sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); @@ -54,56 +78,90 @@ public class NoteEditText extends EditText { } /** - * Call by the {@link NoteEditActivity} to delete or add edit text + * 文本视图变化监听器接口,用于通知NoteEditActivity添加或删除编辑文本框 */ public interface OnTextViewChangeListener { /** - * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens - * and the text is null + * 当按下删除键且文本为空时,删除当前编辑文本框 + * @param index 文本框索引 + * @param text 文本内容 */ void onEditTextDelete(int index, String text); /** - * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} - * happen + * 当按下回车键时,在当前编辑文本框后添加新的编辑文本框 + * @param index 新文本框的索引 + * @param text 文本内容 */ void onEditTextEnter(int index, String text); /** - * Hide or show item option when text change + * 当文本变化时,隐藏或显示项选项 + * @param index 文本框索引 + * @param hasText 是否有文本 */ void onTextChange(int index, boolean hasText); } + /** + * 文本视图变化监听器 + */ private OnTextViewChangeListener mOnTextViewChangeListener; + /** + * 构造函数 + * @param context 上下文 + */ public NoteEditText(Context context) { super(context, null); mIndex = 0; } + /** + * 设置文本框索引 + * @param index 索引 + */ public void setIndex(int index) { mIndex = index; } + /** + * 设置文本视图变化监听器 + * @param listener 监听器 + */ public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } + /** + * 构造函数 + * @param context 上下文 + * @param attrs 属性集 + */ public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } + /** + * 构造函数 + * @param context 上下文 + * @param attrs 属性集 + * @param defStyle 默认样式 + */ public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub } + /** + * 处理触摸事件 + * @param event 触摸事件 + * @return 是否处理了事件 + */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - + // 计算触摸位置对应的文本偏移量 int x = (int) event.getX(); int y = (int) event.getY(); x -= getTotalPaddingLeft(); @@ -114,6 +172,7 @@ public class NoteEditText extends EditText { Layout layout = getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); + // 设置选择范围 Selection.setSelection(getText(), off); break; } @@ -121,6 +180,12 @@ public class NoteEditText extends EditText { return super.onTouchEvent(event); } + /** + * 处理按键按下事件 + * @param keyCode 按键码 + * @param event 按键事件 + * @return 是否处理了事件 + */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { @@ -130,6 +195,7 @@ public class NoteEditText extends EditText { } break; case KeyEvent.KEYCODE_DEL: + // 记录删除前的选择起始位置 mSelectionStartBeforeDelete = getSelectionStart(); break; default: @@ -138,11 +204,18 @@ public class NoteEditText extends EditText { return super.onKeyDown(keyCode, event); } + /** + * 处理按键抬起事件 + * @param keyCode 按键码 + * @param event 按键事件 + * @return 是否处理了事件 + */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { case KeyEvent.KEYCODE_DEL: if (mOnTextViewChangeListener != null) { + // 如果是第一个字符且不是第一个文本框,删除当前文本框 if (0 == mSelectionStartBeforeDelete && mIndex != 0) { mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); return true; @@ -153,6 +226,7 @@ public class NoteEditText extends EditText { break; case KeyEvent.KEYCODE_ENTER: if (mOnTextViewChangeListener != null) { + // 分割文本,在当前文本框后添加新文本框 int selectionStart = getSelectionStart(); String text = getText().subSequence(selectionStart, length()).toString(); setText(getText().subSequence(0, selectionStart)); @@ -167,9 +241,16 @@ public class NoteEditText extends EditText { return super.onKeyUp(keyCode, event); } + /** + * 处理焦点变化事件 + * @param focused 是否获得焦点 + * @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 { @@ -179,6 +260,10 @@ public class NoteEditText extends EditText { super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * 创建上下文菜单 + * @param menu 上下文菜单 + */ @Override protected void onCreateContextMenu(ContextMenu menu) { if (getText() instanceof Spanned) { @@ -188,8 +273,10 @@ public class NoteEditText extends EditText { int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); + // 获取选中区域内的URL链接 final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); if (urls.length == 1) { + // 根据链接前缀获取对应的操作资源ID int defaultResId = 0; for(String schema: sSchemaActionResMap.keySet()) { if(urls[0].getURL().indexOf(schema) >= 0) { @@ -198,14 +285,16 @@ public class NoteEditText extends EditText { } } + // 如果没有匹配的前缀,使用默认操作 if (defaultResId == 0) { defaultResId = R.string.note_link_other; } + // 添加菜单项并设置点击事件 menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - // goto a new intent + // 执行链接点击操作 urls[0].onClick(NoteEditText.this); return true; } diff --git a/src/net/micode/notes/ui/NoteItemData.java b/src/net/micode/notes/ui/NoteItemData.java index 0f5a878..f69b433 100644 --- a/src/net/micode/notes/ui/NoteItemData.java +++ b/src/net/micode/notes/ui/NoteItemData.java @@ -26,7 +26,13 @@ import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.tool.DataUtils; +/** + * 笔记项数据类,用于存储笔记列表项的各种数据 + */ public class NoteItemData { + /** + * 查询投影,包括笔记的各种属性 + */ static final String [] PROJECTION = new String [] { NoteColumns.ID, NoteColumns.ALERTED_DATE, @@ -42,40 +48,138 @@ public class NoteItemData { NoteColumns.WIDGET_TYPE, }; + /** + * ID列索引 + */ private static final int ID_COLUMN = 0; + /** + * 提醒日期列索引 + */ private static final int ALERTED_DATE_COLUMN = 1; + /** + * 背景颜色ID列索引 + */ 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; + /** + * 父ID列索引 + */ private static final int PARENT_ID_COLUMN = 7; + /** + * 摘要列索引 + */ private static final int SNIPPET_COLUMN = 8; + /** + * 类型列索引 + */ private static final int TYPE_COLUMN = 9; + /** + * 小部件ID列索引 + */ private static final int WIDGET_ID_COLUMN = 10; + /** + * 小部件类型列索引 + */ private static final int WIDGET_TYPE_COLUMN = 11; + /** + * 笔记ID + */ private long mId; + /** + * 提醒日期 + */ private long mAlertDate; + /** + * 背景颜色ID + */ private int mBgColorId; + /** + * 创建日期 + */ private long mCreatedDate; + /** + * 是否有附件 + */ private boolean mHasAttachment; + /** + * 修改日期 + */ private long mModifiedDate; + /** + * 笔记数量 + */ private int mNotesCount; + /** + * 父ID + */ private long mParentId; + /** + * 笔记摘要 + */ private String mSnippet; + /** + * 笔记类型 + */ private int mType; + /** + * 小部件ID + */ private int mWidgetId; + /** + * 小部件类型 + */ private int mWidgetType; + /** + * 联系人姓名 + */ private String mName; + /** + * 电话号码 + */ private String mPhoneNumber; + /** + * 是否为最后一项 + */ private boolean mIsLastItem; + /** + * 是否为第一项 + */ private boolean mIsFirstItem; + /** + * 是否为唯一一项 + */ private boolean mIsOnlyOneItem; + /** + * 是否为文件夹后的单个笔记 + */ private boolean mIsOneNoteFollowingFolder; + /** + * 是否为文件夹后的多个笔记 + */ private boolean mIsMultiNotesFollowingFolder; + /** + * 构造函数 + * @param context 上下文 + * @param cursor 游标 + */ public NoteItemData(Context context, Cursor cursor) { mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); @@ -86,6 +190,7 @@ public class NoteItemData { mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + // 移除勾选标记 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); mType = cursor.getInt(TYPE_COLUMN); @@ -93,6 +198,7 @@ public class NoteItemData { mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); mPhoneNumber = ""; + // 如果是通话记录文件夹,获取电话号码和联系人姓名 if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); if (!TextUtils.isEmpty(mPhoneNumber)) { @@ -106,9 +212,14 @@ public class NoteItemData { if (mName == null) { mName = ""; } + // 检查笔记在列表中的位置 checkPostion(cursor); } + /** + * 检查笔记在列表中的位置 + * @param cursor 游标 + */ private void checkPostion(Cursor cursor) { mIsLastItem = cursor.isLast() ? true : false; mIsFirstItem = cursor.isFirst() ? true : false; @@ -116,17 +227,21 @@ public class NoteItemData { mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; + // 如果是笔记类型且不是第一项,检查前一项是否为文件夹 if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { int position = 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)) { mIsMultiNotesFollowingFolder = true; } else { + // 否则设置为单个笔记跟随文件夹 mIsOneNoteFollowingFolder = true; } } + // 移动回原位置 if (!cursor.moveToNext()) { throw new IllegalStateException("cursor move to previous but can't move back"); } @@ -134,90 +249,179 @@ public class NoteItemData { } } + /** + * 是否为文件夹后的单个笔记 + * @return 是否为文件夹后的单个笔记 + */ public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } + /** + * 是否为文件夹后的多个笔记 + * @return 是否为文件夹后的多个笔记 + */ public boolean isMultiFollowingFolder() { return mIsMultiNotesFollowingFolder; } + /** + * 是否为最后一项 + * @return 是否为最后一项 + */ public boolean isLast() { return mIsLastItem; } + /** + * 获取联系人姓名 + * @return 联系人姓名 + */ public String getCallName() { return mName; } + /** + * 是否为第一项 + * @return 是否为第一项 + */ public boolean isFirst() { return mIsFirstItem; } + /** + * 是否为唯一一项 + * @return 是否为唯一一项 + */ public boolean isSingle() { return mIsOnlyOneItem; } + /** + * 获取笔记ID + * @return 笔记ID + */ public long getId() { return mId; } + /** + * 获取提醒日期 + * @return 提醒日期 + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取创建日期 + * @return 创建日期 + */ public long getCreatedDate() { return mCreatedDate; } + /** + * 是否有附件 + * @return 是否有附件 + */ public boolean hasAttachment() { return mHasAttachment; } + /** + * 获取修改日期 + * @return 修改日期 + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景颜色ID + * @return 背景颜色ID + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取父ID + * @return 父ID + */ public long getParentId() { return mParentId; } + /** + * 获取笔记数量 + * @return 笔记数量 + */ public int getNotesCount() { return mNotesCount; } + /** + * 获取文件夹ID + * @return 文件夹ID + */ public long getFolderId () { return mParentId; } + /** + * 获取笔记类型 + * @return 笔记类型 + */ public int getType() { return mType; } + /** + * 获取小部件类型 + * @return 小部件类型 + */ public int getWidgetType() { return mWidgetType; } + /** + * 获取小部件ID + * @return 小部件ID + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取笔记摘要 + * @return 笔记摘要 + */ public String getSnippet() { return mSnippet; } + /** + * 是否有提醒 + * @return 是否有提醒 + */ public boolean hasAlert() { return (mAlertDate > 0); } + /** + * 是否为通话记录 + * @return 是否为通话记录 + */ public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + /** + * 获取笔记类型 + * @param cursor 游标 + * @return 笔记类型 + */ public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } diff --git a/src/net/micode/notes/ui/NotesListActivity.java b/src/net/micode/notes/ui/NotesListActivity.java index e843aec..75988da 100644 --- a/src/net/micode/notes/ui/NotesListActivity.java +++ b/src/net/micode/notes/ui/NotesListActivity.java @@ -78,99 +78,186 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; +/** + * 小米便签列表活动类 + * 显示便签列表,支持创建、查看、编辑、删除便签和文件夹 + * 支持多选操作、搜索功能和同步功能 + */ public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { - private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; - - private static final int FOLDER_LIST_QUERY_TOKEN = 1; - - 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; - - private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; - + /** + * 查询令牌常量 + */ + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 文件夹便签列表查询令牌 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 文件夹列表查询令牌 + + /** + * 文件夹上下文菜单常量 + */ + 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; // 修改文件夹名称 + + /** + * SharedPreferences键常量 + */ + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; // 是否添加过介绍便签 + + /** + * 列表编辑状态枚举 + * - NOTE_LIST:便签列表状态 + * - SUB_FOLDER:子文件夹状态 + * - CALL_RECORD_FOLDER:通话记录文件夹状态 + */ private enum ListEditState { NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER }; + /** + * 当前列表编辑状态 + */ private ListEditState mState; + /** + * 后台查询处理器,用于异步查询数据库 + */ private BackgroundQueryHandler mBackgroundQueryHandler; + /** + * 便签列表适配器,用于显示便签列表 + */ private NotesListAdapter mNotesListAdapter; + /** + * 便签列表视图 + */ private ListView mNotesListView; + /** + * 添加新便签按钮 + */ private Button mAddNewNote; + /** + * 是否分发触摸事件 + */ private boolean mDispatch; + /** + * 原始触摸Y坐标 + */ private int mOriginY; + /** + * 分发触摸Y坐标 + */ private int mDispatchY; + /** + * 标题栏 + */ private TextView mTitleBar; + /** + * 当前文件夹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)"; - private final static int REQUEST_CODE_OPEN_NODE = 102; - private final static int REQUEST_CODE_NEW_NODE = 103; + /** + * 请求码常量 + */ + private final static int REQUEST_CODE_OPEN_NODE = 102; // 打开便签请求码 + private final static int REQUEST_CODE_NEW_NODE = 103; // 新建便签请求码 + /** + * 活动创建时调用,初始化界面和资源 + * @param savedInstanceState 保存的实例状态 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.note_list); - initResources(); + setContentView(R.layout.note_list); // 设置布局 + initResources(); // 初始化资源 /** - * Insert an introduction when user firstly use this application + * 当用户首次使用应用时插入介绍便签 */ setAppInfoFromRawRes(); } + /** + * 从其他活动返回时调用 + * @param requestCode 请求码 + * @param resultCode 结果码 + * @param data 返回的数据 + */ @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); + mNotesListAdapter.changeCursor(null); // 清空适配器游标,触发重新查询 } else { super.onActivityResult(requestCode, resultCode, data); } } + /** + * 从原始资源文件中读取介绍信息并创建介绍便签 + */ private void setAppInfoFromRawRes() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + // 如果还没有添加过介绍便签,则创建 if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { StringBuilder sb = new StringBuilder(); InputStream in = null; try { - in = getResources().openRawResource(R.raw.introduction); + in = getResources().openRawResource(R.raw.introduction); // 打开介绍资源文件 if (in != null) { InputStreamReader isr = new InputStreamReader(in); BufferedReader br = new BufferedReader(isr); char [] buf = new char[1024]; int len = 0; while ((len = br.read(buf)) > 0) { - sb.append(buf, 0, len); + sb.append(buf, 0, len); // 读取文件内容 } } else { Log.e(TAG, "Read introduction file error"); @@ -182,7 +269,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } finally { if(in != null) { try { - in.close(); + in.close(); // 关闭输入流 } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -190,12 +277,13 @@ 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(); + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); // 标记已添加介绍便签 } else { Log.e(TAG, "Save introduction note error"); return; @@ -203,34 +291,45 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 活动启动时调用 + */ @Override protected void onStart() { super.onStart(); - startAsyncNotesListQuery(); + startAsyncNotesListQuery(); // 异步查询便签列表 } + /** + * 初始化界面资源和变量 + */ 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); // 获取便签列表视图 + // 添加列表底部视图 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); - mAddNewNote = (Button) findViewById(R.id.btn_new_note); - 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(); + 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()); // 设置触摸监听器 + mDispatch = false; // 初始化触摸分发标志 + mDispatchY = 0; // 初始化分发Y坐标 + mOriginY = 0; // 初始化原始Y坐标 + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); // 获取标题栏 + mState = ListEditState.NOTE_LIST; // 初始化状态为便签列表状态 + mModeCallBack = new ModeCallback(); // 创建多选模式回调 } + /** + * 列表多选模式回调类 + * 处理多选操作,如删除、移动便签等 + */ private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; @@ -346,6 +445,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 新建便签按钮触摸监听器 + * 处理新建便签按钮的触摸事件,支持将透明区域的触摸事件分发给列表视图 + */ private class NewNoteOnTouchListener implements OnTouchListener { public boolean onTouch(View v, MotionEvent event) { @@ -357,20 +460,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt int start = screenHeight - newNoteViewHeight; int eventY = start + (int) event.getY(); /** - * Minus TitleBar's height + * 减去标题栏高度 */ if (mState == ListEditState.SUB_FOLDER) { eventY -= mTitleBar.getHeight(); start -= mTitleBar.getHeight(); } /** - * HACKME:When click the transparent part of "New Note" button, dispatch - * the event to the list view behind this button. The transparent part of - * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) - * and the line top of the button. The coordinate based on left of the "New - * Note" button. The 94 represents maximum height of the transparent part. - * Notice that, if the background of the button changes, the formula should - * also change. This is very bad, just for the UI designer's strong requirement. + * HACKME:当点击"新建便签"按钮的透明部分时,将事件分发给按钮后面的列表视图。 + * "新建便签"按钮的透明部分可以用公式y=-0.12x+94(单位:像素)和按钮顶部的线来表示。 + * 坐标基于"新建便签"按钮的左侧。94表示透明部分的最大高度。 + * 注意,如果按钮的背景发生变化,公式也应该变化。这是为了满足UI设计师的强烈要求。 */ if (event.getY() < (event.getX() * (-0.12) + 94)) { View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 @@ -408,29 +508,48 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }; + /** + * 异步查询便签列表 + */ private void startAsyncNotesListQuery() { + // 根据当前文件夹ID选择查询条件 String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; + // 开始异步查询 mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { String.valueOf(mCurrentFolderId) }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } + /** + * 后台查询处理器 + * 用于异步处理数据库查询操作 + */ private final class BackgroundQueryHandler extends AsyncQueryHandler { + /** + * 构造方法 + * @param contentResolver 内容解析器 + */ public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver); } + /** + * 查询完成时回调 + * @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); + case FOLDER_NOTE_LIST_QUERY_TOKEN: // 文件夹便签列表查询完成 + mNotesListAdapter.changeCursor(cursor); // 更新适配器游标 break; - case FOLDER_LIST_QUERY_TOKEN: + case FOLDER_LIST_QUERY_TOKEN: // 文件夹列表查询完成 if (cursor != null && cursor.getCount() > 0) { - showFolderListMenu(cursor); + showFolderListMenu(cursor); // 显示文件夹列表菜单 } else { Log.e(TAG, "Query folder failed"); } @@ -441,48 +560,64 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 显示文件夹列表菜单 + * @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() { public void onClick(DialogInterface dialog, int which) { + // 批量移动选中的便签到目标文件夹 DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + // 显示移动成功提示 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(); // 显示对话框 } + /** + * 创建新便签 + */ 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); - this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + 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); // 启动活动 } + /** + * 批量删除便签 + */ private void batchDelete() { new AsyncTask>() { + /** + * 后台执行删除操作 + * @param unused 未使用的参数 + * @return 关联的小部件集合 + */ protected HashSet doInBackground(Void... unused) { - HashSet widgets = mNotesListAdapter.getSelectedWidget(); + HashSet widgets = mNotesListAdapter.getSelectedWidget(); // 获取选中便签关联的小部件 if (!isSyncMode()) { - // if not synced, delete notes directly + // 如果不是同步模式,直接删除便签 if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter .getSelectedItemIds())) { } else { Log.e(TAG, "Delete notes error, should not happens"); } } else { - // in sync mode, we'll move the deleted note into the trash - // folder + // 如果是同步模式,将便签移到回收站 if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { Log.e(TAG, "Move notes to trash folder error, should not happens"); @@ -491,8 +626,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return widgets; } + /** + * 后台操作完成后执行 + * @param widgets 关联的小部件集合 + */ @Override protected void onPostExecute(HashSet widgets) { + // 更新关联的小部件 if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -501,12 +641,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } } - mModeCallBack.finishActionMode(); + mModeCallBack.finishActionMode(); // 结束多选模式 } }.execute(); } + /** + * 删除文件夹 + * @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; @@ -514,15 +659,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt HashSet ids = new HashSet(); ids.add(folderId); + // 获取文件夹中便签关联的小部件 HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); if (!isSyncMode()) { - // if not synced, delete folder directly + // 如果不是同步模式,直接删除文件夹 DataUtils.batchDeleteNotes(mContentResolver, ids); } else { - // in sync mode, we'll move the deleted folder into the trash folder + // 如果是同步模式,将文件夹移到回收站 DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); } + // 更新关联的小部件 if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -533,40 +680,56 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 打开便签进行查看或编辑 + * @param data 便签数据项 + */ private void openNode(NoteItemData data) { - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, data.getId()); - this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + 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); // 启动活动 } + /** + * 打开文件夹,显示文件夹中的便签 + * @param data 文件夹数据项 + */ private void openFolder(NoteItemData data) { - mCurrentFolderId = data.getId(); - startAsyncNotesListQuery(); + mCurrentFolderId = data.getId(); // 设置当前文件夹ID + startAsyncNotesListQuery(); // 异步查询文件夹中的便签 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { - mState = ListEditState.CALL_RECORD_FOLDER; - mAddNewNote.setVisibility(View.GONE); + mState = ListEditState.CALL_RECORD_FOLDER; // 设置状态为通话记录文件夹 + mAddNewNote.setVisibility(View.GONE); // 隐藏新建便签按钮 } else { - mState = ListEditState.SUB_FOLDER; + 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 点击的视图 + */ public void onClick(View v) { switch (v.getId()) { - case R.id.btn_new_note: - createNewNote(); + case R.id.btn_new_note: // 点击新建便签按钮 + createNewNote(); // 创建新便签 break; default: break; } } + /** + * 显示输入法 + */ private void showSoftInput() { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { @@ -574,74 +737,92 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 隐藏输入法 + * @param view 视图对象 + */ private void hideSoftInput(View view) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } + /** + * 显示创建或修改文件夹对话框 + * @param create 是否创建新文件夹,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(); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); // 加载对话框布局 + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); // 获取文件夹名称输入框 + showSoftInput(); // 显示输入法 + + // 设置对话框标题和输入框内容 if (!create) { if (mFocusNoteDataItem != null) { - etName.setText(mFocusNoteDataItem.getSnippet()); - builder.setTitle(getString(R.string.menu_folder_change_name)); + etName.setText(mFocusNoteDataItem.getSnippet()); // 设置现有文件夹名称 + builder.setTitle(getString(R.string.menu_folder_change_name)); // 设置标题为修改文件夹名称 } else { Log.e(TAG, "The long click data item is null"); return; } } else { - etName.setText(""); - builder.setTitle(this.getString(R.string.menu_create_folder)); + etName.setText(""); // 清空输入框 + builder.setTitle(this.getString(R.string.menu_create_folder)); // 设置标题为创建文件夹 } - builder.setPositiveButton(android.R.string.ok, null); + 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); + 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); - String name = etName.getText().toString(); + 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); + values.put(NoteColumns.SNIPPET, name); // 设置新名称 + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 设置类型为文件夹 + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地修改 + // 更新数据库 mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] { String.valueOf(mFocusNoteDataItem.getId()) }); } } 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); } /** - * When the name edit text is null, disable the positive button + * 当文件夹名称输入框为空时,禁用确定按钮 */ etName.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) { @@ -651,9 +832,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt public void onTextChanged(CharSequence s, int start, int before, int count) { if (TextUtils.isEmpty(etName.getText())) { - positive.setEnabled(false); + positive.setEnabled(false); // 输入框为空,禁用确定按钮 } else { - positive.setEnabled(true); + positive.setEnabled(true); // 输入框不为空,启用确定按钮 } } @@ -664,32 +845,42 @@ 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); + 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); - mTitleBar.setVisibility(View.GONE); - startAsyncNotesListQuery(); + case CALL_RECORD_FOLDER: // 通话记录文件夹状态 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 返回根文件夹 + mState = ListEditState.NOTE_LIST; // 设置状态为便签列表 + mAddNewNote.setVisibility(View.VISIBLE); // 显示新建便签按钮 + mTitleBar.setVisibility(View.GONE); // 隐藏标题栏 + startAsyncNotesListQuery(); // 异步查询便签列表 break; - case NOTE_LIST: - super.onBackPressed(); + case NOTE_LIST: // 便签列表状态 + super.onBackPressed(); // 执行默认返回操作 break; default: break; } } + /** + * 更新小部件 + * @param appWidgetId 小部件ID + * @param appWidgetType 小部件类型 + */ private void updateWidget(int appWidgetId, int appWidgetType) { - Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + 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) { @@ -701,10 +892,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { appWidgetId - }); + }); // 设置小部件ID - sendBroadcast(intent); - setResult(RESULT_OK, intent); + sendBroadcast(intent); // 发送广播更新小部件 + setResult(RESULT_OK, intent); // 设置结果 } private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { diff --git a/src/net/micode/notes/ui/NotesListAdapter.java b/src/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..2e715da 100644 --- a/src/net/micode/notes/ui/NotesListAdapter.java +++ b/src/net/micode/notes/ui/NotesListAdapter.java @@ -31,18 +31,41 @@ import java.util.HashSet; import java.util.Iterator; +/** + * NotesListAdapter - 便签列表适配器 + *

+ * 负责将数据库中的便签数据绑定到列表视图,支持多选模式和小部件属性管理 + *

+ * + * @author MiCode Open Source Community + * @version 1.0 + */ public class NotesListAdapter extends CursorAdapter { - private static final String TAG = "NotesListAdapter"; - private Context mContext; - private HashMap mSelectedIndex; - private int mNotesCount; - private boolean mChoiceMode; + private static final String TAG = "NotesListAdapter"; // 日志标签 + private Context mContext; // 上下文对象 + private HashMap mSelectedIndex; // 选中项索引映射 + private int mNotesCount; // 便签数量 + private boolean mChoiceMode; // 是否为选择模式 + /** + * 小部件属性类 + *

+ * 存储小部件的ID和类型信息 + *

+ */ public static class AppWidgetAttribute { - public int widgetId; - public int widgetType; + public int widgetId; // 小部件ID + public int widgetType; // 小部件类型 }; + /** + * 构造函数 + *

+ * 初始化适配器,设置上下文和选中项映射 + *

+ * + * @param context 上下文对象 + */ public NotesListAdapter(Context context) { super(context, null); mSelectedIndex = new HashMap(); @@ -50,11 +73,32 @@ public class NotesListAdapter extends CursorAdapter { mNotesCount = 0; } + /** + * 创建新视图 + *

+ * 当需要新视图时调用,创建一个新的NotesListItem + *

+ * + * @param context 上下文对象 + * @param cursor 游标对象 + * @param parent 父视图 + * @return 新创建的视图 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new NotesListItem(context); } + /** + * 绑定视图 + *

+ * 将游标数据绑定到视图上 + *

+ * + * @param view 要绑定的视图 + * @param context 上下文对象 + * @param cursor 游标对象,包含数据 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof NotesListItem) { @@ -64,20 +108,53 @@ public class NotesListAdapter extends CursorAdapter { } } + /** + * 设置选中项 + *

+ * 设置指定位置的项是否被选中,并通知数据变化 + *

+ * + * @param position 位置索引 + * @param checked 是否选中 + */ public void setCheckedItem(final int position, final boolean checked) { mSelectedIndex.put(position, checked); notifyDataSetChanged(); } + /** + * 是否为选择模式 + *

+ * 获取当前是否处于选择模式 + *

+ * + * @return 是否为选择模式 + */ public boolean isInChoiceMode() { return mChoiceMode; } + /** + * 设置选择模式 + *

+ * 设置适配器的选择模式,清除现有选中状态 + *

+ * + * @param mode 是否为选择模式 + */ public void setChoiceMode(boolean mode) { mSelectedIndex.clear(); mChoiceMode = mode; } + /** + * 全选/取消全选 + *

+ * 选中或取消选中所有便签 + *

+ * + * @param checked 是否选中 + */ public void selectAll(boolean checked) { Cursor cursor = getCursor(); for (int i = 0; i < getCount(); i++) { @@ -89,6 +166,14 @@ public class NotesListAdapter extends CursorAdapter { } } + /** + * 获取选中项ID + *

+ * 获取所有选中项的ID集合 + *

+ * + * @return 选中项ID集合 + */ public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { @@ -105,6 +190,14 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取选中小部件 + *

+ * 获取所有选中项的小部件属性集合 + *

+ * + * @return 选中小部件属性集合 + */ public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { @@ -116,9 +209,7 @@ public class NotesListAdapter extends CursorAdapter { widget.widgetId = item.getWidgetId(); widget.widgetType = item.getWidgetType(); itemSet.add(widget); - /** - * Don't close cursor here, only the adapter could close it - */ + // 不要在这里关闭游标,只有适配器才能关闭它 } else { Log.e(TAG, "Invalid cursor"); return null; @@ -128,6 +219,14 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取选中项数量 + *

+ * 获取当前选中项的数量 + *

+ * + * @return 选中项数量 + */ public int getSelectedCount() { Collection values = mSelectedIndex.values(); if (null == values) { @@ -143,11 +242,28 @@ public class NotesListAdapter extends CursorAdapter { return count; } + /** + * 是否全选 + *

+ * 检查是否所有便签都已被选中 + *

+ * + * @return 是否全选 + */ public boolean isAllSelected() { int checkedCount = getSelectedCount(); return (checkedCount != 0 && checkedCount == mNotesCount); } + /** + * 是否选中项 + *

+ * 检查指定位置的项是否被选中 + *

+ * + * @param position 位置索引 + * @return 是否选中 + */ public boolean isSelectedItem(final int position) { if (null == mSelectedIndex.get(position)) { return false; @@ -155,18 +271,38 @@ public class NotesListAdapter extends CursorAdapter { return mSelectedIndex.get(position); } + /** + * 内容变化监听器 + *

+ * 当内容变化时调用,重新计算便签数量 + *

+ */ @Override protected void onContentChanged() { super.onContentChanged(); calcNotesCount(); } + /** + * 改变游标 + *

+ * 当游标变化时调用,重新计算便签数量 + *

+ * + * @param cursor 新的游标对象 + */ @Override public void changeCursor(Cursor cursor) { super.changeCursor(cursor); calcNotesCount(); } + /** + * 计算便签数量 + *

+ * 遍历游标,计算便签数量 + *

+ */ private void calcNotesCount() { mNotesCount = 0; for (int i = 0; i < getCount(); i++) { diff --git a/src/net/micode/notes/ui/NotesListItem.java b/src/net/micode/notes/ui/NotesListItem.java index 1221e80..7afd3c3 100644 --- a/src/net/micode/notes/ui/NotesListItem.java +++ b/src/net/micode/notes/ui/NotesListItem.java @@ -30,17 +30,44 @@ import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser.NoteItemBgResources; -public class NotesListItem extends LinearLayout { +/** + * 笔记列表项,用于显示笔记列表中的单个笔记项 + */ +public class NotesListItem extends LinearLayout { + /** + * 提醒图标 + */ private ImageView mAlert; + /** + * 标题文本 + */ private TextView mTitle; + /** + * 时间文本 + */ private TextView mTime; + /** + * 联系人姓名文本 + */ private TextView mCallName; + /** + * 笔记项数据 + */ private NoteItemData mItemData; + /** + * 选择复选框 + */ private CheckBox mCheckBox; + /** + * 构造函数 + * @param context 上下文 + */ public NotesListItem(Context context) { super(context); + // 加载布局 inflate(context, R.layout.note_item, this); + // 初始化控件 mAlert = (ImageView) findViewById(R.id.iv_alert_icon); mTitle = (TextView) findViewById(R.id.tv_title); mTime = (TextView) findViewById(R.id.tv_time); @@ -48,7 +75,15 @@ public class NotesListItem extends LinearLayout { mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } + /** + * 绑定数据 + * @param context 上下文 + * @param data 笔记项数据 + * @param choiceMode 是否为选择模式 + * @param checked 是否选中 + */ 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); @@ -57,7 +92,9 @@ public class NotesListItem extends LinearLayout { } mItemData = data; + // 根据不同类型的笔记设置不同的显示内容 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 通话记录文件夹 mCallName.setVisibility(View.GONE); mAlert.setVisibility(View.VISIBLE); mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); @@ -65,10 +102,12 @@ public class NotesListItem extends LinearLayout { + context.getString(R.string.format_folder_files_count, data.getNotesCount())); mAlert.setImageResource(R.drawable.call_record); } 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())); + // 设置提醒图标 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); @@ -76,16 +115,20 @@ public class NotesListItem extends LinearLayout { mAlert.setVisibility(View.GONE); } } else { + // 普通笔记或文件夹 mCallName.setVisibility(View.GONE); mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); if (data.getType() == Notes.TYPE_FOLDER) { + // 文件夹 mTitle.setText(data.getSnippet() + context.getString(R.string.format_folder_files_count, data.getNotesCount())); mAlert.setVisibility(View.GONE); } else { + // 普通笔记 mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 设置提醒图标 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); @@ -94,14 +137,21 @@ public class NotesListItem extends LinearLayout { } } } + // 设置修改时间 mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + // 设置背景 setBackground(data); } + /** + * 设置背景 + * @param data 笔记项数据 + */ private void setBackground(NoteItemData data) { int id = data.getBgColorId(); if (data.getType() == Notes.TYPE_NOTE) { + // 根据笔记在列表中的位置设置不同的背景 if (data.isSingle() || data.isOneFollowingFolder()) { setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); } else if (data.isLast()) { @@ -112,10 +162,15 @@ public class NotesListItem extends LinearLayout { setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); } } else { + // 文件夹背景 setBackgroundResource(NoteItemBgResources.getFolderBgRes()); } } + /** + * 获取笔记项数据 + * @return 笔记项数据 + */ public NoteItemData getItemData() { return mItemData; } diff --git a/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/net/micode/notes/ui/NotesPreferenceActivity.java index 07c5f7e..d043fcc 100644 --- a/src/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -48,27 +48,44 @@ import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.remote.GTaskSyncService; +/** + * 笔记应用的偏好设置活动,用于管理同步账户、背景颜色等设置 + */ public class NotesPreferenceActivity extends PreferenceActivity { + /** 偏好设置文件名 */ public static final String PREFERENCE_NAME = "notes_preferences"; + /** 同步账户名的偏好键 */ public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + /** 最后同步时间的偏好键 */ public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + /** 设置背景颜色的偏好键 */ public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + /** 同步账户类别的偏好键 */ private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + /** 权限过滤器键 */ private static final String AUTHORITIES_FILTER_KEY = "authorities"; + /** 账户设置类别 */ private PreferenceCategory mAccountCategory; + /** GTask同步广播接收器 */ private GTaskReceiver mReceiver; + /** 原始账户列表,用于检测新添加的账户 */ private Account[] mOriAccounts; + /** 是否添加了新账户 */ private boolean mHasAddedAccount; + /** + * 创建活动,初始化界面元素和接收器 + * @param icicle 保存的活动状态 + */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -88,6 +105,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { getListView().addHeaderView(header, null, true); } + /** + * 恢复活动,检查新添加的账户并刷新界面 + */ @Override protected void onResume() { super.onResume(); @@ -116,6 +136,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { refreshUI(); } + /** + * 销毁活动,取消注册接收器 + */ @Override protected void onDestroy() { if (mReceiver != null) { @@ -124,6 +147,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { super.onDestroy(); } + /** + * 加载账户偏好设置,创建账户选择项 + */ private void loadAccountPreference() { mAccountCategory.removeAll(); @@ -154,6 +180,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { mAccountCategory.addPreference(accountPref); } + /** + * 加载同步按钮,设置其状态和同步时间显示 + */ private void loadSyncButton() { Button syncButton = (Button) findViewById(R.id.preference_sync_button); TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); @@ -193,11 +222,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 刷新UI,重新加载账户偏好设置和同步按钮 + */ private void refreshUI() { loadAccountPreference(); loadSyncButton(); } + /** + * 显示选择账户的对话框,允许用户选择或添加Google账户 + */ private void showSelectAccountAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -254,6 +289,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { }); } + /** + * 显示更改账户的确认对话框,提示用户更改账户的风险 + */ private void showChangeAccountConfirmAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -283,11 +321,19 @@ public class NotesPreferenceActivity extends PreferenceActivity { dialogBuilder.show(); } + /** + * 获取设备上的所有Google账户 + * @return Google账户数组 + */ private Account[] getGoogleAccounts() { AccountManager accountManager = AccountManager.get(this); return accountManager.getAccountsByType("com.google"); } + /** + * 设置同步账户,并清理相关同步信息 + * @param account 账户名 + */ private void setSyncAccount(String account) { if (!getSyncAccountName(this).equals(account)) { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -318,6 +364,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 移除同步账户,并清理相关同步信息 + */ private void removeSyncAccount() { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); @@ -340,12 +389,22 @@ public class NotesPreferenceActivity extends PreferenceActivity { }).start(); } + /** + * 获取同步账户名 + * @param context 上下文 + * @return 账户名 + */ public static String getSyncAccountName(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); } + /** + * 设置最后同步时间 + * @param context 上下文 + * @param time 同步时间 + */ public static void setLastSyncTime(Context context, long time) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -354,12 +413,20 @@ public class NotesPreferenceActivity extends PreferenceActivity { editor.commit(); } + /** + * 获取最后同步时间 + * @param context 上下文 + * @return 同步时间 + */ public static long getLastSyncTime(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); } + /** + * GTask同步广播接收器,用于接收同步状态变化的广播 + */ private class GTaskReceiver extends BroadcastReceiver { @Override @@ -374,6 +441,11 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 处理菜单项点击事件 + * @param item 菜单项 + * @return 是否处理了事件 + */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: diff --git a/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/net/micode/notes/widget/NoteWidgetProvider.java index ec6f819..3f54f75 100644 --- a/src/net/micode/notes/widget/NoteWidgetProvider.java +++ b/src/net/micode/notes/widget/NoteWidgetProvider.java @@ -32,19 +32,32 @@ import net.micode.notes.tool.ResourceParser; import net.micode.notes.ui.NoteEditActivity; import net.micode.notes.ui.NotesListActivity; +/** + * 笔记应用部件的抽象基类,定义了部件的基本行为和接口 + */ public abstract class NoteWidgetProvider extends AppWidgetProvider { + /** 查询投影字段,包含笔记ID、背景颜色ID和摘要 */ public static final String [] PROJECTION = new String [] { NoteColumns.ID, NoteColumns.BG_COLOR_ID, NoteColumns.SNIPPET }; + /** 投影字段中的ID列索引 */ public static final int COLUMN_ID = 0; + /** 投影字段中的背景颜色ID列索引 */ public static final int COLUMN_BG_COLOR_ID = 1; + /** 投影字段中的摘要列索引 */ public static final int COLUMN_SNIPPET = 2; + /** 日志标签 */ private static final String TAG = "NoteWidgetProvider"; + /** + * 当部件被删除时调用,更新数据库中相关笔记的widget_id为无效值 + * @param context 上下文 + * @param appWidgetIds 被删除的部件ID数组 + */ @Override public void onDeleted(Context context, int[] appWidgetIds) { ContentValues values = new ContentValues(); @@ -57,6 +70,12 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { } } + /** + * 获取特定widget ID对应的笔记信息 + * @param context 上下文 + * @param widgetId 部件ID + * @return 包含笔记信息的游标 + */ private Cursor getNoteWidgetInfo(Context context, int widgetId) { return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, PROJECTION, @@ -65,10 +84,23 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { null); } + /** + * 更新部件,默认不使用隐私模式 + * @param context 上下文 + * @param appWidgetManager 部件管理器 + * @param appWidgetIds 要更新的部件ID数组 + */ protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { update(context, appWidgetManager, appWidgetIds, false); } + /** + * 更新部件,支持隐私模式 + * @param context 上下文 + * @param appWidgetManager 部件管理器 + * @param appWidgetIds 要更新的部件ID数组 + * @param privacyMode 是否使用隐私模式 + */ private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, boolean privacyMode) { for (int i = 0; i < appWidgetIds.length; i++) { @@ -124,9 +156,22 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { } } + /** + * 根据背景颜色ID获取对应的资源ID + * @param bgId 背景颜色ID + * @return 资源ID + */ protected abstract int getBgResourceId(int bgId); + /** + * 获取部件布局ID + * @return 布局ID + */ protected abstract int getLayoutId(); + /** + * 获取部件类型 + * @return 部件类型 + */ protected abstract int getWidgetType(); } diff --git a/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java index adcb2f7..4337a5c 100644 --- a/src/net/micode/notes/widget/NoteWidgetProvider_2x.java +++ b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java @@ -24,22 +24,67 @@ import net.micode.notes.data.Notes; import net.micode.notes.tool.ResourceParser; +/** + * NoteWidgetProvider_2x - 2x大小便签小部件提供者 + *

+ * 处理2x大小的便签小部件更新、布局和背景资源 + * 继承自NoteWidgetProvider,实现了特定大小的小部件配置 + *

+ * + * @author MiCode Open Source Community + * @version 1.0 + */ public class NoteWidgetProvider_2x extends NoteWidgetProvider { + /** + * 更新小部件 + *

+ * 当小部件需要更新时调用,委托给父类的update方法 + *

+ * + * @param context 上下文对象 + * @param appWidgetManager 小部件管理器 + * @param appWidgetIds 要更新的小部件ID数组 + */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.update(context, appWidgetManager, appWidgetIds); } + /** + * 获取布局ID + *

+ * 返回2x小部件的布局资源ID + *

+ * + * @return 布局资源ID + */ @Override protected int getLayoutId() { return R.layout.widget_2x; } + /** + * 获取背景资源ID + *

+ * 根据背景ID获取2x小部件的背景资源 + *

+ * + * @param bgId 背景ID + * @return 背景资源ID + */ @Override protected int getBgResourceId(int bgId) { return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); } + /** + * 获取小部件类型 + *

+ * 返回2x小部件的类型常量 + *

+ * + * @return 小部件类型 + */ @Override protected int getWidgetType() { return Notes.TYPE_WIDGET_2X; diff --git a/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java index c12a02e..3bb7835 100644 --- a/src/net/micode/notes/widget/NoteWidgetProvider_4x.java +++ b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java @@ -24,21 +24,67 @@ import net.micode.notes.data.Notes; import net.micode.notes.tool.ResourceParser; +/** + * NoteWidgetProvider_4x - 4x大小便签小部件提供者 + *

+ * 处理4x大小的便签小部件更新、布局和背景资源 + * 继承自NoteWidgetProvider,实现了特定大小的小部件配置 + *

+ * + * @author MiCode Open Source Community + * @version 1.0 + */ public class NoteWidgetProvider_4x extends NoteWidgetProvider { + /** + * 更新小部件 + *

+ * 当小部件需要更新时调用,委托给父类的update方法 + *

+ * + * @param context 上下文对象 + * @param appWidgetManager 小部件管理器 + * @param appWidgetIds 要更新的小部件ID数组 + */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.update(context, appWidgetManager, appWidgetIds); } + /** + * 获取布局ID + *

+ * 返回4x小部件的布局资源ID + *

+ * + * @return 布局资源ID + */ + @Override protected int getLayoutId() { return R.layout.widget_4x; } + /** + * 获取背景资源ID + *

+ * 根据背景ID获取4x小部件的背景资源 + *

+ * + * @param bgId 背景ID + * @return 背景资源ID + */ @Override protected int getBgResourceId(int bgId) { return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); } + /** + * 获取小部件类型 + *

+ * 返回4x小部件的类型常量 + *

+ * + * @return 小部件类型 + */ @Override protected int getWidgetType() { return Notes.TYPE_WIDGET_4X; -- 2.34.1