目录
ui:用户界面类
AlarmAlertActivity:闹钟提醒界面
AlarmmlnitReceiver:闹钟提醒启动消息接收器
AlarmReceiver:闹钟提醒接收器
DataTimePicker:设置提醒时间的部件
DataTimePickerDialog:设置提醒时间的对话框界面
DropdownMenu:下拉菜单界面
FoldersListAdapter:文件夹列表链接器(链接数据库)
NoteEditAcfvity:便签编辑活动
NoteEditText:便签的文本编辑界面
NoteltemData:便签项数据
NoteUstActivity:主界面,实现处理文件列表的活动NoteUstAdapter:便签列表链接器(链接数据库)
NoteUstltem:便签列表项
NotePreferenceActivity:便签同步的设置界面
1 AlarmAlertActivity
package net.micode.notes.ui; // 定义了该类的包路径
import android.app.Activity; // 导入Activity类,用于创建Android活动
import android.app.AlertDialog; // 导入AlertDialog类,用于创建对话框
import android.content.Context; // 导入Context类,用于获取应用程序环境
import android.content.DialogInterface; // 导入DialogInterface类,用于处理对话框事件
import android.content.DialogInterface.OnClickListener; // 导入OnClickListener接口,用于处理对话框按钮点击事件
import android.content.DialogInterface.OnDismissListener; // 导入OnDismissListener接口,用于处理对话框关闭事件
import android.content.Intent; // 导入Intent类,用于启动活动和传递数据
import android.media.AudioManager; // 导入AudioManager类,用于管理音频设置
import android.media.MediaPlayer; // 导入MediaPlayer类,用于播放音频
import android.media.RingtoneManager; // 导入RingtoneManager类,用于获取铃声
import android.net.Uri; // 导入Uri类,用于表示资源路径
import android.os.Bundle; // 导入Bundle类,用于保存活动状态
import android.os.PowerManager; // 导入PowerManager类,用于管理电源设置
import android.provider.Settings; // 导入Settings类,用于获取系统设置
import android.view.Window; // 导入Window类,用于管理窗口
import android.view.WindowManager; // 导入WindowManager类,用于管理窗口标志
import net.micode.notes.R; // 导入应用内部定义的资源类
import net.micode.notes.data.Notes; // 导入应用内部定义的笔记数据类
import net.micode.notes.tool.DataUtils; // 导入应用内部定义的数据工具类
import java.io.IOException; // 导入IOException类,用于处理输入输出异常
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { // 定义AlarmAlertActivity类,继承自Activity,并实现OnClickListener和OnDismissListener接口
private long mNoteId; // 定义笔记的ID
private String mSnippet; // 定义笔记内容的简短预览
private static final int SNIPPET_PREW_MAX_LEN = 60; // 定义预览文本的最大长度
private MediaPlayer mPlayer; // 定义用于播放提醒声音的MediaPlayer对象
@Override
protected void onCreate(Bundle savedInstanceState) { // 重写onCreate方法,在活动创建时调用
super.onCreate(savedInstanceState); // 调用父类的onCreate方法
requestWindowFeature(Window.FEATURE_NO_TITLE); // 请求无标题的窗口
final Window win = getWindow(); // 获取当前窗口
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // 设置窗口在锁屏时显示
if (!isScreenOn()) { // 检查屏幕是否处于打开状态
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕唤醒
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 打开屏幕
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许在屏幕打开时锁定
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 布局插入装饰
}
Intent intent = getIntent(); // 获取启动该活动的意图
try {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); // 从意图中获取笔记ID
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); // 获取笔记内容的简短预览
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, // 如果预览文本长度超过最大长度,则截取
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet; // 否则使用原始预览文本
} catch (IllegalArgumentException e) { // 捕获非法参数异常
e.printStackTrace(); // 打印异常信息
return; // 返回
}
mPlayer = new MediaPlayer(); // 创建MediaPlayer对象
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { // 检查笔记在数据库中是否可见
showActionDialog(); // 显示动作对话框
playAlarmSound(); // 播放提醒声音
} else {
finish(); // 结束活动
}
}
private boolean isScreenOn() { // 定义检查屏幕是否处于打开状态的方法
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); // 获取电源管理服务
return pm.isScreenOn(); // 返回屏幕是否处于打开状态
}
private void playAlarmSound() { // 定义播放提醒声音的方法
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); // 获取默认的闹钟铃声URI
int silentModeStreams = Settings.System.getInt(getContentResolver(), // 获取静音模式下影响的音频流
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { // 检查闹钟音频流是否受静音模式影响
mPlayer.setAudioStreamType(silentModeStreams); // 设置音频流类型
} else {
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); // 设置音频流类型为闹钟
}
try {
mPlayer.setDataSource(this, url); // 设置数据源
mPlayer.prepare(); // 准备播放
mPlayer.setLooping(true); // 设置循环播放
mPlayer.start(); // 开始播放
} catch (IllegalArgumentException e) { // 捕获非法参数异常
e.printStackTrace(); // 打印异常信息
} catch (SecurityException e) { // 捕获安全异常
e.printStackTrace(); // 打印异常信息
} catch (IllegalStateException e) { // 捕获非法状态异常
e.printStackTrace(); // 打印异常信息
} catch (IOException e) { // 捕获输入输出异常
e.printStackTrace(); // 打印异常信息
}
}
private void showActionDialog() { // 定义显示动作对话框的方法
AlertDialog.Builder dialog = new AlertDialog.Builder(this); // 创建对话框构建器
dialog.setTitle(R.string.app_name); // 设置对话框标题
dialog.setMessage(mSnippet); // 设置对话框消息
dialog.setPositiveButton(R.string.notealert_ok, this); // 设置正面按钮
if (isScreenOn()) { // 检查屏幕是否处于打开状态
dialog.setNegativeButton(R.string.notealert_enter, this); // 设置负面按钮
}
dialog.show().setOnDismissListener(this); // 显示对话框并设置关闭监听器
}
public void onClick(DialogInterface dialog, int which) { // 实现OnClickListener接口的onClick方法,处理对话框按钮点击事件
switch (which) { // 根据按钮类型进行处理
case DialogInterface.BUTTON_NEGATIVE: // 如果是负面按钮
Intent intent = new Intent(this, NoteEditActivity.class); // 创建启动笔记编辑活动的意图
intent.setAction(Intent.ACTION_VIEW); // 设置动作
intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID
startActivity(intent); // 启动活动
break; // 结束处理
default: // 默认情况
break; // 结束处理
}
}
public void onDismiss(DialogInterface dialog) { // 实现OnDismissListener接口的onDismiss方法,处理对话框关闭事件
stopAlarmSound(); // 停止播放提醒声音
finish(); // 结束活动
}
private void stopAlarmSound() { // 定义停止播放提醒声音的方法
if (mPlayer != null) { // 检查MediaPlayer对象是否为空
mPlayer.stop(); // 停止播放
mPlayer.release(); // 释放资源
mPlayer = null; // 将MediaPlayer对象设置为空
}
}
}
2 AlarmInitReceiver
/*
* 该类是广播接收器,用于在应用启动时初始化提醒设置。
* 当系统启动时,它会检查数据库中所有设置了提醒的笔记,并为每个笔记设置相应的提醒。
*/
package net.micode.notes.ui; // 定义该类的包路径
import android.app.AlarmManager; // 导入AlarmManager类,用于设置提醒
import android.app.PendingIntent; // 导入PendingIntent类,用于延迟执行意图
import android.content.BroadcastReceiver; // 导入BroadcastReceiver类,用于接收广播
import android.content.ContentUris; // 导入ContentUris类,用于处理URI
import android.content.Context; // 导入Context类,用于获取应用环境
import android.content.Intent; // 导入Intent类,用于启动活动和传递数据
import android.database.Cursor; // 导入Cursor类,用于查询数据库
import net.micode.notes.data.Notes; // 导入应用内部定义的笔记数据类
import net.micode.notes.data.Notes.NoteColumns; // 导入笔记数据类的列定义
public class AlarmInitReceiver extends BroadcastReceiver { // 定义AlarmInitReceiver类,继承自BroadcastReceiver
// 查询笔记时需要的列
private static final String[] PROJECTION = new String[]{
NoteColumns.ID, // 笔记ID
NoteColumns.ALERTED_DATE // 提醒日期
};
// 列的索引
private static final int COLUMN_ID = 0; // ID列的索引
private static final int COLUMN_ALERTED_DATE = 1; // 提醒日期列的索引
/**
* 当接收到广播时执行的操作。主要用于设置所有已记录的提醒时间。
*
* @param context 上下文,提供访问应用全局功能的入口。
* @param intent 携带了触发该接收器的广播信息。
*/
@Override
public void onReceive(Context context, Intent intent) { // 重写onReceive方法,当接收到广播时执行
// 获取当前日期和时间
long currentDate = System.currentTimeMillis(); // 获取当前时间戳
// 查询数据库中所有需要提醒的笔记
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, // 查询笔记内容的URI
PROJECTION, // 查询的列
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件
new String[]{String.valueOf(currentDate)}, // 查询参数
null); // 排序方式
if (c != null) { // 检查Cursor是否为空
// 遍历查询结果,为每个需要提醒的笔记设置提醒
if (c.moveToFirst()) { // 移动到第一行
do {
// 获取提醒日期
long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 获取提醒日期
// 创建Intent,用于在提醒时间触发AlarmReceiver
Intent sender = new Intent(context, AlarmReceiver.class); // 创建启动AlarmReceiver的意图
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); // 设置意图的数据URI
// 创建PendingIntent,它是一个延迟的意图,可以在特定时间由系统触发
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); // 创建PendingIntent
// 获取AlarmManager服务,用于设置提醒
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE); // 获取AlarmManager服务
// 设置提醒
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); // 设置提醒时间
} while (c.moveToNext()); // 移动到下一行
}
// 关闭Cursor,释放资源
c.close(); // 关闭Cursor
}
}
}
3 AlarmReceiver
/*
* AlarmReceiver类 - 用于处理闹钟广播接收
* 当接收到闹钟相关的广播时,该类会启动一个指定的Activity
*
* extends BroadcastReceiver: 继承自Android的BroadcastReceiver类
*/
package net.micode.notes.ui; // 定义该类的包路径
import android.content.BroadcastReceiver; // 导入BroadcastReceiver类,用于接收广播
import android.content.Context; // 导入Context类,用于获取应用环境
import android.content.Intent; // 导入Intent类,用于启动活动和传递数据
public class AlarmReceiver extends BroadcastReceiver { // 定义AlarmReceiver类,继承自BroadcastReceiver
/*
* onReceive方法 - 系统调用的接收广播的方法
* 当接收到广播时,该方法会被调用,然后启动AlarmAlertActivity
*
* @param context 上下文对象,提供了调用环境的信息
* @param intent 包含广播的内容
*/
@Override
public void onReceive(Context context, Intent intent) { // 重写onReceive方法,当接收到广播时执行
// 设置Intent的类,以便启动AlarmAlertActivity
intent.setClass(context, AlarmAlertActivity.class); // 设置Intent的目标Activity为AlarmAlertActivity
// 添加标志,表示在一个新的任务中启动Activity
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 添加标志,确保Activity在一个新的任务中启动
// 根据设置的Intent启动Activity
context.startActivity(intent); // 启动AlarmAlertActivity
}
}
4 DateTimePicker
package net.micode.notes.ui;
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;
public class DateTimePicker extends FrameLayout {
// 默认启用状态
private static final boolean DEFAULT_ENABLE_STATE = true;
// 半天的小时数
private static final int HOURS_IN_HALF_DAY = 12;
// 一整天的小时数
private static final int HOURS_IN_ALL_DAY = 24;
// 一周的天数
private static final int DAYS_IN_ALL_WEEK = 7;
// 日期选择器的最小值
private static final int DATE_SPINNER_MIN_VAL = 0;
// 日期选择器的最大值
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
// 24小时制小时选择器的最小值
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
// 24小时制小时选择器的最大值
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
// 12小时制小时选择器的最小值
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
// 12小时制小时选择器的最大值
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
// 分钟选择器的最小值
private static final int MINUT_SPINNER_MIN_VAL = 0;
// 分钟选择器的最大值
private static final int MINUT_SPINNER_MAX_VAL = 59;
// 上下午选择器的最小值
private static final int AMPM_SPINNER_MIN_VAL = 0;
// 上下午选择器的最大值
private static final int AMPM_SPINNER_MAX_VAL = 1;
// 日期选择器
private final NumberPicker mDateSpinner;
// 小时选择器
private final NumberPicker mHourSpinner;
// 分钟选择器
private final NumberPicker mMinuteSpinner;
// 上下午选择器
private final NumberPicker mAmPmSpinner;
// 当前日期
private Calendar mDate;
// 用于显示日期的字符串数组
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
// 当前是否为上午
private boolean mIsAm;
// 当前是否为24小时制视图
private boolean mIs24HourView;
// 控件是否启用
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
// 是否正在初始化
private boolean mInitialising;
// 日期时间改变监听器
private OnDateTimeChangedListener mOnDateTimeChangedListener;
// 日期选择器的值改变监听器
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 根据新旧值的差异更新日期
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
updateDateControl();
onDateTimeChanged();
}
};
// 小时选择器的值改变监听器
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 根据小时的变化更新日期和上下午状态
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
if (!mIs24HourView) {
// 处理12小时制下的日期变化和上下午切换
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
updateAmPmControl();
}
} else {
// 处理24小时制下的日期变化
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
// 更新小时并触发日期时间改变事件
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
onDateTimeChanged();
// 如果日期有变化,则更新年月日
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
// 分别为分钟和AM/PM选择器监听器设置匿名内部类,实现数值变化时的处理逻辑。
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 计算小时的偏移量,当从最大值变为最小值或从最小值变为最大值时调整
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0;
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
} else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
// 根据偏移量更新日期和小时选择器,并检查是否需要切换AM/PM
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
// 更新分钟值并触发日期变化的回调
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 切换AM/PM状态,并更新日期和AM/PM选择器
mIsAm = !mIsAm;
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
updateAmPmControl();
onDateTimeChanged();
}
};
// 定义日期时间变化的回调接口
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小时制视图
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
mDate = Calendar.getInstance();
mInitialising = true;
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
inflate(context, R.layout.datetime_picker, this);
// 初始化日期、小时、分钟和AM/PM选择器,并设置相应的监听器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
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();
set24HourView(is24HourView);
// 设置为当前时间
setCurrentDate(date);
setEnabled(isEnabled());
// 设置内容描述
mInitialising = false;
}
/**
* 设置控件的启用状态。
* 如果当前状态与传入状态相同,则不进行任何操作。
* 启用或禁用日期和时间选择器,并更新内部启用状态。
*
* @param enabled 控件是否启用
*/
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
// 同时启用或禁用日期和时间选择器
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
mIsEnabled = enabled;
}
/**
* 获取控件的启用状态。
*
* @return 控件是否启用
*/
@Override
public boolean isEnabled() {
return mIsEnabled;
}
/**
* 获取当前日期的时间戳(毫秒)。
*
* @return 当前日期的毫秒时间戳
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
* 设置当前日期。
* 根据传入的毫秒时间戳更新日期选择器的值。
*
* @param date The current date in millis
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
// 通过日历实例的详细字段设置当前日期和时间
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
}
/**
* 设置当前日期和时间。
* 分别设置年、月、日、时和分。
*
* @param year 当前年份
* @param month 当前月份
* @param dayOfMonth 当前日
* @param hourOfDay 当前小时
* @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;
}
mDate.set(Calendar.YEAR, year);
updateDateControl(); // 更新日期控件
onDateTimeChanged(); // 触发日期时间改变事件
}
/**
* 获取当前月份
*
* @return 当前的月份(从0开始)
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
* 设置当前月份
*
* @param month 当前的月份(从0开始)
*/
public void setCurrentMonth(int month) {
// 如果不是初始化状态并且设置的月份与当前月份相同,则直接返回
if (!mInitialising && month == getCurrentMonth()) {
return;
}
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;
}
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小时制返回不同的值。
* 如果是24小时制,与{@link #getCurrentHourOfDay()}返回相同结果;
* 否则,将小时转换为12小时制,并考虑上午/下午。
*
* @return 当前的小时(根据视图模式可能是12小时制)
*/
private int getCurrentHour() {
if (mIs24HourView) {
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
// 转换为12小时制
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY;
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
}
}
}
/**
* 设置当前小时(24小时制),范围为(0~23)
*
* @param hourOfDay 当前小时数
*/
public void setCurrentHour(int hourOfDay) {
// 如果在初始化中或者小时未改变,则直接返回
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
// 如果不是24小时视图,则调整小时数并更新AM/PM控制
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false;
if (hourOfDay > HOURS_IN_HALF_DAY) {
hourOfDay -= HOURS_IN_HALF_DAY;
}
} else {
mIsAm = true;
if (hourOfDay == 0) {
hourOfDay = HOURS_IN_HALF_DAY;
}
}
updateAmPmControl();
}
mHourSpinner.setValue(hourOfDay);
onDateTimeChanged();
}
/**
* 获取当前分钟数
*
* @return 当前分钟数
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
* 设置当前分钟数
*
* @param minute 当前分钟数值
*/
public void setCurrentMinute(int minute) {
// 如果在初始化中或者分钟数未改变,则直接返回
if (!mInitialising && minute == getCurrentMinute()) {
return;
}
mMinuteSpinner.setValue(minute);
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged();
}
/**
* 获取当前是否为24小时视图
*
* @return 如果是24小时视图返回true,否则返回false
*/
public boolean is24HourView() {
return mIs24HourView;
}
/**
* 设置当前视图为24小时制还是AM/PM制
*
* @param is24HourView 如果为true表示24小时制,false表示AM/PM制
*/
public void set24HourView(boolean is24HourView) {
// 如果视图模式未改变,则直接返回
if (mIs24HourView == is24HourView) {
return;
}
mIs24HourView = is24HourView;
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
updateHourControl();
setCurrentHour(hour);
updateAmPmControl();
}
/**
* 更新日期控制组件,显示正确的日期选项
*/
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null);
// 循环设置一周内每一天的显示文本
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
mDateSpinner.setDisplayedValues(mDateDisplayValues);
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
mDateSpinner.invalidate();
}
/**
* 根据当前是否为24小时视图来更新AM/PM控制组件的显示和值
*/
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
} else {
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
/**
* 根据当前是否为24小时视图来更新小时控制组件的最小值和最大值
*/
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
/**
* 设置点击“设置”按钮时的回调接口
*
* @param callback 回调接口实例,如果为null则不执行任何操作
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
/**
* 当日期时间被改变时调用此方法,如果设置了日期时间改变监听器,则触发监听器的回调方法
*/
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}
}
5 DateTimePickerDialog
- 包声明:
package net.micode.notes.ui;
声明了该类所在的包。- 导入语句:导入了一系列Java和Android相关的类,包括日历、对话框、上下文、日期时间格式化等。
- 类声明:
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
声明了一个名为DateTimePickerDialog
的公共类,继承自AlertDialog
并实现OnClickListener
接口。- 成员变量:
private Calendar mDate = Calendar.getInstance();
声明了一个私有成员变量mDate
,用于存储当前选择的日期和时间。private boolean mIs24HourView;
声明了一个私有成员变量mIs24HourView
,用于指示日期时间选择器是否使用24小时制。private OnDateTimeSetListener mOnDateTimeSetListener;
声明了一个私有成员变量mOnDateTimeSetListener
,用于存储日期时间设置监听器。private DateTimePicker mDateTimePicker;
声明了一个私有成员变量mDateTimePicker
,用于存储日期时间选择器视图。- 接口声明:
public interface OnDateTimeSetListener { ... }
声明了一个公共接口OnDateTimeSetListener
,用于处理日期时间被设置的事件。- 构造函数:
public DateTimePickerDialog(Context context, long date) { ... }
声明了类的构造函数,接受上下文和初始日期时间值作为参数。super(context);
调用父类构造函数。mDateTimePicker = new DateTimePicker(context);
创建DateTimePicker
实例。setView(mDateTimePicker);
设置对话框视图为DateTimePicker
。mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { ... });
设置日期时间改变的监听器。mDate.setTimeInMillis(date);
设置初始日期时间。mDate.set(Calendar.SECOND, 0);
设置秒数为0。mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
设置DateTimePicker
的当前日期时间。setButton(context.getString(R.string.datetime_dialog_ok), this);
设置对话框的确认按钮。setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener) null);
设置对话框的取消按钮。set24HourView(DateFormat.is24HourFormat(this.getContext()));
根据系统设置决定是否使用24小时制。updateTitle(mDate.getTimeInMillis());
更新标题以显示当前选择的日期和时间。- 方法:
public void set24HourView(boolean is24HourView) { ... }
设置日期时间选择器是否使用24小时制。public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { ... }
设置日期时间被设置时的监听器。private void updateTitle(long date) { ... }
更新对话框标题以显示当前选择的日期和时间。public void onClick(DialogInterface arg0, int arg1) { ... }
点击按钮时的处理逻辑,如果设置了日期时间设置监听器,则调用其OnDateTimeSet
方法。
/*
* DateTimePickerDialog类提供了一个日期和时间选择器对话框。
* 用户可以选择一个日期和时间,然后通过监听器回调返回选择的值。
*/
package net.micode.notes.ui; // 定义包名
import java.util.Calendar; // 导入Calendar类,用于处理日期和时间
import net.micode.notes.R; // 导入资源类,用于访问资源
import net.micode.notes.ui.DateTimePicker; // 导入DateTimePicker类,用于日期时间选择器视图
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; // 导入日期时间改变监听器接口
import android.app.AlertDialog; // 导入AlertDialog类,用于创建对话框
import android.content.Context; // 导入Context类,用于获取上下文
import android.content.DialogInterface; // 导入DialogInterface类,用于处理对话框事件
import android.content.DialogInterface.OnClickListener; // 导入OnClickListener接口,用于处理按钮点击事件
import android.text.format.DateFormat; // 导入DateFormat类,用于日期时间格式化
import android.text.format.DateUtils; // 导入DateUtils类,用于日期时间格式化
public class DateTimePickerDialog extends AlertDialog implements OnClickListener { // 定义DateTimePickerDialog类,继承自AlertDialog并实现OnClickListener接口
// 当前选择的日期和时间
private Calendar mDate = Calendar.getInstance();
// 用于指示日期时间选择器是否使用24小时制
private boolean mIs24HourView;
// 日期时间设置监听器,用于处理日期时间选择后的回调
private OnDateTimeSetListener mOnDateTimeSetListener;
// 日期时间选择器视图
private DateTimePicker mDateTimePicker;
/**
* 日期时间设置监听器接口。
* 实现此接口的类需要提供OnDateTimeSet方法来处理日期时间被设置的事件。
*/
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date);
}
/**
* 构造函数初始化日期时间选择器对话框。
*
* @param context 上下文对象,通常是指Activity。
* @param date 初始显示的日期时间值。
*/
public DateTimePickerDialog(Context context, long date) {
super(context); // 调用父类构造函数
mDateTimePicker = new DateTimePicker(context); // 创建DateTimePicker实例
setView(mDateTimePicker); // 设置对话框视图为DateTimePicker
// 设置日期时间改变的监听器
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 更新内部日期时间值
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
// 更新对话框的标题显示
updateTitle(mDate.getTimeInMillis());
}
});
mDate.setTimeInMillis(date); // 设置初始日期时间
mDate.set(Calendar.SECOND, 0); // 设置秒数为0
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); // 设置DateTimePicker的当前日期时间
// 设置对话框的确认和取消按钮
setButton(context.getString(R.string.datetime_dialog_ok), this);
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener) null);
// 根据系统设置决定是否使用24小时制
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 更新标题以显示当前选择的日期和时间
updateTitle(mDate.getTimeInMillis());
}
/**
* 设置日期时间选择器是否使用24小时制。
*
* @param is24HourView 是否使用24小时制。
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
/**
* 设置日期时间被设置时的监听器。
*
* @param callBack 日期时间设置监听器对象。
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
* 更新对话框标题以显示当前选择的日期和时间。
*
* @param date 当前选择的日期时间值。
*/
private void updateTitle(long date) {
// 根据是否使用24小时制来格式化日期时间显示
int flag =
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
/**
* 点击按钮时的处理逻辑。
* 如果设置了日期时间设置监听器,则调用其OnDateTimeSet方法,传入当前选择的日期时间值。
*
* @param arg0 对话框对象。
* @param arg1 按钮标识。
*/
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}
6 DropdownMenu
- 包声明:
package net.micode.notes.ui;
声明了该类所在的包。- 导入语句:导入了一系列Android相关的类,包括上下文、菜单、视图、按钮、弹出菜单等。
- 类声明:
public class DropdownMenu {
声明了一个名为DropdownMenu
的公共类。- 成员变量:
private Button mButton;
声明了一个私有成员变量mButton
,用于存储按钮实例。private PopupMenu mPopupMenu;
声明了一个私有成员变量mPopupMenu
,用于存储弹出菜单实例。private Menu mMenu;
声明了一个私有成员变量mMenu
,用于存储菜单项集合。- 构造函数:
public DropdownMenu(Context context, Button button, int menuId) {
声明了类的构造函数,接受上下文、按钮和菜单资源ID作为参数。mButton = button;
初始化按钮。mButton.setBackgroundResource(R.drawable.dropdown_icon);
设置按钮背景为下拉图标。mPopupMenu = new PopupMenu(context, mButton);
创建PopupMenu
实例,锚点为按钮。mMenu = mPopupMenu.getMenu();
获取菜单项的集合。mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
加载菜单项。mButton.setOnClickListener(new OnClickListener() { ... });
设置按钮点击事件,点击后显示下拉菜单。- 方法:
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { ... }
设置下拉菜单项的点击事件监听器。public MenuItem findItem(int id) { ... }
根据ID查找菜单项。public void setTitle(CharSequence title) { ... }
设置按钮的标题。
/*
* DropdownMenu类用于创建和管理一个下拉菜单。
* 该类封装了一个Button和一个PopupMenu,通过点击Button来显示下拉菜单。
*/
package net.micode.notes.ui; // 定义包名
import android.content.Context; // 导入Context类,用于获取上下文
import android.view.Menu; // 导入Menu类,用于管理菜单项
import android.view.MenuItem; // 导入MenuItem类,用于表示菜单项
import android.view.View; // 导入View类,用于处理视图
import android.view.View.OnClickListener; // 导入OnClickListener接口,用于处理点击事件
import android.widget.Button; // 导入Button类,用于创建按钮
import android.widget.PopupMenu; // 导入PopupMenu类,用于创建弹出菜单
import android.widget.PopupMenu.OnMenuItemClickListener; // 导入OnMenuItemClickListener接口,用于处理菜单项点击事件
import net.micode.notes.R; // 导入资源类,用于访问资源
public class DropdownMenu { // 定义DropdownMenu类
private Button mButton; // 弹出下拉菜单的按钮
private PopupMenu mPopupMenu; // 弹出的下拉菜单
private Menu mMenu; // 下拉菜单的项目集合
/**
* DropdownMenu的构造函数。
*
* @param context 上下文对象,通常是指Activity。
* @param button 用于触发下拉菜单显示的按钮。
* @param menuId 菜单资源ID,用于加载下拉菜单的项目。
*/
public DropdownMenu(Context context, Button button, int menuId) {
mButton = button; // 初始化按钮
mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮背景为下拉图标
mPopupMenu = new PopupMenu(context, mButton); // 创建PopupMenu实例,锚点为按钮
mMenu = mPopupMenu.getMenu(); // 获取菜单项的集合
mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 加载菜单项
// 设置按钮点击事件,点击后显示下拉菜单
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mPopupMenu.show(); // 显示下拉菜单
}
});
}
/**
* 设置下拉菜单项的点击事件监听器。
*
* @param listener PopupMenu的OnMenuItemClickListener,用于监听菜单项的点击事件。
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener); // 设置菜单项点击监听器
}
}
/**
* 根据ID查找菜单项。
*
* @param id 菜单项的ID。
* @return 返回找到的MenuItem对象,如果未找到则返回null。
*/
public MenuItem findItem(int id) {
return mMenu.findItem(id); // 根据ID查找菜单项
}
/**
* 设置按钮的标题。
*
* @param title 按钮要显示的标题。
*/
public void setTitle(CharSequence title) {
mButton.setText(title); // 设置按钮显示的文本
}
}
7 FoldersListAdapter
- 包声明:
package net.micode.notes.ui;
声明了该类所在的包。- 导入语句:导入了一系列Java和Android相关的类,包括上下文、游标、视图、适配器、布局、文本显示等。
- 类声明:
public class FoldersListAdapter extends CursorAdapter {
声明了一个名为FoldersListAdapter
的公共类,继承自CursorAdapter
。- 常量声明:
public static final String[] PROJECTION = { ... };
声明了一个公共静态最终的字符串数组PROJECTION
,用于查询时使用的列名。public static final int ID_COLUMN = 0;
声明了一个公共静态最终的整数ID_COLUMN
,表示ID列的索引。public static final int NAME_COLUMN = 1;
声明了一个公共静态最终的整数NAME_COLUMN
,表示名称列的索引。- 构造函数:
public FoldersListAdapter(Context context, Cursor c) { ... }
声明了类的构造函数,接受上下文和数据源游标作为参数,并调用父类构造函数。- 方法:
@Override public View newView(Context context, Cursor cursor, ViewGroup parent) { ... }
重写了newView
方法,用于创建新的列表项视图,返回FolderListItem
实例。@Override public void bindView(View view, Context context, Cursor cursor) { ... }
重写了bindView
方法,用于绑定数据到已有的视图上,根据文件夹ID设置文件夹名称。public String getFolderName(Context context, int position) { ... }
声明了一个公共方法getFolderName
,用于根据位置获取文件夹名称。- 内部类:
private class FolderListItem extends LinearLayout { ... }
声明了一个私有内部类FolderListItem
,继承自LinearLayout
,用于表示文件夹列表中的一个项的视图。public FolderListItem(Context context) { ... }
声明了FolderListItem
的构造函数,加载布局文件并初始化文件夹名称的文本视图。public void bind(String name) { ... }
声明了一个公共方法bind
,用于绑定文件夹名称到视图上。
/*
* FoldersListAdapter 类
* 用于管理和适配文件夹列表的适配器,继承自 CursorAdapter。
*/
package net.micode.notes.ui; // 定义包名
// 导入相关类
import android.content.Context; // 导入Context类,用于获取上下文
import android.database.Cursor; // 导入Cursor类,用于数据源游标
import android.view.View; // 导入View类,用于视图
import android.view.ViewGroup; // 导入ViewGroup类,用于视图的父容器
import android.widget.CursorAdapter; // 导入CursorAdapter类,用于适配器
import android.widget.LinearLayout; // 导入LinearLayout类,用于布局
import android.widget.TextView; // 导入TextView类,用于文本显示
import net.micode.notes.R; // 导入资源类,用于访问资源
import net.micode.notes.data.Notes; // 导入Notes类,用于数据操作
import net.micode.notes.data.Notes.NoteColumns; // 导入NoteColumns类,用于数据列名
public class FoldersListAdapter extends CursorAdapter { // 定义FoldersListAdapter类,继承自CursorAdapter
// 查询时使用的列名数组
public static final String[] PROJECTION = {
NoteColumns.ID, // 文件夹ID列
NoteColumns.SNIPPET // 文件夹名称列
};
// 列名数组中的索引常量
public static final int ID_COLUMN = 0; // ID列索引
public static final int NAME_COLUMN = 1; // 名称列索引
/*
* 构造函数
* @param context 上下文对象,通常指Activity或Application对象。
* @param c 数据源游标Cursor。
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c); // 调用父类构造函数
}
/*
* 创建新的列表项View
* @param context 上下文对象。
* @param cursor 当前数据项的游标。
* @param parent 视图的父容器。
* @return 返回新创建的列表项View。
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context); // 返回新的FolderListItem实例
}
/*
* 绑定数据到已有的View上
* @param view 要绑定数据的视图。
* @param context 上下文对象。
* @param cursor 当前数据项的游标。
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof FolderListItem) { // 判断视图是否为FolderListItem类型
// 根据文件夹ID判断是根文件夹还是普通文件夹,并设置文件夹名称
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
((FolderListItem) view).bind(folderName); // 绑定文件夹名称到视图
}
}
/*
* 根据位置获取文件夹名称
* @param context 上下文对象。
* @param position 列表中的位置。
* @return 返回该位置上文件夹的名称。
*/
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position); // 获取指定位置的游标
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); // 返回文件夹名称
}
/*
* FolderListItem 内部类
* 用于表示文件夹列表中的一个项的视图类。
*/
private class FolderListItem extends LinearLayout { // 定义FolderListItem内部类,继承自LinearLayout
private TextView mName; // 文件夹名称的文本视图
/*
* 构造函数
* @param context 上下文对象。
*/
public FolderListItem(Context context) {
super(context); // 调用父类构造函数
// 加载布局文件,并将自己作为根视图
inflate(context, R.layout.folder_list_item, this);
mName = (TextView) findViewById(R.id.tv_folder_name); // 获取文件夹名称的视图
}
/*
* 绑定数据到视图上
* @param name 要显示的文件夹名称。
*/
public void bind(String name) {
mName.setText(name); // 设置文件夹名称
}
}
}
8 NoteEditActivity
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 {
/**
* 头部视图的ViewHolder类,用于存储头部视图中的UI组件引用。
*/
private class HeadViewHolder {
public TextView tvModified; // 显示修改日期的文本视图
public ImageView ivAlertIcon; // 提示图标图像视图
public TextView tvAlertDate; // 显示提醒日期的文本视图
public ImageView ibSetBgColor; // 设置背景颜色的图像视图
}
/**
* 背景选择按钮与其对应选中状态图标的映射。
*/
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
static {
// 初始化背景选择按钮映射
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);
}
/**
* 背景选择按钮选中状态与其对应图标的映射。
*/
private static final Map<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();
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);
}
/**
* 字号选择按钮与其对应字体大小的映射。
*/
private static final Map<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();
static {
// 初始化字号选择按钮映射
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);
}
/**
* 字号选择按钮选中状态与其对应图标的映射。
*/
private static final Map<Integer, Integer> sFontSelectorSelectionMap = new HashMap<Integer, Integer>();
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"; // 日志标签
private HeadViewHolder mNoteHeaderHolder; // 头部视图的ViewHolder
private View mHeadViewPanel; // 头部视图面板
private View mNoteBgColorSelector; // 笔记背景颜色选择器
private View mFontSizeSelector; // 字号选择器
private EditText mNoteEditor; // 笔记编辑器
private View mNoteEditorPanel; // 笔记编辑器面板
private WorkingNote mWorkingNote; // 当前正在编辑的笔记
private SharedPreferences mSharedPrefs; // 共享偏好设置
private int mFontSizeId; // 当前选中的字体大小资源ID
private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好设置键
private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 快捷图标标题的最大长度
public static final String TAG_CHECKED = String.valueOf('\u221A'); // 标记为已检查的字符串
public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); // 标记为未检查的字符串
private LinearLayout mEditTextList; // 编辑文本列表
private String mUserQuery; // 用户查询字符串
private Pattern mPattern; // 正则表达式模式
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.note_edit); // 设置活动的视图布局
// 检查实例状态是否被保存,如果未保存且初始化活动状态失败,则结束该活动
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
return;
}
initResources(); // 初始化资源
}
/**
* 当活动被系统销毁后,为了恢复之前的状态,此方法会被调用。
* 主要用于处理活动重新创建时的数据恢复。
*
* @param savedInstanceState 包含之前保存状态的Bundle,如果活动之前保存了状态,这里会传入非空的Bundle。
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// 检查是否有保存的状态并且包含必要的UID信息,用于恢复活动状态
if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
// 使用intent尝试恢复活动状态,如果失败则结束该活动
if (!initActivityState(intent)) {
finish();
return;
}
Log.d(TAG, "Restoring from killed activity"); // 日志记录,表示活动状态正在从被杀死的状态恢复
}
}
/**
* 初始化活动状态,根据传入的Intent确定是查看笔记、新建笔记还是编辑笔记。
*
* @param intent 传入的Intent,包含了动作类型和相关数据。
* @return boolean 返回true表示成功初始化活动状态,false表示初始化失败。
*/
private boolean initActivityState(Intent intent) {
// 如果用户指定的是查看笔记的动作但未提供笔记ID,则跳转到笔记列表活动
mWorkingNote = null;
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
mUserQuery = "";
// 从搜索结果开始
if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY);
}
// 检查指定的笔记在数据库中是否可见
if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) {
Intent jump = new Intent(this, NotesListActivity.class);
startActivity(jump);
showToast(R.string.error_note_not_exist);
finish();
return false;
} else {
// 加载指定ID的笔记
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load note failed with note id" + noteId);
finish();
return false;
}
}
// 隐藏软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
} else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// 处理新建或编辑笔记的情况
long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0);
int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE,
Notes.TYPE_WIDGET_INVALIDE);
int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID,
ResourceParser.getDefaultBgId(this));
// 解析通话记录笔记
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0);
if (callDate != 0 && phoneNumber != null) {
// 根据电话号码和通话日期尝试获取已有笔记ID
if (TextUtils.isEmpty(phoneNumber)) {
Log.w(TAG, "The call record number is null");
}
long noteId = 0;
if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
phoneNumber, callDate)) > 0) {
// 加载该笔记
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load call note failed with note id" + noteId);
finish();
return false;
}
} else {
// 创建新的通话记录笔记
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId,
widgetType, bgResId);
mWorkingNote.convertToCallNote(phoneNumber, callDate);
}
} else {
// 创建普通空笔记
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType,
bgResId);
}
// 显示软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
// 不支持的Intent动作
Log.e(TAG, "Intent not specified action, should not support");
finish();
return false;
}
// 设置笔记状态改变的监听器
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
/**
* 当Activity恢复到前台时调用。
* 主要负责初始化笔记界面。
*/
@Override
protected void onResume() {
super.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 {
// 否则,显示相对时间
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);
}
}
/**
* 当Activity接收到新的Intent时调用。
* 用于根据新的Intent重新初始化Activity状态。
*
* @param intent 新的Intent
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
initActivityState(intent);
}
/**
* 保存Activity状态时调用。
* 用于保存当前编辑的笔记,如果该笔记还未保存到数据库,则首先保存它。
* 并且保存当前笔记的ID到Bundle中。
*
* @param outState 用于保存Activity状态的Bundle
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 如果当前编辑的笔记不存在于数据库中,即还未保存,先保存它
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
// 保存笔记ID
outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId());
Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState");
}
/**
* 分发触摸事件。如果触摸事件不在指定视图范围内,则隐藏该视图。
*
* @param ev 触摸事件
* @return 如果事件被消费则返回true,否则返回false
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 如果背景颜色选择器可见且不在触摸范围内,则隐藏它并消费事件
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mNoteBgColorSelector, ev)) {
mNoteBgColorSelector.setVisibility(View.GONE);
return true;
}
// 如果字体大小选择器可见且不在触摸范围内,则隐藏它并消费事件
if (mFontSizeSelector.getVisibility() == View.VISIBLE
&& !inRangeOfView(mFontSizeSelector, ev)) {
mFontSizeSelector.setVisibility(View.GONE);
return true;
}
// 如果上述条件都不满足,则将事件传递给父类处理
return super.dispatchTouchEvent(ev);
}
/**
* 判断触摸点是否在指定视图范围内。
*
* @param view 视图
* @param ev 触摸事件
* @return 如果触摸点在视图范围内则返回true,否则返回false
*/
private boolean inRangeOfView(View view, MotionEvent ev) {
int[] location = new int[2];
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
// 判断触摸点是否在视图外
if (ev.getX() < x
|| ev.getX() > (x + view.getWidth())
|| ev.getY() < y
|| ev.getY() > (y + view.getHeight())) {
return false;
}
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);
}
;
// 从偏好设置中读取字体大小
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
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);
}
/**
* 暂停时保存笔记数据并清除设置状态。
*/
@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 = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
// 根据小部件类型设置对应的类
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, "Unspported widget type");
return;
}
// 设置小部件ID
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[]{
mWorkingNote.getWidgetId()
});
// 发送广播更新小部件
sendBroadcast(intent);
// 设置结果为OK
setResult(RESULT_OK, intent);
}
/**
* 点击事件的处理函数。
*
* @param v 被点击的视图对象。
*/
public void onClick(View v) {
int id = v.getId();
// 如果点击的是设置背景颜色的按钮
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
// 根据当前笔记的背景颜色,设置对应的色板按钮可见
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.VISIBLE);
} else if (sBgSelectorBtnsMap.containsKey(id)) {
// 如果点击的是背景色板上的颜色
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.GONE); // 隐藏当前选择的背景颜色
mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); // 更新笔记的背景颜色
mNoteBgColorSelector.setVisibility(View.GONE); // 隐藏背景颜色选择器
} else if (sFontSizeBtnsMap.containsKey(id)) {
// 如果点击的是字体大小按钮
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); // 隐藏当前选择的字体大小
mFontSizeId = sFontSizeBtnsMap.get(id); // 更新选择的字体大小
mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); // 保存选择的字体大小到SharedPreferences
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;
}
// 保存笔记并执行父类的onBackPressed()方法(结束当前Activity)
saveNote();
super.onBackPressed();
}
/**
* 尝试清除设置状态(背景颜色选择或字体大小选择)。
*
* @return 如果成功清除设置状态,返回true;否则返回false。
*/
private boolean clearSettingState() {
// 如果背景颜色选择器可见,则隐藏它并返回true
if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) {
mNoteBgColorSelector.setVisibility(View.GONE);
return true;
} else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { // 如果字体大小选择器可见,则隐藏它并返回true
mFontSizeSelector.setVisibility(View.GONE);
return true;
}
return false; // 没有可见的设置状态需要清除,返回false
}
/**
* 当背景颜色发生变化时的处理函数。
*/
public void onBackgroundColorChanged() {
// 根据当前笔记的背景颜色,设置对应的色板按钮可见,并更新编辑器及标题栏的背景颜色
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.VISIBLE);
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
}
/**
* 准备选项菜单的函数。
*
* @param menu 选项菜单对象。
* @return 总是返回true。
*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// 如果Activity正在结束,则不进行任何操作
if (isFinishing()) {
return true;
}
// 尝试清除设置状态
clearSettingState();
menu.clear(); // 清除菜单项
// 根据笔记所属的文件夹,加载不同的菜单资源
if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_note_edit, menu);
} else {
getMenuInflater().inflate(R.menu.note_edit, menu);
}
// 根据当前笔记的清单模式,更新“清单模式”菜单项的标题
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
} else {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode);
}
// 根据笔记是否有提醒,更新“删除提醒”菜单项的可见性
if (mWorkingNote.hasClockAlert()) {
menu.findItem(R.id.menu_alert).setVisible(false);
} else {
menu.findItem(R.id.menu_delete_remind).setVisible(false);
}
return true;
}
/**
* 处理选项菜单项的选择事件。
*
* @param item 选中的菜单项
* @return 总是返回true,表示事件已处理。
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
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));
builder.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 确认删除当前笔记并结束当前活动
deleteCurrentNote();
finish();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
case R.id.menu_font_size:
// 显示字体大小选择器
mFontSizeSelector.setVisibility(View.VISIBLE);
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
break;
case R.id.menu_list_mode:
// 切换笔记的列表模式
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;
}
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} 类型的应用。
*
* @param context 上下文
* @param info 要分享的信息
*/
private void sendTo(Context context, String info) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, info);
intent.setType("text/plain");
context.startActivity(intent);
}
/**
* 首先保存当前正在编辑的笔记,然后启动一个新的NoteEditActivity用于创建新笔记。
*/
private void createNewNote() {
// 首先保存当前笔记
saveNote();
// 安全地开始一个新的NoteEditActivity
finish();
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId());
startActivity(intent);
}
/**
* 删除当前正在编辑的笔记。
* 如果笔记存在于数据库中,并且当前不是同步模式,将直接删除该笔记;
* 如果处于同步模式,则将笔记移动到回收站文件夹。
*/
private void deleteCurrentNote() {
if (mWorkingNote.existInDatabase()) {
HashSet<Long> ids = new HashSet<Long>();
long id = mWorkingNote.getNoteId();
if (id != Notes.ID_ROOT_FOLDER) {
ids.add(id);
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
if (!isSyncMode()) {
// 非同步模式下直接删除笔记
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
Log.e(TAG, "Delete Note error");
}
} else {
// 同步模式下将笔记移动到回收站
if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happens");
}
}
}
mWorkingNote.markDeleted(true);
}
/**
* 判断当前是否为同步模式。
* 同步模式是指在设置中配置了同步账户名。
*
* @return 如果配置了同步账户名返回true,否则返回false。
*/
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
/**
* 处理时钟提醒变更事件。
* 首先检查当前笔记是否已保存,未保存则先保存。
* 如果笔记存在,根据set参数设置或取消提醒。
* 如果笔记不存在(即无有效ID),记录错误并提示用户输入内容。
*
* @param date 提醒的日期时间戳
* @param set 是否设置提醒
*/
public void onClockAlertChanged(long date, boolean set) {
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if (!set) {
alarmManager.cancel(pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
}
} else {
Log.e(TAG, "Clock alert setting error");
showToast(R.string.error_note_empty_for_clock);
}
}
/**
* 更新小部件显示。
*/
public void onWidgetChanged() {
updateWidget();
}
/**
* 当删除某个编辑框中的文本时的处理逻辑。
* 重新设置后续编辑框的索引,并将删除的文本添加到前一个或当前编辑框中。
*
* @param index 被删除文本的编辑框索引
* @param text 被删除的文本内容
*/
public void onEditTextDelete(int index, String text) {
int childCount = mEditTextList.getChildCount();
if (childCount == 1) {
return;
}
for (int i = index + 1; i < childCount; i++) {
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i - 1);
}
mEditTextList.removeViewAt(index);
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);
}
int length = edit.length();
edit.append(text);
edit.requestFocus();
edit.setSelection(length);
}
/**
* 当在编辑框中按下“Enter”键时的处理逻辑。
* 在列表中添加一个新的编辑框,并重新设置后续编辑框的索引。
*
* @param index 当前编辑框的索引
* @param text 当前编辑框中的文本内容
*/
public void onEditTextEnter(int index, String text) {
if (index > mEditTextList.getChildCount()) {
Log.e(TAG, "Index out of mEditTextList boundrary, should not happen");
}
View view = getListItem(text, index);
mEditTextList.addView(view, index);
NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.requestFocus();
edit.setSelection(0);
for (int i = index + 1; i < mEditTextList.getChildCount(); i++) {
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i);
}
}
/**
* 切换到列表模式。
* 将文本分割成多行,并为每行创建一个列表项,展示在编辑文本列表中。
*
* @param text 要转换成列表模式的文本,每行代表一个列表项。
*/
private void switchToListMode(String text) {
// 清空当前的视图
mEditTextList.removeAllViews();
// 使用换行符分割文本,创建列表项
String[] items = text.split("\n");
int index = 0;
for (String item : items) {
// 忽略空行
if (!TextUtils.isEmpty(item)) {
mEditTextList.addView(getListItem(item, index));
index++;
}
}
// 添加一个空的列表项作为占位符
mEditTextList.addView(getListItem("", index));
// 请求焦点以便于编辑
mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus();
// 隐藏编辑器,显示列表
mNoteEditor.setVisibility(View.GONE);
mEditTextList.setVisibility(View.VISIBLE);
}
/**
* 高亮显示查询结果。
* 在给定的文本中,根据用户查询字符串高亮显示匹配的部分。
*
* @param fullText 完整的文本。
* @param userQuery 用户的查询字符串。
* @return 包含高亮显示的文本的Spannable对象。
*/
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
// 如果有查询字符串,则进行高亮处理
if (!TextUtils.isEmpty(userQuery)) {
mPattern = Pattern.compile(userQuery);
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();
}
}
return spannable;
}
/**
* 创建列表项视图。
* 为列表模式创建并配置一个包含文本编辑框和复选框的视图。
*
* @param item 列表项的文本内容。
* @param index 列表项的索引。
* @return 配置好的列表项视图。
*/
private View getListItem(String item, int index) {
// 加载列表项布局
View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null);
final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
// 设置文本样式
edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item));
// 复选框的监听器,用于切换文本的划线状态
cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
} else {
edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
}
}
});
// 根据文本前缀设置复选框状态和文本内容
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();
}
// 设置文本变化监听和索引
edit.setOnTextViewChangeListener(this);
edit.setIndex(index);
// 设置带有查询结果高亮的文本
edit.setText(getHighlightQueryResult(item, mUserQuery));
return view;
}
/**
* 根据文本内容是否为空,切换复选框的可见性。
*
* @param index 列表项索引。
* @param hasText 列表项是否包含文本。
*/
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);
}
}
/**
* 在切换编辑模式和列表模式时更新UI。
*
* @param oldMode 旧的编辑模式。
* @param newMode 新的编辑模式。
*/
public void onCheckListModeChanged(int oldMode, int newMode) {
// 切换到列表模式
if (newMode == TextNote.MODE_CHECK_LIST) {
switchToListMode(mNoteEditor.getText().toString());
} else {
// 切换回编辑模式
if (!getWorkingText()) {
mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ",
""));
}
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
}
/**
* 根据当前列表项的选中状态,构建并返回工作文本。
*
* @return 是否有已选中的列表项。
*/
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 {
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
}
return hasChecked;
}
/**
* 保存笔记。
* 更新笔记内容并保存。
*
* @return 是否成功保存笔记。
*/
private boolean saveNote() {
getWorkingText();
boolean saved = mWorkingNote.saveNote();
if (saved) {
// 设置结果为成功,以便外部调用者知道保存操作的状态
setResult(RESULT_OK);
}
return saved;
}
/**
* 将当前编辑的笔记发送到桌面。首先会检查当前编辑的笔记是否已存在于数据库中,
* 如果不存在,则先保存。如果存在,会创建一个快捷方式放在桌面。
*/
private void sendToDesktop() {
// 检查当前编辑的笔记是否存在于数据库,若不存在则先保存
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
// 如果笔记存在于数据库(有noteId),则创建快捷方式
if (mWorkingNote.getNoteId() > 0) {
Intent sender = new Intent();
Intent shortcutIntent = new Intent(this, NoteEditActivity.class);
shortcutIntent.setAction(Intent.ACTION_VIEW);
shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId());
sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
sender.putExtra(Intent.EXTRA_SHORTCUT_NAME,
makeShortcutIconTitle(mWorkingNote.getContent()));
sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app));
sender.putExtra("duplicate", true);
sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
showToast(R.string.info_note_enter_desktop);
sendBroadcast(sender);
} else {
// 如果用户未输入任何内容,无法创建快捷方式,提醒用户输入内容
Log.e(TAG, "Send to desktop error");
showToast(R.string.error_note_empty_for_send_to_desktop);
}
}
/**
* 根据笔记内容生成快捷方式的标题。移除内容中的已选和未选标签,并确保标题长度不超过上限。
*
* @param content 符合快捷方式图标标题要求的笔记内容
* @return 标题字符串
*/
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消息。
*
* @param resId 资源ID,指向要显示的字符串
*/
private void showToast(int resId) {
showToast(resId, Toast.LENGTH_SHORT);
}
/**
* 显示一个指定时长的Toast消息。
*
* @param resId 资源ID,指向要显示的字符串
* @param duration 显示时长,可以是Toast.LENGTH_SHORT或Toast.LENGTH_LONG
*/
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
}
9 NoteltemData
- 包声明:
package net.micode.notes.ui;
声明了该类所在的包。- 导入语句:导入了一系列Java和Android相关的类,包括上下文、游标、字符串操作、联系人数据操作、笔记数据操作、数据工具操作等。
- 类声明:
public class NoteItemData {
声明了一个名为NoteItemData
的公共类。- 常量声明:
static final String[] PROJECTION = new String[]{ ... };
声明了一个静态最终的字符串数组PROJECTION
,用于查询时使用的列名。private static final int ID_COLUMN = 0;
声明了一个私有静态最终的整数ID_COLUMN
,表示ID列的索引。private static final int ALERTED_DATE_COLUMN = 1;
声明了一个私有静态最终的整数ALERTED_DATE_COLUMN
,表示提醒日期列的索引。- 其他列索引常量的声明类似。
- 成员变量:
private long mId;
声明了一个私有长整型变量mId
,用于存储笔记ID。private long mAlertDate;
声明了一个私有长整型变量mAlertDate
,用于存储提醒日期。- 其他成员变量的声明类似。
- 构造函数:
public NoteItemData(Context context, Cursor cursor) { ... }
声明了类的构造函数,接受上下文和数据源游标作为参数,并从游标中提取各项数据赋值给成员变量。- 方法:
private void checkPostion(Cursor cursor) { ... }
声明了一个私有方法checkPostion
,用于根据当前游标位置更新笔记在列表中的位置状态。public boolean isOneFollowingFolder() { ... }
声明了一个公共方法isOneFollowingFolder
,用于返回一个笔记跟随文件夹状态。public boolean isMultiFollowingFolder() { ... }
声明了一个公共方法isMultiFollowingFolder
,用于返回多个笔记跟随文件夹状态。- 其他获取属性的方法类似。
- 静态方法:
public static int getNoteType(Cursor cursor) { ... }
声明了一个公共静态方法getNoteType
,用于从游标中获取笔记的类型。
package net.micode.notes.ui; // 定义包名
import android.content.Context; // 导入Context类,用于获取上下文
import android.database.Cursor; // 导入Cursor类,用于数据源游标
import android.text.TextUtils; // 导入TextUtils类,用于字符串操作
import net.micode.notes.data.Contact; // 导入Contact类,用于联系人数据操作
import net.micode.notes.data.Notes; // 导入Notes类,用于笔记数据操作
import net.micode.notes.data.Notes.NoteColumns; // 导入NoteColumns类,用于笔记数据列名
import net.micode.notes.tool.DataUtils; // 导入DataUtils类,用于数据工具操作
/**
* 代表一个笔记项的数据类,用于存储和管理笔记的各种信息。
*/
public class NoteItemData { // 定义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
NoteColumns.SNIPPET, // 摘要
NoteColumns.TYPE, // 类型
NoteColumns.WIDGET_ID, // 小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型
};
// 各列数据的索引
private static final int ID_COLUMN = 0; // ID列索引
private static final int ALERTED_DATE_COLUMN = 1; // 提醒日期列索引
private static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色ID列索引
private static final int CREATED_DATE_COLUMN = 3; // 创建日期列索引
private static final int HAS_ATTACHMENT_COLUMN = 4; // 是否有附件列索引
private static final int MODIFIED_DATE_COLUMN = 5; // 修改日期列索引
private static final int NOTES_COUNT_COLUMN = 6; // 笔记数量列索引
private static final int PARENT_ID_COLUMN = 7; // 父ID列索引
private static final int SNIPPET_COLUMN = 8; // 摘要列索引
private static final int TYPE_COLUMN = 9; // 类型列索引
private static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引
private static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列索引
// 笔记的各项数据
private long mId; // 笔记ID
private long mAlertDate; // 提醒日期
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对象。
*
* @param context 上下文对象,用于访问应用全局功能。
* @param cursor 包含笔记数据的Cursor对象。
*/
public NoteItemData(Context context, Cursor cursor) {
// 从Cursor中提取各项数据并赋值
mId = cursor.getLong(ID_COLUMN); // 获取笔记ID
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); // 获取提醒日期
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); // 获取背景颜色ID
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); // 获取创建日期
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; // 获取是否有附件
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); // 获取修改日期
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); // 获取笔记数量
mParentId = cursor.getLong(PARENT_ID_COLUMN); // 获取父ID
mSnippet = cursor.getString(SNIPPET_COLUMN); // 获取摘要
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, ""); // 清理摘要中的特殊标记
mType = cursor.getInt(TYPE_COLUMN); // 获取类型
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); // 获取小部件ID
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); // 获取小部件类型
// 如果是通话记录笔记,尝试获取通话号码和联系人名称
mPhoneNumber = ""; // 初始化电话号码
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { // 判断是否为通话记录笔记
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); // 获取电话号码
if (!TextUtils.isEmpty(mPhoneNumber)) { // 判断电话号码是否为空
mName = Contact.getContact(context, mPhoneNumber); // 获取联系人名称
if (mName == null) { // 判断联系人名称是否为空
mName = mPhoneNumber; // 如果为空,则使用电话号码作为名称
}
}
}
// 如果没有获取到联系人名称,则默认为空字符串
if (mName == null) { // 判断联系人名称是否为空
mName = ""; // 如果为空,则默认为空字符串
}
checkPostion(cursor); // 检查笔记在列表中的位置
}
/**
* 根据当前Cursor位置,更新NoteItemData的状态信息(如是否为列表中的最后一个项目等)。
*
* @param cursor 包含笔记数据的Cursor对象。
*/
private void checkPostion(Cursor cursor) {
// 更新位置状态信息
mIsLastItem = cursor.isLast(); // 判断是否为最后一个项目
mIsFirstItem = cursor.isFirst(); // 判断是否为第一个项目
mIsOnlyOneItem = (cursor.getCount() == 1); // 判断是否为唯一一个项目
mIsMultiNotesFollowingFolder = false; // 初始化多个笔记跟随文件夹状态
mIsOneNoteFollowingFolder = false; // 初始化一个笔记跟随文件夹状态
// 检查当前笔记是否跟随文件夹,并更新相应状态
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { // 判断是否为笔记且不是第一个项目
int position = cursor.getPosition(); // 获取当前位置
if (cursor.moveToPrevious()) { // 移动到前一个位置
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { // 判断前一个项目是否为文件夹或系统类型
if (cursor.getCount() > (position + 1)) { // 判断是否有多于一个项目
mIsMultiNotesFollowingFolder = true; // 设置多个笔记跟随文件夹状态
} else {
mIsOneNoteFollowingFolder = true; // 设置一个笔记跟随文件夹状态
}
}
// 确保Cursor能够回到原来的位置
if (!cursor.moveToNext()) { // 移动回原来的位置
throw new IllegalStateException("cursor move to previous but can't move back"); // 抛出异常
}
}
}
}
// 以下为获取NoteItemData各项属性的方法
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder; // 返回一个笔记跟随文件夹状态
}
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder; // 返回多个笔记跟随文件夹状态
}
public boolean isLast() {
return mIsLastItem; // 返回是否为最后一个项目
}
public String getCallName() {
return mName; // 返回联系人名称
}
public boolean isFirst() {
return mIsFirstItem; // 返回是否为第一个项目
}
public boolean isSingle() {
return mIsOnlyOneItem; // 返回是否为唯一一个项目
}
public long getId() {
return mId; // 返回笔记ID
}
public long getAlertDate() {
return mAlertDate; // 返回提醒日期
}
public long getCreatedDate() {
return mCreatedDate; // 返回创建日期
}
public boolean hasAttachment() {
return mHasAttachment; // 返回是否有附件
}
public long getModifiedDate() {
return mModifiedDate; // 返回修改日期
}
public int getBgColorId() {
return mBgColorId; // 返回背景颜色ID
}
public long getParentId() {
return mParentId; // 返回父ID
}
public int getNotesCount() {
return mNotesCount; // 返回笔记数量
}
public long getFolderId() {
return mParentId; // 返回文件夹ID
}
public int getType() {
return mType; // 返回类型
}
public int getWidgetType() {
return mWidgetType; // 返回小部件类型
}
public int getWidgetId() {
return mWidgetId; // 返回小部件ID
}
public String getSnippet() {
return mSnippet; // 返回摘要
}
public boolean hasAlert() {
return (mAlertDate > 0); // 返回是否有提醒
}
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); // 返回是否为通话记录笔记
}
/**
* 从Cursor中获取笔记的类型。
*
* @param cursor 包含笔记数据的Cursor对象。
* @return 笔记的类型。
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN); // 返回笔记类型
}
}
10 NotesListActivity
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.appwidget.AppWidgetManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Display;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.View.OnTouchListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
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;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.BackupUtils;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener {
// 定义文件夹中笔记列表查询的标记
private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;
// 定义文件夹列表查询的标记
private static final int FOLDER_LIST_QUERY_TOKEN = 1;
// 菜单中删除文件夹的选项
private static final int MENU_FOLDER_DELETE = 0;
// 菜单中查看文件夹的选项
private static final int MENU_FOLDER_VIEW = 1;
// 菜单中更改文件夹名称的选项
private static final int MENU_FOLDER_CHANGE_NAME = 2;
// 首次使用应用时,添加介绍信息的偏好设置键
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
// 列表编辑状态的枚举,包括笔记列表、子文件夹和通话记录文件夹
private enum ListEditState {
NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
}
;
// 当前编辑状态
private ListEditState mState;
// 后台查询处理器
private BackgroundQueryHandler mBackgroundQueryHandler;
// 笔记列表的适配器
private NotesListAdapter mNotesListAdapter;
// 笔记列表视图
private ListView mNotesListView;
// 添加新笔记的按钮
private Button mAddNewNote;
// 是否分发事件的标志
private boolean mDispatch;
// 触摸点的原始Y坐标
private int mOriginY;
// 分发事件时的Y坐标
private int mDispatchY;
// 标题栏文本视图
private TextView mTitleBar;
// 当前文件夹的ID
private long mCurrentFolderId;
// 内容解析器
private ContentResolver mContentResolver;
// 模式回调接口
private ModeCallback mModeCallBack;
// 日志标签
private static final String TAG = "NotesListActivity";
// 笔记列表视图滚动速率
public static final int NOTES_LISTVIEW_SCROLL_RATE = 30;
// 聚焦的笔记数据项
private NoteItemData mFocusNoteDataItem;
// 普通文件夹选择条件
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
// 根文件夹选择条件
private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
+ Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR ("
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND "
+ NoteColumns.NOTES_COUNT + ">0)";
// 打开节点请求代码
private final static int REQUEST_CODE_OPEN_NODE = 102;
// 新建节点请求代码
private final static int REQUEST_CODE_NEW_NODE = 103;
/**
* 在活动创建时调用,用于初始化资源和设置应用信息。
*
* @param savedInstanceState 如果活动之前被销毁,这参数包含之前的状态。如果活动没被销毁之前,这参数是null。
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.note_list);
initResources();
// 用户首次使用时插入介绍信息
setAppInfoFromRawRes();
}
/**
* 处理从其他活动返回的结果。
*
* @param requestCode 启动其他活动时传入的请求代码。
* @param resultCode 其他活动返回的结果代码。
* @param data 其他活动返回的数据。
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 如果返回结果为OK且请求代码为打开节点或新建节点,则刷新列表
if (resultCode == RESULT_OK
&& (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
mNotesListAdapter.changeCursor(null);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* 从原始资源中设置应用信息。此方法会读取R.raw.introduction中的内容,
* 并且只有当之前未添加介绍信息时,才将读取到的内容保存为一个工作笔记。
*/
private void setAppInfoFromRawRes() {
// 获取SharedPreferences实例
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
// 检查是否已经添加了介绍信息
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
try {
// 从资源中打开introduction文件
in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
// 读取文件内容到StringBuilder
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
char[] buf = new char[1024];
int len = 0;
while ((len = br.read(buf)) > 0) {
sb.append(buf, 0, len);
}
} else {
// 打印错误日志,如果无法打开文件
Log.e(TAG, "Read introduction file error");
return;
}
} catch (IOException e) {
e.printStackTrace();
return;
} finally {
// 确保InputStream被关闭
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());
// 保存工作笔记并标记已添加介绍信息
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();
}
/**
* 初始化资源,包括ListView、适配器和其他UI组件。
*/
private void initResources() {
// 获取ContentResolver实例
mContentResolver = this.getContentResolver();
// 创建后台查询处理器
mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver());
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
// 初始化ListView和相关监听器
mNotesListView = (ListView) findViewById(R.id.notes_list);
mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null),
null, false);
mNotesListView.setOnItemClickListener(new OnListItemClickListener());
mNotesListView.setOnItemLongClickListener(this);
// 初始化并设置笔记列表适配器
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
// 初始化新建笔记按钮并设置点击监听器
mAddNewNote = (Button) findViewById(R.id.btn_new_note);
mAddNewNote.setOnClickListener(this);
mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
// 初始化状态变量和触摸相关的变量
mDispatch = false;
mDispatchY = 0;
mOriginY = 0;
// 初始化标题栏和其他状态变量
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST;
mModeCallBack = new ModeCallback();
}
/**
* 用于处理列表的多选择模式和菜单点击事件的回调类。
*/
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
private DropdownMenu mDropDownMenu; // 下拉菜单
private ActionMode mActionMode; // 动作模式
private MenuItem mMoveMenu; // 移动菜单项
/**
* 创建动作模式时的回调方法。
*
* @param mode 动作模式实例。
* @param menu 菜单实例。
* @return 如果成功创建动作模式,返回true;否则返回false。
*/
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);
}
}
}
/**
* 准备动作模式时的回调方法。
*
* @param mode 动作模式实例。
* @param menu 菜单实例。
* @return 返回false,表示未进行任何操作。
*/
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// TODO Auto-generated method stub
return false;
}
/**
* 点击动作模式中的菜单项时的回调方法。
*
* @param mode 动作模式实例。
* @param item 被点击的菜单项。
* @return 返回false,表示未进行任何操作。
*/
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// TODO Auto-generated method stub
return false;
}
/**
* 销毁动作模式时的回调方法。
*
* @param mode 动作模式实例。
*/
public void onDestroyActionMode(ActionMode mode) {
// 还原列表选择模式和设置
mNotesListAdapter.setChoiceMode(false);
mNotesListView.setLongClickable(true);
mAddNewNote.setVisibility(View.VISIBLE);
}
public void finishActionMode() {
mActionMode.finish();
}
/**
* 处理列表项选择状态变化的回调方法。
*
* @param mode 动作模式实例。
* @param position 列表中被改变选择状态的项的位置。
* @param id 项的ID。
* @param checked 项的新选择状态。
*/
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
// 更新列表项的选择状态并更新菜单
mNotesListAdapter.setCheckedItem(position, checked);
updateMenu();
}
/**
* 处理菜单项点击事件的回调方法。
*
* @param item 被点击的菜单项。
* @return 如果已处理点击事件,返回true;否则返回false。
*/
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;
}
// 根据菜单项ID执行相应操作
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 {
/**
* 处理触摸事件。
*
* @param v 触摸的视图。
* @param event 触摸事件。
* @return 如果事件被处理则返回true,否则返回false。
*/
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;
}
}
// 如果事件未被分发,则返回false
return false;
}
}
;
/**
* 异步查询笔记列表。
* 根据当前文件夹ID选择不同的查询条件,启动一个后台查询处理该查询。
*/
private void startAsyncNotesListQuery() {
// 根据当前文件夹ID选择查询条件
String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
: NORMAL_SELECTION;
// 启动查询,排序方式为类型降序,修改日期降序
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[]{
String.valueOf(mCurrentFolderId)
}, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
/**
* 处理后台查询的类。
* 继承自AsyncQueryHandler,用于处理异步查询完成后的操作。
*/
private final class BackgroundQueryHandler extends AsyncQueryHandler {
public BackgroundQueryHandler(ContentResolver contentResolver) {
super(contentResolver);
}
/**
* 查询完成时的处理逻辑。
* 根据查询标记的不同,执行不同的操作,如更新笔记列表或显示文件夹列表。
*
* @param token 查询标记,用于区分不同的查询。
* @param cookie 查询时传入的附加对象。
* @param cursor 查询结果的游标。
*/
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
switch (token) {
case FOLDER_NOTE_LIST_QUERY_TOKEN:
// 更新笔记列表适配器的数据源
mNotesListAdapter.changeCursor(cursor);
break;
case FOLDER_LIST_QUERY_TOKEN:
// 根据查询结果展示或记录错误
if (cursor != null && cursor.getCount() > 0) {
showFolderListMenu(cursor);
} else {
Log.e(TAG, "Query folder failed");
}
break;
default:
// 对未知标记不做处理
return;
}
}
}
/**
* 显示文件夹列表的菜单。
* 使用查询结果构建一个对话框,让用户选择一个文件夹。
*
* @param cursor 查询结果的游标,包含文件夹信息。
*/
private void showFolderListMenu(Cursor cursor) {
// 构建文件夹列表选择的对话框
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(R.string.menu_title_select_folder);
final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor);
builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
/**
* 用户选择文件夹时的处理逻辑。
* 将选中的笔记移动到用户选择的文件夹中,并给出反馈。
*
* @param dialog 对话框实例。
* @param which 用户选择的项的索引。
*/
public void onClick(DialogInterface dialog, int which) {
// 批量移动选中的笔记到目标文件夹
DataUtils.batchMoveToFolder(mContentResolver,
mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which));
// 显示移动操作的反馈信息
Toast.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() {
// 构建意图并指定动作为插入或编辑,以及初始文件夹ID
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId);
// 启动该意图并期待返回结果
this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
}
/**
* 批量删除笔记的函数。根据当前是否处于同步模式,采取不同的删除策略:如果不处于同步模式,则直接删除笔记;如果处于同步模式,则将笔记移动到回收站文件夹。
* 执行删除操作后,会更新相应的widgets。
*/
private void batchDelete() {
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
// 在后台执行任务,获取选中的widgets并执行删除操作
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
// 获取当前选中的widgets
HashSet<AppWidgetAttribute> 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;
}
// 删除操作完成后,在UI线程执行后续操作
@Override
protected void onPostExecute(HashSet<AppWidgetAttribute> widgets) {
// 遍历所有受影响的widgets,对有效的widgets进行更新
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
// 更新有效的widget
updateWidget(widget.widgetId, widget.widgetType);
}
}
}
// 结束动作模式
mModeCallBack.finishActionMode();
}
}.execute();
}
/**
* 删除指定的文件夹。
* 如果是在同步模式下,文件夹会被移动到回收站,否则直接删除。
* 同时,也会更新与该文件夹相关的所有小部件。
*
* @param folderId 要删除的文件夹ID。
*/
private void deleteFolder(long folderId) {
// 根据ID判断是否为根文件夹,根文件夹不能被删除
if (folderId == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Wrong folder id, should not happen " + folderId);
return;
}
HashSet<Long> ids = new HashSet<Long>();
ids.add(folderId);
// 获取与文件夹相关联的小部件信息
HashSet<AppWidgetAttribute> 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) {
// 有效的小部件才进行更新
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType);
}
}
}
}
/**
* 打开指定的笔记节点进行编辑。
*
* @param data 包含要打开的笔记节点信息的对象。
*/
private void openNode(NoteItemData data) {
// 构造Intent并设置动作和额外数据,然后启动Activity
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, data.getId());
this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
}
/**
* 打开指定的文件夹,并加载其笔记列表。
* 根据文件夹ID的不同,更新UI状态,包括标题和新增笔记按钮的可见性。
*
* @param data 包含要打开的文件夹信息的对象。
*/
private void openFolder(NoteItemData data) {
// 设置当前文件夹ID并启动异步查询
mCurrentFolderId = data.getId();
startAsyncNotesListQuery();
// 根据文件夹ID更新UI状态
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
mAddNewNote.setVisibility(View.GONE);
} else {
mState = ListEditState.SUB_FOLDER;
}
// 更新标题栏显示
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mTitleBar.setText(R.string.call_record_folder_name);
} else {
mTitleBar.setText(data.getSnippet());
}
mTitleBar.setVisibility(View.VISIBLE);
}
/**
* 点击事件的处理方法。
* 目前仅处理新建笔记按钮的点击事件。
*
* @param v 被点击的视图对象。
*/
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);
}
}
/**
* 隐藏软键盘。
*
* @param view 触发隐藏软键盘的视图。
*/
private void hideSoftInput(View view) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
/**
* 显示创建或修改文件夹的对话框。
*
* @param create 如果为true,则为创建文件夹;如果为false,则为修改文件夹。
*/
private void showCreateOrModifyFolderDialog(final boolean create) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
showSoftInput(); // 显示软键盘
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"); // 日志记录,长按的数据项为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) {
// 空实现
}
});
}
/**
* 当用户按下返回键时调用的方法,根据当前状态执行不同的操作。
* 在子文件夹状态下,返回根文件夹并显示笔记列表;
* 在通话记录文件夹状态下,也返回根文件夹但显示添加新笔记按钮;
* 在笔记列表状态下,执行父类的onBackPressed方法,通常是退出或返回上一级。
*/
@Override
public void onBackPressed() {
switch (mState) {
case SUB_FOLDER:
// 从子文件夹状态返回到根文件夹的笔记列表状态
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
startAsyncNotesListQuery();
mTitleBar.setVisibility(View.GONE);
break;
case CALL_RECORD_FOLDER:
// 从通话记录文件夹状态返回到根文件夹的笔记列表状态,并显示添加新笔记按钮
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
mAddNewNote.setVisibility(View.VISIBLE);
mTitleBar.setVisibility(View.GONE);
startAsyncNotesListQuery();
break;
case NOTE_LIST:
// 在笔记列表状态下,执行父类的返回操作
super.onBackPressed();
break;
default:
// 对于其他状态,不执行任何操作
break;
}
}
/**
* 更新小部件显示。
* 根据传入的小部件类型,设置对应的Provider并发送更新广播。
*
* @param appWidgetId 小部件ID
* @param appWidgetType 小部件类型
*/
private void updateWidget(int appWidgetId, int appWidgetType) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
// 根据小部件类型设置Provider
if (appWidgetType == Notes.TYPE_WIDGET_2X) {
intent.setClass(this, NoteWidgetProvider_2x.class);
} else if (appWidgetType == Notes.TYPE_WIDGET_4X) {
intent.setClass(this, NoteWidgetProvider_4x.class);
} else {
Log.e(TAG, "Unspported widget type");
return;
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[]{
appWidgetId
});
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);
}
}
};
/**
* 上下文菜单关闭时的回调方法。
* 在列表视图中取消上下文菜单的监听器。
*
* @param menu 被关闭的菜单对象
*/
@Override
public void onContextMenuClosed(Menu menu) {
if (mNotesListView != null) {
mNotesListView.setOnCreateContextMenuListener(null);
}
super.onContextMenuClosed(menu);
}
/**
* 当上下文菜单中的项目被选择时调用。
*
* @param item 被选择的菜单项。
* @return 如果事件已成功处理,则返回true;否则如果事件未处理,则返回false。
*/
@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;
}
/**
* 准备选项菜单。
*
* @param menu 菜单对象。
* @return 总是返回true。
*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear(); // 清除之前的菜单项
// 根据当前状态加载不同的菜单布局
if (mState == ListEditState.NOTE_LIST) {
getMenuInflater().inflate(R.menu.note_list, menu);
// 设置同步或取消同步菜单项的标题
menu.findItem(R.id.menu_sync).setTitle(
GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
} else if (mState == ListEditState.SUB_FOLDER) {
getMenuInflater().inflate(R.menu.sub_folder, menu);
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_record_folder, menu);
} else {
Log.e(TAG, "Wrong state:" + mState);
}
return true;
}
/**
* 处理选项菜单项的选择。
*
* @param item 被选择的菜单项。
* @return 如果事件已成功处理,则返回true;否则如果事件未处理,则返回false。
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_new_folder: {
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;
}
/**
* 处理搜索请求。
*
* @return 总是返回true。
*/
@Override
public boolean onSearchRequested() {
startSearch(null, false, null /* appData */, false);
return true;
}
/**
* 将笔记导出为文本文件。
* 在后台任务中执行导出操作,并根据操作结果展示不同的对话框。
*/
private void exportNoteToText() {
final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this);
new AsyncTask<Void, Void, Integer>() {
@Override
protected Integer doInBackground(Void... unused) {
// 执行导出操作
return backup.exportToText();
}
@Override
protected void onPostExecute(Integer result) {
// 根据导出结果展示不同的对话框
if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
showExportFailedDialog(NotesListActivity.this.getString(R.string.failed_sdcard_export),
NotesListActivity.this.getString(R.string.error_sdcard_unmounted));
} else if (result == BackupUtils.STATE_SUCCESS) {
showExportSuccessDialog(NotesListActivity.this.getString(R.string.success_sdcard_export),
backup.getExportedTextFileName(), backup.getExportedTextFileDir());
} else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
showExportFailedDialog(NotesListActivity.this.getString(R.string.failed_sdcard_export),
NotesListActivity.this.getString(R.string.error_sdcard_export));
}
}
}.execute();
}
/**
* 检查当前是否为同步模式。
*
* @return 如果已配置同步账户名则返回true,否则返回false。
*/
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
/**
* 启动设置活动。
* 用于打开设置界面。
*/
private void startPreferenceActivity() {
Activity from = getParent() != null ? getParent() : this;
Intent intent = new Intent(from, NotesPreferenceActivity.class);
from.startActivityIfNeeded(intent, -1);
}
/**
* 列表项点击监听器。
* 处理列表项的点击事件,根据不同的状态和项类型执行相应的操作。
*/
private class OnListItemClickListener implements OnItemClickListener {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
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;
}
}
}
}
/**
* 启动查询目标文件夹。
* 根据当前状态查询并显示文件夹列表。
*/
private void startQueryDestinationFolders() {
String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?";
selection = (mState == ListEditState.NOTE_LIST) ? selection :
"(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
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),
String.valueOf(mCurrentFolderId)
},
NoteColumns.MODIFIED_DATE + " DESC");
}
/**
* 长按列表项时的处理。
* 根据不同的项类型启动选择模式或显示上下文菜单。
*
* @return 总是返回false。
*/
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
if (view instanceof NotesListItem) {
mFocusNoteDataItem = ((NotesListItem) view).getItemData();
if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
// 长按笔记项时启动选择模式
if (mNotesListView.startActionMode(mModeCallBack) != null) {
mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
} else {
Log.e(TAG, "startActionMode fails");
}
} else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
// 长按文件夹项时设置上下文菜单监听器
mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
}
}
return false;
}
/**
* 显示导出失败的对话框。
*
* @param title 对话框标题
* @param message 对话框消息内容
*/
private void showExportFailedDialog(String title, String message) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(title);
builder.setMessage(message);
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
}
/**
* 显示导出成功的对话框。
*
* @param title 对话框标题
* @param fileName 导出文件的名称
* @param fileDir 导出文件的目录
*/
private void showExportSuccessDialog(String title, String fileName, String fileDir) {
AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this);
builder.setTitle(title);
builder.setMessage(NotesListActivity.this.getString(R.string.format_exported_file_location, fileName, fileDir));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
}
}
11 NoteEditText
/*
* 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;
/**
* 自定义的可编辑文本视图,支持文本变化监听、删除和新增文本事件。
*/
public class NoteEditText extends EditText {
private static final String TAG = "NoteEditText";
private int mIndex; // 当前文本视图的索引
private int mSelectionStartBeforeDelete; // 删除操作前的选择起始位置
private static final String SCHEME_TEL = "tel:";
private static final String SCHEME_HTTP = "http:";
private static final String SCHEME_EMAIL = "mailto:";
// URL方案与对应操作资源ID的映射
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
* 文本视图变化监听接口。
*/
public interface OnTextViewChangeListener {
/**
* 当按下删除键且文本为空时,删除当前文本视图。
*/
void onEditTextDelete(int index, String text);
/**
* 当按下回车键时,新增一个文本视图。
*/
void onEditTextEnter(int index, String text);
/**
* 当文本变化时,隐藏或显示项目选项。
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
* 构造函数,初始化编辑文本视图。
*
* @param context 上下文对象
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
/**
* 设置当前文本视图的索引。
*
* @param index 当前文本视图的索引
*/
public void setIndex(int index) {
mIndex = index;
}
/**
* 设置文本视图变化监听器。
*
* @param listener 文本视图变化监听器
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
* 构造函数,初始化编辑文本视图。
*
* @param context 上下文对象
* @param attrs 属性集
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
* 构造函数,初始化编辑文本视图。
*
* @param context 上下文对象
* @param attrs 属性集
* @param defStyle 样式
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 处理触摸事件,调整光标位置。
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 计算触摸位置相对于文本的偏移量,并调整光标位置
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX();
y += getScrollY();
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
Selection.setSelection(getText(), off);
break;
}
return super.onTouchEvent(event);
}
/**
* 处理键盘按下事件。
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 处理回车键事件,准备新增文本视图
if (mOnTextViewChangeListener != null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录删除操作前的选择位置
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
return super.onKeyDown(keyCode, event);
}
/**
* 处理键盘弹起事件。
*/
@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;
}
return super.onKeyUp(keyCode, event);
}
/**
* 当焦点变化时,通知文本变化情况。
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
* 创建上下文菜单,支持点击URL跳转。
*/
@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);
if (urls.length == 1) {
int defaultResId = 0;
for (String schema : sSchemaActionResMap.keySet()) {
if (urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 跳转到URL指向的页面
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
}
12 NotesListAdapter
- 包声明:
package net.micode.notes.ui;
声明了该类所在的包。- 导入语句:导入了一系列Java和Android相关的类,包括上下文、游标、日志输出、视图操作、游标适配器、集合操作等。
- 类声明:
public class NotesListAdapter extends CursorAdapter {
声明了一个名为NotesListAdapter
的公共类,继承自CursorAdapter
。- 常量声明:
private static final String TAG = "NotesListAdapter";
声明了一个私有静态最终的字符串TAG
,用于日志输出。- 成员变量:
private Context mContext;
声明了一个私有上下文对象mContext
。private HashMap<Integer, Boolean> mSelectedIndex;
声明了一个私有哈希映射mSelectedIndex
,用于存储选中项的索引和状态。private int mNotesCount;
声明了一个私有整数mNotesCount
,用于存储笔记总数。private boolean mChoiceMode;
声明了一个私有布尔值mChoiceMode
,用于存储选择模式标志。- 内部类声明:
public static class AppWidgetAttribute { ... }
声明了一个公共静态内部类AppWidgetAttribute
,用于存储与小部件相关的数据。- 构造函数:
public NotesListAdapter(Context context) { ... }
声明了类的构造函数,接受上下文对象作为参数,并初始化成员变量。- 方法:
@Override public View newView(Context context, Cursor cursor, ViewGroup parent) { ... }
重写了newView
方法,用于创建新的列表项视图。@Override public void bindView(View view, Context context, Cursor cursor) { ... }
重写了bindView
方法,用于绑定数据到视图。public void setCheckedItem(final int position, final boolean checked) { ... }
声明了一个公共方法setCheckedItem
,用于设置指定位置的项为选中或未选中状态。public boolean isInChoiceMode() { ... }
声明了一个公共方法isInChoiceMode
,用于获取当前是否处于选择模式。public void setChoiceMode(boolean mode) { ... }
声明了一个公共方法setChoiceMode
,用于设置选择模式。public void selectAll(boolean checked) { ... }
声明了一个公共方法selectAll
,用于全选或全不选。public HashSet<Long> getSelectedItemIds() { ... }
声明了一个公共方法getSelectedItemIds
,用于获取所有选中项的ID集合。public HashSet<AppWidgetAttribute> getSelectedWidget() { ... }
声明了一个公共方法getSelectedWidget
,用于获取所有选中小部件的属性集合。public int getSelectedCount() { ... }
声明了一个公共方法getSelectedCount
,用于获取选中项的数量。public boolean isAllSelected() { ... }
声明了一个公共方法isAllSelected
,用于判断是否全部选中。public boolean isSelectedItem(final int position) { ... }
声明了一个公共方法isSelectedItem
,用于检查指定位置的项是否被选中。@Override protected void onContentChanged() { ... }
重写了onContentChanged
方法,用于当内容改变时调用,更新笔记数量。@Override public void changeCursor(Cursor cursor) { ... }
重写了changeCursor
方法,用于当游标改变时调用,更新笔记数量。private void calcNotesCount() { ... }
声明了一个私有方法calcNotesCount
,用于计算并更新笔记总数。
package net.micode.notes.ui; // 定义包名
import android.content.Context; // 导入Context类,用于获取上下文
import android.database.Cursor; // 导入Cursor类,用于数据源游标
import android.util.Log; // 导入Log类,用于日志输出
import android.view.View; // 导入View类,用于视图操作
import android.view.ViewGroup; // 导入ViewGroup类,用于视图组操作
import android.widget.CursorAdapter; // 导入CursorAdapter类,用于游标适配器
import net.micode.notes.data.Notes; // 导入Notes类,用于笔记数据操作
import java.util.Collection; // 导入Collection类,用于集合操作
import java.util.HashMap; // 导入HashMap类,用于哈希映射
import java.util.HashSet; // 导入HashSet类,用于哈希集合
import java.util.Iterator; // 导入Iterator类,用于迭代器
/**
* 用于管理笔记列表的适配器,继承自CursorAdapter。
*/
public class NotesListAdapter extends CursorAdapter { // 定义NotesListAdapter类,继承自CursorAdapter
private static final String TAG = "NotesListAdapter"; // 定义日志标签
private Context mContext; // 上下文对象
// 用于存储选中项的索引和状态
private HashMap<Integer, Boolean> mSelectedIndex; // 选中项索引和状态的哈希映射
private int mNotesCount; // 笔记总数
private boolean mChoiceMode; // 选择模式标志
/**
* AppWidget属性容器,用于存储与小部件相关的数据。
*/
public static class AppWidgetAttribute { // 定义AppWidgetAttribute类
public int widgetId; // 小部件ID
public int widgetType; // 小部件类型
}
/**
* 构造函数。
*
* @param context 上下文对象
*/
public NotesListAdapter(Context context) { // 构造函数
super(context, null); // 调用父类构造函数
mSelectedIndex = new HashMap<Integer, Boolean>(); // 初始化选中项索引和状态的哈希映射
mContext = context; // 赋值上下文对象
mNotesCount = 0; // 初始化笔记总数
}
/**
* 创建新的列表项视图。
*
* @param context 上下文对象
* @param cursor 数据游标
* @param parent 父视图
* @return 新的列表项视图
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) { // 重写newView方法
return new NotesListItem(context); // 返回新的列表项视图
}
/**
* 绑定数据到视图。
*
* @param view 列表项视图
* @param context 上下文对象
* @param cursor 数据游标
*/
@Override
public void bindView(View view, Context context, Cursor cursor) { // 重写bindView方法
if (view instanceof NotesListItem) { // 判断视图是否为NotesListItem类型
NoteItemData itemData = new NoteItemData(context, cursor); // 创建NoteItemData对象
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition())); // 绑定数据到视图
}
}
/**
* 设置指定位置的项为选中或未选中状态。
*
* @param position 项的位置
* @param checked 选中状态
*/
public void setCheckedItem(final int position, final boolean checked) { // 设置选中项方法
mSelectedIndex.put(position, checked); // 更新选中项索引和状态
notifyDataSetChanged(); // 通知数据集改变
}
/**
* 获取当前是否处于选择模式。
*
* @return 选择模式状态
*/
public boolean isInChoiceMode() { // 获取选择模式方法
return mChoiceMode; // 返回选择模式状态
}
/**
* 设置选择模式。
*
* @param mode 选择模式状态
*/
public void setChoiceMode(boolean mode) { // 设置选择模式方法
mSelectedIndex.clear(); // 清空选中项索引和状态
mChoiceMode = mode; // 更新选择模式状态
}
/**
* 全选或全不选。
*
* @param checked 选中状态
*/
public void selectAll(boolean checked) { // 全选或全不选方法
Cursor cursor = getCursor(); // 获取游标
for (int i = 0; i < getCount(); i++) { // 遍历所有项
if (cursor.moveToPosition(i)) { // 移动到指定位置
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { // 判断是否为笔记类型
setCheckedItem(i, checked); // 设置选中状态
}
}
}
}
/**
* 获取所有选中项的ID集合。
*
* @return 选中项ID的HashSet
*/
public HashSet<Long> getSelectedItemIds() { // 获取选中项ID集合方法
HashSet<Long> itemSet = new HashSet<Long>(); // 创建哈希集合
for (Integer position : mSelectedIndex.keySet()) { // 遍历选中项索引
if (mSelectedIndex.get(position) == true) { // 判断是否为选中状态
Long id = getItemId(position); // 获取项ID
if (id == Notes.ID_ROOT_FOLDER) { // 判断是否为根文件夹
Log.d(TAG, "Wrong item id, should not happen"); // 输出日志
} else {
itemSet.add(id); // 添加到哈希集合
}
}
}
return itemSet; // 返回哈希集合
}
/**
* 获取所有选中小部件的属性集合。
*
* @return 选中小部件属性的HashSet
*/
public HashSet<AppWidgetAttribute> getSelectedWidget() { // 获取选中小部件属性集合方法
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>(); // 创建哈希集合
for (Integer position : mSelectedIndex.keySet()) { // 遍历选中项索引
if (mSelectedIndex.get(position) == true) { // 判断是否为选中状态
Cursor c = (Cursor) getItem(position); // 获取游标
if (c != null) { // 判断游标是否为空
AppWidgetAttribute widget = new AppWidgetAttribute(); // 创建AppWidgetAttribute对象
NoteItemData item = new NoteItemData(mContext, c); // 创建NoteItemData对象
widget.widgetId = item.getWidgetId(); // 获取小部件ID
widget.widgetType = item.getWidgetType(); // 获取小部件类型
itemSet.add(widget); // 添加到哈希集合
} else {
Log.e(TAG, "Invalid cursor"); // 输出日志
return null; // 返回空
}
}
}
return itemSet; // 返回哈希集合
}
/**
* 获取选中项的数量。
*
* @return 选中项数量
*/
public int getSelectedCount() { // 获取选中项数量方法
Collection<Boolean> values = mSelectedIndex.values(); // 获取选中项状态集合
if (null == values) { // 判断集合是否为空
return 0; // 返回0
}
Iterator<Boolean> iter = values.iterator(); // 获取迭代器
int count = 0; // 初始化计数器
while (iter.hasNext()) { // 遍历集合
if (true == iter.next()) { // 判断是否为选中状态
count++; // 计数器加1
}
}
return count; // 返回计数器
}
/**
* 判断是否全部选中。
*
* @return 全部选中的状态
*/
public boolean isAllSelected() { // 判断是否全部选中方法
int checkedCount = getSelectedCount(); // 获取选中项数量
return (checkedCount != 0 && checkedCount == mNotesCount); // 判断是否全部选中
}
/**
* 检查指定位置的项是否被选中。
*
* @param position 项的位置
* @return 选中状态
*/
public boolean isSelectedItem(final int position) { // 检查选中项方法
if (null == mSelectedIndex.get(position)) { // 判断是否为空
return false; // 返回未选中状态
}
return mSelectedIndex.get(position); // 返回选中状态
}
/**
* 当内容改变时调用,更新笔记数量。
*/
@Override
protected void onContentChanged() { // 重写onContentChanged方法
super.onContentChanged(); // 调用父类方法
calcNotesCount(); // 计算笔记数量
}
/**
* 当游标改变时调用,更新笔记数量。
*
* @param cursor 新的游标
*/
@Override
public void changeCursor(Cursor cursor) { // 重写changeCursor方法
super.changeCursor(cursor); // 调用父类方法
calcNotesCount(); // 计算笔记数量
}
/**
* 计算并更新笔记总数。
*/
private void calcNotesCount() { // 计算笔记数量方法
mNotesCount = 0; // 初始化笔记总数
for (int i = 0; i < getCount(); i++) { // 遍历所有项
Cursor c = (Cursor) getItem(i); // 获取游标
if (c != null) { // 判断游标是否为空
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { // 判断是否为笔记类型
mNotesCount++; // 笔记总数加1
}
} else {
Log.e(TAG, "Invalid cursor"); // 输出日志
return; // 返回
}
}
}
}
13 NotesListltem
- 包声明:
package net.micode.notes.ui;
声明了该类所在的包。- 导入语句:导入了一系列Java和Android相关的类,包括上下文、日期格式化、视图操作、复选框、图像显示、线性布局、文本显示等。
- 类声明:
public class NotesListItem extends LinearLayout {
声明了一个名为NotesListItem
的公共类,继承自LinearLayout
。- 成员变量:
private ImageView mAlert;
声明了一个私有ImageView
对象mAlert
,用于显示提醒图标。private TextView mTitle;
声明了一个私有TextView
对象mTitle
,用于显示笔记标题。private TextView mTime;
声明了一个私有TextView
对象mTime
,用于显示修改时间。private TextView mCallName;
声明了一个私有TextView
对象mCallName
,用于在通话记录笔记中显示通话名称。private NoteItemData mItemData;
声明了一个私有NoteItemData
对象mItemData
,用于绑定笔记数据。private CheckBox mCheckBox;
声明了一个私有CheckBox
对象mCheckBox
,用于多选模式。- 构造函数:
public NotesListItem(Context context) { ... }
声明了类的构造函数,接受上下文对象作为参数,并初始化视图组件。- 方法:
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { ... }
声明了一个公共方法bind
,用于绑定数据到视图,根据数据设置视图状态。private void setBackground(NoteItemData data) { ... }
声明了一个私有方法setBackground
,用于根据笔记数据设置列表项的背景资源。public NoteItemData getItemData() { ... }
声明了一个公共方法getItemData
,用于获取绑定的笔记数据。
package net.micode.notes.ui; // 定义包名
import android.content.Context; // 导入Context类,用于获取上下文
import android.text.format.DateUtils; // 导入DateUtils类,用于日期格式化
import android.view.View; // 导入View类,用于视图操作
import android.widget.CheckBox; // 导入CheckBox类,用于复选框
import android.widget.ImageView; // 导入ImageView类,用于图像显示
import android.widget.LinearLayout; // 导入LinearLayout类,用于线性布局
import android.widget.TextView; // 导入TextView类,用于文本显示
import net.micode.notes.R; // 导入R类,用于资源引用
import net.micode.notes.data.Notes; // 导入Notes类,用于笔记数据操作
import net.micode.notes.tool.DataUtils; // 导入DataUtils类,用于数据处理
import net.micode.notes.tool.ResourceParser.NoteItemBgResources; // 导入NoteItemBgResources类,用于背景资源解析
/*
* 该类表示一个笔记列表项,继承自LinearLayout,并包含了显示笔记各种信息的组件。
* 它用于在UI中展示一个笔记或文件夹的条目。
*/
public class NotesListItem extends LinearLayout { // 定义NotesListItem类,继承自LinearLayout
private ImageView mAlert; // 用于显示提醒图标
private TextView mTitle; // 显示笔记标题
private TextView mTime; // 显示修改时间
private TextView mCallName; // 在通话记录笔记中显示通话名称
private NoteItemData mItemData; // 绑定的笔记数据
private CheckBox mCheckBox; // 选择框,用于多选模式
/*
* 构造函数,初始化视图组件。
*/
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); // 初始化复选框
}
/*
* 绑定数据到视图,根据数据设置视图状态。
*
* @param context 上下文
* @param data 要绑定的笔记数据
* @param choiceMode 是否为选择模式
* @param checked 是否选中
*/
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { // 绑定数据方法
// 根据是否为选择模式和笔记类型,控制复选框的可见性和选中状态
if (choiceMode && data.getType() == Notes.TYPE_NOTE) { // 判断是否为选择模式且为笔记类型
mCheckBox.setVisibility(View.VISIBLE); // 显示复选框
mCheckBox.setChecked(checked); // 设置复选框选中状态
} else {
mCheckBox.setVisibility(View.GONE); // 隐藏复选框
}
mItemData = data; // 赋值笔记数据
// 根据笔记类型和状态,设置标题、提醒图标和背景
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()); // 设置文件夹背景
}
}
/*
* 获取绑定的笔记数据。
*
* @return 绑定的NoteItemData对象
*/
public NoteItemData getItemData() { // 获取笔记数据方法
return mItemData; // 返回笔记数据
}
}
14 NotesPreferenceActivity
/*
* 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"; // 权限过滤键
// 类成员变量定义部分:主要用于账户同步和UI更新
private PreferenceCategory mAccountCategory; // 账户分类偏好项
private GTaskReceiver mReceiver; // 接收同步任务的广播接收器
private Account[] mOriAccounts; // 原始账户数组
private boolean mHasAddedAccount; // 标记是否已添加新账户
/**
* 当设置Activity创建时调用。
* 主要进行界面初始化和设置账户同步。
*
* @param icicle 保存Activity状态的Bundle,用于恢复状态。
*/
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 设置返回按钮
getActionBar().setDisplayHomeAsUpEnabled(true);
// 从XML加载偏好设置
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter); // 注册广播接收器以监听同步服务
mOriAccounts = null;
// 添加设置头部视图
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
/**
* 当设置Activity恢复到前台时调用。
* 主要用于检查并自动设置新添加的账户进行同步。
*/
@Override
protected void onResume() {
super.onResume();
// 自动设置新添加的账户进行同步
if (mHasAddedAccount) {
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;
}
}
}
}
// 刷新UI
refreshUI();
}
/**
* 当Activity即将被销毁时调用,用于注销广播接收器。
*/
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver); // 注销广播接收器,避免内存泄漏
}
super.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() {
public boolean onPreferenceClick(Preference preference) {
// 处理账户点击事件
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// 如果尚未设置账户,则展示选择账户对话框
showSelectAccountAlertDialog();
} else {
// 如果已经设置账户,则展示更改账户确认对话框
showChangeAccountConfirmAlertDialog();
}
} else {
// 如果正在同步中,则展示无法更改账户的提示
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
}
});
mAccountCategory.addPreference(accountPref); // 将账户偏好项添加到账户分类下
}
/**
* 加载同步按钮,并根据同步状态设置其文本和点击事件。
*/
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button); // 获取同步按钮
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); // 获取上次同步时间视图
// 根据同步状态设置按钮文本和点击事件
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); // 显示上次同步时间视图
} else {
long lastSyncTime = getLastSyncTime(this); // 获取上次同步时间
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime))); // 格式化并显示上次同步时间
lastSyncTimeView.setVisibility(View.VISIBLE); // 显示上次同步时间视图
} else {
lastSyncTimeView.setVisibility(View.GONE); // 如果未同步过,则隐藏上次同步时间视图
}
}
}
/**
* 刷新用户界面,加载账户偏好设置和同步按钮。
*/
private void refreshUI() {
loadAccountPreference(); // 加载账户偏好设置
loadSyncButton(); // 加载同步按钮
}
/**
* 显示选择账户的对话框。
* 该对话框列出了已连接的Google账户,并允许用户选择一个账户用于同步。
* 如果没有账户,对话框将提供添加账户的选项。
*/
private void showSelectAccountAlertDialog() {
// 创建对话框构建器并设置自定义标题
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
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();
}
});
}
// 添加“添加账户”选项
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
// 点击“添加账户”执行添加账户操作
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[]{
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
/**
* 显示更改账户确认对话框。
* 提供用户更改当前同步账户或取消更改的选择。
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 设置自定义标题,包含当前同步账户名称
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
// 创建菜单项并设置点击事件
CharSequence[] menuItemArray = new CharSequence[]{
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
// 选择更改账户,显示账户选择对话框
showSelectAccountAlertDialog();
} else if (which == 1) {
// 选择移除账户,执行移除操作并刷新UI
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
/**
* 获取设备上的Google账户列表。
*
* @return Account[] 返回设备上所有类型为“com.google”的账户数组。
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
* 设置同步账户信息。
* 如果当前账户与传入账户不一致,则更新SharedPreferences中的账户信息,并清理本地相关的gtask信息。
*
* @param account 需要设置的账户名
*/
private void setSyncAccount(String account) {
// 检查当前账户是否与传入账户名一致,不一致则更新账户信息
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
// 如果账户名非空,则保存账户名,否则清除账户名
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// 清理上次同步时间
setLastSyncTime(this, 0);
// 清理本地相关的gtask信息
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
// 显示设置成功的提示信息
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
* 移除同步账户信息。
* 清除SharedPreferences中的账户信息和上次同步时间,并清理本地相关的gtask信息。
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
// 如果存在账户信息,则移除
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
// 如果存在上次同步时间信息,则移除
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.commit();
// 清理本地相关的gtask信息
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
/**
* 获取当前同步账户名。
* 从SharedPreferences中获取存储的账户名,默认为空字符串。
*
* @param context 上下文
* @return 同步账户名
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
* 设置上次同步的时间。
* 将指定的时间保存到SharedPreferences中。
*
* @param context 上下文
* @param time 上次同步的时间戳
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
}
/**
* 获取上次同步的时间。
* 从SharedPreferences中获取上次同步的时间戳,默认为0。
*
* @param context 上下文
* @return 上次同步的时间戳
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/**
* 广播接收器类,用于接收gtask同步相关的广播消息,并据此刷新UI。
*/
private class GTaskReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
// 如果广播消息表明正在同步,则更新UI显示的同步状态信息
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
/**
* 处理选项菜单项的选择事件。
*
* @param item 选中的菜单项
* @return 如果事件已处理,则返回true;否则返回false。
*/
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 当选择返回按钮时,启动NotesListActivity并清除当前活动栈顶以上的所有活动
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
return false;
}
}
}