From bd5168322bd25610329e01e6bfc0adb5ae3bb3d9 Mon Sep 17 00:00:00 2001 From: ywzx <2965788399@qq.com> Date: Sat, 21 Dec 2024 15:20:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DateTimePicker.java | 511 ++++++++++++++++ DateTimePickerDialog.java | 105 ++++ DropdownMenu.java | 61 ++ FoldersListAdapter.java | 66 ++ NoteEditActivity.java | 1113 ++++++++++++++++++++++++++++++++++ NoteEditText.java | 239 ++++++++ NoteItemData.java | 121 ++++ NotesListActivity.java | 1026 +++++++++++++++++++++++++++++++ NotesListAdapter.java | 203 +++++++ NotesListItem.java | 147 +++++ NotesPreferenceActivity.java | 477 +++++++++++++++ 11 files changed, 4069 insertions(+) create mode 100644 DateTimePicker.java create mode 100644 DateTimePickerDialog.java create mode 100644 DropdownMenu.java create mode 100644 FoldersListAdapter.java create mode 100644 NoteEditActivity.java create mode 100644 NoteEditText.java create mode 100644 NoteItemData.java create mode 100644 NotesListActivity.java create mode 100644 NotesListAdapter.java create mode 100644 NotesListItem.java create mode 100644 NotesPreferenceActivity.java diff --git a/DateTimePicker.java b/DateTimePicker.java new file mode 100644 index 0000000..35e2f13 --- /dev/null +++ b/DateTimePicker.java @@ -0,0 +1,511 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +// 自定义的日期时间选择器类,继承自FrameLayout +public class DateTimePicker extends FrameLayout { + + // 默认启用状态 + private static final boolean DEFAULT_ENABLE_STATE = true; + + // 半天和整天的小时数,一周的天数,以及日期、小时、分钟和AM/PM选择器的最小和最大值 + 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; + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + 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; + private static final int AMPM_SPINNER_MIN_VAL = 0; + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // NumberPicker组件用于选择日期、小时、分钟和(在12小时模式下)上午/下午 + private final NumberPicker mDateSpinner; + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mAmPmSpinner; + // 用于存储和计算日期的Calendar对象 + 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(); + } + }; + + // AM/PM选择器值改变时的监听器 +private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 切换上午/下午的状态 + mIsAm = !mIsAm; + // 根据上午/下午的状态调整日期中的小时数 + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); // 如果是上午,减去12小时 + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); // 如果是下午,加上12小时 + } + // 更新AM/PM控件的显示 + updateAmPmControl(); + // 通知日期时间已改变 + onDateTimeChanged(); + } +}; + +// 日期时间改变时的回调接口 +public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); // 当日期时间改变时调用此方法 +} + +// 构造方法:仅接收上下文 +public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); // 调用下一个构造方法,使用当前时间作为初始时间 +} + +// 构造方法:接收上下文和初始日期(以毫秒为单位的时间戳) +public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); // 调用下一个构造方法,根据系统设置决定是否使用24小时制 +} + +// 构造方法:接收上下文、初始日期和是否使用24小时制的标志 +public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); // 调用父类(FrameLayout)的构造方法 + mDate = Calendar.getInstance(); // 初始化Calendar对象 + 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); + // ...(省略了小时选择器的监听器设置,可能在后续代码中) + + // 初始化分钟选择器并设置监听器 + 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); + mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL); + mAmPmSpinner.setDisplayedValues(stringsForAmPm); // 设置显示的上午/下午字符串 + mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); // 设置监听器 + + // 更新控件到初始状态 + updateDateControl(); + updateHourControl(); // 方法在后续代码中定义,用于更新小时控件 + updateAmPmControl(); // 方法在后续代码中定义,用于更新AM/PM控件 + + // 设置是否使用24小时制 + set24HourView(is24HourView); + + // 设置到当前时间 + setCurrentDate(date); // 方法在后续代码中定义,用于设置当前日期时间 + + // 设置启用状态 + setEnabled(isEnabled()); // 这里的isEnabled()可能是指mIsEnabled成员变量,但更可能是调用父类方法 + + // 设置内容描述(用于无障碍功能) + // ...(可能省略了设置内容描述的代码) + + mInitialising = false; // 标记初始化完成 +} + + // 重写setEnabled方法,用于设置组件的启用状态 +@Override +public void setEnabled(boolean enabled) { + // 如果当前状态与要设置的状态相同,则直接返回,不做任何操作 + if (mIsEnabled == enabled) { + return; + } + // 调用父类的setEnabled方法,设置当前组件的启用状态 + super.setEnabled(enabled); + // 设置日期、分钟、小时和AM/PM选择器的启用状态 + mDateSpinner.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + mHourSpinner.setEnabled(enabled); + mAmPmSpinner.setEnabled(enabled); + // 更新成员变量mIsEnabled,记录当前组件的启用状态 + mIsEnabled = enabled; +} + +// 重写isEnabled方法,用于获取组件的启用状态 +@Override +public boolean isEnabled() { + return mIsEnabled; +} + +/** + * 获取当前日期的时间戳(毫秒) + * + * @return 当前日期的时间戳(毫秒) + */ +public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); +} + +/** + * 设置当前日期 + * + * @param date 当前日期的时间戳(毫秒) + */ +public void setCurrentDate(long date) { + // 创建一个Calendar实例,并根据传入的时间戳设置其时间 + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(date); + // 调用另一个setCurrentDate方法,传入年、月、日、小时和分钟来设置日期 + setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); +} + +/** + * 设置当前日期 + * + * @param year 当前年份 + * @param month 当前月份(从0开始,0代表1月) + * @param dayOfMonth 当前月份中的天数 + * @param hourOfDay 当前小时(24小时制) + * @param minute 当前分钟 + */ +public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 分别设置年、月、日、小时和分钟 + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); +} + +/** + * 获取当前年份 + * + * @return 当前年份 + */ +public int getCurrentYear() { + return mDate.get(Calendar.YEAR); +} + +/** + * 设置当前年份 + * + * @param year 要设置的年份 + */ +public void setCurrentYear(int year) { + // 如果不是在初始化阶段且年份未改变,则直接返回 + if (!mInitialising && year == getCurrentYear()) { + return; + } + // 使用Calendar的set方法设置年份 + mDate.set(Calendar.YEAR, year); + // 更新日期控件的显示 + updateDateControl(); + // 通知日期时间已更改 + onDateTimeChanged(); +} + +/** + * 获取当前月份(在一年中的月份,从0开始,0代表1月) + * + * @return 当前月份 + */ +public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); +} + + /** + * Set current month in the year + * + * @param month The month in the year + */ + /** + * 设置当前月份 + * + * @param month 要设置的月份(从0开始,0代表1月) + */ +public void setCurrentMonth(int month) { + // 如果不是在初始化阶段且月份未改变,则直接返回 + if (!mInitialising && month == getCurrentMonth()) { + return; + } + // 使用Calendar的set方法设置月份 + mDate.set(Calendar.MONTH, month); + // 更新日期控件的显示 + updateDateControl(); + // 通知日期时间已更改 + onDateTimeChanged(); +} + +/** + * 获取当前月份中的天数 + * + * @return 当前月份中的天数 + */ +public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); +} + +/** + * 设置当前月份中的天数 + * + * @param dayOfMonth 要设置的天数 + */ +public void setCurrentDay(int dayOfMonth) { + // 如果不是在初始化阶段且天数未改变,则直接返回 + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + // 使用Calendar的set方法设置天数 + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + // 更新日期控件的显示 + updateDateControl(); + // 通知日期时间已更改 + onDateTimeChanged(); +} + +/** + * 获取当前小时(24小时制),范围在0到23之间 + * + * @return 当前小时(24小时制) + */ +public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); +} + +/** + * 获取当前小时,根据是否为24小时制返回不同的小时值 + * + * @return 当前小时(在24小时制下为0~23,在12小时制下为1~12,并处理午夜和中午的特殊情况) + */ +private int getCurrentHour() { + // 如果为24小时制,则直接返回当前小时(24小时制) + if (mIs24HourView){ + return getCurrentHourOfDay(); + } else { + // 如果为12小时制,则进行转换 + int hour = getCurrentHourOfDay(); + if (hour > HOURS_IN_HALF_DAY) { // HOURS_IN_HALF_DAY应为12,表示半天的小时数 + // 如果小时数大于12,则转换为PM并减去12 + return hour - HOURS_IN_HALF_DAY; + } else { + // 处理午夜和中午的特殊情况 + return hour == 0 ? HOURS_IN_HALF_DAY : hour; // 午夜12点转换为12点(AM),其他情况保持不变 + } + } +} + +/** + * 设置当前小时(24小时制),范围在0到23之间 + * + * @param hourOfDay 要设置的小时(24小时制) + */ +public void setCurrentHour(int hourOfDay) { + // 如果不是在初始化阶段且小时未改变,则直接返回 + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + // 使用Calendar的set方法设置小时(24小时制) + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + // 如果不是24小时制,则进行AM/PM转换 + if (!mIs24HourView) { + if (hourOfDay >= HOURS_IN_HALF_DAY) { // 如果小时数大于等于12,则设置为PM + mIsAm = false; + if (hourOfDay > HOURS_IN_HALF_DAY) { // 如果小时数大于12,则减去12 + hourOfDay -= HOURS_IN_HALF_DAY; + } + } else { // 如果小时数小于12,则设置为AM + mIsAm = true; + if (hourOfDay == 0) { // 处理午夜12点的特殊情况,转换为12点(AM) + hourOfDay = HOURS_IN_HALF_DAY; + } + } + // 更新AM/PM控件的显示 + updateAmPmControl(); + } + // 更新小时选择器的值(注意:这里可能需要额外的逻辑来确保小时选择器在12小时制下正确显示) + mHourSpinner.setValue(hourOfDay); // 注意:这里可能需要根据mIs24HourView来决定是否转换小时值 + // 通知日期时间已更改 + onDateTimeChanged(); +} + + /** + * 获取当前分钟 + * + * @return 当前分钟 + */ +public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); // 从mDate日历对象中获取当前分钟 +} + +/** + * 设置当前分钟 + * + * @param minute 要设置的分钟 + */ +public void setCurrentMinute(int minute) { + // 如果不在初始化阶段且分钟未改变,则直接返回 + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); // 在分钟选择器中设置值 + mDate.set(Calendar.MINUTE, minute); // 在mDate日历对象中设置分钟 + onDateTimeChanged(); // 触发日期时间变化事件 +} + +/** + * 判断是否处于24小时视图模式 + * + * @return 如果是24小时视图返回true,否则返回false + */ +public boolean is24HourView() { + return mIs24HourView; // 返回mIs24HourView的值 +} + +/** + * 设置是否处于24小时或AM/PM模式 + * + * @param is24HourView true表示24小时模式,false表示AM/PM模式 + */ +public void set24HourView(boolean is24HourView) { + // 如果当前模式与要设置的模式相同,则直接返回 + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; // 设置新的模式 + // 根据模式设置AM/PM选择器的可见性 + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); // 获取当前小时 + updateHourControl(); // 更新小时选择器控制 + setCurrentHour(hour); // 设置当前小时 + updateAmPmControl(); // 更新AM/PM控制 +} + +/** + * 更新日期选择器控制 + */ +private void updateDateControl() { + Calendar cal = Calendar.getInstance(); // 获取当前日历实例 + cal.setTimeInMillis(mDate.getTimeInMillis()); // 设置日历时间为mDate的时间 + // 调整日历以显示一周内的日期 + 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) { + mAmPmSpinner.setVisibility(View.GONE); // 在24小时模式下隐藏AM/PM选择器 + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; // 根据当前是上午还是下午设置索引 + mAmPmSpinner.setValue(index); // 设置AM/PM选择器的值 + mAmPmSpinner.setVisibility(View.VISIBLE); // 显示AM/PM选择器 + } +} + +/** + * 更新小时选择器控制 + */ +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); + } +} + +/** + * 设置日期时间变化监听器 + * + * @param callback 日期时间变化时的回调,如果为null则不执行任何操作 + */ +public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + mOnDateTimeChangedListener = callback; // 设置监听器 +} + +/** + * 当日期时间变化时触发 + */ +private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + // 如果设置了监听器,则调用其onDateTimeChanged方法 + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } +} diff --git a/DateTimePickerDialog.java b/DateTimePickerDialog.java new file mode 100644 index 0000000..ad1920b --- /dev/null +++ b/DateTimePickerDialog.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// 导入必要的包 +import java.util.Calendar; + +import net.micode.notes.R; // 导入项目资源 +import net.micode.notes.ui.DateTimePicker; // 导入自定义的日期时间选择器 +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; // 导入日期时间变化的监听器 + +import android.app.AlertDialog; // 导入对话框类 +import android.content.Context; // 导入上下文类 +import android.content.DialogInterface; // 导入对话框接口 +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(); // 用于存储当前选择的日期和时间 + private boolean mIs24HourView; // 标记是否使用24小时制显示时间 + private OnDateTimeSetListener mOnDateTimeSetListener; // 日期时间设置完成后的回调接口 + private DateTimePicker mDateTimePicker; // 自定义的日期时间选择器 + + // 定义日期时间设置完成后的回调接口 + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long 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) { + // 更新Calendar对象的日期和时间 + mDate.set(Calendar.YEAR, year); + mDate.set(Calendar.MONTH, month); + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute); + // 更新对话框的标题 + updateTitle(mDate.getTimeInMillis()); + } + }); + mDate.setTimeInMillis(date); // 设置初始日期和时间 + mDate.set(Calendar.SECOND, 0); // 将秒设置为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小时制显示时间 + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 设置日期时间设置完成后的回调接口 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 更新对话框的标题 + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | // 显示年份 + DateUtils.FORMAT_SHOW_DATE | // 显示日期 + DateUtils.FORMAT_SHOW_TIME; // 显示时间 + // 根据是否使用24小时制设置显示格式 + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + // 设置对话框的标题 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + // 当点击确定按钮时调用 + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + // 调用回调接口,传递对话框和选择的日期时间 + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } +} \ No newline at end of file diff --git a/DropdownMenu.java b/DropdownMenu.java new file mode 100644 index 0000000..613dc74 --- /dev/null +++ b/DropdownMenu.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +public class DropdownMenu { + private Button mButton; + private PopupMenu mPopupMenu; + private Menu mMenu; + + 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(); + } + }); + } + + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} diff --git a/FoldersListAdapter.java b/FoldersListAdapter.java new file mode 100644 index 0000000..23e5817 --- /dev/null +++ b/FoldersListAdapter.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; // 引入Android的Context类,用于访问应用的特定资源和类以及调用应用级操作 +import android.view.Menu; // 引入Android的Menu类,用于创建菜单 +import android.view.MenuItem; // 引入Android的MenuItem类,表示菜单中的一项 +import android.view.View; // 引入Android的View类,是所有用户界面组件的基类 +import android.view.View.OnClickListener; // 引入Android的OnClickListener接口,用于点击事件的监听 +import android.widget.Button; // 引入Android的Button类,用于创建一个按钮 +import android.widget.PopupMenu; // 引入Android的PopupMenu类,用于创建一个弹出菜单 +import android.widget.PopupMenu.OnMenuItemClickListener; // 引入Android的OnMenuItemClickListener接口,用于菜单项点击事件的监听 + +import net.micode.notes.R; // 引入项目的资源类,用于访问项目中的资源 + +// DropdownMenu类,用于创建和管理一个下拉菜单 +public class DropdownMenu { + private Button mButton; // 声明一个Button变量,用于显示下拉菜单的触发按钮 + private PopupMenu mPopupMenu; // 声明一个PopupMenu变量,用于创建和管理下拉菜单 + private Menu mMenu; // 声明一个Menu变量,用于访问和操作下拉菜单的菜单项 + + // DropdownMenu类的构造函数,用于初始化下拉菜单 + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; // 将传入的Button赋值给mButton变量 + mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮的背景资源,即下拉菜单的图标 + mPopupMenu = new PopupMenu(context, mButton); // 创建一个PopupMenu实例,并将其与按钮关联 + mMenu = mPopupMenu.getMenu(); // 获取PopupMenu的Menu对象,用于操作菜单项 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 使用MenuInflater将指定的菜单资源文件填充到Menu对象中 + mButton.setOnClickListener(new OnClickListener() { // 为按钮设置点击事件监听器 + public void onClick(View v) { + mPopupMenu.show(); // 当按钮被点击时,显示下拉菜单 + } + }); + } + + // 设置下拉菜单项点击事件的监听器 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); // 为PopupMenu设置菜单项点击事件的监听器 + } + } + + // 根据菜单项的ID查找菜单项 + public MenuItem findItem(int id) { + return mMenu.findItem(id); // 在Menu中查找具有指定ID的菜单项并返回 + } + + // 设置按钮的标题 + public void setTitle(CharSequence title) { + mButton.setText(title); // 设置按钮的文本为指定的标题 + } +} \ No newline at end of file diff --git a/NoteEditActivity.java b/NoteEditActivity.java new file mode 100644 index 0000000..125a41b --- /dev/null +++ b/NoteEditActivity.java @@ -0,0 +1,1113 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 内部类,用于持有笔记头部视图的引用 + private class HeadViewHolder { + public TextView tvModified; // 最后修改时间的TextView + public ImageView ivAlertIcon; // 提醒图标的ImageView + public TextView tvAlertDate; // 提醒日期的TextView + public ImageView ibSetBgColor; // 设置背景颜色的ImageView(可能是个按钮图标) + } + + // 静态成员变量,用于映射背景选择器按钮的ID到对应的背景颜色资源ID + private static final Map sBgSelectorBtnsMap = new HashMap(); + static { + // 初始化背景选择器按钮的ID到颜色的映射 + sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); + sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED); + sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE); + sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN); + 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); + sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select); + sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select); + sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select); + sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); + } + + // 静态成员变量,用于映射字体大小选择器按钮的ID到对应的字体大小资源ID + private static final Map sFontSizeBtnsMap = new HashMap(); + static { + // 初始化字体大小选择器按钮的ID到字体大小的映射 + sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); + sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL); + sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM); + 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); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + // 日志标签,用于输出日志信息 + private static final String TAG = "NoteEditActivity"; + + // 持有笔记头部视图的ViewHolder实例 + private HeadViewHolder mNoteHeaderHolder; + + // 笔记头部面板的View + private View mHeadViewPanel; + + // 笔记背景颜色选择器的View + private View mNoteBgColorSelector; + + // 字体大小选择器的View + private View mFontSizeSelector; + + // 笔记编辑器的EditText + private EditText mNoteEditor; + + // 笔记编辑器面板的View + private View mNoteEditorPanel; + + // 当前正在编辑的工作笔记实例 + private WorkingNote mWorkingNote; + + // SharedPreferences实例,用于存取应用偏好设置 + private SharedPreferences mSharedPrefs; + + // 当前字体大小的资源ID + private int mFontSizeId; + + // SharedPreferences中用于存取字体大小的键 + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + + // 快捷方式图标标题的最大长度 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + + // 选中和未选中的标记字符串,可能用于复选框等UI元素 + public static final String TAG_CHECKED = String.valueOf('\u221A'); // √ + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // ▱ + + // 用于添加或显示多个EditText的LinearLayout + private LinearLayout mEditTextList; + + // 用户查询字符串,可能用于搜索或过滤功能 + private String mUserQuery; + + // 正则表达式模式,可能用于匹配用户输入或查询字符串 + private Pattern mPattern; + + // 省略了Activity的生命周期方法和事件处理方法的实现... +} + + // 重写onCreate方法,用于活动创建时的初始化 +@Override +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); // 调用父类的onCreate方法 + this.setContentView(R.layout.note_edit); // 设置活动的内容视图为note_edit布局 + + // 如果savedInstanceState为空(意味着这是第一次创建活动),并且无法通过Intent初始化活动状态,则结束活动 + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); // 结束当前活动 + return; // 退出onCreate方法 + } + initResources(); // 初始化资源 +} + +/** + * 当内存不足导致活动被杀死后,用户再次加载这个活动时,我们应该恢复它之前的状态 + */ +@Override +protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); // 调用父类的onRestoreInstanceState方法 + // 如果savedInstanceState不为空,并且包含Intent.EXTRA_UID键,说明活动之前的状态中有笔记ID + if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + // 创建一个新的Intent,用于查看操作 + Intent intent = new Intent(Intent.ACTION_VIEW); + // 从savedInstanceState中获取笔记ID,并放入intent中 + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + // 如果无法通过intent初始化活动状态,则结束活动 + if (!initActivityState(intent)) { + finish(); // 结束当前活动 + return; // 退出onRestoreInstanceState方法 + } + Log.d(TAG, "Restoring from killed activity"); // 在日志中打印恢复被杀死活动的信息 + } +} + +/** + * 初始化活动状态的方法 + * @param intent 用来初始化活动状态的Intent + * @return 如果成功初始化活动状态,返回true;否则返回false + */ +private boolean initActivityState(Intent intent) { + // 将当前工作的笔记对象置为空 + mWorkingNote = null; + // 如果Intent的动作是查看(ACTION_VIEW),但是未提供笔记ID,则跳转到NotesListActivity + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + // 从intent中获取笔记ID,默认为0 + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + // mUserQuery 初始化为空字符串(这段代码未展示mUserQuery的声明和用途) + mUserQuery = ""; + + /** + * Starting from the searched result + */ + // 检查Intent中是否包含SearchManager的EXTRA_DATA_KEY额外数据 +if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + // 从Intent中获取笔记ID,并将其转换为long类型 + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + // 从Intent中获取用户查询字符串 + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + + // 检查该笔记ID在笔记数据库中是否可见(具有指定的类型) + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + // 如果不可见,则跳转到NotesListActivity + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + // 显示一个Toast消息,提示笔记不存在 + showToast(R.string.error_note_not_exist); + // 结束当前活动 + finish(); + // 返回false,表示初始化失败 + return false; + } else { + // 如果可见,则尝试加载笔记 + mWorkingNote = WorkingNote.load(this, noteId); + // 如果加载失败(即mWorkingNote为null) + if (mWorkingNote == null) { + // 记录错误日志 + Log.e(TAG, "load note failed with note id" + noteId); + // 结束当前活动 + finish(); + // 返回false,表示初始化失败 + 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())) { + // 处理插入或编辑笔记的Intent动作 + // 从Intent中获取文件夹ID、小部件ID、小部件类型、背景资源ID等额外数据 + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + + // 解析来自通话记录的笔记 + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + // 如果通话日期不为0且电话号码不为null + if (callDate != 0 && phoneNumber != null) { + // 如果电话号码为空,则记录警告日志 + if (TextUtils.isEmpty(phoneNumber)) { + Log.w(TAG, "The call record number is null"); + } + long noteId = 0; + // 尝试根据电话号码和通话日期获取笔记ID + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), + phoneNumber, callDate)) > 0) { + // 如果找到笔记ID,则尝试加载笔记 + mWorkingNote = WorkingNote.load(this, noteId); + // 如果加载失败,则记录错误日志并结束活动 + if (mWorkingNote == null) { + Log.e(TAG, "load call note failed with note id" + noteId); + finish(); + return false; + } + } else { + // 如果没有找到笔记ID,则创建一个空的笔记对象,并将其转换为通话记录笔记 + 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 { + // 如果Intent没有指定有效的动作,则记录错误日志,结束活动,并返回false + Log.e(TAG, "Intent not specified action, should not support"); + finish(); + return false; +} +// 为mWorkingNote设置设置状态改变监听器 +mWorkingNote.setOnSettingStatusChangedListener(this); +// 返回true,表示初始化成功 +return true; + + // 当活动恢复时调用此方法,例如从暂停状态返回时 +@Override +protected void onResume() { + super.onResume(); // 调用父类的onResume方法 + initNoteScreen(); // 初始化笔记屏幕 +} + +// 初始化笔记屏幕的方法 +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)); + + // 显示提醒头部(目前因为DateTimePicker未准备好而禁用相关菜单) + showAlertHeader(); +} + +// 显示提醒头部信息的方法 +private void showAlertHeader() { + // 如果笔记有设置提醒 + if (mWorkingNote.hasClockAlert()) { + long time = System.currentTimeMillis(); // 获取当前时间 + if (time > mWorkingNote.getAlertDate()) { + // 如果提醒时间已过,显示“提醒已过期” + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + } else { + // 否则,显示相对时间跨度(例如“5分钟后”) + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + } + // 显示提醒日期文本和图标 + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + // 如果没有设置提醒,隐藏提醒日期文本和图标 + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + }; +} + +// 当活动接收到新的Intent时调用此方法 +@Override +protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); // 调用父类的onNewIntent方法 + initActivityState(intent); // 初始化活动状态 +} + +// 当活动即将保存其当前状态时调用此方法 +@Override +protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); // 调用父类的onSaveInstanceState方法 + + // 如果当前编辑的笔记尚未存在于数据库中,则先保存它以生成ID + if (!mWorkingNote.existInDatabase()) { + saveNote(); // 保存笔记 + } + + // 将当前编辑的笔记ID保存到outState中 + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); // 记录日志 +} + +// 拦截触摸事件的方法 +@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; // 表示事件已处理 + } + + // 如果事件未由上述条件处理,则调用父类的dispatchTouchEvent方法 + return super.dispatchTouchEvent(ev); +} + + // 判断一个触摸事件(MotionEvent)是否发生在指定的视图(View)范围内 +private boolean inRangeOfView(View view, MotionEvent ev) { + // 创建一个长度为2的数组,用于存储视图在屏幕上的位置(x, y) + int []location = new int[2]; + // 获取视图在屏幕上的绝对位置 + view.getLocationOnScreen(location); + // 从数组中取出x和y坐标 + int x = location[0]; + int y = location[1]; + // 判断触摸事件的坐标是否超出了视图的边界 + if (ev.getX() < x // 触摸点x坐标小于视图左边界 + || ev.getX() > (x + view.getWidth()) // 触摸点x坐标大于视图右边界 + || ev.getY() < y // 触摸点y坐标小于视图上边界 + || ev.getY() > (y + view.getHeight())) { // 触摸点y坐标大于视图下边界 + // 如果任一条件为真,表示触摸事件不在视图范围内,返回false + return false; + } + // 如果触摸事件在视图范围内,返回true + return true; +} + +// 初始化资源和方法 +private void initResources() { + // 初始化头部视图面板 + mHeadViewPanel = findViewById(R.id.note_title); + // 创建头部视图持有者实例 + mNoteHeaderHolder = new HeadViewHolder(); + // 初始化并关联头部视图中的各个组件 + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); + // 为设置背景色的按钮设置点击监听器 + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + // 初始化并关联编辑器视图 + 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); + } + // 获取默认的SharedPreferences实例 + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + // 从SharedPreferences中获取字体大小设置 + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + // 检查字体大小ID是否有效,如果无效则重置为默认字体大小 + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + // 初始化并关联编辑器列表视图 + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); +} + +// 当Activity暂停时调用此方法 +@Override +protected void onPause() { + super.onPause(); + // 保存笔记数据,如果保存成功则打印日志 + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + // 清除设置状态 + clearSettingState(); +} + + // 更新小部件的方法 +private void updateWidget() { + // 创建一个用于更新小部件的Intent + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据当前工作笔记的小部件类型设置Intent的目标类 + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + // 如果小部件类型不受支持,记录错误日志并返回 + Log.e(TAG, "Unsupported widget type"); + return; + } + // 将要更新的小部件ID添加到Intent的额外数据中 + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { mWorkingNote.getWidgetId() }); + // 发送广播以更新小部件 + sendBroadcast(intent); + // 设置结果代码为RESULT_OK,并附带Intent,通常用于返回结果给之前的Activity或Fragment + setResult(RESULT_OK, intent); +} + +// 处理点击事件的方法 +public void onClick(View v) { + int id = v.getId(); // 获取被点击视图的ID + // 如果点击的是设置背景颜色的按钮 + if (id == R.id.btn_set_bg_color) { + mNoteBgColorSelector.setVisibility(View.VISIBLE); // 显示背景颜色选择器 + // 隐藏当前选中的背景颜色指示器 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE); + } + // 如果点击的是背景颜色选择器中的按钮 + else if (sBgSelectorBtnsMap.containsKey(id)) { + // 隐藏之前选中的背景颜色指示器 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.GONE); + // 更新笔记的背景颜色ID + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + // 隐藏背景颜色选择器 + mNoteBgColorSelector.setVisibility(View.GONE); + } + // 如果点击的是字体大小选择器中的按钮 + else if (sFontSizeBtnsMap.containsKey(id)) { + // 隐藏之前选中的字体大小指示器 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + // 更新字体大小ID + mFontSizeId = sFontSizeBtnsMap.get(id); + // 将新的字体大小保存到SharedPreferences中 + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + // 显示新的字体大小指示器 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + // 如果当前笔记是检查列表模式,则更新列表显示 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + // 否则,更新编辑器中的文本外观 + mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + // 隐藏字体大小选择器 + mFontSizeSelector.setVisibility(View.GONE); + } +} + +// 处理返回按钮按下的方法 +@Override +public void onBackPressed() { + // 如果清除设置状态成功,则直接返回,不执行后续操作 + if(clearSettingState()) { + return; + } + // 保存笔记 + saveNote(); + // 调用父类的onBackPressed方法,通常用于关闭当前Activity + super.onBackPressed(); +} + + // 定义一个私有方法,用于清除设置状态(隐藏选择器) +private boolean clearSettingState() { + // 如果笔记背景颜色选择器是可见的 + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + // 将其设置为不可见 + mNoteBgColorSelector.setVisibility(View.GONE); + // 返回true表示进行了隐藏操作 + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { // 如果字体大小选择器是可见的 + // 将其设置为不可见 + mFontSizeSelector.setVisibility(View.GONE); + // 返回true表示进行了隐藏操作 + return true; + } + // 如果没有任何选择器是可见的,返回false + return false; +} + +// 定义一个公开方法,用于处理背景颜色改变时的逻辑 +public void onBackgroundColorChanged() { + // 根据当前笔记的背景颜色ID,找到对应的视图,并将其设置为可见 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE); + // 更新笔记编辑面板的背景资源 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // 更新头部视图面板的背景资源 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); +} + +// 重写onPrepareOptionsMenu方法,用于准备菜单项 +@Override +public boolean onPrepareOptionsMenu(Menu menu) { + // 如果Activity正在结束,直接返回true + if (isFinishing()) { + return true; + } + // 清除设置状态(隐藏选择器) + clearSettingState(); + // 清空菜单 + menu.clear(); + // 根据当前笔记所属的文件夹ID,决定加载哪个菜单布局 + 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 { + menu.findItem(R.id.menu_delete_remind).setVisible(false); // 否则隐藏删除提醒的菜单项 + } + // 返回true表示菜单准备完毕 + return true; +} + + // 重写onOptionsItemSelected方法,用于处理菜单项被点击时的逻辑 +@Override +public boolean onOptionsItemSelected(MenuItem item) { + // 根据被点击的菜单项的ID进行分支处理 + switch (item.getItemId()) { + // 如果点击的是新建笔记的菜单项 + case R.id.menu_new_note: + createNewNote(); // 调用方法创建新笔记 + break; + // 如果点击的是删除的菜单项 + case R.id.menu_delete: + // 构建一个对话框,用于确认是否删除当前笔记 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); // 设置对话框标题 + builder.setIcon(android.R.drawable.ic_dialog_alert); // 设置对话框图标 + builder.setMessage(getString(R.string.alert_message_delete_note)); // 设置对话框消息 + // 设置确定按钮的点击事件,用于删除当前笔记并结束Activity + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteCurrentNote(); // 调用方法删除当前笔记 + finish(); // 结束Activity + } + }); + // 设置取消按钮,不设置点击事件,默认点击后对话框消失 + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); // 显示对话框 + break; + // 如果点击的是字体大小的菜单项 + case R.id.menu_font_size: + mFontSizeSelector.setVisibility(View.VISIBLE); // 将字体大小选择器设置为可见 + // 根据当前字体大小ID,找到对应的视图,并将其设置为可见 + 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: + break; + } + // 返回true表示菜单项事件已被处理 + return true; +} + + // 设置提醒的方法 +private void setReminder() { + // 创建一个日期时间选择器对话框,初始时间设置为系统当前时间 + DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); + // 设置日期时间选择完成后的监听器 + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + // 当用户设置完日期时间后,回调此方法 + public void OnDateTimeSet(AlertDialog dialog, long date) { + // 根据用户选择的日期时间,为当前工作笔记设置提醒 + mWorkingNote.setAlertDate(date, true); + } + }); + // 显示日期时间选择器对话框 + d.show(); +} + +/** + * 将笔记内容分享到支持{@link Intent#ACTION_SEND}动作和{@text/plain}类型的应用 + */ +private void sendTo(Context context, String info) { + // 创建一个发送意图,动作设置为ACTION_SEND + Intent intent = new Intent(Intent.ACTION_SEND); + // 向意图中添加要分享的内容,作为EXTRA_TEXT的额外数据 + intent.putExtra(Intent.EXTRA_TEXT, info); + // 设置意图的数据类型为纯文本 + intent.setType("text/plain"); + // 使用上下文启动意图,触发分享动作 + context.startActivity(intent); +} + +/** + * 创建一个新笔记的方法 + */ +private void createNewNote() { + // 首先,保存当前正在编辑的笔记 + saveNote(); + + // 为了安全起见,结束当前的NoteEditActivity活动 + finish(); + // 创建一个新的Intent,用于启动NoteEditActivity以编辑新笔记 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作,表示要插入或编辑笔记 + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 向Intent中添加额外数据,指定新笔记所属的文件夹ID + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + // 启动Intent,跳转到NoteEditActivity进行新笔记的编辑 + startActivity(intent); +} + + // 删除当前笔记的方法 +private void deleteCurrentNote() { + // 如果当前工作笔记存在于数据库中 + if (mWorkingNote.existInDatabase()) { + // 创建一个HashSet来存储要删除的笔记ID + HashSet ids = new HashSet(); + // 获取当前工作笔记的ID + long id = mWorkingNote.getNoteId(); + // 如果笔记ID不是根文件夹ID(通常根文件夹不应被删除) + if (id != Notes.ID_ROOT_FOLDER) { + // 将笔记ID添加到HashSet中 + ids.add(id); + } else { + // 如果出现错误的笔记ID(这种情况通常不会发生),则记录日志 + 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); +} + +// 判断当前是否处于同步模式的方法 +private boolean isSyncMode() { + // 从NotesPreferenceActivity中获取同步账户名称,并检查其是否非空 + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} + +// 当笔记的提醒时钟发生变化时调用的方法 +public void onClockAlertChanged(long date, boolean set) { + // 如果当前工作笔记不存在于数据库中,则先保存笔记 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + // 如果当前工作笔记有有效的ID(即已保存) + if (mWorkingNote.getNoteId() > 0) { + // 创建一个Intent,用于当提醒时间到达时广播给AlarmReceiver + Intent intent = new Intent(this, AlarmReceiver.class); + // 设置Intent的数据URI,指向当前工作笔记的内容URI并附加笔记ID + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + // 创建一个PendingIntent,用于AlarmManager设置或取消提醒 + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + // 获取AlarmManager服务 + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + // 显示提醒相关的UI元素(如标题栏等,具体实现未给出) + showAlertHeader(); + // 如果set为false,则取消提醒;如果为true,则设置提醒 + if(!set) { + alarmManager.cancel(pendingIntent); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + // 如果当前工作笔记没有有效的ID(即用户未输入任何内容) + // 则记录错误日志,并通过Toast提示用户输入内容 + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } +} + +// 当小部件内容发生变化时调用的方法 +public void onWidgetChanged() { + // 更新小部件的显示内容(具体实现未给出) + updateWidget(); +} + + // 当EditText中的文本被删除时调用的方法 +public void onEditTextDelete(int index, String text) { + // 获取mEditTextList(一个容器,可能是LinearLayout或类似视图)中的子视图数量 + int childCount = mEditTextList.getChildCount(); + // 如果mEditTextList中只有一个子视图,则不进行任何操作 + if (childCount == 1) { + return; + } + + // 从被删除EditText的下一个开始,更新每个后续EditText的索引 + for (int i = index + 1; i < childCount; i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } + + // 从mEditTextList中移除指定索引的视图 + mEditTextList.removeViewAt(index); + + // 获取被删除EditText的前一个或当前位置(如果是第一个)的EditText + NoteEditText edit = null; + if(index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById(R.id.et_edit_text); + } else { + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById(R.id.et_edit_text); + } + // 获取当前EditText的文本长度 + int length = edit.length(); + // 将被删除的文本追加到当前EditText的末尾 + edit.append(text); + // 请求焦点,以便用户可以立即输入 + edit.requestFocus(); + // 设置光标位置到文本末尾 + edit.setSelection(length); +} + +// 当在EditText中按下回车键时调用的方法 +public void onEditTextEnter(int index, String text) { + // 如果索引超出了mEditTextList的范围,则记录错误日志(理论上不应该发生) + if(index > mEditTextList.getChildCount()) { + Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + } + + // 根据文本和索引创建一个新的视图 + View view = getListItem(text, index); + // 将新视图添加到mEditTextList中的指定索引位置 + mEditTextList.addView(view, index); + // 获取新添加的EditText并请求焦点 + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.requestFocus(); + // 设置光标位置到文本开头 + edit.setSelection(0); + + // 更新从被添加EditText的下一个开始,每个后续EditText的索引 + for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i); + } +} + +// 切换到列表模式的方法,通常用于将单行文本转换为多行编辑列表 +private void switchToListMode(String text) { + // 清空mEditTextList中的所有视图 + mEditTextList.removeAllViews(); + // 将文本按换行符分割成多个项 + String[] items = text.split("\n"); + int index = 0; + // 遍历每个项,如果不为空,则添加到mEditTextList中 + for (String item : items) { + if(!TextUtils.isEmpty(item)) { + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + // 添加一个空项,作为新输入的占位符 + mEditTextList.addView(getListItem("", index)); + // 请求焦点到最后一个(新添加的)EditText + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); + + // 隐藏单行编辑器,显示列表编辑器 + mNoteEditor.setVisibility(View.GONE); + mEditTextList.setVisibility(View.VISIBLE); +} + +// 根据用户查询在文本中高亮显示查询结果的方法 +private Spannable getHighlightQueryResult(String fullText, String userQuery) { + // 创建一个SpannableString,用于支持文本格式(如颜色) + SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); + // 如果用户查询不为空,则进行高亮处理 + if (!TextUtils.isEmpty(userQuery)) { + // 编译用户查询为Pattern + mPattern = Pattern.compile(userQuery); + // 使用Matcher查找匹配项 + Matcher m = mPattern.matcher(fullText); + int start = 0; + // 遍历所有匹配项,设置高亮背景 + while (m.find(start)) { + spannable.setSpan( + new BackgroundColorSpan(this.getResources().getColor( + R.color.user_query_highlight)), m.start(), m.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + start = m.end(); + } + } + // 返回处理后的SpannableString + return spannable; +} + + // 定义一个方法,根据传入的项和索引生成一个列表项视图 +private View getListItem(String item, int index) { + // 使用布局填充器从当前上下文加载布局文件note_edit_list_item + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + // 获取布局中的自定义EditText组件 + final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + // 设置EditText的文本外观 + edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + // 获取布局中的CheckBox组件 + CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); + // 为CheckBox设置监听器,用于处理选中状态的变化 + cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + // 选中时,为EditText的文本添加删除线 + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + // 未选中时,设置EditText的文本绘制标志(但这里使用的标志似乎不正确) + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); // 注意:DEV_KERN_TEXT_FLAG可能是一个错误或伪代码 + } + } + }); + + // 根据item的前缀(已选中或未选中标签)设置CheckBox的选中状态和EditText的文本 + if (item.startsWith(TAG_CHECKED)) { + cb.setChecked(true); + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + item = item.substring(TAG_CHECKED.length(), item.length()).trim(); + } else if (item.startsWith(TAG_UNCHECKED)) { + cb.setChecked(false); + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); // 同上,这里使用的标志可能有误 + item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); + } + + // 设置EditText的文本变化监听器和索引,并设置文本内容 + edit.setOnTextViewChangeListener(this); + edit.setIndex(index); + edit.setText(getHighlightQueryResult(item, mUserQuery)); // 根据用户查询高亮显示文本 + return view; // 返回生成的视图 +} + +// 当文本内容变化时调用的方法 +public void onTextChange(int index, boolean hasText) { + // 检查索引是否有效 + if (index >= mEditTextList.getChildCount()) { + Log.e(TAG, "Wrong index, should not happen"); + return; + } + // 根据文本内容是否存在来显示或隐藏复选框 + if(hasText) { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); + } else { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); + } +} + +// 当检查列表模式改变时调用的方法 +public void onCheckListModeChanged(int oldMode, int newMode) { + if (newMode == TextNote.MODE_CHECK_LIST) { + // 切换到列表模式 + switchToListMode(mNoteEditor.getText().toString()); + } else { + // 切换回普通文本模式 + if (!getWorkingText()) { + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", "")); + } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mEditTextList.setVisibility(View.GONE); + mNoteEditor.setVisibility(View.VISIBLE); + } +} + +// 获取当前工作文本的方法,并根据复选框状态更新文本内容 +private boolean getWorkingText() { + boolean hasChecked = false; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + // 根据复选框状态添加相应的前缀到文本中 + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + hasChecked = true; + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + mWorkingNote.setWorkingText(sb.toString()); + } else { + // 非列表模式下,直接使用NoteEditor的文本内容 + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + } + return hasChecked; +} + +// 保存笔记的方法 +private boolean saveNote() { + getWorkingText(); // 获取当前工作文本 + boolean saved = mWorkingNote.saveNote(); // 保存笔记 + if (saved) { + // 根据操作结果(创建/编辑)设置返回结果 + setResult(RESULT_OK); + } + return saved; +} + + // 定义一个私有方法,用于将当前笔记发送到桌面 +private void sendToDesktop() { + /** + * 在发送消息到桌面之前,我们应确保当前编辑的笔记在数据库中存在。 + * 因此,对于新笔记,首先需要保存它 + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); // 如果笔记不存在于数据库中,则保存笔记 + } + + // 如果笔记ID大于0,说明笔记已成功保存到数据库 + if (mWorkingNote.getNoteId() > 0) { + Intent sender = new Intent(); // 创建一个新的Intent对象,用于发送广播 + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); // 创建一个Intent,用于打开笔记编辑界面 + shortcutIntent.setAction(Intent.ACTION_VIEW); // 设置动作为查看 + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); // 将笔记ID作为额外数据传递 + + // 配置发送的Intent,以创建桌面快捷方式 + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); // 设置快捷方式的Intent + sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, + makeShortcutIconTitle(mWorkingNote.getContent())); // 设置快捷方式的名称,通过调用makeShortcutIconTitle方法生成 + sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); // 设置快捷方式的图标 + sender.putExtra("duplicate", true); // 添加一个额外的标志,可能是用于处理快捷方式的重复创建(此行代码的作用未在代码中明确,可能是特定于应用或环境的逻辑) + sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); // 设置动作为安装快捷方式 + + showToast(R.string.info_note_enter_desktop); // 显示一个Toast提示,通知用户笔记已发送到桌面 + sendBroadcast(sender); // 发送广播,执行快捷方式的安装 + } else { + /** + * 如果用户没有输入任何内容(笔记不值得保存),我们没有笔记ID, + * 提醒用户他应该输入一些内容 + */ + Log.e(TAG, "Send to desktop error"); // 记录错误日志 + showToast(R.string.error_note_empty_for_send_to_desktop); // 显示一个Toast提示,通知用户笔记为空,无法发送到桌面 + } +} + +// 定义一个私有方法,用于生成快捷方式的标题 +// 该方法通过移除特定标签并限制长度来生成标题 +private String makeShortcutIconTitle(String content) { + content = content.replace(TAG_CHECKED, ""); // 移除已检查标签 + content = content.replace(TAG_UNCHECKED, ""); // 移除未检查标签 + // 如果内容长度超过最大长度,则截取前部分;否则,返回原始内容 + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN ? content.substring(0, + SHORTCUT_ICON_TITLE_MAX_LEN) : content; +} + +// 定义一个私有方法,用于显示短时间的Toast提示 +private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); // 调用重载方法,设置Toast持续时间为短 +} + +// 定义一个私有方法,用于显示Toast提示,允许指定持续时间 +private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); // 根据资源ID和持续时间创建并显示Toast +} diff --git a/NoteEditText.java b/NoteEditText.java new file mode 100644 index 0000000..093114d --- /dev/null +++ b/NoteEditText.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +// 定义一个名为NoteEditText的类,它继承自EditText +public class NoteEditText extends EditText { + // 定义一个用于日志记录的标签 + private static final String TAG = "NoteEditText"; + // 定义当前NoteEditText的索引 + private int mIndex; + // 定义在删除操作前光标所在的起始位置 + private int mSelectionStartBeforeDelete; + + // 定义一些URL方案常量 + private static final String SCHEME_TEL = "tel:"; // 电话方案 + private static final String SCHEME_HTTP = "http:"; // HTTP方案 + private static final String SCHEME_EMAIL = "mailto:"; // 电子邮件方案 + + // 定义一个静态的HashMap,用于存储URL方案与对应的资源ID的映射 + private static final Map sSchemaActionResMap = new HashMap(); + // 静态代码块,用于初始化sSchemaActionResMap + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话方案对应的资源ID + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // HTTP方案对应的资源ID + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 电子邮件方案对应的资源ID + } + + // 定义一个接口,用于监听文本视图的变化 + public interface OnTextViewChangeListener { + // 当按下删除键且文本为空时,删除当前的EditText + void onEditTextDelete(int index, String text); + // 当按下回车键时,在当前EditText后添加一个新的EditText + void onEditTextEnter(int index, String text); + // 当文本变化时,隐藏或显示选项项 + void onTextChange(int index, boolean hasText); + } + + // 定义一个OnTextViewChangeListener类型的成员变量,用于存储监听器 + private OnTextViewChangeListener mOnTextViewChangeListener; + + // 构造函数,通过传入上下文来初始化NoteEditText + public NoteEditText(Context context) { + super(context, null); // 调用父类的构造函数 + mIndex = 0; // 初始化索引为0 + } + + // 设置NoteEditText的索引 + public void setIndex(int index) { + mIndex = index; + } + + // 设置OnTextViewChangeListener监听器 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 构造函数,通过传入上下文和属性集来初始化NoteEditText + // 这里使用了android.R.attr.editTextStyle作为默认样式 + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 构造函数,通过传入上下文、属性集和默认样式来初始化NoteEditText + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO: 这个构造函数是自动生成的,可以根据需要进行修改 + } + + // 重写onTouchEvent方法,用于处理触摸事件 + @Override + public boolean onTouchEvent(MotionEvent event) { + // 根据触摸事件的类型进行处理 + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: // 当用户按下屏幕时 + + // 获取触摸点的坐标 + int x = (int) event.getX(); + int y = (int) event.getY(); + // 减去左边的内边距和顶部的内边距,得到相对于EditText内部的坐标 + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + // 加上滚动偏移量,得到真实的坐标 + x += getScrollX(); + y += getScrollY(); + + // 获取Layout对象,它描述了文本的布局 + Layout layout = getLayout(); + // 根据y坐标获取所在的行号 + int line = layout.getLineForVertical(y); + // 根据行号和x坐标获取在该行的字符偏移量 + int off = layout.getOffsetForHorizontal(line, x); + // 设置光标位置到触摸点对应的字符位置 + Selection.setSelection(getText(), off); + break; + } + + // 调用父类的onTouchEvent方法,继续处理其他触摸事件 + return super.onTouchEvent(event); + } +} // 重写onKeyDown方法,用于处理按键按下事件 +@Override +public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: // 当按下回车键时 + if (mOnTextViewChangeListener != null) { + // 如果设置了监听器,但不处理回车键事件,直接返回false + return false; + } + break; + case KeyEvent.KEYCODE_DEL: // 当按下删除键时 + mSelectionStartBeforeDelete = getSelectionStart(); // 记录删除前的光标位置 + break; + default: + break; + } + // 如果没有特别处理,调用父类的onKeyDown方法 + return super.onKeyDown(keyCode, event); +} + +// 重写onKeyUp方法,用于处理按键抬起事件 +@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; // 事件已处理 + } + } else { + // 如果没有设置监听器,记录日志 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: // 当抬起回车键时 + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); // 获取光标位置 + String text = getText().subSequence(selectionStart, length()).toString(); // 获取光标后的文本 + setText(getText().subSequence(0, selectionStart)); // 清除光标后的文本 + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); // 通知监听器 + } else { + // 如果没有设置监听器,记录日志 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + // 如果没有特别处理,调用父类的onKeyUp方法 + return super.onKeyUp(keyCode, event); +} + +// 重写onFocusChanged方法,用于处理焦点变化事件 +@Override +protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { // 失去焦点且文本为空时 + mOnTextViewChangeListener.onTextChange(mIndex, false); // 通知监听器文本已清空 + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); // 否则,通知监听器文本已变化 + } + } + // 调用父类的onFocusChanged方法 + super.onFocusChanged(focused, direction, previouslyFocusedRect); +} + +// 重写onCreateContextMenu方法,用于创建上下文菜单 +@Override +protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { // 检查文本是否包含可跨越的元素(如链接) + int selStart = getSelectionStart(); // 获取选择开始位置 + int selEnd = getSelectionEnd(); // 获取选择结束位置 + + int min = Math.min(selStart, selEnd); // 选择范围的最小值 + int max = Math.max(selStart, selEnd); // 选择范围的最大值 + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); // 获取选择范围内的所有URL链接 + if (urls.length == 1) { // 如果只有一个URL链接 + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { // 遍历所有支持的协议 + if(urls[0].getURL().indexOf(schema) >= 0) { // 如果URL包含该协议 + defaultResId = sSchemaActionResMap.get(schema); // 获取对应的资源ID + break; + } + } + + if (defaultResId == 0) { // 如果没有找到匹配的协议,使用默认资源ID + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( // 向菜单添加项并设置点击监听器 + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // 调用URLSpan的onClick方法,通常用于打开链接 + urls[0].onClick(NoteEditText.this); + return true; // 事件已处理 + } + }); + } + } + // 调用父类的onCreateContextMenu方法 + super.onCreateContextMenu(menu); +} diff --git a/NoteItemData.java b/NoteItemData.java new file mode 100644 index 0000000..a96edc0 --- /dev/null +++ b/NoteItemData.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + +// NoteItemData类用于封装从数据库查询得到的笔记项数据 +public class NoteItemData { + // 定义从数据库中查询笔记项时需要返回的列 + static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 笔记ID + NoteColumns.ALERTED_DATE, // 提醒日期 + NoteColumns.BG_COLOR_ID, // 背景颜色ID + NoteColumns.CREATED_DATE, // 创建日期 + NoteColumns.HAS_ATTACHMENT, // 是否有附件 + NoteColumns.MODIFIED_DATE, // 修改日期 + NoteColumns.NOTES_COUNT, // 笔记数量(对于文件夹类型有效) + NoteColumns.PARENT_ID, // 父ID(如果是文件夹内的笔记,则为文件夹ID) + NoteColumns.SNIPPET, // 摘要 + NoteColumns.TYPE, // 笔记类型(笔记、文件夹、系统笔记等) + NoteColumns.WIDGET_ID, // 小部件ID + NoteColumns.WIDGET_TYPE, // 小部件类型 + }; + + // 列索引常量,用于从Cursor中获取对应列的值 + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + // 笔记项的属性 + private long mId; // 笔记ID + private long mAlertDate; // 提醒日期 + private int mBgColorId; // 背景颜色ID + private long mCreatedDate; // 创建日期 + private boolean mHasAttachment; // 是否有附件 + private long mModifiedDate; // 修改日期 + private int mNotesCount; // 笔记数量 + private long mParentId; // 父ID + private String mSnippet; // 摘要 + private int mType; // 笔记类型 + private int mWidgetId; // 小部件ID + private int mWidgetType; // 小部件类型 + private String mName; // 联系人姓名(如果是通话记录笔记) + private String mPhoneNumber; // 联系电话(如果是通话记录笔记) + + // 笔记项的位置信息 + private boolean mIsLastItem; // 是否是最后一个项目 + private boolean mIsFirstItem; // 是否是第一个项目 + private boolean mIsOnlyOneItem; // 是否是唯一的项目 + private boolean mIsOneNoteFollowingFolder; // 是否是一个笔记紧跟在文件夹后面 + private boolean mIsMultiNotesFollowingFolder; // 是否是多个笔记紧跟在文件夹后面 + + // 构造函数,通过Cursor初始化NoteItemData对象 + public NoteItemData(Context context, Cursor cursor) { + // 从Cursor中读取数据并赋值给成员变量 + // ...(省略了具体的赋值代码) + + // 如果是通话记录笔记,则通过电话号码获取联系人姓名 + // ...(省略了具体的逻辑代码) + + // 初始化名称,如果获取不到联系人姓名,则使用空字符串 + if (mName == null) { + mName = ""; + } + // 检查当前笔记项的位置信息 + checkPostion(cursor); + } + + // 检查当前笔记项的位置信息 + private void checkPostion(Cursor cursor) { + // ...(省略了具体的逻辑代码) + } + + // Getter方法,用于获取笔记项的各种属性 + // ...(省略了具体的Getter方法) + + // 判断是否有提醒 + public boolean hasAlert() { + return (mAlertDate > 0); + } + + // 判断是否是通话记录笔记 + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + } + + // 静态方法,用于从Cursor中获取笔记类型 + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} \ No newline at end of file diff --git a/NotesListActivity.java b/NotesListActivity.java new file mode 100644 index 0000000..1b7c602 --- /dev/null +++ b/NotesListActivity.java @@ -0,0 +1,1026 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 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; + private int mOriginY, mDispatchY; + private TextView mTitleBar; + private long mCurrentFolderId; + private ContentResolver mContentResolver; + private ModeCallback mModeCallBack; + private static final String TAG = "NotesListActivity"; + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + private NoteItemData mFocusNoteDataItem; + // 定义SQL查询条件 + 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; + + // 当Activity被创建时调用 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); // 设置布局 + initResources(); // 初始化资源 + // 插入一个引导说明,当用户首次使用此应用时 + setAppInfoFromRawRes(); + } + + // 处理从其他Activity返回的结果 + @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); // 更改适配器中的游标 + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + // 从原始资源中设置应用信息(引导说明) + private void setAppInfoFromRawRes() { + // 获取SharedPreferences实例 + 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); // 打开原始资源文件 + if (in != null) { + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char[] buf = new char[1024]; + int len; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); // 读取文件内容到StringBuilder + } + } else { + Log.e(TAG, "Read introduction file error"); // 日志记录错误 + return; + } + } catch (IOException e) { + e.printStackTrace(); // 打印堆栈跟踪 + return; + } finally { + if (in != null) { + try { + in.close(); // 关闭输入流 + } catch (IOException e) { + e.printStackTrace(); // 打印堆栈跟踪 + } + } + } + + // 创建一个空的工作笔记,并设置内容 + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + note.setWorkingText(sb.toString()); + // 保存笔记,并将引导说明已读标志设置为true + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + Log.e(TAG, "Save introduction note error"); // 日志记录错误 + return; + } + } + } + + // 当Activity开始与用户交互时调用 + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); // 开始异步查询笔记列表 + } + // 初始化资源的方法 +private void initResources() { + // 获取内容解析器,用于访问内容提供者提供的数据 + mContentResolver = this.getContentResolver(); + // 创建后台查询处理器,用于在后台线程中执行查询 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + // 设置当前文件夹ID为根文件夹ID + 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; + // 获取标题栏的TextView,可能用于显示当前状态或标题 + 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; + // 移动菜单项,用于移动笔记到另一个文件夹 + private MenuItem mMoveMenu; + + // 创建操作模式时的回调 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // 加载菜单资源 + getMenuInflater().inflate(R.menu.note_list_options, menu); + // 设置删除菜单项的点击监听器 + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + // 根据条件设置移动菜单项的可见性 + mMoveMenu = menu.findItem(R.id.move); + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + // 保存操作模式实例 + mActionMode = mode; + // 设置适配器为多选模式 + mNotesListAdapter.setChoiceMode(true); + // 禁用列表视图的长按,因为已经在操作模式中 + mNotesListView.setLongClickable(false); + // 隐藏添加新笔记的按钮 + mAddNewNote.setVisibility(View.GONE); + + // 自定义操作模式的视图 + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + // 初始化并设置下拉菜单 + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + // 设置下拉菜单项的点击监听器 + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(MenuItem item) { + // 切换选择所有笔记的状态 + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + // 更新菜单项的状态 + updateMenu(); + return true; + } + }); + return true; // 表示创建操作模式成功 + } + + // 更新菜单项的方法,根据选择状态更新标题和选择/取消选择所有项的状态 + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // 更新下拉菜单的标题 + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + // 更新选择/取消选择所有项的菜单项 + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } +} + + // 这是一个回调接口的实现,用于处理ActionMode生命周期中的事件 +public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // 准备ActionMode时调用,这里只是简单地返回false,表示没有特殊准备 + return false; +} + +// 当ActionMode中的菜单项被点击时调用 +public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // 这里只是简单地返回false,表示没有处理点击事件 + return false; +} + +// 当ActionMode被销毁时调用 +public void onDestroyActionMode(ActionMode mode) { + // 取消列表适配器的选择模式 + mNotesListAdapter.setChoiceMode(false); + // 允许列表视图长按 + mNotesListView.setLongClickable(true); + // 使添加新笔记的按钮可见 + mAddNewNote.setVisibility(View.VISIBLE); +} + +// 结束当前ActionMode +public void finishActionMode() { + mActionMode.finish(); +} + +// 当列表项的选择状态改变时调用 +public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + // 更新列表适配器中对应位置项的选中状态 + mNotesListAdapter.setCheckedItem(position, checked); + // 更新ActionMode的菜单项(可能根据选中的项数显示或隐藏某些选项) + updateMenu(); +} + +// 当ActionMode中的菜单项被点击时调用(这里似乎是另一个版本的onActionItemClicked,可能是特定于某个实现) +public boolean onMenuItemClick(MenuItem item) { + // 如果没有选中任何项,显示提示信息 + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), Toast.LENGTH_SHORT).show(); + return true; + } + + // 根据点击的菜单项执行相应的操作 + switch (item.getItemId()) { + case R.id.delete: + // 显示删除确认对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + batchDelete(); // 执行批量删除操作 + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + // 开始查询目标文件夹的操作(可能是移动选中的笔记) + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; +} + +// 一个内部类,用于处理“添加新笔记”按钮的触摸事件 +private class NewNoteOnTouchListener implements OnTouchListener { + + public boolean onTouch(View v, MotionEvent event) { + // 根据触摸事件的动作进行处理 + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // 获取屏幕高度和“添加新笔记”按钮的高度 + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + int eventY = start + (int) event.getY(); + + // 如果处于子文件夹状态,需要减去标题栏的高度 + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + + // 这是一个特殊的处理,用于处理“添加新笔记”按钮透明部分的触摸事件 + // 将触摸事件转发给列表视图,如果触摸位置满足特定条件 + if (event.getY() < (event.getX() * (-0.12) + 94)) { + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - mNotesListView.getFooterViewsCount()); + if (view != null && view.getBottom() > start && (view.getTop() < (start + 94))) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + case MotionEvent.ACTION_MOVE: { + // 如果之前已经决定转发事件,继续转发并更新位置 + if (mDispatch) { + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + default: { + // 停止转发事件 + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + return false; + } +}; + + // 开始一个异步查询以获取笔记列表 +private void startAsyncNotesListQuery() { + // 根据当前文件夹ID决定使用哪个SQL选择语句 + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + // 使用BackgroundQueryHandler启动异步查询 + 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"); +} + +// 一个内部类,继承自AsyncQueryHandler,用于处理异步查询 +private final class BackgroundQueryHandler extends AsyncQueryHandler { + // 构造函数,传入ContentResolver以访问内容提供者 + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + // 当查询完成时调用此方法 + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + // 根据查询的token判断是哪一个查询完成了 + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN: + // 更新笔记列表适配器的游标 + mNotesListAdapter.changeCursor(cursor); + break; + case FOLDER_LIST_QUERY_TOKEN: + // 如果查询成功且有结果,显示文件夹列表菜单 + if (cursor != null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + // 查询失败,记录错误日志 + Log.e(TAG, "Query folder failed"); + } + break; + default: + // 未知的token,不做处理 + return; + } + } +} + +// 显示一个包含文件夹列表的对话框,允许用户选择一个文件夹 +private void showFolderListMenu(Cursor cursor) { + // 创建AlertDialog.Builder + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + // 使用查询结果初始化FoldersListAdapter + 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提示移动成功 + 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(); + } + }); + // 显示对话框 + builder.show(); +} + +// 创建一个新的笔记 +private void createNewNote() { + // 创建一个Intent以启动NoteEditActivity + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 将当前文件夹ID作为额外数据传递给NoteEditActivity + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + // 启动NoteEditActivity,并请求结果 + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); +} + + // 定义一个方法用于批量删除笔记 +private void batchDelete() { + // 使用AsyncTask在后台线程中执行批量删除操作,以避免阻塞UI线程 + new AsyncTask>() { + // 在后台线程中执行的操作 + protected HashSet doInBackground(Void... unused) { + // 获取当前选中的笔记小部件集合 + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 检查是否处于同步模式 + if (!isSyncMode()) { + // 如果未同步,则直接删除选中的笔记 + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter.getSelectedItemIds())) { + // 删除成功,无操作 + } else { + // 删除失败,记录错误日志 + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 如果处于同步模式,则将选中的笔记移动到回收站文件夹 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + // 移动失败,记录错误日志 + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + // 返回选中的笔记小部件集合,用于后续更新 + return widgets; + } + + // 在后台操作完成后,在主线程上执行的操作 + @Override + protected void onPostExecute(HashSet widgets) { + // 如果小部件集合不为空,则遍历更新每个小部件 + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + // 检查小部件ID和类型是否有效 + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); // 更新小部件 + } + } + } + // 结束操作模式(如选中模式) + mModeCallBack.finishActionMode(); + } + }.execute(); // 执行AsyncTask +} + +// 定义一个方法用于删除文件夹 +private void deleteFolder(long folderId) { + // 检查是否尝试删除根文件夹,这是不允许的 + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + // 创建一个集合,用于存储要删除的文件夹ID + HashSet ids = new HashSet(); + ids.add(folderId); + // 获取与要删除的文件夹相关的笔记小部件集合 + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); + // 检查是否处于同步模式 + if (!isSyncMode()) { + // 如果未同步,则直接删除文件夹及其内容 + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // 如果处于同步模式,则将文件夹及其内容移动到回收站文件夹 + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + // 如果小部件集合不为空,则遍历更新每个小部件 + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + // 检查小部件ID和类型是否有效 + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); // 更新小部件 + } + } + } +} + +// 定义一个方法用于打开笔记节点(即笔记详情页面) +private void openNode(NoteItemData data) { + // 创建一个Intent,用于跳转到NoteEditActivity + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); // 设置Action为查看 + intent.putExtra(Intent.EXTRA_UID, data.getId()); // 将笔记ID作为额外数据传递给新Activity + // 启动新Activity,并请求结果 + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); +} + + // 定义一个方法,用于打开指定的文件夹 +private void openFolder(NoteItemData data) { + // 更新当前文件夹ID + mCurrentFolderId = data.getId(); + // 开始异步查询笔记列表 + startAsyncNotesListQuery(); + // 如果打开的是通话记录文件夹 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 设置状态为通话记录文件夹 + mState = ListEditState.CALL_RECORD_FOLDER; + // 隐藏添加新笔记的按钮 + mAddNewNote.setVisibility(View.GONE); + } else { + // 否则,设置为子文件夹状态 + mState = ListEditState.SUB_FOLDER; + } + // 根据文件夹ID设置标题栏文本 + 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); +} + +// 定义一个点击事件处理方法 +public void onClick(View v) { + // 根据点击的视图ID执行不同的操作 + switch (v.getId()) { + case R.id.btn_new_note: + // 创建新笔记 + createNewNote(); + break; + default: + break; + } +} + +// 显示软键盘 +private void showSoftInput() { + // 获取输入法管理器 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 如果输入法管理器不为空,则强制显示软键盘 + if (inputMethodManager != null) { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } +} + +// 隐藏软键盘 +private void hideSoftInput(View view) { + // 获取输入法管理器 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 隐藏指定视图窗口上的软键盘 + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); +} + +// 显示创建或修改文件夹的对话框 +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(); + // 根据是创建还是修改文件夹,设置不同的标题和初始文本 + if (!create) { + if (mFocusNoteDataItem != null) { + 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)); + } + // 设置确定和取消按钮 + 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); + } + }); + // 显示对话框 + 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(); + // 检查文件夹名是否已存在 + if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), + Toast.LENGTH_LONG).show(); + 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); + 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); + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + // 关闭对话框 + dialog.dismiss(); + } + }); + // 根据输入框内容启用或禁用确定按钮 + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + // 添加文本变化监听器,动态启用或禁用确定按钮 + etName.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // 不需要处理 + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + // 根据输入框内容启用或禁用确定按钮 + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + // 其他方法不需要处理 + }); +} + + // 当文本内容变化后触发的回调方法(此处的实现为空) +public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub +} + +// 当用户按下返回键时的处理逻辑 +@Override +public void onBackPressed() { + // 根据当前的状态(mState)执行不同的操作 + switch (mState) { + case SUB_FOLDER: // 如果是子文件夹状态 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 回到根文件夹 + mState = ListEditState.NOTE_LIST; // 切换到笔记列表状态 + startAsyncNotesListQuery(); // 开始异步查询笔记列表 + mTitleBar.setVisibility(View.GONE); // 隐藏标题栏 + break; + case CALL_RECORD_FOLDER: // 如果是通话记录文件夹状态 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 回到根文件夹 + mState = ListEditState.NOTE_LIST; // 切换到笔记列表状态 + mAddNewNote.setVisibility(View.VISIBLE); // 显示添加新笔记按钮 + mTitleBar.setVisibility(View.GONE); // 隐藏标题栏 + startAsyncNotesListQuery(); // 开始异步查询笔记列表 + break; + case NOTE_LIST: // 如果是笔记列表状态 + super.onBackPressed(); // 执行默认的返回键处理逻辑 + break; + default: + break; + } +} + +// 更新指定ID和类型的小部件 +private void updateWidget(int appWidgetId, int appWidgetType) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); // 创建更新小部件的Intent + if (appWidgetType == Notes.TYPE_WIDGET_2X) { // 如果是2x大小的小部件 + intent.setClass(this, NoteWidgetProvider_2x.class); // 设置目标为2x小部件的Provider + } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { // 如果是4x大小的小部件 + intent.setClass(this, NoteWidgetProvider_4x.class); // 设置目标为4x小部件的Provider + } else { + Log.e(TAG, "Unsupported widget type"); // 不支持的类型,记录错误日志 + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { appWidgetId }); // 添加小部件ID到Intent + sendBroadcast(intent); // 发送广播以更新小部件 + setResult(RESULT_OK, intent); // 设置结果 +} + +// 为文件夹列表视图创建上下文菜单的监听器 +private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (mFocusNoteDataItem != null) { // 如果当前有选中的笔记数据项 + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 设置菜单标题为笔记的摘要 + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); // 添加查看文件夹选项 + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); // 添加删除文件夹选项 + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); // 添加更改文件夹名称选项 + } + } +}; + +// 当上下文菜单关闭时的处理逻辑 +@Override +public void onContextMenuClosed(Menu menu) { + if (mNotesListView != null) { + mNotesListView.setOnCreateContextMenuListener(null); // 清除列表视图的上下文菜单监听器 + } + super.onContextMenuClosed(menu); // 调用父类的处理逻辑 +} + +// 当用户选择上下文菜单项时的处理逻辑 +@Override +public boolean onContextItemSelected(MenuItem item) { + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); // 如果没有选中的笔记数据项,记录错误日志 + return false; + } + switch (item.getItemId()) { + case MENU_FOLDER_VIEW: // 查看文件夹 + openFolder(mFocusNoteDataItem); // 打开文件夹 + break; + case MENU_FOLDER_DELETE: // 删除文件夹 + // 显示确认删除的对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_folder)); + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); // 删除文件夹 + } + }); + builder.setNegativeButton(android.R.string.cancel, null); // 取消按钮不做处理 + builder.show(); // 显示对话框 + break; + case MENU_FOLDER_CHANGE_NAME: // 更改文件夹名称 + showCreateOrModifyFolderDialog(false); // 显示创建或修改文件夹的对话框 + break; + default: + break; + } + + return true; // 表示事件已处理 +} + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // set sync or sync_cancel + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + Log.e(TAG, "Wrong state:" + mState); + } + return true; + } + +// 重写onOptionsItemSelected方法,用于处理菜单项的点击事件 +@Override +public boolean onOptionsItemSelected(MenuItem item) { + // 根据点击的菜单项ID执行不同的操作 + switch (item.getItemId()) { + case R.id.menu_new_folder: { + // 显示创建或修改文件夹的对话框,传入true表示是创建新文件夹 + showCreateOrModifyFolderDialog(true); + break; + } + case R.id.menu_export_text: { + // 导出笔记到文本文件 + exportNoteToText(); + break; + } + case R.id.menu_sync: { + // 根据当前是否处于同步模式,执行同步或取消同步操作 + if (isSyncMode()) { + // 如果菜单标题是"同步",则开始同步;否则取消同步 + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + GTaskSyncService.startSync(this); + } else { + GTaskSyncService.cancelSync(this); + } + } else { + // 如果不是同步模式,则跳转到设置界面 + startPreferenceActivity(); + } + break; + } + case R.id.menu_setting: { + // 跳转到设置界面 + startPreferenceActivity(); + break; + } + case R.id.menu_new_note: { + // 创建新笔记 + createNewNote(); + break; + } + case R.id.menu_search: + // 请求搜索 + onSearchRequested(); + break; + default: + break; + } + return true; // 表示事件已处理 +} + +// 重写onSearchRequested方法,用于处理搜索请求 +@Override +public boolean onSearchRequested() { + // 发起搜索请求,不指定搜索初始查询、不调用搜索建议接口、不使用应用提供的额外数据 + startSearch(null, false, null /* appData */, false); + return true; // 表示事件已处理 +} + +// 导出笔记到文本文件的私有方法 +private void exportNoteToText() { + // 获取BackupUtils实例 + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + // 使用AsyncTask在后台线程执行导出操作 + new AsyncTask() { + + @Override + protected Integer doInBackground(Void... unused) { + // 执行导出操作,并返回结果码 + return backup.exportToText(); + } + + @Override + protected void onPostExecute(Integer result) { + // 根据结果码显示不同的对话框 + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + // SD卡未挂载 + showSdCardUnmountedDialog(); + } else if (result == BackupUtils.STATE_SUCCESS) { + // 导出成功 + showExportSuccessDialog(backup.getExportedTextFileName(), backup.getExportedTextFileDir()); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + // 系统错误 + showExportFailedDialog(); + } + } + + // 省略了对话框的创建和显示代码,以showSdCardUnmountedDialog()、showExportSuccessDialog()、showExportFailedDialog()表示 + }.execute(); +} + +// 判断当前是否处于同步模式的私有方法 +private boolean isSyncMode() { + // 获取同步账户名,如果非空则处于同步模式 + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} + +// 跳转到设置界面的私有方法 +private void startPreferenceActivity() { + // 获取启动Intent的Activity,如果当前Activity有父Activity,则使用父Activity,否则使用当前Activity + Activity from = getParent() != null ? getParent() : this; + // 创建跳转到设置界面的Intent + Intent intent = new Intent(from, NotesPreferenceActivity.class); + // 根据需要启动设置界面Activity + from.startActivityIfNeeded(intent, -1); +} + +// 内部类OnListItemClickListener实现了OnItemClickListener接口,用于处理列表项的点击事件 +private class OnListItemClickListener implements OnItemClickListener { + + // 当列表项被点击时调用的方法 + public void onItemClick(AdapterView parent, View view, int position, long id) { + // 如果点击的视图是NotesListItem的实例 + if (view instanceof NotesListItem) { + // 获取点击的列表项数据 + NoteItemData item = ((NotesListItem) view).getItemData(); + // 如果列表处于选择模式 + if (mNotesListAdapter.isInChoiceMode()) { + // 根据点击的项是否是笔记类型,更新选择状态 + if (item.getType() == Notes.TYPE_NOTE) { + position = position - mNotesListView.getHeaderViewsCount(); + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + return; + } + + // 根据当前状态(主列表、子文件夹、通话记录文件夹)和点击的项类型执行不同的操作 + switch (mState) { + case NOTE_LIST: + // 如果是文件夹或系统项,打开文件夹;如果是笔记项,打开笔记 + if (item.getType() == Notes.TYPE_FOLDER || item.getType() == Notes.TYPE_SYSTEM) { + openFolder(item); + } else if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + // 日志记录:主列表中出现了错误的笔记类型 + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + // 如果是笔记项,打开笔记;否则记录错误日志 + if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + default: + break; + } + } + } + + // 省略了openFolder、openNode等方法的实现细节 +} + + // 定义一个私有方法,用于启动查询目标文件夹的操作 +private void startQueryDestinationFolders() { + // 定义一个SQL查询条件字符串,用于筛选特定类型的笔记,排除特定父ID和ID的笔记 + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + + // 根据当前的状态(mState),如果是笔记列表状态,则使用上述筛选条件; + // 如果不是,则添加一个额外的条件,允许查询根文件夹(ID_ROOT_FOLDER) + selection = (mState == ListEditState.NOTE_LIST) ? selection: + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + // 使用后台查询处理器(mBackgroundQueryHandler)开始执行查询。 + // 查询的目标是笔记内容URI(Notes.CONTENT_NOTE_URI), + // 投影列(FoldersListAdapter.PROJECTION)指定了查询返回的列, + // selection参数是上述定义的SQL查询条件, + // selectionArgs是查询条件的参数值数组,包括文件夹类型、垃圾桶文件夹ID和当前文件夹ID, + // orderBy参数指定了结果按修改日期降序排列。 + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + FoldersListAdapter.PROJECTION, + selection, + new String[] { + String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), // 注意:这里可能是一个拼写错误,应该是ID_TRASH_FOLDER + String.valueOf(mCurrentFolderId) + }, + NoteColumns.MODIFIED_DATE + " DESC"); +} + +// 定义一个公开方法,处理列表项的长按事件 +public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + // 如果被长按的视图是NotesListItem的实例 + if (view instanceof NotesListItem) { + // 获取被长按的列表项的数据 + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + + // 如果该项是笔记类型(Notes.TYPE_NOTE),并且当前不在选择模式下 + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + // 尝试启动动作模式(ActionMode),用于选择或操作多个笔记 + if (mNotesListView.startActionMode(mModeCallBack) != null) { + // 如果动作模式启动成功,则更新选中状态,并给出触觉反馈 + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + // 如果启动动作模式失败,则记录错误日志 + Log.e(TAG, "startActionMode fails"); + } + } + // 如果该项是文件夹类型(Notes.TYPE_FOLDER) + else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 设置上下文菜单创建监听器,用于文件夹的上下文菜单 + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + // 返回false,表示事件没有被消费(即没有阻止后续的事件处理) + return false; +} diff --git a/NotesListAdapter.java b/NotesListAdapter.java new file mode 100644 index 0000000..ede80da --- /dev/null +++ b/NotesListAdapter.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// 导入所需的包和类 +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +// NotesListAdapter类,继承自CursorAdapter,用于管理笔记列表的展示 +public class NotesListAdapter extends CursorAdapter { + // 定义日志标签 + private static final String TAG = "NotesListAdapter"; + // 上下文对象,用于访问应用资源 + private Context mContext; + // 用于记录哪些项被选中的哈希映射,键为位置索引,值为选中状态 + private HashMap mSelectedIndex; + // 笔记总数 + private int mNotesCount; + // 标记是否处于选择模式 + private boolean mChoiceMode; + + // 内部静态类,用于表示应用小部件的属性(此处可能与笔记列表功能不直接相关) + public static class AppWidgetAttribute { + public int widgetId; // 小部件ID + public int widgetType; // 小部件类型 + }; + + // 构造函数,初始化上下文、选中索引映射和笔记计数 + public NotesListAdapter(Context context) { + super(context, null); // 调用父类构造函数,传入上下文和null的Cursor(稍后通过swapCursor设置) + mSelectedIndex = new HashMap(); // 初始化选中索引映射 + mContext = context; // 保存上下文 + mNotesCount = 0; // 初始化笔记计数为0(可能通过其他方式更新) + } + + // 当需要创建新视图以展示数据时调用此方法 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); // 创建并返回NotesListItem实例 + } + + // 当需要绑定视图和数据时调用此方法 + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { // 确保视图是NotesListItem的实例 + NoteItemData itemData = new NoteItemData(context, cursor); // 根据Cursor创建NoteItemData实例 + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); // 绑定数据到视图,包括选择模式和选中状态 + } + } + + // 设置指定位置的项为选中或未选中状态,并通知适配器数据已更改 + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); // 更新选中索引映射 + notifyDataSetChanged(); // 通知适配器数据已更改,触发视图更新 + } + + // 检查当前是否处于选择模式 + public boolean isInChoiceMode() { + return mChoiceMode; // 返回选择模式的状态 + } + + // 设置选择模式的状态,并清除之前的选中项 + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); // 清除之前的选中项 + mChoiceMode = mode; // 更新选择模式的状态 + } + + // 全选或全不选笔记项(仅针对笔记类型) + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); // 获取当前的Cursor + for (int i = 0; i < getCount(); i++) { // 遍历所有项 + if (cursor.moveToPosition(i)) { // 移动到指定位置 + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { // 检查是否为笔记类型 + setCheckedItem(i, checked); // 设置选中状态 + } + } + } + } + + // 获取所有被选中的项的ID集合 + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); // 创建HashSet用于存储选中项的ID + for (Integer position : mSelectedIndex.keySet()) { // 遍历选中索引映射的键集合 + if (mSelectedIndex.get(position) == true) { // 检查是否选中 + Long id = getItemId(position); // 获取项的ID + if (id == Notes.ID_ROOT_FOLDER) { // 检查是否为根文件夹ID(通常不应被选中) + Log.d(TAG, "Wrong item id, should not happen"); // 记录日志 + } else { + itemSet.add(id); // 将ID添加到HashSet中 + } + } + } + return itemSet; // 返回包含所有选中项ID的HashSet + } +} // 获取已选择的小部件集合 +public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); // 创建一个用于存储小部件属性的HashSet + for (Integer position : mSelectedIndex.keySet()) { // 遍历所有已索引的位置 + if (mSelectedIndex.get(position) == true) { // 如果该位置被选中 + Cursor c = (Cursor) getItem(position); // 从适配器中获取该位置的Cursor + if (c != null) { // 如果Cursor不为空 + AppWidgetAttribute widget = new AppWidgetAttribute(); // 创建一个新的AppWidgetAttribute对象 + NoteItemData item = new NoteItemData(mContext, c); // 使用Cursor和上下文创建一个NoteItemData对象 + widget.widgetId = item.getWidgetId(); // 设置小部件ID + widget.widgetType = item.getWidgetType(); // 设置小部件类型 + itemSet.add(widget); // 将小部件属性添加到HashSet中 + /** + * 不要在这里关闭Cursor,只有适配器可以关闭它 + */ + } else { + Log.e(TAG, "Invalid cursor"); // 如果Cursor为空,记录错误日志 + return null; // 返回null表示出错 + } + } + } + return itemSet; // 返回包含所有已选择小部件属性的HashSet +} + +// 获取已选择项的数量 +public int getSelectedCount() { + Collection values = mSelectedIndex.values(); // 获取所有索引值的集合 + if (null == values) { + return 0; // 如果集合为空,返回0 + } + Iterator iter = values.iterator(); // 创建迭代器遍历集合 + int count = 0; // 初始化计数器 + while (iter.hasNext()) { // 遍历集合 + if (true == iter.next()) { // 如果值为true + count++; // 计数器加1 + } + } + return count; // 返回计数器的值 +} + +// 判断是否全部项都被选中 +public boolean isAllSelected() { + int checkedCount = getSelectedCount(); // 获取已选择项的数量 + return (checkedCount != 0 && checkedCount == mNotesCount); // 如果已选择项的数量不为0且等于总项数,则返回true +} + +// 判断指定位置的项是否被选中 +public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; // 如果指定位置的索引值为null,返回false + } + return mSelectedIndex.get(position); // 返回指定位置的索引值 +} + +// 当内容发生变化时调用 +@Override +protected void onContentChanged() { + super.onContentChanged(); // 调用父类方法 + calcNotesCount(); // 计算笔记数量 +} + +// 当Cursor改变时调用 +@Override +public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); // 调用父类方法 + calcNotesCount(); // 计算笔记数量 +} + +// 计算笔记数量 +private void calcNotesCount() { + mNotesCount = 0; // 初始化笔记数量为0 + for (int i = 0; i < getCount(); i++) { // 遍历所有项 + Cursor c = (Cursor) getItem(i); // 获取当前项的Cursor + if (c != null) { // 如果Cursor不为空 + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { // 如果Cursor指向的笔记类型是NOTE + mNotesCount++; // 笔记数量加1 + } + } else { + Log.e(TAG, "Invalid cursor"); // 如果Cursor为空,记录错误日志 + return; // 退出方法 + } + } +} diff --git a/NotesListItem.java b/NotesListItem.java new file mode 100644 index 0000000..fd9f12a --- /dev/null +++ b/NotesListItem.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// 导入所需的包和类 +package net.micode.notes.ui; + +import android.content.Context; +import android.text.format.DateUtils; // 提供日期和时间格式化的工具类 +import android.view.View; // Android视图系统的基类 +import android.widget.CheckBox; // 复选框控件 +import android.widget.ImageView; // 图像视图控件 +import android.widget.LinearLayout; // 线性布局控件 +import android.widget.TextView; // 文本视图控件 + +import net.micode.notes.R; // 引用应用的资源文件 +import net.micode.notes.data.Notes; // 引用Notes数据类 +import net.micode.notes.tool.DataUtils; // 引用数据工具类 +import net.micode.notes.tool.ResourceParser.NoteItemBgResources; // 引用背景资源解析类 + +// NotesListItem类继承自LinearLayout,用于表示笔记列表中的每一项 +public class NotesListItem extends LinearLayout { + // 成员变量,分别表示警告图标、标题、时间、联系人名称、数据项和复选框 + private ImageView mAlert; + private TextView mTitle; + private TextView mTime; + private TextView mCallName; + private NoteItemData mItemData; // 自定义类,用于存储笔记项的数据 + private CheckBox mCheckBox; + + // 构造函数,初始化NotesListItem + 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); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); // 注意这里使用的是Android系统提供的ID + } + + // bind方法用于绑定数据和设置UI + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 根据choiceMode和数据类型设置复选框的可见性 + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + mCheckBox.setVisibility(View.GONE); + } + + mItemData = data; // 保存数据项 + // 根据数据项的不同类型设置UI + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 如果是通话记录文件夹 + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题的文本样式 + // 设置标题文本,包括文件夹名称和文件数量 + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + mAlert.setImageResource(R.drawable.call_record); // 设置警告图标的资源 + } 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); + } else { + 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); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + // 设置时间文本 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 设置背景 + setBackground(data); + } + + // 根据数据项设置背景 + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); // 获取背景颜色ID + if (data.getType() == Notes.TYPE_NOTE) { + // 如果是笔记 + if (data.isSingle() || data.isOneFollowingFolder()) { + // 如果是单独一条笔记或者后面紧跟着一个文件夹 + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + // 如果是最后一条笔记 + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + // 如果是第一条笔记或者前面有多个文件夹 + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + // 其他情况 + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + // 如果是文件夹 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + // 获取当前绑定的数据项 + public NoteItemData getItemData() { + return mItemData; + } +} \ No newline at end of file diff --git a/NotesPreferenceActivity.java b/NotesPreferenceActivity.java new file mode 100644 index 0000000..005383f --- /dev/null +++ b/NotesPreferenceActivity.java @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +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; + + private GTaskReceiver mReceiver; + + private Account[] mOriAccounts; + + private boolean mHasAddedAccount; + + // 重写onCreate方法,这是Activity生命周期中的一个重要方法,用于初始化Activity +@Override +protected void onCreate(Bundle icicle) { + super.onCreate(icicle); // 调用父类的onCreate方法 + + // 设置应用图标作为导航按钮,并启用向上导航功能 + getActionBar().setDisplayHomeAsUpEnabled(true); + + // 从资源文件中加载偏好设置布局 + addPreferencesFromResource(R.xml.preferences); + + // 获取同步账户的分类偏好设置项 + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + + // 实例化广播接收器,用于接收来自GTaskSyncService的广播 + mReceiver = new GTaskReceiver(); + + // 创建IntentFilter并添加要监听的广播动作 + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + + // 注册广播接收器 + registerReceiver(mReceiver, filter); + + // 初始化原始账户数组为空 + mOriAccounts = null; + + // 使用LayoutInflater从布局资源中创建视图,并将其作为头部添加到偏好设置的列表中 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); +} + +// 重写onResume方法,当Activity重新进入用户视野时调用 +@Override +protected void onResume() { + super.onResume(); + + // 如果用户添加了新账户,则自动设置同步账户 + if (mHasAddedAccount) { + // 获取Google账户数组 + Account[] accounts = getGoogleAccounts(); + + // 如果原始账户数组不为空且当前账户数量多于原始账户数量 + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + // 遍历当前账户数组 + for (Account accountNew : accounts) { + boolean found = false; // 标记是否找到匹配的原始账户 + + // 遍历原始账户数组 + for (Account accountOld : mOriAccounts) { + // 如果账户名称相同,则标记为找到并跳出循环 + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + + // 如果未找到匹配的原始账户,则设置新账户为同步账户并跳出循环 + if (!found) { + setSyncAccount(accountNew.name); + break; + } + } + } + } + + // 刷新用户界面 + refreshUI(); +} + +// 重写onDestroy方法,当Activity即将被销毁时调用 +@Override +protected void onDestroy() { + // 如果广播接收器不为空,则注销广播接收器 + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + super.onDestroy(); // 调用父类的onDestroy方法 +} + +// 私有方法,用于加载账户偏好设置 +private void loadAccountPreference() { + // 清空同步账户分类下的所有偏好设置项 + mAccountCategory.removeAll(); + + // 创建一个新的偏好设置项 + Preference accountPref = new Preference(this); + + // 获取当前设置的同步账户名称 + final String defaultAccount = getSyncAccountName(this); + + // 设置偏好设置项的标题和摘要 + accountPref.setTitle(getString(R.string.preferences_account_title)); + accountPref.setSummary(getString(R.string.preferences_account_summary)); + + // 设置偏好设置项的点击监听器 + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + // 如果当前不在同步中 + if (!GTaskSyncService.isSyncing()) { + // 如果当前没有设置同步账户,则显示选择账户对话框 + if (TextUtils.isEmpty(defaultAccount)) { + showSelectAccountAlertDialog(); + } else { + // 如果已经设置了同步账户,则显示更改账户确认对话框 + showChangeAccountConfirmAlertDialog(); + } + } else { + // 如果当前在同步中,则显示无法更改账户的Toast提示 + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; // 表示事件已处理 + } + }); +} + + // 将一个偏好项(可能是账户相关的设置)添加到偏好分类中 +mAccountCategory.addPreference(accountPref); + +// 加载并设置同步按钮的状态和文本 +private void loadSyncButton() { + // 从布局文件中找到同步按钮和最后同步时间显示的TextView + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.preference_sync_status_textview); // 注意这里的ID拼写错误已修正 + + // 根据是否正在同步来设置按钮的文本和点击事件 + if (GTaskSyncService.isSyncing()) { // 如果正在同步 + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); // 设置按钮文本为取消同步 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 点击时取消同步 + } + }); + } else { // 如果不在同步 + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); // 设置按钮文本为立即同步 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); // 点击时开始同步 + } + }); + } + // 根据是否有同步账户来启用或禁用同步按钮 + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // 设置最后同步时间的显示 + if (GTaskSyncService.isSyncing()) { // 如果正在同步 + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); // 显示同步进度 + lastSyncTimeView.setVisibility(View.VISIBLE); // 显示TextView + } else { // 如果不在同步 + long lastSyncTime = getLastSyncTime(this); // 获取最后同步时间 + if (lastSyncTime != 0) { // 如果有最后同步时间 + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); // 格式化并显示最后同步时间 + lastSyncTimeView.setVisibility(View.VISIBLE); // 显示TextView + } else { // 如果没有最后同步时间 + lastSyncTimeView.setVisibility(View.GONE); // 隐藏TextView + } + } +} + +// 刷新用户界面,包括加载账户偏好和同步按钮的状态 +private void refreshUI() { + loadAccountPreference(); // 加载账户偏好(方法未在代码段中给出) + loadSyncButton(); // 加载并设置同步按钮 +} + +// 显示一个对话框让用户选择一个同步账户 +private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 自定义对话框的标题部分 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); // 设置标题文本 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); // 设置副标题文本 + + dialogBuilder.setCustomTitle(titleView); // 设置自定义标题 + dialogBuilder.setPositiveButton(null, null); // 不设置默认的确定按钮及其事件 + + // 获取Google账户列表和当前同步的账户 + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + + // 初始化一些变量 + mOriAccounts = accounts; // 原始账户列表 + mHasAddedAccount = false; // 是否添加了新账户(未在代码段中明确使用) + + if (accounts.length > 0) { // 如果有账户 + CharSequence[] items = new CharSequence[accounts.length]; // 创建账户名称数组 + final CharSequence[] itemMapping = items; // 创建一个引用,用于在点击事件中访问 + int checkedItem = -1; // 默认没有选中的项 + int index = 0; + for (Account account : accounts) { // 遍历账户列表 + if (TextUtils.equals(account.name, defAccount)) { // 如果当前账户是默认同步账户 + checkedItem = index; // 设置选中项 + } + items[index++] = account.name; // 添加账户名称到数组 + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, // 设置单选列表项和默认选中项 + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); // 设置选中的账户为同步账户 + dialog.dismiss(); // 关闭对话框 + refreshUI(); // 刷新用户界面 + } + }); + } + + // 创建一个对话框,用于添加账户 +// 首先,通过LayoutInflater从当前上下文(this)加载add_account_text布局 +View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); +// 设置对话框的内容视图为addAccountView +dialogBuilder.setView(addAccountView); + +// 显示对话框 +final AlertDialog dialog = dialogBuilder.show(); +// 为addAccountView设置点击监听器 +addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // 标记账户已添加 + mHasAddedAccount = true; + // 创建一个意图,用于打开添加账户的设置页面 + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + // 为意图添加额外的数据,指定账户类型过滤器为gmail-ls(Gmail账户) + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {"gmail-ls"}); + // 启动活动以响应意图,并等待结果(这里请求码使用了-1,通常应使用有意义的正整数) + startActivityForResult(intent, -1); + // 关闭对话框 + dialog.dismiss(); + } +}); + +// 显示一个确认更改账户的对话框 +private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 通过LayoutInflater加载account_dialog_title布局 + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + // 设置标题文本 + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, getSyncAccountName(this))); + // 设置副标题文本 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + // 设置自定义标题视图 + dialogBuilder.setCustomTitle(titleView); + + // 设置对话框的选项菜单 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + // 为选项菜单设置点击监听器 + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 根据用户的选择执行不同的操作 + if (which == 0) { + showSelectAccountAlertDialog(); // 显示选择账户的对话框 + } else if (which == 1) { + removeSyncAccount(); // 移除同步账户 + refreshUI(); // 刷新用户界面 + } + } + }); + // 显示对话框 + dialogBuilder.show(); +} + +// 获取设备上所有的Google账户 +private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); // 获取Google类型的账户 +} + +// 设置同步账户 +private void setSyncAccount(String account) { + // 如果当前设置的同步账户与传入的账户不同 + if (!getSyncAccountName(this).equals(account)) { + // 更新SharedPreferences中的同步账户名称 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // 清理上一次的同步时间 + setLastSyncTime(this, 0); + + // 清理本地与Google任务相关的信息 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); // 清除GTASK_ID + values.put(NoteColumns.SYNC_ID, 0); // 重置SYNC_ID + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); // 更新数据库 + } + }).start(); + + // 显示成功设置账户的Toast消息 + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } +} + +// 移除同步账户 +private void removeSyncAccount() { + // 从SharedPreferences中移除同步账户名称和最后一次同步时间 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // 清理本地与Google任务相关的信息 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); // 清除GTASK_ID + values.put(NoteColumns.SYNC_ID, 0); // 重置SYNC_ID + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); // 更新数据库 + } + }).start(); +} + +// 定义一个方法,用于从SharedPreferences中获取同步账户名称 +public static String getSyncAccountName(Context context) { + // 使用context获取SharedPreferences实例,PREFERENCE_NAME是偏好文件的名称,Context.MODE_PRIVATE表示只有当前应用可以访问 + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + // 从偏好设置中获取字符串类型的同步账户名称,如果不存在则返回空字符串"" + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); +} + +// 定义一个方法,用于设置最后同步时间到SharedPreferences中 +public static void setLastSyncTime(Context context, long time) { + // 获取SharedPreferences实例 + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + // 获取SharedPreferences.Editor对象,用于修改偏好设置 + SharedPreferences.Editor editor = settings.edit(); + // 将最后同步时间保存到偏好设置中 + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + // 提交更改 + editor.commit(); +} + +// 定义一个方法,用于从SharedPreferences中获取最后同步时间 +public static long getLastSyncTime(Context context) { + // 获取SharedPreferences实例 + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + // 从偏好设置中获取最后同步时间,如果不存在则返回0 + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); +} + +// 定义一个内部类GTaskReceiver,它继承自BroadcastReceiver +private class GTaskReceiver extends BroadcastReceiver { + + // 当接收到广播时调用此方法 + @Override + public void onReceive(Context context, Intent intent) { + // 刷新用户界面 + refreshUI(); + // 检查广播是否包含表示正在同步的额外数据 + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + // 获取同步状态文本视图,并设置其文本为广播中的进度消息 + TextView syncStatus = (TextView) findViewById(R.id.preference_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + } +} + +// 定义一个方法,用于处理菜单项的点击事件 +public boolean onOptionsItemSelected(MenuItem item) { + // 根据点击的菜单项ID执行不同的操作 + switch (item.getItemId()) { + // 如果是点击了应用的“返回”图标(通常位于左上角) + case android.R.id.home: + // 创建一个Intent,用于启动NotesListActivity + Intent intent = new Intent(this, NotesListActivity.class); + // 添加标志,以清除当前活动之上的所有活动 + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + // 启动目标Activity + startActivity(intent); + // 表示事件已处理 + return true; + // 默认情况下,返回false表示事件未处理 + default: + return false; + } +}