diff --git a/ui/AlarmAlertActivity.java b/ui/AlarmAlertActivity.java new file mode 100644 index 0000000..dff1ccd --- /dev/null +++ b/ui/AlarmAlertActivity.java @@ -0,0 +1,213 @@ +/* + * 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. + */ + +// 包声明,表明该类所在的包名,此处在net.micode.notes.ui包下 +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + +// AlarmAlertActivity类继承自Activity类,实现了OnClickListener和OnDismissListener接口,用于处理闹钟提醒相关的界面展示、声音播放等功能 +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + // 记录笔记的ID,用于后续操作中标识具体的笔记 + private long mNoteId; + // 笔记的摘要内容,用于在提醒对话框中展示部分笔记内容 + private String mSnippet; + // 定义摘要预览的最大长度为60个字符 + private static final int SNIPPET_PREW_MAX_LEN = 60; + // 用于播放闹钟提醒声音的MediaPlayer对象 + MediaPlayer mPlayer; + + // 重写Activity的onCreate方法,在Activity创建时被调用,进行一些初始化操作 + @Override + protected void onCreate(Bundle savedInstanceState) { + // 调用父类的onCreate方法,完成Activity创建的基本初始化流程,这是必须的操作 + super.onCreate(savedInstanceState); + // 请求去除Activity的标题栏,使界面更加简洁,通常用于全屏等特定显示需求 + requestWindowFeature(Window.FEATURE_NO_TITLE); + + // 获取当前Activity的窗口对象,后续用于设置窗口相关的属性 + final Window win = getWindow(); + // 给窗口添加标志位,使得在屏幕锁定时也能显示该Activity,方便用户看到闹钟提醒 + 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); + } + + // 获取启动该Activity的Intent对象,用于获取传递过来的数据等信息 + Intent intent = getIntent(); + + try { + // 从Intent携带的数据中获取笔记的ID,具体是从数据的路径片段中的第二个元素获取(假设数据格式符合特定规范) + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 通过工具类DataUtils根据笔记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) { + // 如果在获取笔记ID或处理摘要过程中出现参数异常,打印异常堆栈信息,并且直接返回,不再继续执行后续可能出错的操作 + e.printStackTrace(); + return; + } + + // 创建一个MediaPlayer对象,用于播放闹钟提醒的声音 + mPlayer = new MediaPlayer(); + // 判断该笔记是否在笔记数据库中可见(通过DataUtils工具类的方法进行判断) + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 如果可见,展示操作对话框,给用户提供相关操作选项 + showActionDialog(); + // 调用方法播放闹钟提醒声音 + playAlarmSound(); + } else { + // 如果笔记不可见,直接结束该Activity,不需要进行后续的提醒相关操作 + finish(); + } + } + + // 方法用于判断屏幕是否处于开启状态,通过获取PowerManager服务并调用其isScreenOn方法来判断 + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + // 方法用于播放闹钟提醒声音,进行一系列设置并启动声音播放 + private void playAlarmSound() { + // 获取系统默认的闹钟铃声的Uri,通过RingtoneManager来获取实际使用的默认铃声的Uri + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 获取系统设置中受静音模式影响的音频流相关设置,用于后续判断闹钟音频流的设置情况 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 判断静音模式下是否影响闹钟音频流,如果受影响,则按照相应设置设置MediaPlayer的音频流类型 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + // 如果不受影响,将MediaPlayer的音频流类型设置为普通的闹钟音频流类型 + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + // 设置MediaPlayer的数据源为获取到的闹钟铃声的Uri,指定从哪里获取音频数据来播放 + mPlayer.setDataSource(this, url); + // 准备MediaPlayer,使其进入可播放状态,加载音频数据等资源 + mPlayer.prepare(); + // 设置MediaPlayer循环播放,使得闹钟声音一直响,直到被用户操作停止 + mPlayer.setLooping(true); + // 启动MediaPlayer开始播放闹钟声音 + mPlayer.start(); + } catch (IllegalArgumentException e) { + // 如果在设置数据源等操作中出现参数异常,打印异常堆栈信息 + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (SecurityException e) { + // 如果出现安全相关异常,比如没有权限访问音频资源等情况,打印异常堆栈信息 + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // 如果MediaPlayer处于不正确的状态(比如已经在播放等情况)导致操作失败,打印异常堆栈信息 + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // 如果在读取音频数据等操作中出现IO异常,比如文件不存在等情况,打印异常堆栈信息 + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + // 方法用于展示操作对话框,给用户提供针对闹钟提醒的操作选项,比如确认、进入编辑等 + private void showActionDialog() { + // 创建一个AlertDialog的构建器,用于构建自定义的对话框 + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框的标题为应用的名称,通过资源文件中的字符串资源来设置 + dialog.setTitle(R.string.app_name); + // 设置对话框的消息内容为前面获取和处理后的笔记摘要内容,展示给用户提醒相关的笔记信息 + dialog.setMessage(mSnippet); + // 设置对话框的确认按钮(通常是正向操作按钮)的文本及点击监听器,点击时会触发OnClickListener中的相应逻辑 + dialog.setPositiveButton(R.string.notealert_ok, this); + // 判断屏幕是否开启,如果开启,设置对话框的取消按钮(通常是进入相关操作按钮)的文本及点击监听器 + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + // 构建并显示对话框,同时给对话框设置关闭监听器,当对话框关闭时会触发相应逻辑 + dialog.show().setOnDismissListener(this); + } + + // 实现OnClickListener接口的方法,处理对话框按钮点击事件,根据点击的按钮执行相应的操作 + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + // 如果点击的是取消按钮(通常是进入相关操作按钮),创建一个Intent用于启动笔记编辑Activity + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作,这里设置为查看动作,表明是查看笔记详情的意图 + intent.setAction(Intent.ACTION_VIEW); + // 将笔记的ID作为额外数据添加到Intent中,传递给NoteEditActivity,以便其能识别要编辑的具体笔记 + intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 启动NoteEditActivity,切换到笔记编辑界面 + startActivity(intent); + break; + default: + break; + } + } + + // 实现OnDismissListener接口的方法,当对话框关闭时被调用,用于停止闹钟声音并结束当前Activity + public void onDismiss(DialogInterface dialog) { + // 调用方法停止闹钟声音,释放相关资源 + stopAlarmSound(); + // 结束当前Activity,关闭闹钟提醒界面 + finish(); + } + + // 方法用于停止闹钟声音,释放MediaPlayer相关的资源,将其设置为null + private void stopAlarmSound() { + if (mPlayer!= null) { + // 停止MediaPlayer的播放 + mPlayer.stop(); + // 释放MediaPlayer占用的资源,如音频缓冲区等 + mPlayer.release(); + // 将MediaPlayer对象设置为null,避免后续误操作 + mPlayer = null; + } + } +} \ No newline at end of file diff --git a/ui/AlarmInitReceiver.java b/ui/AlarmInitReceiver.java new file mode 100644 index 0000000..06c3898 --- /dev/null +++ b/ui/AlarmInitReceiver.java @@ -0,0 +1,88 @@ +/* + * 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. + */ + +// 包声明,表明该类所在的包名为net.micode.notes.ui +package net.micode.notes.ui; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +// AlarmInitReceiver类继承自BroadcastReceiver,用于接收广播并处理与闹钟初始化相关的逻辑,比如根据条件设置闹钟提醒等操作 +public class AlarmInitReceiver extends BroadcastReceiver { + + // 定义一个字符串数组,用于指定从数据库查询时需要获取的列名,这里获取笔记的ID和提醒日期这两列信息 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + // 定义常量,用于表示查询结果中笔记ID所在列的索引,方便后续从Cursor中获取对应数据,值为0表示第一列 + private static final int COLUMN_ID = 0; + // 定义常量,用于表示查询结果中提醒日期所在列的索引,方便后续从Cursor中获取对应数据,值为1表示第二列 + private static final int COLUMN_ALERTED_DATE = 1; + + // 重写BroadcastReceiver的onReceive方法,当接收到广播时会被调用,在此方法中实现具体的业务逻辑 + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前的系统时间(以毫秒为单位),用于后续与笔记的提醒日期进行比较,判断哪些笔记需要设置闹钟提醒 + long currentDate = System.currentTimeMillis(); + + // 通过ContentResolver发起一个数据库查询操作,从指定的笔记内容URI(Notes.CONTENT_NOTE_URI)中查询数据 + // 查询的列由PROJECTION指定,查询条件是提醒日期大于当前日期并且笔记类型为普通笔记(Notes.TYPE_NOTE) + // 条件中的参数通过后面的字符串数组传入当前时间的值 + // 最后一个参数null表示查询不需要排序等额外设置 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + // 判断查询结果的Cursor是否为空,如果不为空,表示有满足条件的数据,进行后续操作 + if (c!= null) { + // 将Cursor的指针移动到第一条数据位置,如果有数据则返回true,可以开始遍历数据 + if (c.moveToFirst()) { + // 使用do-while循环来遍历查询结果集,确保至少会执行一次循环体中的操作 + do { + // 从Cursor中获取提醒日期这一列的数据(通过之前定义的列索引COLUMN_ALERTED_DATE),得到具体的提醒时间戳 + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + // 创建一个Intent,用于指定当闹钟触发时要启动的组件,这里指定为AlarmReceiver类 + Intent sender = new Intent(context, AlarmReceiver.class); + // 给Intent设置数据,将笔记的ID附加到对应的内容URI上,这样可以传递具体是哪个笔记的闹钟触发了 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + // 创建一个PendingIntent,用于包装前面创建的Intent,以便交给AlarmManager使用, + // 这里的请求码设置为0,表示没有特定的请求标识,并且使用的是广播类型的PendingIntent + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + // 获取系统的AlarmManager服务,用于设置闹钟相关的操作 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + // 通过AlarmManager设置一个闹钟,使用RTC_WAKEUP模式(表示在指定的绝对时间唤醒设备来触发闹钟), + // 指定提醒时间为前面获取的alertDate,并且关联对应的PendingIntent,当时间到达时会触发对应的广播 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); // 判断是否还有下一条数据,如果有则继续循环执行操作,处理下一条满足条件的笔记数据 + } + // 关闭Cursor,释放相关的资源,避免内存泄漏等问题 + c.close(); + } + } +} \ No newline at end of file diff --git a/ui/AlarmReceiver.java b/ui/AlarmReceiver.java new file mode 100644 index 0000000..ac6a095 --- /dev/null +++ b/ui/AlarmReceiver.java @@ -0,0 +1,37 @@ +/* + * 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. + */ + +// 包声明,表明该类所在的包名为net.micode.notes.ui +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// AlarmReceiver类继承自BroadcastReceiver,用于接收广播消息,并在接收到广播后进行相应的操作,这里主要是启动相关的Activity +public class AlarmReceiver extends BroadcastReceiver { + // 重写BroadcastReceiver的onReceive方法,当该广播接收器接收到广播时,此方法会被调用,用于处理具体的业务逻辑 + @Override + public void onReceive(Context context, Intent intent) { + // 将传入的Intent的目标组件设置为AlarmAlertActivity.class,也就是指定当这个广播触发后,要启动的Activity是AlarmAlertActivity + intent.setClass(context, AlarmAlertActivity.class); + // 给Intent添加一个标志位,FLAG_ACTIVITY_NEW_TASK,表示以新任务的形式启动Activity。 + // 这通常是在从广播接收器启动Activity时需要添加的,因为广播接收器没有自己的任务栈,需要明确告知系统以新任务来启动Activity + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 通过传入的Context对象启动设置好的Activity,使得应用能够从接收到广播的这个逻辑跳转到对应的AlarmAlertActivity,展示闹钟提醒相关的界面等内容 + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/ui/DateTimePicker.java b/ui/DateTimePicker.java new file mode 100644 index 0000000..496b0cd --- /dev/null +++ b/ui/DateTimePicker.java @@ -0,0 +1,485 @@ +/* + * 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 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; + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + private static final int MINUT_SPINNER_MIN_VAL = 0; + private static final int MINUT_SPINNER_MAX_VAL = 59; + private static final int AMPM_SPINNER_MIN_VAL = 0; + private static final int AMPM_SPINNER_MAX_VAL = 1; + + 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; + + 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) { + 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 { + 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)); + } + } + }; + + 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; + } + 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) { + 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)); + } + + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + inflate(context, R.layout.datetime_picker, this); + + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + 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); + + // update controls to initial state + updateDateControl(); + updateHourControl(); + updateAmPmControl(); + + set24HourView(is24HourView); + + // set to current time + setCurrentDate(date); + + setEnabled(isEnabled()); + + // set the content descriptions + mInitialising = false; + } + + @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; + } + + @Override + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * Get the current date in millis + * + * @return the current date in millis + */ + public long getCurrentDateInTimeMillis() { + return mDate.getTimeInMillis(); + } + + /** + * Set the current date + * + * @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)); + } + + /** + * Set the current date + * + * @param year The current year + * @param month The current month + * @param dayOfMonth The current dayOfMonth + * @param hourOfDay The current hourOfDay + * @param minute The current minute + */ + public void setCurrentDate(int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + setCurrentYear(year); + setCurrentMonth(month); + setCurrentDay(dayOfMonth); + setCurrentHour(hourOfDay); + setCurrentMinute(minute); + } + + /** + * Get current year + * + * @return The current year + */ + public int getCurrentYear() { + return mDate.get(Calendar.YEAR); + } + + /** + * Set current year + * + * @param year The current year + */ + public void setCurrentYear(int year) { + if (!mInitialising && year == getCurrentYear()) { + return; + } + mDate.set(Calendar.YEAR, year); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current month in the year + * + * @return The current month in the year + */ + public int getCurrentMonth() { + return mDate.get(Calendar.MONTH); + } + + /** + * Set current month in the year + * + * @param month The month in the year + */ + public void setCurrentMonth(int month) { + if (!mInitialising && month == getCurrentMonth()) { + return; + } + mDate.set(Calendar.MONTH, month); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current day of the month + * + * @return The day of the month + */ + public int getCurrentDay() { + return mDate.get(Calendar.DAY_OF_MONTH); + } + + /** + * Set current day of the month + * + * @param dayOfMonth The day of the month + */ + public void setCurrentDay(int dayOfMonth) { + if (!mInitialising && dayOfMonth == getCurrentDay()) { + return; + } + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + updateDateControl(); + onDateTimeChanged(); + } + + /** + * Get current hour in 24 hour mode, in the range (0~23) + * @return The current hour in 24 hour mode + */ + public int getCurrentHourOfDay() { + return mDate.get(Calendar.HOUR_OF_DAY); + } + + private int getCurrentHour() { + if (mIs24HourView){ + return getCurrentHourOfDay(); + } else { + int hour = getCurrentHourOfDay(); + if (hour > HOURS_IN_HALF_DAY) { + return hour - HOURS_IN_HALF_DAY; + } else { + return hour == 0 ? HOURS_IN_HALF_DAY : hour; + } + } + } + + /** + * Set current hour in 24 hour mode, in the range (0~23) + * + * @param hourOfDay + */ + public void setCurrentHour(int hourOfDay) { + if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { + return; + } + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + 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(); + } + + /** + * Get currentMinute + * + * @return The Current Minute + */ + public int getCurrentMinute() { + return mDate.get(Calendar.MINUTE); + } + + /** + * Set current minute + */ + public void setCurrentMinute(int minute) { + if (!mInitialising && minute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(minute); + mDate.set(Calendar.MINUTE, minute); + onDateTimeChanged(); + } + + /** + * @return true if this is in 24 hour view else false. + */ + public boolean is24HourView () { + return mIs24HourView; + } + + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True for 24 hour mode. False for AM/PM mode. + */ + 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(); + } + + private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } + } + + 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); + } + } + + /** + * Set the callback that indicates the 'Set' button has been pressed. + * @param callback the callback, if null will do nothing + */ + public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { + mOnDateTimeChangedListener = callback; + } + + private void onDateTimeChanged() { + if (mOnDateTimeChangedListener != null) { + mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), + getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute()); + } + } +} diff --git a/ui/DateTimePickerDialog.java b/ui/DateTimePickerDialog.java new file mode 100644 index 0000000..fe64b1f --- /dev/null +++ b/ui/DateTimePickerDialog.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * 版权声明,表明该代码受Apache License 2.0协议的许可,并且说明了版权归属以及相关的许可范围等信息。 + * + * 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. + */ + +// 声明该类所属的包名,表明这个类位于net.micode.notes.ui包下,在项目的包结构中有其特定的组织和访问层级关系。 +package net.micode.notes.ui; + +// 导入必要的类库,这里导入了Calendar类,用于处理日期和时间相关的操作,比如获取、设置具体的年、月、日、时、分等信息。 +import java.util.Calendar; + +// 导入R类,这个类通常是由Android的资源编译系统自动生成的,用于访问项目中的各种资源(如布局文件、字符串资源、图片资源等),这里主要用于获取相关的字符串资源等。 +import net.micode.notes.R; +// 导入DateTimePicker类,应该是自定义的用于选择日期和时间的控件类,提供了日期时间选择的相关功能和界面交互逻辑。 +import net.micode.notes.ui.DateTimePicker; +// 导入DateTimePicker类中定义的OnDateTimeChangedListener接口,用于监听日期时间选择器中的日期时间发生变化的事件,以便进行相应的处理操作。 +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +// 导入相关的Android系统类,AlertDialog用于创建弹出式的对话框,提供一种提示、确认等交互的UI形式; +// Context用于获取应用的上下文环境信息,是很多Android操作的基础; +// DialogInterface用于处理对话框相关的交互逻辑,比如按钮点击等事件; +// OnClickListener用于监听点击事件,在这里用于监听对话框按钮的点击操作并执行相应逻辑。 +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +// 导入用于处理日期时间格式化相关操作的类和方法,比如判断是否是24小时制格式、按照指定格式格式化日期时间等功能。 +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +// DateTimePickerDialog类继承自AlertDialog类,意味着它是一个特殊的对话框,用于展示日期时间选择的界面,并实现了OnClickListener接口, +// 用于处理对话框中按钮的点击事件,整体实现了一个可以让用户选择日期时间并在确定后进行相应回调处理的对话框功能。 +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // 定义一个Calendar类型的成员变量mDate,用于存储当前日期时间选择器所表示的日期时间信息,通过Calendar.getInstance()获取初始值,即当前系统时间对应的日期时间对象。 + private Calendar mDate = Calendar.getInstance(); + // 定义一个布尔类型的成员变量mIs24HourView,用于标记当前日期时间选择器是否采用24小时制视图模式,初始值会根据系统设置等情况后续进行设置。 + private boolean mIs24HourView; + // 定义一个OnDateTimeSetListener类型的成员变量mOnDateTimeSetListener,这是一个自定义的接口类型变量, + // 外部类可以实现这个接口并传入该对话框,用于在用户点击对话框中的确定按钮并设置好日期时间后,接收相应的回调通知并执行自定义的业务逻辑。 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 定义一个DateTimePicker类型的成员变量mDateTimePicker,这是前面导入的自定义日期时间选择器控件的实例,用于在对话框中展示给用户进行日期时间的选择操作。 + private DateTimePicker mDateTimePicker; + + // 定义一个内部接口OnDateTimeSetListener,外部类需要实现这个接口来监听用户在对话框中完成日期时间选择并点击确定按钮后的事件, + // 接口中定义了OnDateTimeSet方法,该方法会在相应事件触发时被调用,传入当前的对话框实例以及用户选择的日期时间(以毫秒为单位的时间戳表示)作为参数,方便外部类进行后续处理。 + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + // 这是DateTimePickerDialog类的构造函数,用于创建该对话框的实例,接收一个Context类型的参数context用于获取应用的上下文环境信息, + // 以及一个long类型的参数date用于指定初始的日期时间(以毫秒为单位的时间戳形式),表示对话框打开时日期时间选择器默认显示的日期时间。 + public DateTimePickerDialog(Context context, long date) { + // 调用父类(AlertDialog)的构造函数,传入上下文环境context,完成父类的初始化操作,确保这个自定义对话框能继承AlertDialog的相关属性和功能,正常展示为一个对话框的形式。 + super(context); + // 创建一个DateTimePicker的实例,传入上下文环境context,用于生成日期时间选择器控件,这个控件将被添加到对话框中供用户进行日期时间的选择操作。 + mDateTimePicker = new DateTimePicker(context); + // 将创建好的日期时间选择器控件(mDateTimePicker)设置为对话框的视图内容,这样当对话框显示时,用户就能看到并操作这个日期时间选择器了。 + setView(mDateTimePicker); + // 为日期时间选择器(mDateTimePicker)设置日期时间变化监听器,当用户在日期时间选择器上选择不同的日期、时间等操作导致日期时间发生变化时, + // 会触发监听器中的onDateTimeChanged方法,在这个方法内部,会将选择器上的新日期时间信息更新到mDate对象中(即存储对话框当前表示的日期时间的对象), + // 并调用updateTitle方法,根据新的日期时间更新对话框的标题显示内容,使其始终能准确反映当前选择的日期时间情况。 + 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()); + } + }); + // 将传入的初始日期时间(以毫秒为单位的时间戳date)设置到mDate对象中,通过setTimeInMillis方法,以此来初始化对话框中日期时间选择器默认显示的日期时间信息。 + mDate.setTimeInMillis(date); + // 将mDate对象中表示秒的部分设置为0,确保日期时间的精确性以及符合一般的日期时间使用场景,通常在选择日期时间时不需要关注秒的设置,这里统一初始化为0秒。 + mDate.set(Calendar.SECOND, 0); + // 调用日期时间选择器(mDateTimePicker)的setCurrentDate方法,传入mDate对象的时间戳(通过getTimeInMillis方法获取), + // 用于将日期时间选择器的显示状态更新为与mDate对象所表示的日期时间一致,保证界面显示和内部存储的初始日期时间是匹配的。 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + // 设置对话框的确定按钮,通过调用setButton方法,传入从资源文件中获取的表示确定按钮文本的字符串(通过context.getString方法获取R.string.datetime_dialog_ok对应的字符串资源), + // 以及当前类(this)作为点击事件的监听器(因为当前类实现了OnClickListener接口,会处理确定按钮的点击事件),使得用户点击确定按钮时能触发相应的逻辑。 + setButton(context.getString(R.string.datetime_dialog_ok), this); + // 设置对话框的取消按钮,通过调用setButton2方法,传入从资源文件中获取的表示取消按钮文本的字符串(通过context.getString方法获取R.string.datetime_dialog_cancel对应的字符串资源), + // 以及传入null作为点击事件的监听器,表示取消按钮点击后不做额外的处理(一般就是关闭对话框等默认操作)。 + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + // 调用set24HourView方法,传入通过DateFormat.is24HourFormat方法获取的布尔值,该方法用于判断当前系统是否采用24小时制格式, + // 将获取到的布尔值传入set24HourView方法用于设置日期时间选择器是否为24小时制视图模式,以此来保证日期时间选择器的显示模式与系统设置相符。 + set24HourView(DateFormat.is24HourFormat(this.getContext())); + // 调用updateTitle方法,传入mDate对象的时间戳(通过getTimeInMillis方法获取),用于初始化对话框的标题显示内容,使其能根据初始的日期时间正确展示相关信息给用户。 + updateTitle(mDate.getTimeInMillis()); + } + + // 用于设置日期时间选择器是否为24小时制视图模式的方法,接收一个布尔类型的参数is24HourView,将其值赋给成员变量mIs24HourView, + // 以此来更新日期时间选择器的显示模式,后续可以根据这个变量的值来调整日期时间选择器中相关控件(如小时选择器、AM/PM选择器等)的显示和操作逻辑。 + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 用于设置日期时间设置监听器的方法,接收一个OnDateTimeSetListener类型的参数callBack,将其赋值给成员变量mOnDateTimeSetListener, + // 外部类可以通过实现OnDateTimeSetListener接口并传入相应的实现类实例,来监听用户在对话框中完成日期时间选择并点击确定按钮后的事件, + // 以便在该事件触发时执行自定义的业务逻辑,比如保存选择的日期时间到数据库、更新相关UI显示等操作。 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 私有方法用于更新对话框的标题显示内容,根据传入的日期时间(以毫秒为单位的时间戳date)来进行格式化并设置为对话框的标题。 + // 首先会定义一个整数类型的变量flag,用于设置日期时间格式化的标志位,通过按位或操作组合多个标志位,确定要显示的日期时间信息的格式。 + // 例如,这里设置了显示年、显示日期、显示时间等标志位,并且根据当前是否是24小时制视图模式(由mIs24HourView变量决定)来添加对应的24小时制或12小时制的格式化标志位。 + // 最后通过DateUtils.formatDateTime方法,传入应用的上下文环境(this.getContext())、日期时间时间戳(date)以及格式化标志位(flag), + // 将格式化后的字符串设置为对话框的标题,使得对话框标题能准确展示当前选择的日期时间信息,并且符合相应的时间格式要求。 + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + // 实现OnClickListener接口中的onClick方法,用于处理对话框中按钮的点击事件,这里主要处理确定按钮的点击事件(因为取消按钮点击时没有额外设置监听器,会执行默认操作)。 + // 当用户点击确定按钮时,会判断是否已经设置了日期时间设置监听器(通过判断mOnDateTimeSetListener是否为null), + // 如果已经设置了有效的监听器,就会触发该监听器的OnDateTimeSet方法,并传入当前的对话框实例(this)以及mDate对象的时间戳(通过getTimeInMillis方法获取), + // 使得外部实现了OnDateTimeSetListener接口的类能够接收到用户选择的日期时间信息,并执行相应的业务逻辑,比如保存选择的日期时间到数据库、更新相关UI显示等操作。 + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener!= null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} \ No newline at end of file diff --git a/ui/DropdownMenu.java b/ui/DropdownMenu.java new file mode 100644 index 0000000..b4cd94a --- /dev/null +++ b/ui/DropdownMenu.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * 版权声明部分,表明该代码遵循Apache License 2.0协议,同时说明了版权所属的开源社区以及相关许可的获取途径等信息。 + */ + +/* + * 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. + */ + +// 声明该类所属的包名,表明这个类位于net.micode.notes.ui包下,在项目的整体包结构中有其特定的组织和访问层级关系。 +package net.micode.notes.ui; + +// 导入必要的Android系统相关类,Context用于获取应用的上下文环境信息,是很多Android操作(如资源获取、系统服务调用等)的基础。 +import android.content.Context; +// 导入用于操作菜单相关的类,Menu用于表示菜单对象,包含了菜单项等相关信息;MenuItem用于表示具体的菜单项,可以设置其属性、响应点击事件等。 +import android.view.Menu; +import android.view.MenuItem; +// 导入View相关类,View是Android中所有可视化组件的基类,这里用于处理视图相关的操作和事件监听等; +// OnClickListener用于监听视图的点击事件,以便在视图被点击时执行相应的逻辑。 +import android.view.View; +import android.view.View.OnClickListener; +// 导入Button类,用于创建按钮控件,按钮是常见的用户交互组件,可响应用户点击操作等。 +import android.widget.Button; +// 导入PopupMenu相关类,PopupMenu用于创建弹出式菜单,提供一种在特定视图(通常是某个按钮等)上点击弹出菜单选项的交互方式; +// OnMenuItemClickListener用于监听弹出式菜单中菜单项的点击事件,以便执行相应的业务逻辑。 +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +// 导入项目中自动生成的资源类R,通过它可以访问项目中的各种资源(如布局文件、图片资源、字符串资源等),这里主要用于获取特定的图片资源。 +import net.micode.notes.R; + +// DropdownMenu类用于创建一个带有弹出式菜单功能的组件,它关联一个按钮(Button),点击该按钮会弹出一个菜单(PopupMenu), +// 并且可以对菜单的相关属性(如菜单项点击监听、查找菜单项、设置按钮显示文本等)进行操作,方便在Android应用中实现下拉菜单式的交互功能。 +public class DropdownMenu { + // 定义一个Button类型的成员变量mButton,用于存储关联的按钮控件,这个按钮将作为触发弹出式菜单显示的入口。 + private Button mButton; + // 定义一个PopupMenu类型的成员变量mPopupMenu,用于存储创建的弹出式菜单对象,通过这个对象可以进行弹出菜单的显示、设置菜单项点击监听等操作。 + private PopupMenu mPopupMenu; + // 定义一个Menu类型的成员变量mMenu,用于存储弹出式菜单对应的菜单对象,通过它可以访问和操作菜单中的具体菜单项(如添加、查找、移除菜单项等)。 + private Menu mMenu; + + // DropdownMenu类的构造函数,用于创建该类的实例,接收三个参数: + // Context类型的context用于获取应用的上下文环境信息,以便能够正确加载资源、创建相关的系统组件等; + // Button类型的button是要关联的按钮控件,点击这个按钮将弹出菜单; + // int类型的menuId是菜单资源的ID,用于指定要加载并显示的菜单布局资源(通常在XML文件中定义了具体的菜单项等内容)。 + public DropdownMenu(Context context, Button button, int menuId) { + // 将传入的按钮控件参数button赋值给成员变量mButton,建立与外部传入按钮的关联,后续通过这个成员变量对按钮进行相关操作(如设置背景、添加点击监听等)。 + mButton = button; + // 通过调用setBackgroundResource方法,为按钮(mButton)设置背景图片资源,这里使用了R.drawable.dropdown_icon所对应的图片资源, + // 该图片通常会作为按钮的可视化标识,提示用户点击它可以弹出菜单,使按钮在界面上有符合功能需求的外观显示。 + mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 创建一个PopupMenu实例,传入应用的上下文环境context和关联的按钮控件mButton,这样创建的弹出式菜单会在点击mButton时弹出,并且依赖于传入的上下文环境进行相关操作(如资源加载等)。 + mPopupMenu = new PopupMenu(context, mButton); + // 获取创建好的弹出式菜单(mPopupMenu)对应的菜单对象,并赋值给成员变量mMenu,通过这个对象后续可以对菜单中的具体菜单项进行操作(如添加、查找等)。 + mMenu = mPopupMenu.getMenu(); + // 通过弹出式菜单(mPopupMenu)的getMenuInflater方法获取菜单填充器对象,然后调用inflate方法,传入菜单资源ID(menuId)和菜单对象(mMenu), + // 这一步操作会根据指定的菜单资源ID对应的XML布局文件内容,将菜单项填充到mMenu对象中,从而构建出具有实际菜单项的菜单结构,用于后续展示给用户选择操作。 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 为按钮(mButton)设置点击事件监听器,当按钮被点击时,会触发监听器中的onClick方法,在这个方法内部,通过调用mPopupMenu的show方法, + // 实现弹出式菜单的显示,使得用户点击按钮后能看到弹出的菜单供其选择相应的菜单项,实现下拉菜单的基本交互功能。 + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + // 用于设置弹出式菜单中菜单项点击事件监听器的方法,接收一个OnMenuItemClickListener类型的参数listener, + // 如果弹出式菜单(mPopupMenu)不为空,就将传入的监听器设置给mPopupMenu,使得当用户点击弹出菜单中的菜单项时,会触发listener中定义的相应逻辑, + // 以便执行根据菜单项选择而不同的业务操作(如跳转到不同页面、执行不同功能等)。 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu!= null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + // 用于在菜单(mMenu)中查找指定ID的菜单项的方法,接收一个int类型的参数id,表示要查找的菜单项的资源ID, + // 通过调用mMenu对象的findItem方法,根据传入的ID查找对应的菜单项,并返回查找到的MenuItem对象,方便后续对该菜单项进行进一步操作(如获取其属性、设置其状态等)。 + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + // 用于设置关联按钮(mButton)显示文本的方法,接收一个CharSequence类型的参数title,通过调用mButton的setText方法, + // 将传入的文本设置为按钮上显示的内容,这样可以根据不同的业务场景动态改变按钮上显示的提示信息,增强交互的友好性和表意性。 + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} \ No newline at end of file diff --git a/ui/FoldersListAdapter.java b/ui/FoldersListAdapter.java new file mode 100644 index 0000000..d52f356 --- /dev/null +++ b/ui/FoldersListAdapter.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * 版权声明部分,表明该代码遵循Apache License 2.0协议,同时说明了版权所属的开源社区以及相关许可的获取途径等信息。 + */ + +/* + * 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. + */ + +// 声明该类所属的包名,表明这个类位于net.micode.notes.ui包下,在项目的整体包结构中有其特定的组织和访问层级关系。 +package net.micode.notes.ui; + +// 导入必要的Android系统相关类,Context用于获取应用的上下文环境信息,是很多Android操作(如资源获取、系统服务调用等)的基础。 +// Cursor用于处理数据库查询结果集,通过它可以遍历查询返回的数据记录等操作。 +import android.content.Context; +import android.database.Cursor; +// 导入View和ViewGroup相关类,View是Android中所有可视化组件的基类,ViewGroup是可以包含其他子视图的视图容器类,用于构建复杂的UI布局结构。 +import android.view.View; +import android.view.ViewGroup; +// 导入CursorAdapter类,这是Android中用于将数据库查询结果(Cursor)与ListView等列表视图进行适配的抽象类,子类需要实现特定方法来定制数据展示逻辑。 +import android.widget.CursorAdapter; +// 导入LinearLayout类,用于创建线性布局,是一种常见的布局方式,可以按照水平或垂直方向排列子视图。 +import android.widget.LinearLayout; +// 导入TextView类,用于在界面上展示文本信息,是显示文字内容的常用组件。 +import android.widget.TextView; + +// 导入项目中自动生成的资源类R,通过它可以访问项目中的各种资源(如布局文件、图片资源、字符串资源等),这里主要用于获取特定的字符串资源和布局文件资源。 +// 同时导入了与数据相关的类Notes以及Notes类中的NoteColumns内部类,用于处理笔记数据相关的操作,比如获取数据列信息等。 +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +// FoldersListAdapter类继承自CursorAdapter抽象类,主要用于将数据库查询到的文件夹相关数据(以Cursor形式表示)适配到列表视图中进行展示, +// 并且提供了获取文件夹名称以及定制列表项视图显示内容等功能,方便在Android应用中展示文件夹列表信息。 +public class FoldersListAdapter extends CursorAdapter { + // 定义一个字符串数组常量PROJECTION,用于指定从数据库查询时需要获取的列信息,这里只包含了NoteColumns.ID(可能是文件夹的唯一标识符列)和NoteColumns.SNIPPET(可能是文件夹相关的简短描述等信息列)。 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义一个常量ID_COLUMN,表示在PROJECTION数组中ID列的索引位置,这里初始化为0,方便后续通过索引快速获取对应列的数据。 + public static final int ID_COLUMN = 0; + // 定义一个常量NAME_COLUMN,表示在PROJECTION数组中用于显示名称的列(这里实际对应NoteColumns.SNIPPET列)的索引位置,初始化为1,用于后续按索引获取文件夹名称相关数据。 + public static final int NAME_COLUMN = 1; + + // FoldersListAdapter类的构造函数,用于创建该适配器的实例,接收一个Context类型的参数context用于获取应用的上下文环境信息, + // 以及一个Cursor类型的参数c,这个Cursor包含了从数据库查询出来的文件夹相关数据,通过调用父类(CursorAdapter)的构造函数将这两个参数传递进去,完成父类的初始化以及基础的适配设置工作。 + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + // 这里的TODO注释通常表示此处还有待完善或者补充的代码逻辑,可能后续需要根据具体业务需求进一步添加初始化相关的操作等内容。 + } + + // 重写CursorAdapter类中的newView方法,该方法用于创建一个新的视图(View)对象,用于在列表中展示每一项数据(在这里就是每个文件夹对应的列表项视图)。 + // 接收三个参数:Context类型的context用于获取应用的上下文环境信息,以便加载相关资源等;Cursor类型的cursor虽然在这里未使用,但在更复杂的场景下可以用于获取数据来初始化视图; + // ViewGroup类型的parent表示该视图的父容器,用于确定视图的布局等相关属性。在这里返回一个新创建的FolderListItem对象,它是自定义的用于展示文件夹信息的列表项视图类。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + // 重写CursorAdapter类中的bindView方法,该方法用于将数据库查询到的数据(通过Cursor表示)绑定到已经创建好的视图(View)对象上,实现数据在视图上的展示。 + // 接收三个参数:View类型的view表示要绑定数据的视图对象,这里实际上就是前面newView方法创建的FolderListItem对象;Context类型的context用于获取应用的上下文环境信息; + // Cursor类型的cursor包含了要绑定的数据记录。在方法内部,首先判断传入的视图是否是FolderListItem类型,如果是,则进行如下操作: + // 获取文件夹的名称,通过判断当前记录中ID列的值是否等于Notes.ID_ROOT_FOLDER(可能是表示根文件夹的特定标识符)来决定显示的名称, + // 如果是根文件夹,则获取R.string.menu_move_parent_folder对应的字符串资源作为名称(可能是显示“上级文件夹”之类的特定文本),否则获取NAME_COLUMN列对应的字符串作为文件夹名称。 + // 最后调用((FolderListItem) view).bind(folderName)方法,将获取到的文件夹名称传递给FolderListItem对象的bind方法,用于在对应的视图上(具体是TextView组件)显示文件夹名称。 + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); + } + } + + // 定义一个方法用于获取指定位置的文件夹名称,接收一个Context类型的参数context用于获取应用的上下文环境信息,以及一个int类型的参数position表示在列表中的位置索引。 + // 首先通过调用getItem方法(继承自CursorAdapter类)获取指定位置对应的Cursor对象(包含了该位置对应的文件夹数据记录),然后按照与bindView方法中类似的逻辑, + // 判断该记录中ID列的值是否等于Notes.ID_ROOT_FOLDER,来决定返回的文件夹名称是特定的根文件夹名称字符串(从资源中获取)还是从NAME_COLUMN列获取的普通文件夹名称字符串,最终返回获取到的文件夹名称。 + 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,它继承自LinearLayout,用于表示文件夹列表中的每一个列表项视图,内部包含了用于显示文件夹名称的TextView组件, + // 并且提供了一个bind方法用于将文件夹名称数据绑定到TextView组件上进行显示,实现了列表项视图的数据展示逻辑定制。 + private class FolderListItem extends LinearLayout { + // 定义一个TextView类型的成员变量mName,用于存储和显示文件夹的名称,是列表项视图中展示文本信息的核心组件。 + private TextView mName; + + // FolderListItem类的构造函数,用于创建该列表项视图的实例,接收一个Context类型的参数context用于获取应用的上下文环境信息,以便加载相关资源等。 + // 首先调用父类(LinearLayout)的构造函数,传入上下文环境context,完成父类的初始化操作,确保该列表项视图能继承LinearLayout的布局属性和功能。 + // 然后通过inflate方法加载名为R.layout.folder_list_item的布局文件,并将其填充到当前的FolderListItem实例中(也就是this所代表的当前对象), + // 这个布局文件应该包含了用于展示文件夹信息的各个子控件(如这里的mName对应的TextView)的布局定义,使得这些控件能够在界面上显示出来。 + // 最后通过findViewById方法在已加载的布局中查找ID为R.id.tv_folder_name的视图控件,并将其转换为TextView类型赋值给mName成员变量,以便后续可以操作这个TextView来显示文件夹名称。 + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + // 定义一个bind方法,用于将传入的文件夹名称数据绑定到TextView组件(mName)上进行显示,接收一个String类型的参数name表示要显示的文件夹名称, + // 通过调用mName的setText方法,将传入的名称字符串设置为TextView的显示内容,从而实现在列表项视图上展示文件夹名称的功能。 + public void bind(String name) { + mName.setText(name); + } + } + +} \ No newline at end of file diff --git a/ui/NoteEditActivity.java b/ui/NoteEditActivity.java new file mode 100644 index 0000000..43c5337 --- /dev/null +++ b/ui/NoteEditActivity.java @@ -0,0 +1,1367 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * 版权声明部分,表明该代码遵循Apache License 2.0协议,同时说明了版权所属的开源社区以及相关许可的获取途径等信息。 + */ + +/* + * 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. + */ + +// 声明该类所属的包名,表明这个类位于net.micode.notes.ui包下,在项目的整体包结构中有其特定的组织和访问层级关系。 +package net.micode.notes.ui; + +// 导入一系列Android系统相关的类,以下是各导入类的简要说明: +// Activity:Android应用中界面的基本组成单元,承载用户界面及相关交互逻辑。 +// AlarmManager:用于设置定时任务、闹钟等,可在特定时间触发指定操作。 +// AlertDialog:创建弹出式对话框,实现提示、确认等交互功能。 +// PendingIntent:在不同组件间传递意图,常用于延迟执行或跨进程操作触发场景。 +// SearchManager:处理应用内搜索功能相关逻辑,如管理搜索框、展示搜索结果等。 +// AppWidgetManager:管理应用的桌面小部件相关操作,例如更新小部件显示内容、布局等。 +// Context:获取应用的上下文环境信息,是很多Android操作的基础,如资源获取、系统服务调用等。 +// DialogInterface:处理对话框相关交互逻辑,比如按钮点击等事件。 +// Intent:用于在Android组件之间传递消息、启动其他组件或执行特定操作,是组件间通信的重要方式。 +// SharedPreferences:存储和获取应用的轻量级配置数据,以键值对形式保存,常用于保存用户偏好设置等信息。 +// Paint:用于设置绘制文本、图形等的样式属性,比如颜色、字体等。 +// Bundle:在Activity之间传递数据,常用于启动Activity或保存恢复Activity状态时。 +// PreferenceManager:方便获取应用的默认SharedPreferences实例,简化相关操作代码编写。 +// Spannable和SpannableString:处理文本样式设置,可对文本部分内容应用不同样式(如设置背景色、字体等),实现富文本效果。 +// TextUtils:提供文本处理相关实用工具方法,如判断文本是否为空等操作。 +// DateUtils:处理日期时间相关的格式化、比较等操作,便于在界面展示日期时间信息或进行时间相关逻辑判断。 +// BackgroundColorSpan:给文本部分内容设置背景颜色,常与Spannable相关类配合使用,实现文本样式设置。 +// Log:用于在应用中输出日志信息,方便调试和查看应用运行状态及错误信息等。 +// LayoutInflater:将XML布局文件实例化为对应的View对象,是动态创建UI界面的重要工具。 +// Menu和MenuItem:用于创建和管理应用的菜单相关功能,如定义菜单项、响应菜单项点击事件等。 +// MotionEvent:处理触摸屏幕相关事件,如触摸按下、滑动、抬起等操作的监听和处理。 +// View:Android中所有可视化组件的基类,构建和操作界面上的各种视图元素,OnClickListener用于监听视图点击事件以执行相应逻辑。 +// WindowManager:管理应用的窗口相关属性,如获取窗口大小、设置窗口显示状态等操作。 +// CheckBox:常见UI组件,提供可勾选的选项功能,用户可选择或取消选择,能监听其勾选状态变化。 +// CompoundButton:CheckBox等具有两种状态切换组件的父类,OnCheckedChangeListener用于监听这类组件的勾选状态变化事件以执行相应逻辑。 +// EditText:供用户输入文本信息的UI组件,常用于填写表单、编辑内容等场景。 +// ImageView:在界面展示图片资源,可加载本地或网络图片,并设置图片显示样式等属性。 +// LinearLayout:按水平或垂直方向排列子视图元素的常见布局方式,构建界面布局结构。 +// TextView:用于在界面展示文本信息的常用组件。 +// Toast:在界面短暂弹出提示信息给用户,常用于显示简单操作反馈、提示消息等。 +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; + +// 导入项目中自动生成的资源类R,通过它访问项目中的各种资源(如布局文件、图片资源、字符串资源等),此处主要用于获取相关布局、字符串等资源。 +// 同时导入与笔记数据相关的类Notes、TextNote,用于处理笔记具体数据结构和操作。 +// 导入WorkingNote类及其中定义的NoteSettingChangedListener接口,用于处理笔记相关设置变更的监听和逻辑处理。 +// 导入一些工具类,如DataUtils用于数据处理通用操作,ResourceParser用于解析资源相关信息(如颜色、字体大小等资源对应的标识)。 +// 还导入与日期时间选择对话框、笔记编辑文本变化监听、桌面小部件相关的类和接口,用于实现相应功能逻辑。 +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; + +// 导入Java标准库中的相关类,HashMap用于存储键值对形式的数据,方便按键查找对应值;HashSet用于存储不重复元素集合,常用于去重等操作; +// Map是映射接口,定义键值对数据结构基本操作方法,HashMap是其常用实现类之一;Pattern和Matcher用于处理正则表达式相关操作,比如匹配文本特定模式内容等。 +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// NoteEditActivity类继承自Activity类,是一个Android的Activity,用于实现笔记编辑相关功能界面和交互逻辑, +// 同时实现了OnClickListener、NoteSettingChangedListener、OnTextViewChangeListener接口,分别用于处理视图点击事件、笔记设置变更事件以及笔记编辑文本变化事件等。 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 定义一个内部私有类HeadViewHolder,用于将笔记编辑界面头部相关视图元素的引用进行封装,方便统一管理和操作这些视图, + // 提高代码可读性和可维护性,使代码结构更清晰,后续对头部视图元素的操作可以通过该类的成员变量来进行。 + private class HeadViewHolder { + // 用于显示笔记修改时间相关信息的TextView组件引用,通过这个变量可以操作该TextView的属性,如设置文本内容、文本样式等。 + public TextView tvModified; + // 用于显示提醒图标(可能表示笔记是否设置了提醒等情况)的ImageView组件引用,可用于设置图片资源、控制图片显示隐藏等操作。 + public ImageView ivAlertIcon; + // 用于显示提醒日期时间信息的TextView组件引用,同样可以对其文本显示相关操作进行设置,比如更新显示的提醒时间等。 + public TextView tvAlertDate; + // 用于设置笔记背景颜色的ImageView组件引用,通常可以设置点击事件监听器,让用户点击它来触发选择背景颜色的操作等。 + public ImageView ibSetBgColor; + } + + // 定义一个静态的HashMap类型的成员变量sBgSelectorBtnsMap,用于建立背景颜色选择按钮的ID与对应的颜色资源标识之间的映射关系。 + // 这样在代码中就可以方便地根据按钮的ID来获取对应的颜色设置值,例如在处理用户点击某个背景颜色选择按钮时,通过该映射快速确定要设置的颜色。 + private static final Map sBgSelectorBtnsMap = new HashMap(); + // 静态代码块,用于初始化sBgSelectorBtnsMap,将各个背景颜色选择按钮的ID(这些ID通常在布局文件中定义,对应界面上的具体按钮)与对应的颜色资源标识(由ResourceParser类定义的相关常量,代表不同的颜色)进行关联映射。 + 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); + } + + // 定义一个静态的HashMap类型的成员变量sBgSelectorSelectionMap,用于建立颜色资源标识与对应的颜色选择按钮选中状态图标ID之间的映射关系。 + // 其目的是方便根据当前选择的颜色来确定应该显示哪个选中状态的图标,以此提示用户当前选中的颜色选项,增强用户界面的交互反馈效果。 + private static final Map sBgSelectorSelectionMap = new HashMap(); + // 静态代码块,用于初始化sBgSelectorSelectionMap,将各个颜色资源标识(由ResourceParser类定义的不同颜色常量)与对应的颜色选择按钮选中状态图标ID(同样是在布局文件中定义的用于显示选中效果的图标对应的ID)进行关联映射。 + 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); + } + // 定义一个静态的HashMap类型的成员变量sFontSizeBtnsMap,用于建立字体大小选择按钮所在布局的ID与对应的字体大小资源标识之间的映射关系。 +// 通过这种映射,后续在代码中可以依据按钮所在布局的ID快速获取对应的字体大小设置值,方便实现根据用户点击不同布局按钮来切换笔记编辑文本字体大小的功能。 +private static final Map sFontSizeBtnsMap = new HashMap(); +// 静态代码块,用于初始化sFontSizeBtnsMap,将各个代表字体大小选择按钮所在布局的ID(如R.id.ll_font_large等,这些ID在对应的布局文件中定义,对应界面上不同的字体大小选择按钮所在的布局区域) +// 与对应的字体大小资源标识(由ResourceParser类定义的相关常量,如ResourceParser.TEXT_LARGE等,表示不同的字体大小规格)进行关联映射。 +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); +} + +// 定义一个静态的HashMap类型的成员变量sFontSelectorSelectionMap,用于建立字体大小资源标识与对应的字体大小选择按钮选中状态图标ID之间的映射关系。 +// 借助这个映射关系,在用户选择了某种字体大小后,可以依据所选字体大小对应的资源标识,快速确定要显示哪个选中状态的图标,从而直观地向用户展示当前所选中的字体大小选项。 +private static final Map sFontSelectorSelectionMap = new HashMap(); +// 静态代码块,用于初始化sFontSelectorSelectionMap,将各个字体大小资源标识(如ResourceParser.TEXT_LARGE等)与对应的字体大小选择按钮选中状态图标ID(如R.id.iv_large_select等,这些ID对应界面上用于显示字体大小选中状态的图标)进行关联映射。 +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); +} + +// 定义一个静态的字符串常量TAG,用于在日志输出(通过Log类进行日志记录)中标识当前类,方便在调试应用以及查看日志信息时,能够快速区分是由哪个类输出的内容, +// 通常会将其设置为当前类的简单且具有辨识度的名称,这里就是"NoteEditActivity"。 +private static final String TAG = "NoteEditActivity"; + +// 定义一个HeadViewHolder类型的成员变量mNoteHeaderHolder,用于存储笔记编辑界面头部视图元素的引用,通过前面定义的HeadViewHolder类来统一管理这些视图组件, +// 后续可以通过这个变量方便地操作头部的各个视图,比如获取或设置它们的显示内容、样式等属性。 +private HeadViewHolder mNoteHeaderHolder; +// 定义一个View类型的成员变量mHeadViewPanel,它可能代表笔记编辑界面头部的整个面板视图,通过这个变量可以对头部区域进行整体的操作,例如控制其显示隐藏、调整布局等。 +private View mHeadViewPanel; +// 定义一个View类型的成员变量mNoteBgColorSelector,它大概率用于表示背景颜色选择相关的视图区域,比如包含了多个用于选择笔记背景颜色的按钮等,方便用户进行背景颜色的选择操作。 +private View mNoteBgColorSelector; +// 定义一个View类型的成员变量mFontSizeSelector,它可能用于表示字体大小选择相关的视图区域,例如包含了不同字体大小选择按钮所在的布局等,供用户选择笔记编辑文本的字体大小。 +private View mFontSizeSelector; +// 定义一个EditText类型的成员变量mNoteEditor,这是用于用户输入和编辑笔记文本内容的核心组件,用户可以在这个文本框中输入文字、进行文本编辑操作, +// 并且可以通过代码对其文本内容、样式等属性进行获取和设置。 +private EditText mNoteEditor; +// 定义一个View类型的成员变量mNoteEditorPanel,它可能用于表示包含笔记编辑文本框(mNoteEditor)以及相关编辑功能按钮等的整个面板视图,通过这个变量可以对编辑区域进行整体的操作, +// 比如调整整个编辑面板的布局、显示隐藏等。 +private View mNoteEditorPanel; +// 定义一个WorkingNote类型的成员变量mWorkingNote,用于存储正在编辑的笔记的相关数据和状态信息,WorkingNote类应该封装了笔记的各种属性,如内容、设置(像背景颜色、字体大小等)以及其他业务相关的属性, +// 通过这个变量可以方便地调用WorkingNote类提供的方法来操作和管理笔记的业务逻辑。 +private WorkingNote mWorkingNote; +// 定义一个SharedPreferences类型的成员变量mSharedPrefs,它用于获取应用的共享偏好设置数据,借助这个变量可以读取和保存用户在应用中设置的一些偏好选项, +// 例如字体大小偏好等信息,方便在应用下次启动或者不同界面间保持用户设置的一致性。 +private SharedPreferences mSharedPrefs; +// 定义一个int类型的成员变量mFontSizeId,用于存储当前选择的字体大小对应的资源标识,后续可以依据这个标识来设置笔记编辑文本的字体大小等相关操作,确保文本显示的字体大小符合用户的选择。 +private int mFontSizeId; + +// 定义一个静态的字符串常量PREFERENCE_FONT_SIZE,用于作为存储字体大小偏好设置的键值,在使用SharedPreferences存储和获取用户设置的字体大小偏好信息时, +// 通过这个键来准确地定位和操作对应的偏好值,保持数据的一致性和可访问性。 +private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + +// 定义一个静态的整数常量SHORTCUT_ICON_TITLE_MAX_LEN,用于限定快捷方式图标标题的最大长度,在创建笔记快捷方式等相关场景下, +// 可以依据这个常量来控制标题显示的长度,避免标题过长导致显示不美观或者不符合界面设计要求等问题。 +private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + +// 定义两个静态的字符串常量TAG_CHECKED和TAG_UNCHECKED,分别用于表示勾选状态(可能是某种标记为已选中的符号)和未勾选状态(可能是某种标记为未选中的符号)的文本表示, +// 这里通过字符的Unicode编码值来创建对应的字符串,具体用途可能是在显示勾选框等相关场景下作为显示的文本内容,方便直观地向用户展示勾选与否的状态。 +public static final String TAG_CHECKED = String.valueOf('\u221A'); +public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); + +// 定义一个LinearLayout类型的成员变量mEditTextList,从变量名推测它可能用于存放多个编辑文本相关的视图元素,不过仅从当前代码不太能明确其具体用途, +// 也许在更复杂的文本编辑场景下,用于展示多个可编辑文本的列表或者相关的布局结构等情况。 +private LinearLayout mEditTextList; +// 定义一个String类型的成员变量mUserQuery,它可能用于存储用户输入的查询内容,比如在笔记内进行搜索操作时,用户输入的搜索关键词等信息会存储在这个变量中, +// 以便后续依据这个查询内容进行相应的搜索逻辑处理。 +private String mUserQuery; +// 定义一个Pattern类型的成员变量mPattern,用于存储正则表达式的编译模式,通常在需要使用正则表达式对文本进行匹配、查找等操作时,会先将正则表达式编译为Pattern对象, +// 然后利用这个对象结合Matcher类来进行具体的文本处理操作,比如查找符合特定模式的文本内容等。 + +// onCreate方法是Android系统在创建Activity时调用的方法,用于进行Activity的初始化操作,如设置界面布局、初始化成员变量等。 +// 首先调用父类(Activity类)的onCreate方法,确保完成系统要求的基础初始化工作,然后通过setContentView方法设置当前Activity的界面布局为R.layout.note_edit所对应的布局文件, +// 该布局文件定义了笔记编辑界面的整体结构和各个视图组件的布局方式。接着判断savedInstanceState是否为null(表示是否是首次创建Activity), +// 如果是首次创建且initActivityState方法(根据传入的Intent初始化Activity状态)返回false,则直接调用finish方法结束当前Activity, +// 否则继续执行后续的initResources方法来进一步初始化相关资源(虽然当前代码中未看到initResources方法的具体实现,但推测是用于初始化一些资源相关的操作,比如加载图片、初始化样式等)。 +@Override +protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + + if (savedInstanceState == null &&!initActivityState(getIntent())) { + finish(); + return; + } + initResources(); +} + +// onRestoreInstanceState方法在Activity由于系统内存不足等原因被销毁后重新创建时被调用,用于恢复Activity之前的状态,比如之前用户输入的内容、选择的设置等信息。 +// 首先调用父类的onRestoreInstanceState方法,确保完成系统要求的基础恢复操作,然后判断savedInstanceState是否不为null且包含了Intent.EXTRA_UID这个键(通常用于标识某个唯一的资源或数据标识), +// 如果满足条件,则创建一个新的Intent对象,设置其动作为Intent.ACTION_VIEW,并将从savedInstanceState中获取的Intent.EXTRA_UID对应的长整型值作为额外数据放入新的Intent中, +// 接着调用initActivityState方法尝试依据这个新的Intent来恢复Activity的状态,如果该方法返回false,则调用finish方法结束当前Activity,否则在日志中输出恢复信息(通过Log.d方法记录调试信息),表示从被销毁的Activity中成功恢复了状态。 +@Override +protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + 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)); + if (!initActivityState(intent)) { + finish(); + return; + } + Log.d(TAG, "Restoring from killed activity"); + } +} + +// initActivityState方法用于依据传入的Intent来初始化Activity的状态,比如根据Intent中的信息加载对应的笔记数据等。 +// 首先将mWorkingNote设置为null,表示初始化前先清除之前可能存在的笔记相关数据,然后判断传入的Intent的动作是否为Intent.ACTION_VIEW(通常用于查看某个具体资源的动作), +// 如果是,则从Intent中获取名为Intent.EXTRA_UID的长整型额外数据(这里应该是用于标识笔记的唯一ID),如果获取不到(即返回值为0)则使用默认值0,同时将mUserQuery设置为空字符串, +// 接下来代码中的注释提到了"Starting from the searched result",推测后续可能会根据这个ID以及是否是从搜索结果进入等情况来进一步加载和初始化笔记相关的数据以及界面状态,不过此处代码未完整展示相关逻辑。 +private boolean initActivityState(Intent intent) { + /** + * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id, + * then jump to the NotesListActivity + */ + mWorkingNote = null; + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + /** + * Starting from the searched result + */ + // 判断传入的Intent是否包含SearchManager.EXTRA_DATA_KEY这个额外数据(通常用于传递搜索相关的关键数据),如果包含,则说明当前Activity可能是从搜索结果进入的。 +if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + // 从Intent中获取SearchManager.EXTRA_DATA_KEY对应的字符串数据,并将其转换为长整型数值,赋值给noteId变量,这里假设该数据是用于标识笔记的唯一ID, + // 通过这种方式获取到从搜索结果中对应的笔记ID信息,以便后续加载相关笔记数据。 + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + // 同时从Intent中获取SearchManager.USER_QUERY对应的字符串数据,赋值给mUserQuery变量,这个数据应该就是用户在搜索框中输入的查询内容, + // 可以用于后续在笔记中进行相关的搜索匹配等操作,或者在界面上展示用户输入的搜索关键词等情况。 + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); +} + +// 调用DataUtils类的visibleInNoteDatabase方法,传入当前应用的内容解析器(getContentResolver方法获取,用于操作应用的内容数据)、noteId(前面获取到的笔记ID)以及Notes.TYPE_NOTE(可能是表示笔记类型的常量), +// 该方法用于判断指定的笔记在笔记数据库中是否可见(可能涉及到权限、数据有效性等多方面的判断逻辑),如果不可见,说明对应的笔记不存在或者不符合显示条件。 +if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + // 创建一个新的Intent,用于启动NotesListActivity(可能是笔记列表展示的Activity),表示要跳转到笔记列表界面,因为当前要查看的笔记不存在或者不可见。 + Intent jump = new Intent(this, NotesListActivity.class); + // 启动刚创建的Intent对应的Activity,即跳转到NotesListActivity,让用户可以在笔记列表中进行其他操作,比如选择查看其他笔记等。 + startActivity(jump); + // 调用showToast方法(虽然当前代码中未看到其具体实现,但推测是用于弹出一个短暂提示信息给用户的方法),传入R.string.error_note_not_exist对应的字符串资源ID, + // 目的是给用户弹出一个提示,告知用户要查看的笔记不存在。 + showToast(R.string.error_note_not_exist); + // 调用finish方法结束当前的NoteEditActivity,因为对应的笔记不存在,没必要继续展示当前编辑界面了。 + finish(); + // 返回false,表示初始化Activity状态失败,方便调用该方法的地方根据返回值进行相应的后续处理,比如结束Activity等操作。 + return false; +} else { + // 如果笔记在数据库中是可见的,也就是存在且符合显示条件,则调用WorkingNote类的load方法,传入当前的Activity上下文(this)以及noteId, + // 尝试从数据库或者其他存储位置加载对应的笔记数据,并将加载后的笔记对象赋值给mWorkingNote变量,以便后续在当前Activity中对该笔记进行编辑等操作。 + mWorkingNote = WorkingNote.load(this, noteId); + // 判断加载后的笔记对象是否为null,如果是null,说明加载笔记数据失败,可能是数据库出现问题或者数据损坏等原因导致。 + if (mWorkingNote == null) { + // 通过Log.e方法输出错误日志信息,TAG是前面定义的用于标识当前类的常量,方便在查看日志时定位到是这个类中出现的加载笔记失败的错误, + // 日志内容中还包含了具体的笔记ID信息,以便更详细地排查问题所在。 + Log.e(TAG, "load note failed with note id" + noteId); + // 调用finish方法结束当前的NoteEditActivity,因为无法正常加载笔记数据,没办法进行后续的编辑操作了。 + finish(); + // 返回false,表示初始化Activity状态失败,方便调用该方法的地方根据返回值进行相应的后续处理,比如结束Activity等操作。 + return false; + } +} + +// 设置当前Activity窗口的软键盘显示模式,通过按位或操作组合了两个软键盘相关的参数: +// WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN表示软键盘初始状态是隐藏的,即进入该Activity时软键盘不会自动弹出; +// WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE表示当软键盘弹出时,Activity的布局会自动调整大小,以适应软键盘显示而不被遮挡,保证界面的可视性和交互性。 +getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); +} else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { + // 如果传入的Intent的动作是Intent.ACTION_INSERT_OR_EDIT(通常表示要插入新笔记或者编辑现有笔记的动作),则进入这个分支,以下是创建或编辑笔记相关的初始化操作。 + + // 从Intent中获取名为Notes.INTENT_EXTRA_FOLDER_ID的长整型额外数据,赋值给folderId变量,这个数据可能是用于指定笔记所属的文件夹ID,如果获取不到则使用默认值0, + // 该文件夹ID可以用于后续将新建的笔记关联到对应的文件夹等操作,方便笔记的分类管理。 + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + // 从Intent中获取名为Notes.INTENT_EXTRA_WIDGET_ID的整型额外数据,赋值给widgetId变量,如果获取不到则使用AppWidgetManager.INVALID_APPWIDGET_ID这个默认的无效小部件ID值, + // 该数据可能与桌面小部件相关,比如新建的笔记是否要关联到某个特定的桌面小部件等情况,这里先获取对应的ID信息。 + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + // 从Intent中获取名为Notes.INTENT_EXTRA_WIDGET_TYPE的整型额外数据,赋值给widgetType变量,如果获取不到则使用Notes.TYPE_WIDGET_INVALIDE这个默认的无效小部件类型值, + // 该数据用于确定小部件的类型,可能会影响笔记在小部件上的显示方式等相关业务逻辑。 + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + // 从Intent中获取名为Notes.INTENT_EXTRA_BACKGROUND_ID的整型额外数据,赋值给bgResId变量,如果获取不到则调用ResourceParser类的getDefaultBgId方法(传入当前上下文this)获取默认的背景资源ID, + // 这个背景资源ID可能用于设置新建笔记的初始背景样式等情况,确保笔记有一个合适的背景显示效果。 + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + + // 从Intent中获取名为Intent.EXTRA_PHONE_NUMBER的字符串数据,赋值给phoneNumber变量,这个数据可能与通话记录相关(从变量名推测),比如可能是要创建一个基于通话记录信息的笔记, + // 这里先获取对应的电话号码信息,后续会根据该信息以及其他相关数据来判断如何创建笔记。 + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + // 从Intent中获取名为Notes.INTENT_EXTRA_CALL_DATE的长整型额外数据,赋值给callDate变量,这个数据可能是通话的日期时间信息(同样从变量名推测与通话记录相关), + // 结合前面获取的电话号码信息,可以更全面地判断是否要基于通话记录创建笔记以及如何创建笔记等情况。 + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate!= 0 && phoneNumber!= null) { + // 判断获取到的电话号码是否为空字符串,如果是空字符串,说明电话号码数据可能存在问题,通过Log.w方法输出一个警告日志信息, + // TAG是用于标识当前类的常量,方便在查看日志时定位到是这个类中出现的电话号码为空的警告情况,虽然这种情况可能不影响程序继续运行,但提示可能存在数据异常。 + if (TextUtils.isEmpty(phoneNumber)) { + Log.w(TAG, "The call record number is null"); + } + long noteId = 0; + // 调用DataUtils类的getNoteIdByPhoneNumberAndCallDate方法,传入当前应用的内容解析器(getContentResolver方法获取)、phoneNumber(前面获取的电话号码)以及callDate(通话日期时间), + // 该方法可能用于根据电话号码和通话日期时间去数据库中查找对应的笔记ID,如果查找到的笔记ID大于0,说明存在对应的笔记记录。 + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), + phoneNumber, callDate)) > 0) { + // 如果查找到对应的笔记ID,则调用WorkingNote类的load方法,传入当前的Activity上下文(this)以及noteId,尝试加载对应的笔记数据,并将加载后的笔记对象赋值给mWorkingNote变量, + // 以便后续对该笔记进行编辑等操作,这里相当于找到了已有的基于通话记录的笔记,直接加载进行编辑。 + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + // 如果加载笔记数据失败(返回的笔记对象为null),通过Log.e方法输出错误日志信息,包含具体的笔记ID信息,方便排查加载失败的原因, + // 然后调用finish方法结束当前的NoteEditActivity,因为无法正常加载笔记数据,没办法进行后续的编辑操作了。 + Log.e(TAG, "load call note failed with note id" + noteId); + finish(); + return false; + } + } else { + // 如果根据电话号码和通话日期时间没有查找到对应的笔记ID(即noteId小于等于0),则调用WorkingNote类的createEmptyNote方法,传入当前的Activity上下文(this)、folderId(文件夹ID)、 + // widgetId(小部件ID)、widgetType(小部件类型)以及bgResId(背景资源ID),创建一个新的空笔记对象,并将其赋值给mWorkingNote变量,后续可以对这个新笔记进行编辑等操作。 + // 接着调用新创建笔记对象(mWorkingNote)的convertToCallNote方法,传入phoneNumber(电话号码)和callDate(通话日期时间), + // 目的是将这个新创建的空笔记转换为基于通话记录信息的笔记,可能会设置笔记的相关内容、格式等符合通话记录笔记的要求。 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, + widgetType, bgResId); + mWorkingNote.convertToCallNote(phoneNumber, callDate); + } + } else { + // 如果通话日期时间为0或者电话号码为null(即不满足基于通话记录创建笔记的条件),则调用WorkingNote类的createEmptyNote方法,传入当前的Activity上下文(this)、folderId(文件夹ID)、 + // widgetId(小部件ID)、widgetType(小部件类型)以及bgResId(背景资源ID),创建一个新的空笔记对象,并将其赋值给mWorkingNote变量,后续可以对这个新笔记进行编辑等操作, + // 这里就是创建一个普通的新笔记,没有基于通话记录信息进行特殊设置。 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType, + bgResId); + } + + // 设置当前Activity窗口的软键盘显示模式,通过按位或操作组合了两个软键盘相关的参数: + // WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE表示当软键盘弹出时,Activity的布局会自动调整大小,以适应软键盘显示而不被遮挡; + // WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE表示软键盘初始状态是可见的,即进入该Activity时软键盘会自动弹出,方便用户直接输入内容,符合新建笔记时通常需要输入内容的场景。 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); +} else { + // 如果传入的Intent的动作既不是Intent.ACTION_VIEW也不是Intent.ACTION_INSERT_OR_EDIT,说明传入的Intent不符合预期的动作要求, + // 通过Log.e方法输出错误日志信息,提示不应该支持这种未指定正确动作的Intent情况,方便排查问题,然后调用finish方法结束当前的NoteEditActivity, + // 因为无法根据这种不符合要求的Intent进行正确的初始化和后续操作了。 + Log.e(TAG, "Intent not specified action, should not support"); + finish(); + return false; +} + +// 调用mWorkingNote对象(代表正在编辑的笔记)的setOnSettingStatusChangedListener方法,传入当前类(this),因为当前类实现了NoteSettingChangedListener接口, +// 这样就可以监听笔记相关设置(比如背景颜色、字体大小等设置)发生变化的事件,当设置发生改变时,会触发相应的回调方法,方便在界面上进行实时更新等操作,保证笔记的显示和设置保持一致。 +mWorkingNote.setOnSettingStatusChangedListener(this); +// 返回true,表示成功初始化了Activity的状态,方便调用该方法的地方根据返回值进行相应的后续处理,比如继续执行其他初始化完成后的操作等。 +return true; +} + +// onResume方法是在Activity从暂停状态恢复到前台可见时被调用的方法,通常用于在Activity重新可见时进行一些数据更新、界面刷新等操作,确保界面显示的内容是最新的、符合当前状态的。 +// 这里首先调用父类的onResume方法,确保完成系统要求的基础恢复操作,然后调用initNoteScreen方法(虽然当前代码中未看到其具体实现,但推测是用于初始化笔记编辑界面的相关显示内容、布局等情况), +// 使得笔记编辑界面在恢复可见时能正确展示笔记的相关信息以及各种编辑功能组件等内容,提供良好的用户编辑体验。 +@Override +protected void onResume() { + super.onResume(); + initNoteScreen(); +} + + // initNoteScreen方法用于初始化笔记编辑界面的显示内容和相关布局等,确保界面正确展示笔记的各种信息以及编辑功能组件的状态。 +private void initNoteScreen() { + // 设置笔记编辑文本框(mNoteEditor)的文本外观样式,通过调用setTextAppearance方法,传入当前的Activity上下文(this)以及通过TextAppearanceResources.getTexAppearanceResource(mFontSizeId)获取的文本外观资源ID, + // 这里的mFontSizeId应该是之前确定的字体大小对应的资源标识,以此来设置笔记编辑文本显示的字体、颜色、大小等外观样式,使其符合用户选择或者默认的设置要求。 + mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + // 判断正在编辑的笔记(mWorkingNote)的复选列表模式(getCheckListMode方法获取)是否等于TextNote.MODE_CHECK_LIST(可能是表示复选列表模式的常量),如果是,则说明笔记处于复选列表模式。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 如果是复选列表模式,则调用switchToListMode方法,传入笔记的内容(mWorkingNote.getContent方法获取),该方法应该是用于将笔记内容按照复选列表的形式进行展示等相关处理, + // 不过从当前代码看不到switchToListMode方法的具体实现细节,推测是会将文本内容解析并转换为带有复选框等适合列表展示的形式在界面上显示出来。 + switchToListMode(mWorkingNote.getContent()); + } else { + // 如果笔记不是复选列表模式,则调用getHighlightQueryResult方法,传入笔记的内容(mWorkingNote.getContent方法获取)以及用户查询内容(mUserQuery), + // 该方法可能是用于在笔记内容中根据用户查询内容进行高亮显示相关的文本匹配处理,然后将处理后的结果设置为笔记编辑文本框(mNoteEditor)的显示文本内容,方便用户查看查询结果在笔记中的位置等情况。 + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + // 设置笔记编辑文本框(mNoteEditor)的光标位置为文本的末尾,通过调用setSelection方法并传入文本的长度(mNoteEditor.getText().length()获取文本长度), + // 这样当界面显示笔记内容后,光标会定位在文本末尾,符合一般的编辑场景,方便用户直接在末尾继续输入内容等操作。 + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + + // 遍历sBgSelectorSelectionMap集合中的所有键(这些键应该是代表不同的颜色资源标识),通过findViewById方法根据sBgSelectorSelectionMap中每个键对应的ID(即颜色选择按钮选中状态图标ID)找到对应的视图, + // 然后将这些视图的可见性设置为View.GONE(即隐藏这些视图),目的可能是初始化时先隐藏所有的颜色选择按钮选中状态图标,后续根据实际选择的颜色再显示对应的选中图标来提示用户当前选择的颜色。 + for (Integer id : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + } + + // 设置笔记编辑界面头部视图面板(mHeadViewPanel)的背景资源,通过调用setBackgroundResource方法,传入正在编辑的笔记(mWorkingNote)的标题背景资源ID(getTitleBgResId方法获取), + // 以此来设置头部面板的背景样式,使其与笔记的相关设置匹配,比如可能是设置一个特定颜色或者图片背景等效果。 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + // 设置笔记编辑文本框所在的面板(mNoteEditorPanel)的背景资源,通过调用setBackgroundResource方法,传入正在编辑的笔记(mWorkingNote)的背景颜色资源ID(getBgColorResId方法获取), + // 这样可以设置笔记编辑区域的背景颜色等样式,使整个编辑界面的背景显示符合笔记的设置情况。 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + + // 设置笔记编辑界面头部中用于显示笔记修改时间的TextView(mNoteHeaderHolder.tvModified)的文本内容,通过调用DateUtils.formatDateTime方法,传入当前的Activity上下文(this)、 + // 正在编辑的笔记(mWorkingNote)的修改日期(getModifiedDate方法获取)以及一些日期时间格式化的标志位(如显示日期、显示数字格式的日期、显示时间、显示年份等标志通过按位或操作组合), + // 将笔记的修改日期按照指定的格式进行格式化后设置为TextView的显示文本,方便用户查看笔记的修改时间信息。 + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + /** + * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker + * is not ready + */ + // 调用showAlertHeader方法,该方法用于根据笔记是否设置了提醒等情况来显示或隐藏相关的提醒头部信息(如提醒图标、提醒时间文本等),虽然这里有个TODO注释提到要添加设置提醒的菜单, + // 但目前因为日期时间选择器(DateTimePicker)还没准备好所以暂时未实现完整的提醒设置功能,只是进行了基本的提醒信息显示处理。 + showAlertHeader(); +} + +// showAlertHeader方法用于根据笔记是否设置了提醒以及提醒时间是否过期等情况,来显示或隐藏笔记编辑界面头部的提醒相关视图组件(如提醒图标、提醒时间文本等),并设置相应的文本内容。 +private void showAlertHeader() { + // 判断正在编辑的笔记(mWorkingNote)是否设置了时钟提醒(hasClockAlert方法判断),如果设置了时钟提醒,则进入以下逻辑。 + if (mWorkingNote.hasClockAlert()) { + // 获取当前系统时间的时间戳(以毫秒为单位),通过System.currentTimeMillis方法获取,用于后续与笔记的提醒时间进行比较,判断提醒是否过期等情况。 + long time = System.currentTimeMillis(); + // 判断当前系统时间是否大于笔记的提醒时间(mWorkingNote.getAlertDate方法获取提醒时间),如果大于,则说明提醒已经过期。 + if (time > mWorkingNote.getAlertDate()) { + // 如果提醒过期,将笔记编辑界面头部用于显示提醒时间的TextView(mNoteHeaderHolder.tvAlertDate)的文本内容设置为R.string.note_alert_expired对应的字符串资源(可能是显示“提醒已过期”之类的提示文本), + // 告知用户该笔记的提醒已经过期了。 + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + } else { + // 如果提醒未过期,则调用DateUtils.getRelativeTimeSpanString方法,传入笔记的提醒时间(mWorkingNote.getAlertDate)、当前系统时间(time)以及DateUtils.MINUTE_IN_MILLIS(可能是用于表示分钟的时间间隔常量,用于格式化相对时间的精度等情况), + // 该方法会根据传入的时间参数计算并返回一个相对时间的字符串表示,比如“10分钟后”之类的格式,然后将这个相对时间字符串设置为笔记编辑界面头部用于显示提醒时间的TextView(mNoteHeaderHolder.tvAlertDate)的文本内容, + // 方便用户直观地了解距离提醒还有多久时间等信息。 + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + } + // 将笔记编辑界面头部用于显示提醒时间的TextView(mNoteHeaderHolder.tvAlertDate)的可见性设置为View.VISIBLE(即显示该视图),以便用户能看到提醒时间相关信息。 + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + // 将笔记编辑界面头部用于显示提醒图标(可能表示笔记是否设置了提醒等情况)的ImageView(mNoteHeaderHolder.ivAlertIcon)的可见性设置为View.VISIBLE(即显示该视图), + // 让用户能看到提醒图标,直观地知道该笔记设置了提醒功能。 + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + // 如果正在编辑的笔记(mWorkingNote)没有设置时钟提醒,则将笔记编辑界面头部用于显示提醒时间的TextView(mNoteHeaderHolder.tvAlertDate)的可见性设置为View.GONE(即隐藏该视图), + // 因为没有提醒所以不需要显示提醒时间相关信息。 + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + // 将笔记编辑界面头部用于显示提醒图标(可能表示笔记是否设置了提醒等情况)的ImageView(mNoteHeaderHolder.ivAlertIcon)的可见性设置为View.GONE(即隐藏该视图), + // 同样因为没有设置提醒,不需要显示提醒图标。 + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + }; +} + +// onNewIntent方法在Activity已经存在于任务栈中,又接收到一个新的Intent启动该Activity时被调用,用于根据新的Intent来更新Activity的状态等相关操作。 +// 首先调用父类的onNewIntent方法,确保完成系统要求的基础操作,然后调用initActivityState方法,传入新接收到的Intent,尝试依据这个新Intent来重新初始化Activity的状态, +// 比如根据新Intent中的信息加载不同的笔记数据、更新界面显示等操作,保证Activity的显示和操作逻辑与新的Intent要求相符。 +@Override +protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); +} + +// onSaveInstanceState方法在Activity即将被系统销毁(例如由于系统内存不足、屏幕旋转等情况)时被调用,用于保存Activity的当前状态数据,以便在Activity重新创建时能够恢复到之前的状态。 +@Override +protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + /** + * For new note without note id, we should firstly save it to + * generate a id. If the editing note is not worth saving, there + * is no id which is equivalent to create new note + */ + // 判断正在编辑的笔记(mWorkingNote)是否已经存在于数据库中(existInDatabase方法判断),如果不存在(比如是新建的还未保存过的笔记),则调用saveNote方法(虽然当前代码中未看到其具体实现,但推测是用于保存笔记数据到数据库等操作), + // 目的是先保存笔记以生成一个笔记ID,因为如果笔记不存在数据库中,重新创建Activity时就没办法恢复到当前的编辑状态了,所以需要先保存获取ID。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + // 将正在编辑的笔记(mWorkingNote)的笔记ID(getNoteId方法获取)放入outState这个Bundle对象中,以Intent.EXTRA_UID作为键,方便在Activity重新创建时通过这个键从保存的状态数据中获取笔记ID, + // 进而恢复笔记的相关数据和编辑状态等信息,同时通过Log.d方法输出调试日志信息,记录保存的笔记ID等情况,方便查看和排查问题。 + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); +} + +// dispatchTouchEvent方法用于处理Activity内的触摸事件分发逻辑,它会在触摸事件发生时被调用,决定是否将触摸事件继续传递给子视图或者进行相应的处理操作。 +@Override +public boolean dispatchTouchEvent(MotionEvent ev) { + // 判断笔记背景颜色选择相关的视图(mNoteBgColorSelector)的可见性是否为View.VISIBLE(即是否显示),并且判断触摸事件(ev)是否不在该视图的范围内(通过inRangeOfView方法判断,虽然当前代码中未看到其具体实现,但推测是用于判断触摸点是否在视图范围内的方法), + // 如果背景颜色选择视图是显示状态且触摸事件不在其范围内,则将该视图的可见性设置为View.GONE(即隐藏该视图),目的可能是当用户点击了背景颜色选择视图区域外时,自动隐藏该选择视图,提升用户界面的交互友好性, + // 然后返回true,表示已经处理了该触摸事件,不需要再继续传递给其他子视图或者系统默认的处理逻辑了。 + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + &&!inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } + + // 与上面处理背景颜色选择视图类似的逻辑,判断字体大小选择相关的视图(mFontSizeSelector)的可见性是否为View.VISIBLE(即是否显示),并且判断触摸事件(ev)是否不在该视图的范围内(通过inRangeOfView方法判断), + // 如果字体大小选择视图是显示状态且触摸事件不在其范围内,则将该视图的可见性设置为View.GONE(即隐藏该视图),同样是为了在用户点击字体大小选择视图区域外时自动隐藏该选择视图, + // 然后返回true,表示已经处理了该触摸事件,不需要再继续传递给其他子视图或者系统默认的处理逻辑了。 + if (mFontSizeSelector.getVisibility() == View.VISIBLE + &&!inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + // 如果上述条件都不满足,即触摸事件不需要在这两个视图相关的逻辑中处理,则调用父类的dispatchTouchEvent方法,将触摸事件继续传递给系统默认的触摸事件处理逻辑或者子视图进行相应的处理, + // 并返回父类方法的处理结果,保证触摸事件能按照正常的Android系统触摸事件分发机制进行后续处理。 + return super.dispatchTouchEvent(ev); +} + + // `inRangeOfView`方法用于判断给定的触摸事件(`MotionEvent`)发生的位置是否处于指定的视图(`View`)范围内,返回一个布尔值来表示判断结果。 +private boolean inRangeOfView(View view, MotionEvent ev) { + // 创建一个长度为2的整数数组`location`,用于存储视图在屏幕上的坐标位置信息。 + // 数组中第一个元素对应视图左上角的x坐标,第二个元素对应视图左上角的y坐标。 + int []location = new int[2]; + // 通过调用视图(`view`)的`getLocationOnScreen`方法,获取该视图在屏幕坐标系中的坐标位置,并将其存储到`location`数组中。 + // 这样就能得到视图左上角顶点相对于屏幕左上角的坐标值,方便后续与触摸事件的坐标进行比较。 + view.getLocationOnScreen(location); + // 从`location`数组中取出第一个元素,即视图左上角的x坐标值,赋值给变量`x`,用于后续与触摸事件的x坐标进行对比判断。 + int x = location[0]; + // 从`location`数组中取出第二个元素,即视图左上角的y坐标值,赋值给变量`y`,用于后续与触摸事件的y坐标进行对比判断。 + int y = location[1]; + // 以下是判断触摸事件的坐标是否在视图范围内的逻辑: + // 首先判断触摸事件的x坐标(通过`ev.getX()`获取)是否小于视图的x坐标(`x`),如果小于,说明触摸点在视图的左侧外部,不在视图范围内,返回`false`。 + // 接着判断触摸事件的x坐标是否大于视图的x坐标加上视图的宽度(`x + view.getWidth()`),如果大于,意味着触摸点在视图的右侧外部,同样不在视图范围内,返回`false`。 + // 然后判断触摸事件的y坐标(通过`ev.getY()`获取)是否小于视图的y坐标(`y`),若小于,表示触摸点在视图的上方外部,不在视图范围内,返回`false`。 + // 最后判断触摸事件的y坐标是否大于视图的y坐标加上视图的高度(`y + view.getHeight()`),要是大于,说明触摸点在视图的下方外部,不在视图范围内,返回`false`。 + if (ev.getX() < x + || ev.getX() > (x + view.getWidth()) + || ev.getY() < y + || ev.getY() > (y + view.getHeight())) { + return false; + } + // 如果上述所有判断条件都不满足,也就是触摸事件的坐标在视图的坐标范围之内,那么返回`true`,表示触摸事件发生在该视图的范围内。 + return true; +} +// `initResources`方法主要用于初始化与笔记编辑界面相关的各种资源,例如查找并赋值界面中的各个视图组件、设置点击事件监听器、获取并处理用户偏好设置等,为后续的界面交互操作做准备。 +private void initResources() { + // 通过`findViewById`方法,依据资源ID(`R.id.note_title`)在当前布局中查找对应的视图,并将其赋值给`mHeadViewPanel`变量。 + // 这个视图通常代表笔记编辑界面头部的整体面板,后续可以对其进行如设置背景、调整布局等各种操作,以定制头部面板的显示效果。 + mHeadViewPanel = findViewById(R.id.note_title); + // 创建一个`HeadViewHolder`类型的实例对象`mNoteHeaderHolder`,用于集中管理笔记编辑界面头部相关的各个视图元素的引用。 + // 这样可以更方便地对这些头部视图进行统一操作,提高代码的可读性和可维护性,避免在代码中分散地操作各个头部视图组件。 + mNoteHeaderHolder = new HeadViewHolder(); + // 通过`findViewById`方法,依据资源ID(`R.id.tv_modified_date`)查找对应的视图,将其转换为`TextView`类型后赋值给`mNoteHeaderHolder.tvModified`变量。 + // 该`TextView`组件用于在界面上显示笔记的修改日期信息,后续可以通过这个变量来设置文本内容、调整文本样式等,以向用户展示正确的笔记修改时间。 + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + // 同样使用`findViewById`方法,按照资源ID(`R.id.iv_alert_icon`)查找视图,转换为`ImageView`类型后赋值给`mNoteHeaderHolder.ivAlertIcon`变量。 + // 这个`ImageView`组件可能用于显示与笔记提醒相关的图标,比如用来提示用户该笔记是否设置了提醒、提醒是否已过期等情况,可通过代码对其图片资源、可见性等属性进行操作。 + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + // 借助`findViewById`方法,根据资源ID(`R.id.tv_alert_date`)找到对应的视图,转换为`TextView`类型后赋值给`mNoteHeaderHolder.tvAlertDate`变量。 + // 此`TextView`用于在界面上展示笔记的提醒日期和时间信息,方便用户直观地知晓笔记的提醒时间安排,可对其文本内容进行更新等操作以反映最新的提醒时间情况。 + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + // 通过`findViewById`方法,依据资源ID(`R.id.btn_set_bg_color`)查找对应的视图,转换为`ImageView`类型后赋值给`mNoteHeaderHolder.ibSetBgColor`变量。 + // 该`ImageView`很可能是用于触发设置笔记背景颜色的操作按钮,例如用户点击它后会弹出可供选择背景颜色的界面等,同时为其设置点击事件监听器(`this`表示当前类实现了`OnClickListener`接口,会在点击时执行对应的点击逻辑)。 + mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + // 使用`findViewById`方法,按照资源ID(`R.id.note_edit_view`)查找对应的视图,转换为`EditText`类型后赋值给`mNoteEditor`变量。 + // 这个`EditText`组件是笔记编辑界面的核心部分,是供用户输入和编辑笔记文本内容的文本框,后续可以通过代码对其文本内容进行获取、修改、设置样式等各种操作,以实现笔记的编辑功能。 + mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + // 通过`findViewById`方法,依据资源ID(`R.id.sv_note_edit`)查找对应的视图,并将其赋值给`mNoteEditorPanel`变量。 + // 这个视图大概是包含笔记编辑文本框(`mNoteEditor`)以及其他相关编辑功能按钮等元素的整个面板视图,通过操作这个变量,可以对编辑区域进行整体的布局调整、显示隐藏等操作,方便管理编辑界面的整体显示效果。 + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + // 通过`findViewById`方法,根据资源ID(`R.id.note_bg_color_selector`)查找对应的视图,并将其赋值给`mNoteBgColorSelector`变量。 + // 该视图通常用于展示背景颜色选择相关的操作界面,比如包含多个用于选择不同背景颜色的按钮等,用户可以通过操作这个区域来选择自己想要的笔记背景颜色。 + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + // 遍历`sBgSelectorBtnsMap`集合中的所有键(这些键对应的是各个背景颜色选择按钮的资源ID),针对每个键(即每个背景颜色选择按钮的资源ID)执行以下操作: + for (int id : sBgSelectorBtnsMap.keySet()) { + // 通过`findViewById`方法,依据当前的资源ID(`id`)查找对应的视图,并将其转换为`ImageView`类型后赋值给`iv`变量。 + // 这里找到的`ImageView`就是具体的背景颜色选择按钮视图,每个按钮对应一种可选择的背景颜色。 + ImageView iv = (ImageView) findViewById(id); + // 为查找到的每个背景颜色选择按钮(`ImageView`)设置点击事件监听器(`this`,因为当前类实现了`OnClickListener`接口), + // 这样当用户点击任意一个背景颜色选择按钮时,就会触发对应的点击事件处理逻辑,实现选择背景颜色的功能。 + iv.setOnClickListener(this); + } + + // 通过`findViewById`方法,依据资源ID(`R.id.font_size_selector`)查找对应的视图,并将其赋值给`mFontSizeSelector`变量。 + // 这个视图可能用于展示字体大小选择相关的操作界面,例如包含了不同字体大小选择按钮所在的布局区域等,方便用户为笔记编辑文本选择合适的字体大小。 + mFontSizeSelector = findViewById(R.id.font_size_selector); + // 遍历`sFontSizeBtnsMap`集合中的所有键(这些键对应的是字体大小选择按钮所在布局的资源ID),针对每个键(即每个字体大小选择按钮所在布局的资源ID)执行以下操作: + for (int id : sFontSizeBtnsMap.keySet()) { + // 通过`findViewById`方法,依据当前的资源ID(`id`)查找对应的视图,并将其赋值给`view`变量。 + // 这里找到的视图就是具体的字体大小选择按钮所在的布局视图或者相关的操作区域视图,用户点击这些区域就能进行字体大小的选择操作。 + View view = findViewById(id); + // 为查找到的字体大小选择按钮所在的布局视图(`View`)设置点击事件监听器(`this`,因为当前类实现了`OnClickListener`接口), + // 以便在用户点击相应区域时执行对应的选择字体大小的操作逻辑,实现改变笔记编辑文本字体大小的功能。 + view.setOnClickListener(this); + }; + // 通过`PreferenceManager`的`getDefaultSharedPreferences`方法获取应用的默认`SharedPreferences`实例,用于读取和保存用户在应用中设置的偏好信息,将其赋值给`mSharedPrefs`变量。 + // 借助这个变量,后续可以方便地操作如字体大小偏好等各种具体的用户偏好设置数据,实现个性化的应用功能。 + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + // 从`SharedPreferences`(`mSharedPrefs`)中获取名为`PREFERENCE_FONT_SIZE`(之前定义的用于存储字体大小偏好设置的键)对应的整数值, + // 如果不存在该键对应的值,则使用`ResourceParser.BG_DEFAULT_FONT_SIZE`作为默认值,将获取到的值赋值给`mFontSizeId`变量。 + // 这个`mFontSizeId`变量用于记录当前选择的字体大小对应的资源标识,后续会依据这个标识来设置笔记编辑文本的字体大小等相关操作,确保文本显示符合用户的偏好设置。 + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + // 判断`mFontSizeId`的值是否大于等于`TextAppearanceResources.getResourcesSize()`(推测是获取所有文本外观资源数量的方法,这里用于判断字体大小资源ID是否超出有效范围), + // 如果超出了有效范围,说明可能出现了异常情况或者数据错误,为了避免程序出现问题,将`mFontSizeId`重新设置为`ResourceParser.BG_DEFAULT_FONT_SIZE`, + // 这是一种简单的容错处理机制,以确保后续使用字体大小资源标识时不会因为非法值而导致错误,从注释中的“HACKME”可以看出这可能是一种临时的修复手段,后续可能需要更完善的处理方式。 + if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + // 通过`findViewById`方法,依据资源ID(`R.id.note_edit_list`)查找对应的视图,并将其转换为`LinearLayout`类型后赋值给`mEditTextList`变量。 + // 从变量名推测,这个`LinearLayout`可能用于存放多个与编辑文本相关的视图元素,不过具体用途还需要结合更多的代码逻辑来进一步确定,可能用于展示一些辅助编辑的文本内容或者相关操作按钮等。 + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); +} + + // `onPause`方法是Android系统提供的生命周期方法,当Activity即将失去焦点、进入暂停状态时会被调用。 +// 通常在这个方法中进行一些数据保存、资源释放或者界面状态清理等操作,以确保应用在Activity暂时不可见时的数据一致性和资源合理利用。 +@Override +protected void onPause() { + // 首先调用父类(`Activity`类)的`onPause`方法,确保执行系统默认的暂停相关操作,比如暂停一些动画、释放部分系统资源等,这是Android开发规范要求的,保证整个Activity生命周期流程的完整性。 + super.onPause(); + // 调用`saveNote`方法来保存正在编辑的笔记数据,`saveNote`方法的具体实现这里看不到,但推测它会将笔记的相关内容(如文本内容、各种设置等)保存到合适的存储位置(比如数据库等)。 + // 如果`saveNote`方法返回`true`,表示笔记数据保存成功,此时通过`Log.d`方法输出一条调试日志信息,用于记录笔记数据已成功保存以及笔记内容的长度情况。 + // 其中`TAG`是之前定义的用于标识当前类的常量,方便在查看日志时定位到是这个类中的操作记录,日志内容里包含了笔记内容的长度(通过`mWorkingNote.getContent().length()`获取),有助于后续排查数据保存相关的问题或者了解笔记内容的大致情况。 + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + // 调用`clearSettingState`方法,该方法用于清理一些界面设置相关的状态,比如隐藏某些临时显示的视图(像背景颜色选择、字体大小选择相关的视图等),确保界面在Activity进入暂停状态时处于一个合理、整洁的状态,避免出现显示异常等问题,具体的清理逻辑在`clearSettingState`方法中实现。 + clearSettingState(); +} +// `updateWidget`方法的主要作用是更新与当前正在编辑的笔记相关联的桌面小部件的显示内容,通过发送广播的方式通知对应的小部件进行更新操作。 +private void updateWidget() { + // 创建一个新的`Intent`对象,设置其动作为`AppWidgetManager.ACTION_APPWIDGET_UPDATE`,这个特定的动作是Android系统用于触发桌面小部件更新的标准动作, + // 发送这样的`Intent`广播后,系统就能识别并通知相关的小部件提供类(实现了小部件更新逻辑的类)来执行更新操作,以刷新小部件显示的内容(比如更新小部件上展示的笔记相关信息等)。 + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 判断正在编辑的笔记(`mWorkingNote`)的小部件类型(通过`getWidgetType`方法获取)是否等于`Notes.TYPE_WIDGET_2X`(可能是表示某种特定2倍尺寸小部件的常量),如果是,则进入以下逻辑,指定要更新的小部件对应的提供类。 + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + // 通过`intent`的`setClass`方法,将`Intent`的目标类设置为`NoteWidgetProvider_2x.class`,这样当广播发送出去后,系统会根据这个设置将更新意图传递给对应的2倍尺寸小部件的提供类, + // 由`NoteWidgetProvider_2x`类来处理具体的小部件更新操作,比如重新加载笔记数据并更新小部件上显示的文本、样式等信息,使其与当前笔记的最新状态保持一致。 + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + // 类似地,如果笔记的小部件类型是`Notes.TYPE_WIDGET_4X`(可能是表示某种特定4倍尺寸小部件的常量),则将`Intent`的目标类设置为`NoteWidgetProvider_4x.class`, + // 以便广播能准确地将更新意图传递给对应的4倍尺寸小部件的提供类,由该类来负责处理小部件的更新逻辑,确保4倍尺寸小部件展示的内容能反映当前笔记的最新情况。 + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + // 如果笔记的小部件类型既不是`Notes.TYPE_WIDGET_2X`也不是`Notes.TYPE_WIDGET_4X`,说明遇到了不支持的小部件类型,此时通过`Log.e`方法输出一条错误日志信息, + // 记录不支持的小部件类型情况,方便后续排查问题(比如是否是小部件配置错误或者代码逻辑没有涵盖该类型等原因导致),然后直接返回,不再进行后续的广播发送等操作,因为无法确定要更新的目标小部件了。 + Log.e(TAG, "Unspported widget type"); + return; + } + + // 将正在编辑的笔记(`mWorkingNote`)的小部件ID(通过`getWidgetId`方法获取)放入一个新创建的整数数组中,然后通过`putExtra`方法将这个数组作为`AppWidgetManager.EXTRA_APPWIDGET_IDS`这个额外数据添加到`Intent`中。 + // 这样接收广播的小部件提供类就可以根据这个小部件ID来准确地确定具体要更新哪个小部件了,避免对其他无关小部件进行不必要的更新操作,保证更新操作是针对正确的目标小部件进行的。 + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + mWorkingNote.getWidgetId() + }); + + // 通过调用`sendBroadcast`方法发送创建好的`Intent`广播,将小部件更新的意图传递给系统,系统会根据`Intent`中的各种设置(如动作、目标类、小部件ID等信息)找到对应的小部件提供类,并触发其更新逻辑,从而实现小部件显示内容的更新。 + sendBroadcast(intent); + // 通过`setResult`方法设置当前Activity返回的结果码为`RESULT_OK`,并将更新小部件的`Intent`作为返回结果数据,这样在一些启动该Activity并等待结果返回的场景下(比如通过`startActivityForResult`启动), + // 调用方可以根据返回的结果码和数据来判断小部件更新操作是否成功等情况,方便进行后续的流程控制和错误处理。 + setResult(RESULT_OK, intent); +} +// `onClick`方法是实现了`OnClickListener`接口后必须重写的方法,用于处理各个设置了点击事件监听器的视图被点击时的具体操作逻辑,根据被点击视图的不同资源ID来执行相应的功能。 +public void onClick(View v) { + // 获取被点击视图的资源ID,并赋值给变量`id`,通过这个`id`来判断到底是哪个视图被点击了,进而执行不同的操作逻辑分支,实现不同的功能响应。 + int id = v.getId(); + // 判断被点击的视图的资源ID是否等于`R.id.btn_set_bg_color`,也就是判断是否是用于触发设置笔记背景颜色的那个`ImageView`按钮被点击了,如果是,则进入以下相关的背景颜色选择显示逻辑。 + if (id == R.id.btn_set_bg_color) { + // 将笔记背景颜色选择相关的视图(`mNoteBgColorSelector`)的可见性设置为`View.VISIBLE`(即显示该视图),这样用户就能看到背景颜色选择的相关界面了,例如包含的各种颜色选择按钮等,方便用户进行下一步的背景颜色选择操作。 + mNoteBgColorSelector.setVisibility(View.VISIBLE); + // 通过`findViewById`方法,依据`sBgSelectorSelectionMap`中当前笔记背景颜色ID(通过`mWorkingNote.getBgColorId`方法获取)对应的资源ID查找对应的视图, + // 然后将该视图的可见性设置为`View.VISIBLE`(即显示该视图),这个视图可能是用于提示当前选择的背景颜色的选中状态图标,显示出来可以直观地告知用户当前所选择的背景颜色情况。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + } else if (sBgSelectorBtnsMap.containsKey(id)) { + // 判断`sBgSelectorBtnsMap`这个映射表中是否包含被点击视图的资源ID,若包含,则说明是背景颜色选择按钮被点击了(因为`sBgSelectorBtnsMap`建立了背景颜色选择按钮ID与颜色资源标识的映射关系),此时进入以下背景颜色切换逻辑。 + // 首先通过`findViewById`方法,依据`sBgSelectorSelectionMap`中当前笔记背景颜色ID(通过`mWorkingNote.getBgColorId`方法获取)对应的资源ID查找对应的视图, + // 并将该视图的可见性设置为`View.GONE`(即隐藏该视图),目的是先隐藏之前显示的背景颜色选中状态图标,因为即将要切换到新的背景颜色了,需要更新显示状态。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); + // 根据被点击的背景颜色选择按钮的资源ID(通过`sBgSelectorBtnsMap.get(id)`获取对应的颜色资源标识),调用`mWorkingNote`的`setBgColorId`方法来设置正在编辑的笔记的背景颜色ID, + // 这样就完成了背景颜色的切换选择操作,将笔记的背景颜色设置更新为用户新选择的颜色了。 + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + // 将笔记背景颜色选择相关的视图(`mNoteBgColorSelector`)的可见性设置为`View.GONE`(即隐藏该视图),在完成背景颜色选择后隐藏选择界面,保持界面的简洁和交互友好性,避免选择界面一直显示影响用户正常操作其他功能。 + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(id)) { + // 判断`sFontSizeBtnsMap`这个映射表中是否包含被点击视图的资源ID,若包含,则表示是字体大小选择按钮所在的布局或者相关操作区域被点击了(因为`sFontSizeBtnsMap`建立了字体大小选择按钮所在布局ID与字体大小资源标识的映射关系),进入以下字体大小切换相关的逻辑。 + // 首先通过`findViewById`方法,依据`sFontSelectorSelectionMap`中当前字体大小资源标识(`mFontSizeId`)对应的资源ID查找对应的视图, + // 并将该视图的可见性设置为`View.GONE`(即隐藏该视图),目的是先隐藏之前显示的字体大小选中状态图标,因为要进行字体大小的切换操作了,需要更新显示状态。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + // 根据被点击的字体大小选择按钮所在布局的资源ID(通过`sFontSizeBtnsMap.get(id)`获取对应的字体大小资源标识),将获取到的字体大小资源标识赋值给`mFontSizeId`变量, + // 以此来更新当前选择的字体大小对应的资源标识,实现字体大小的切换选择,后续会依据这个新的`mFontSizeId`来设置笔记编辑文本的字体大小。 + mFontSizeId = sFontSizeBtnsMap.get(id); + // 通过`mSharedPrefs`(之前获取的用于存储用户偏好设置的`SharedPreferences`实例)的`edit`方法获取一个编辑器对象,然后使用这个编辑器对象的`putInt`方法, + // 将新的字体大小资源标识(`mFontSizeId`)存储到名为`PREFERENCE_FONT_SIZE`(之前定义的用于存储字体大小偏好设置的键)的偏好设置项中,最后调用`commit`方法提交修改, + // 这样就将用户新选择的字体大小保存到了用户偏好设置中,下次打开应用或者进入相关界面时可以恢复到这个字体大小设置。 + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + // 通过`findViewById`方法,依据`sFontSelectorSelectionMap`中更新后的字体大小资源标识(`mFontSizeId`)对应的资源ID查找对应的视图, + // 并将该视图的可见性设置为`View.VISIBLE`(即显示该视图),这个视图是用于提示当前选择的字体大小的选中状态图标,显示出来可以让用户直观地看到当前所选择的字体大小情况。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + // 判断正在编辑的笔记(`mWorkingNote`)的复选列表模式(通过`getCheckListMode`方法获取)是否等于`TextNote.MODE_CHECK_LIST`(可能是表示笔记处于复选列表模式的常量),如果是,则进入以下逻辑。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 调用`getWorkingText`方法(虽然当前代码中看不到其具体实现,但推测是用于获取笔记编辑文本框中的当前文本内容等相关操作的方法)获取相关文本内容, + // 然后调用`switchToListMode`方法,传入笔记的内容(`mWorkingNote.getContent`方法获取),该方法应该是用于将笔记内容按照复选列表的形式进行重新展示等相关处理, + // 不过从当前代码看不到`switchToListMode`方法的具体实现细节,推测是会将文本内容解析并转换为带有复选框等适合列表展示的形式在界面上显示出来,以适配字体大小变化后的显示效果。 + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + // 如果笔记不是处于复选列表模式,则调用`mNoteEditor`(笔记编辑文本框)的`setTextAppearance`方法,传入当前的Activity上下文(`this`)以及通过`TextAppearanceResources.getTexAppearanceResource(mFontSizeId)`获取的文本外观资源ID, + // 以此来设置笔记编辑文本的字体、颜色、大小等外观样式,使其根据用户新选择的字体大小进行相应的更新显示,确保文本显示符合新的字体大小设置要求。 + mNoteEditor.setTextAppearance(this, + TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + // 将字体大小选择相关的视图(`mFontSizeSelector`)的可见性设置为`View.GONE`(即隐藏该视图),在完成字体大小选择后隐藏选择界面,保持界面的整洁和便于用户操作其他功能。 + mFontSizeSelector.setVisibility(View.GONE); + } +} +// `onBackPressed`方法是Android系统提供的生命周期方法,当用户按下设备的返回键时会被调用,通常用于处理返回操作相关的逻辑,比如保存数据、确认是否退出等。 +@Override +public void onBackPressed() { + // 调用`clearSettingState`方法,该方法用于清理一些界面设置相关的状态,比如隐藏某些临时显示的视图(像背景颜色选择、字体大小选择相关的视图等), + // 如果`clearSettingState`方法返回`true`,表示成功清理了相关状态,此时直接返回,不再执行后续的操作,因为一些临时显示的界面元素已经被清理掉了,可能已经满足了返回的前置条件。 + if(clearSettingState()) { + return; + } + + // 调用`saveNote`方法来保存正在编辑的笔记数据,确保在用户返回前将笔记的最新内容和设置等保存好,避免数据丢失,`saveNote`方法的具体实现这里看不到,但推测它会将笔记相关内容保存到合适的存储位置(比如数据库等)。 + saveNote(); + // 调用父类(`Activity`类)的`onBackPressed`方法,执行系统默认的返回操作逻辑,比如关闭当前Activity、返回上一个界面等,这是Android开发规范要求的,保证整个Activity生命周期流程的完整性以及返回操作的正常执行。 + super.onBackPressed(); +} + + // `clearSettingState`方法用于清理界面上与设置相关的一些临时显示状态,主要是隐藏背景颜色选择和字体大小选择相关的视图,返回一个布尔值表示是否成功进行了状态清理操作。 +private boolean clearSettingState() { + // 首先判断笔记背景颜色选择相关的视图(`mNoteBgColorSelector`)的可见性是否等于`View.VISIBLE`,也就是判断该视图当前是否处于显示状态。 + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + // 如果背景颜色选择相关视图处于显示状态,则将其可见性设置为`View.GONE`,即将该视图隐藏起来,这样在不需要显示背景颜色选择界面时(比如完成背景颜色选择操作后等情况),可以清理界面,使其更加简洁。 + mNoteBgColorSelector.setVisibility(View.GONE); + // 返回`true`表示已经成功进行了一次状态清理操作,即隐藏了背景颜色选择相关的视图。 + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + // 如果背景颜色选择相关视图不是处于显示状态(即上述`if`条件不满足),则继续判断字体大小选择相关的视图(`mFontSizeSelector`)的可见性是否等于`View.VISIBLE`,也就是检查字体大小选择界面是否正在显示。 + // 如果字体大小选择相关视图处于显示状态,则执行以下操作。 + mFontSizeSelector.setVisibility(View.GONE); + // 将字体大小选择相关视图的可见性设置为`View.GONE`,即隐藏该视图,同样是为了在不需要显示字体大小选择界面时(比如完成字体大小选择操作后)清理界面,避免界面元素过多影响用户操作和视觉效果。 + return true; + // 返回`true`表示成功进行了这次针对字体大小选择相关视图的状态清理操作。 + } + // 如果背景颜色选择相关视图和字体大小选择相关视图都不是处于显示状态(即上述两个`if`条件都不满足),说明没有需要清理的相关设置状态了,此时返回`false`,表示没有进行实际的状态清理操作。 + return false; +} +// `onBackgroundColorChanged`方法用于在笔记背景颜色发生改变时,更新界面上相关视图的显示,以反映新的背景颜色设置情况,比如显示对应的选中图标以及更新编辑区域和头部面板的背景资源。 +public void onBackgroundColorChanged() { + // 通过`findViewById`方法,依据`sBgSelectorSelectionMap`中当前笔记背景颜色ID(通过`mWorkingNote.getBgColorId`方法获取)对应的资源ID查找对应的视图, + // 然后将该视图的可见性设置为`View.VISIBLE`(即显示该视图),这个视图可能是用于提示当前选择的背景颜色的选中状态图标,通过显示该图标,可以直观地告知用户当前所选择的背景颜色情况,让用户清楚知晓当前生效的背景颜色设置。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + // 设置笔记编辑文本框所在的面板(`mNoteEditorPanel`)的背景资源,通过调用`setBackgroundResource`方法,传入正在编辑的笔记(`mWorkingNote`)的背景颜色资源ID(通过`getBgColorResId`方法获取), + // 这样就能根据新选择的背景颜色更新编辑区域的背景显示效果,使其与笔记的背景颜色设置保持一致,提供视觉上统一的编辑界面。 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + // 设置笔记编辑界面头部视图面板(`mHeadViewPanel`)的背景资源,通过调用`setBackgroundResource`方法,传入正在编辑的笔记(`mWorkingNote`)的标题背景资源ID(通过`getTitleBgResId`方法获取), + // 以此来更新头部面板的背景样式,使其与笔记的整体背景颜色设置相匹配,确保整个笔记编辑界面的背景风格协调统一。 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); +} +// `onPrepareOptionsMenu`方法是Android系统提供的用于在每次显示选项菜单(Options Menu)前进行准备工作的方法,比如根据当前Activity的状态、数据等来动态调整菜单的显示内容、菜单项的可见性等。 +@Override +public boolean onPrepareOptionsMenu(Menu menu) { + // 首先判断当前Activity是否正在结束(通过`isFinishing`方法判断),如果`isFinishing`方法返回`true`,表示Activity正在结束过程中,此时直接返回`true`, + // 这样系统就不会再进行后续的菜单准备操作了,因为Activity即将关闭,不需要再更新菜单显示了。 + if (isFinishing()) { + return true; + } + // 调用`clearSettingState`方法,清理界面上与设置相关的一些临时显示状态,比如隐藏背景颜色选择和字体大小选择相关的视图等,确保在准备显示菜单时界面处于一个合适、整洁的状态,避免菜单显示被这些临时视图遮挡或者影响视觉效果。 + clearSettingState(); + // 调用`menu`的`clear`方法,清空当前菜单中的所有菜单项,这是为了重新根据当前的业务逻辑和Activity状态来填充合适的菜单项,避免出现旧的、不符合当前情况的菜单项显示在菜单中。 + menu.clear(); + // 判断正在编辑的笔记(`mWorkingNote`)的文件夹ID(通过`getFolderId`方法获取)是否等于`Notes.ID_CALL_RECORD_FOLDER`(可能是表示通话记录文件夹的常量ID),如果相等,则说明当前笔记属于通话记录类型的文件夹,进入以下逻辑。 + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + // 通过`getMenuInflater`方法获取一个菜单填充器对象,然后调用其`inflate`方法,传入`R.menu.call_note_edit`(可能是定义通话记录笔记编辑相关菜单布局的资源ID)和`menu`对象, + // 这样就会根据`R.menu.call_note_edit`所对应的菜单布局文件来填充`menu`对象,将通话记录笔记编辑场景下合适的菜单项添加到菜单中,以提供符合该类型笔记操作需求的菜单功能选项。 + getMenuInflater().inflate(R.menu.call_note_edit, menu); + } else { + // 如果笔记的文件夹ID不等于`Notes.ID_CALL_RECORD_FOLDER`,说明不是通话记录类型的笔记,此时通过`getMenuInflater`方法获取菜单填充器对象,然后调用其`inflate`方法,传入`R.menu.note_edit`(可能是定义普通笔记编辑相关菜单布局的资源ID)和`menu`对象, + // 按照`R.menu.note_edit`对应的菜单布局文件来填充`menu`对象,将普通笔记编辑场景下常用的菜单项添加到菜单中,满足一般笔记编辑操作的功能需求。 + getMenuInflater().inflate(R.menu.note_edit, menu); + } + // 判断正在编辑的笔记(`mWorkingNote`)的复选列表模式(通过`getCheckListMode`方法获取)是否等于`TextNote.MODE_CHECK_LIST`(可能是表示笔记处于复选列表模式的常量),如果是,则进入以下逻辑。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 通过`menu`对象的`findItem`方法,依据资源ID(`R.id.menu_list_mode`)查找对应的菜单项,然后调用该菜单项的`setTitle`方法,传入`R.string.menu_normal_mode`对应的字符串资源(可能是表示“普通模式”之类的文本内容), + // 这样就将该菜单项的标题更新为“普通模式”,以提示用户当前处于复选列表模式下,点击该菜单项可以切换回普通模式,符合根据笔记当前模式动态调整菜单显示文本的需求。 + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); + } else { + // 如果笔记不是处于复选列表模式,则通过`menu`对象的`findItem`方法,依据资源ID(`R.id.menu_list_mode`)查找对应的菜单项,然后调用该菜单项的`setTitle`方法,传入`R.string.menu_list_mode`对应的字符串资源(可能是表示“列表模式”之类的文本内容), + // 将该菜单项的标题更新为“列表模式”,提示用户当前处于普通模式,点击该菜单项可以切换到列表模式,同样是根据笔记当前模式动态调整菜单显示文本,方便用户进行模式切换操作。 + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); + } + // 判断正在编辑的笔记(`mWorkingNote`)是否设置了时钟提醒(通过`hasClockAlert`方法判断),如果设置了时钟提醒,则进入以下逻辑。 + if (mWorkingNote.hasClockAlert()) { + // 通过`menu`对象的`findItem`方法,依据资源ID(`R.id.menu_alert`)查找对应的菜单项,然后调用该菜单项的`setVisible`方法,传入`false`,将该菜单项设置为不可见, + // 因为笔记已经设置了时钟提醒,可能就不需要再显示“设置提醒”之类的菜单项了,通过这种方式根据笔记的提醒设置情况动态调整菜单项的可见性,提供更符合实际情况的菜单显示。 + menu.findItem(R.id.menu_alert).setVisible(false); + } else { + // 如果笔记没有设置时钟提醒,则通过`menu`对象的`findItem`方法,依据资源ID(`R.id.menu_delete_remind`)查找对应的菜单项,然后调用该菜单项的`setVisible`方法,传入`false`,将该菜单项设置为不可见, + // 可能意味着在没有设置提醒的情况下,不需要显示“删除提醒”之类的菜单项,同样是根据笔记的提醒设置情况动态调整菜单项的可见性,使菜单显示更贴合当前笔记的实际状态。 + menu.findItem(R.id.menu_delete_remind).setVisible(false); + } + // 最后返回`true`,表示菜单准备工作已完成,系统可以根据当前`menu`对象的状态来显示菜单了,返回`false`可能会导致菜单不显示或者显示异常等情况,所以正常情况下这里返回`true`。 + return true; +} + + // `onOptionsItemSelected`方法是Android系统提供的回调方法,当用户在选项菜单(Options Menu)中选择某个菜单项时会被调用,用于处理不同菜单项对应的具体操作逻辑。 +@Override +public boolean onOptionsItemSelected(MenuItem item) { + // 通过`switch`语句根据被选中菜单项的资源ID(通过`item.getItemId()`获取)来执行不同的操作分支,以实现各种菜单功能。 + switch (item.getItemId()) { + // 当菜单项的资源ID等于`R.id.menu_new_note`时,表示用户点击了“新建笔记”菜单项,此时调用`createNewNote`方法来执行新建笔记的相关操作,具体的新建逻辑在`createNewNote`方法中实现。 + case R.id.menu_new_note: + createNewNote(); + break; + // 当菜单项的资源ID等于`R.id.menu_delete`时,表示用户点击了“删除”菜单项,以下是处理删除笔记相关的逻辑,通过弹出一个确认对话框来让用户确认是否真的要删除笔记。 + case R.id.menu_delete: + // 创建一个`AlertDialog.Builder`对象,用于构建一个警告对话框,传入当前的Activity上下文(`this`),以便对话框能够正确显示并与当前Activity进行交互。 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 设置对话框的标题,通过调用`getString`方法获取`R.string.alert_title_delete`对应的字符串资源作为标题文本,这个标题通常用于提示用户当前操作是删除相关的操作,让用户明确操作意图。 + builder.setTitle(getString(R.string.alert_title_delete)); + // 设置对话框的图标,使用`android.R.drawable.ic_dialog_alert`这个系统自带的图标资源,该图标一般是一个表示警告的图标,用于在视觉上提示用户此操作具有一定风险性(即删除操作不可逆等情况)。 + builder.setIcon(android.R.drawable.ic_dialog_alert); + // 设置对话框的消息内容,通过调用`getString`方法获取`R.string.alert_message_delete_note`对应的字符串资源作为消息文本,这个消息文本可能是一些提示用户删除笔记后数据将无法恢复等相关内容,进一步提醒用户谨慎操作。 + builder.setMessage(getString(R.string.alert_message_delete_note)); + // 设置对话框的“确定”按钮,传入`android.R.string.ok`作为按钮文本(这是系统自带的表示“确定”“好的”之类意思的字符串资源),同时传入一个`DialogInterface.OnClickListener`匿名内部类对象,用于处理用户点击“确定”按钮时的操作逻辑。 + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 当用户点击“确定”按钮时,调用`deleteCurrentNote`方法来执行实际的删除当前笔记的操作,该方法会处理与数据库交互等相关的删除逻辑,具体实现见`deleteCurrentNote`方法。 + deleteCurrentNote(); + // 调用`finish`方法关闭当前的Activity,因为笔记已经被删除,当前编辑界面可能就不需要再展示了,直接结束当前Activity回到上一个界面(比如笔记列表界面等)。 + finish(); + } + }); + // 设置对话框的“取消”按钮,传入`android.R.string.cancel`作为按钮文本(这是系统自带的表示“取消”的字符串资源),并传入`null`表示不设置点击该按钮的额外监听逻辑,点击“取消”按钮时对话框会直接关闭,不执行其他额外操作。 + builder.setNegativeButton(android.R.string.cancel, null); + // 调用`builder`的`show`方法显示构建好的警告对话框,将对话框呈现给用户,让用户进行确认操作选择。 + builder.show(); + break; + // 当菜单项的资源ID等于`R.id.menu_font_size`时,表示用户点击了“字体大小”菜单项,以下是处理字体大小相关操作的逻辑,用于显示字体大小选择相关的界面元素,方便用户进行字体大小选择。 + case R.id.menu_font_size: + // 将字体大小选择相关的视图(`mFontSizeSelector`)的可见性设置为`View.VISIBLE`(即显示该视图),这样用户就能看到字体大小选择的相关界面了,例如包含不同字体大小选项的按钮等操作区域。 + mFontSizeSelector.setVisibility(View.VISIBLE); + // 通过`findViewById`方法,依据`sFontSelectorSelectionMap`中当前字体大小资源标识(`mFontSizeId`)对应的资源ID查找对应的视图, + // 然后将该视图的可见性设置为`View.VISIBLE`(即显示该视图),这个视图可能是用于提示当前选择的字体大小的选中状态图标,显示出来可以直观地告知用户当前所选择的字体大小情况,方便用户在选择时对比查看。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + break; + // 当菜单项的资源ID等于`R.id.menu_list_mode`时,表示用户点击了“列表模式”菜单项,以下是处理笔记列表模式切换相关的逻辑,通过改变笔记的列表模式状态来实现模式切换。 + case R.id.menu_list_mode: + // 根据当前笔记(`mWorkingNote`)的复选列表模式(通过`getCheckListMode`方法获取)的值来进行切换操作。如果当前模式值为0(通常表示非列表模式,具体根据业务逻辑定义),则将其设置为`TextNote.MODE_CHECK_LIST`(可能是表示列表模式的常量),即切换到列表模式; + // 如果当前模式已经是列表模式(即`getCheckListMode`方法返回非0值),则将其设置为0,切换回非列表模式,以此实现列表模式和非列表模式之间的切换功能。 + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0? + TextNote.MODE_CHECK_LIST : 0); + break; + // 当菜单项的资源ID等于`R.id.menu_share`时,表示用户点击了“分享”菜单项,以下是处理笔记分享相关的逻辑,先获取笔记的内容,然后调用`sendTo`方法将笔记内容分享到其他支持分享功能的应用中。 + case R.id.menu_share: + // 调用`getWorkingText`方法(虽然当前代码中看不到其具体实现,但推测是用于获取笔记编辑文本框中的当前文本内容等相关操作的方法)获取笔记内容,以便后续进行分享操作。 + getWorkingText(); + // 调用`sendTo`方法,传入当前的Activity上下文(`this`)以及获取到的笔记内容(`mWorkingNote.getContent`方法获取),将笔记内容发送出去进行分享,具体的分享逻辑在`sendTo`方法中实现。 + sendTo(this, mWorkingNote.getContent()); + break; + // 当菜单项的资源ID等于`R.id.menu_send_to_desktop`时,表示用户点击了“发送到桌面”菜单项,此时调用`sendToDesktop`方法来执行将笔记相关内容发送到桌面的操作,具体的发送逻辑在`sendToDesktop`方法中实现(不过从当前代码看该方法未给出具体实现)。 + case R.id.menu_send_to_desktop: + sendToDesktop(); + break; + // 当菜单项的资源ID等于`R.id.menu_alert`时,表示用户点击了“提醒”菜单项,此时调用`setReminder`方法来设置笔记的提醒功能,具体的提醒设置逻辑在`setReminder`方法中实现,比如弹出时间选择对话框让用户选择提醒时间等操作。 + case R.id.menu_alert: + setReminder(); + break; + // 当菜单项的资源ID等于`R.id.menu_delete_remind`时,表示用户点击了“删除提醒”菜单项,以下是处理删除笔记提醒相关的逻辑,通过调用`mWorkingNote`的`setAlertDate`方法将提醒日期设置为0(表示无提醒),并传入`false`(可能表示不启用提醒相关的其他设置等情况,具体根据方法定义)来取消笔记的提醒设置。 + case R.id.menu_delete_remind: + mWorkingNote.setAlertDate(0, false); + break; + // 如果菜单项的资源ID不属于上述任何已定义的情况(即默认情况),则直接执行`break`跳出`switch`语句,不做任何额外操作,这是一种常规的处理方式,避免出现未处理的异常情况。 + default: + break; + } + // 无论执行了哪个菜单项对应的操作分支,最后都返回`true`,表示已经成功处理了菜单项的点击事件,这样系统就不会再对该点击事件进行其他默认处理了,符合`onOptionsItemSelected`方法的返回要求。 + return true; +} +// `setReminder`方法用于设置笔记的提醒功能,具体实现是弹出一个日期时间选择对话框,让用户选择提醒的日期和时间,然后将用户选择的时间设置为笔记的提醒时间。 +private void setReminder() { + // 创建一个`DateTimePickerDialog`对象,传入当前的Activity上下文(`this`)以及系统当前时间的时间戳(通过`System.currentTimeMillis()`获取), + // 这个时间戳可能用于初始化日期时间选择对话框的默认显示时间(比如默认显示当前时间,方便用户基于当前时间来选择未来的提醒时间等情况),该对话框用于让用户选择具体的提醒日期和时间。 + DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); + // 为`DateTimePickerDialog`对象设置日期时间设置监听器(`OnDateTimeSetListener`),通过传入一个匿名内部类对象实现该监听器接口,用于处理用户在对话框中选择好日期时间后的操作逻辑。 + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + public void OnDateTimeSet(AlertDialog dialog, long date) { + // 当用户在日期时间选择对话框中选择好日期时间后,会触发该回调方法,通过调用`mWorkingNote`的`setAlertDate`方法,传入用户选择的日期时间(`date`参数)以及`true`(可能表示启用提醒相关的设置等情况,具体根据方法定义), + // 将用户选择的日期时间设置为笔记的提醒时间,完成提醒时间的设置操作,使得笔记在到达设定的提醒时间时可以触发相应的提醒功能(比如弹出提醒通知等,具体的提醒触发逻辑可能在其他地方实现)。 + mWorkingNote.setAlertDate(date, true); + } + }); + // 调用`d`(即`DateTimePickerDialog`对象)的`show`方法,显示日期时间选择对话框,将其呈现给用户,让用户能够进行提醒时间的选择操作。 + d.show(); +} +// `sendTo`方法用于将笔记内容分享到其他支持分享功能的应用中,通过创建一个分享意图(`Intent`),设置意图的动作、类型以及要分享的文本内容等信息,然后启动该意图来实现分享操作。 +// 此方法遵循Android系统中使用`Intent`进行跨应用操作的机制,利用`ACTION_SEND`动作和`text/plain`类型来表示分享纯文本内容的意图。 +/** + * Share note to apps that support {@link Intent#ACTION_SEND} action + * and {@text/plain} type + */ +private void sendTo(Context context, String info) { + // 创建一个新的`Intent`对象,设置其动作为`Intent.ACTION_SEND`,这是Android系统中用于表示分享操作的标准动作,告诉系统接下来要执行分享相关的操作,其他应用可以通过识别这个动作来响应分享请求。 + Intent intent = new Intent(Intent.ACTION_SEND); + // 通过`putExtra`方法,将笔记内容(`info`参数,即`mWorkingNote.getContent`方法获取的笔记文本内容)作为`Intent.EXTRA_TEXT`这个额外数据添加到`Intent`中, + // 这样接收分享意图的应用就能获取到要分享的具体文本内容了,实现将笔记内容传递给其他应用的目的。 + intent.putExtra(Intent.EXTRA_TEXT, info); + // 设置`Intent`的类型为`"text/plain"`,表示要分享的内容是纯文本格式,这有助于系统筛选出能够处理纯文本分享的应用,比如支持接收文本并进行分享的社交应用、笔记应用等,确保分享操作能正确地找到合适的目标应用来接收和处理笔记内容。 + intent.setType("text/plain"); + // 通过传入的`Context`对象(`context`参数,这里是当前的Activity上下文`this`)调用`startActivity`方法,启动创建好的分享意图(`intent`), + // 系统会根据意图中的动作、类型以及额外数据等信息,查找并启动能够处理该分享操作的应用,将笔记内容分享出去,完成整个分享流程。 + context.startActivity(intent); +} +// `createNewNote`方法用于创建一个新的笔记,其操作逻辑包括先保存当前正在编辑的笔记(如果有),然后结束当前的`NoteEditActivity`,再启动一个新的`NoteEditActivity`,并传递一些必要的参数来初始化新笔记的相关设置。 +private void createNewNote() { + // 首先调用`saveNote`方法来保存当前正在编辑的笔记,这是一种数据保护机制,确保在创建新笔记之前,当前编辑的笔记内容和相关设置不会丢失,`saveNote`方法的具体实现这里看不到,但推测它会将笔记相关数据保存到合适的存储位置(比如数据库等)。 + // 这样做可以避免用户在新建笔记过程中意外丢失之前未保存的编辑内容,保证数据的完整性和安全性。 + // Firstly, save current editing notes + saveNote(); + + // 为了确保操作的安全性和流程的合理性,调用`finish`方法结束当前的`NoteEditActivity`,这一步可能是为了清理当前Activity占用的资源、关闭相关的编辑界面等, + // 为启动新的`NoteEditActivity`做好准备,避免出现多个编辑界面混乱或者资源冲突等情况。 + // For safety, start a new NoteEditActivity + finish(); + // 创建一个新的`Intent`对象,设置其目标类为`NoteEditActivity.class`,也就是要启动的新Activity是`NoteEditActivity`,用于进入新的笔记编辑界面,创建新的笔记。 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置`Intent`的动作(Action)为`Intent.ACTION_INSERT_OR_EDIT`,这个动作通常用于表示插入新笔记或者编辑现有笔记的意图,在这里表示要创建一个新笔记的操作,符合创建新笔记的业务逻辑需求。 + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 通过`putExtra`方法,将当前正在编辑的笔记(`mWorkingNote`)的文件夹ID(通过`getFolderId`方法获取)作为`Notes.INTENT_EXTRA_FOLDER_ID`这个额外数据添加到`Intent`中, + // 这样在新启动的`NoteEditActivity`中可以获取到这个文件夹ID信息,可能用于将新创建的笔记关联到对应的文件夹等操作,方便笔记的分类管理,保持文件夹相关逻辑的连贯性。 + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + // 通过调用`startActivity`方法,启动创建好的`Intent`,启动新的`NoteEditActivity`,进入新的笔记编辑界面,开始创建新笔记的流程,系统会根据`Intent`中的各种设置(如动作、额外数据等)来初始化新的`NoteEditActivity`。 + startActivity(intent); +} +// `deleteCurrentNote`方法用于执行删除当前笔记的实际操作,其逻辑涉及与数据库的交互,根据不同的同步模式(`isSyncMode`方法判断,具体逻辑未给出)来决定是直接删除笔记还是将笔记移动到回收站文件夹等操作,同时还会对笔记对象进行标记删除操作。 +private void deleteCurrentNote() { + // 首先判断当前笔记(`mWorkingNote`)是否已经存在于数据库中(通过`existInDatabase`方法判断),只有存在于数据库中的笔记才能进行后续的删除相关操作,避免对不存在的笔记进行无效操作导致异常情况发生。 + if (mWorkingNote.existInDatabase()) { + // 创建一个`HashSet`类型的集合对象`ids`,用于存放要操作的笔记的ID,这里主要是存放当前要删除的笔记的ID,后续根据不同的操作逻辑(如批量删除或移动到回收站等)来处理这些ID对应的笔记。 + HashSet ids = new HashSet(); + // 获取当前笔记(`mWorkingNote`)的笔记ID(通过`getNoteId`方法获取),赋值给变量`id`,用于后续判断和操作。 + long id = mWorkingNote.getNoteId(); + // 判断获取到的笔记ID是否不等于`Notes.ID_ROOT_FOLDER`(可能是表示根文件夹的常量ID,具体根据业务逻辑定义),如果不等于,说明不是根文件夹相关的特殊情况(可能根文件夹不允许直接删除等原因),则将该笔记ID添加到`ids`集合中,准备进行后续的删除或移动操作。 + if (id!= Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + // 如果笔记ID等于`Notes.ID_ROOT_FOLDER`,说明出现了不应该出现的情况(从注释`Wrong note id, should not happen`可知),通过`Log.d`方法输出一条调试日志信息,记录这种异常情况,方便后续排查问题,不过这里并没有进行实际的删除操作,避免错误删除重要数据。 + + // `isSyncMode`方法用于判断当前是否处于同步模式。其判断逻辑是通过获取同步账户名称(从`NotesPreferenceActivity`类的相关方法获取), +// 然后检查该名称去除首尾空白字符后的长度是否大于0,如果大于0则表示存在同步账户,即处于同步模式,返回`true`;否则返回`false`。 +private boolean isSyncMode() { + // 调用`NotesPreferenceActivity`类的静态方法`getSyncAccountName`,传入当前的Activity上下文(`this`)来获取同步账户名称, + // 然后使用`trim`方法去除名称字符串的首尾空白字符,再通过获取其长度并与0比较来判断是否存在有效的同步账户名称,以此确定是否处于同步模式。 + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} +// `onClockAlertChanged`方法用于处理笔记的时钟提醒相关的变更操作,比如根据用户设置或取消提醒的操作,来与系统的闹钟服务(`AlarmManager`)进行交互,实现提醒的设置或取消, +// 同时在操作前会考虑笔记是否已保存等情况,以确保操作的合理性和数据的完整性。 +public void onClockAlertChanged(long date, boolean set) { + /** + * User could set clock to an unsaved note, so before setting the + * alert clock, we should save the note first + */ + // 判断当前正在编辑的笔记(`mWorkingNote`)是否已经存在于数据库中(通过`existInDatabase`方法判断), + // 因为用户有可能对还未保存的笔记设置时钟提醒,所以如果笔记不存在数据库中,就先调用`saveNote`方法保存笔记, + // `saveNote`方法的具体实现这里看不到,但推测它会将笔记的相关内容(如文本内容、各种设置等)保存到数据库等合适的存储位置,确保后续操作有完整的数据基础。 + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + // 进一步判断笔记(`mWorkingNote`)的笔记ID(通过`getNoteId`方法获取)是否大于0,只有有合法笔记ID的情况下才能进行与闹钟相关的设置操作, + // 若笔记ID大于0,则说明笔记已经有了合适的标识,可以与系统的闹钟服务等进行关联操作,进入以下逻辑。 + if (mWorkingNote.getNoteId() > 0) { + // 创建一个新的`Intent`对象,设置其目标类为`AlarmReceiver.class`,这个`Intent`通常用于触发`AlarmReceiver`广播接收器(具体实现未给出,但推测用于处理闹钟触发后的相关逻辑,比如弹出提醒通知等), + // 当闹钟时间到达时,系统会根据这个`Intent`来找到对应的广播接收器并执行相应的操作。 + Intent intent = new Intent(this, AlarmReceiver.class); + // 通过`ContentUris.withAppendedId`方法,将笔记的内容URI(`Notes.CONTENT_NOTE_URI`)与笔记的ID(`mWorkingNote.getNoteId()`)进行拼接, + // 然后设置到`intent`的`Data`属性上,这样在`AlarmReceiver`接收广播时,可以通过这个`Data`属性获取到具体是哪个笔记触发的闹钟提醒,方便后续进行针对性的提醒操作(比如展示对应笔记的相关信息等)。 + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + // 创建一个`PendingIntent`对象,用于包装前面创建的`Intent`,使其可以在合适的时间(即闹钟触发时间)被系统触发执行, + // 这里通过`PendingIntent.getBroadcast`方法创建一个用于广播的`PendingIntent`,传入当前的Activity上下文(`this`)、请求码(这里设置为0,通常用于区分不同的`PendingIntent`请求情况)、要包装的`Intent`以及标志位(这里设置为0,可根据具体需求设置不同的标志来控制`PendingIntent`的行为), + // 这样就创建好了一个可以由系统闹钟服务触发的广播`PendingIntent`,用于后续设置闹钟提醒时关联使用。 + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + // 获取系统的`AlarmManager`服务,通过`getSystemService`方法传入`ALARM_SERVICE`常量来获取,`AlarmManager`用于管理系统的闹钟功能,比如设置、取消闹钟等操作, + // 将获取到的`AlarmManager`服务对象赋值给`alarmManager`变量,方便后续调用其相关方法来操作闹钟。 + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + // 调用`showAlertHeader`方法,该方法用于更新界面上与提醒相关的头部显示信息(比如提醒时间文本、提醒图标等的显示状态和内容), + // 确保界面上的提醒相关信息能及时反映当前提醒设置的变更情况,让用户看到最新的提醒状态,具体的显示逻辑在`showAlertHeader`方法中实现。 + showAlertHeader(); + // 根据传入的`set`参数来判断是设置闹钟提醒还是取消闹钟提醒操作,如果`set`为`false`,表示要取消闹钟提醒,进入以下逻辑。 + if (!set) { + // 调用`alarmManager`的`cancel`方法,传入前面创建的`PendingIntent`(`pendingIntent`)来取消对应的闹钟提醒, + // 这样系统就会根据这个`PendingIntent`找到对应的闹钟设置并取消掉,避免不必要的提醒发生,实现用户取消提醒的操作需求。 + alarmManager.cancel(pendingIntent); + } else { + // 如果`set`为`true`,表示要设置闹钟提醒,此时调用`alarmManager`的`set`方法来设置闹钟,传入闹钟类型(`AlarmManager.RTC_WAKEUP`表示在指定的实时时钟时间唤醒设备并触发提醒,即使设备处于睡眠状态也会唤醒,常用于重要提醒场景)、 + // 提醒的时间(`date`参数,即用户设置的提醒时间对应的时间戳)以及用于触发提醒的`PendingIntent`(`pendingIntent`),这样系统就会根据传入的参数在指定时间触发闹钟提醒,通过广播触发`AlarmReceiver`来执行相应的提醒逻辑。 + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + /** + * There is the condition that user has input nothing (the note is + * not worthy saving), we have no note id, remind the user that he + * should input something + */ + // 如果笔记的笔记ID不大于0,说明可能出现了用户没有输入任何有效内容(笔记不值得保存)的情况,此时没有合适的笔记ID来关联闹钟提醒操作, + // 通过`Log.e`方法输出一条错误日志信息,记录“Clock alert setting error”(时钟提醒设置错误),方便后续排查问题,了解出现提醒设置异常的原因。 + Log.e(TAG, "Clock alert setting error"); + // 调用`showToast`方法(虽然当前代码中看不到其具体实现,但推测是用于弹出一个短暂提示信息的方法,类似Toast提示框),传入`R.string.error_note_empty_for_clock`对应的字符串资源, + // 向用户弹出一个提示信息,告知用户因为笔记内容为空等原因无法设置时钟提醒,提示用户应该输入一些内容后再尝试设置提醒,增强用户交互的友好性和提示性。 + showToast(R.string.error_note_empty_for_clock); + } +} +// `onWidgetChanged`方法比较简单,其功能就是调用`updateWidget`方法来更新与当前笔记相关联的桌面小部件的显示内容, +// 具体的小部件更新逻辑在`updateWidget`方法中实现,比如发送广播通知小部件提供类更新小部件上显示的笔记相关信息等操作。 +public void onWidgetChanged() { + updateWidget(); +} +// `onEditTextDelete`方法用于处理编辑文本框(从变量名`mEditTextList`推测是包含多个编辑文本框的列表)中某个文本框被删除后的相关操作, +// 比如调整剩余文本框的索引、更新文本内容、重新设置焦点和光标位置等,以保证编辑界面的文本显示和操作逻辑的连贯性和正确性。 +public void onEditTextDelete(int index, String text) { + // 获取`mEditTextList`(可能是一个`LinearLayout`等布局容器,用于存放多个编辑文本框相关的视图元素)中包含的子视图数量,也就是编辑文本框的数量,赋值给`childCount`变量, + // 用于后续判断和循环操作,确定要处理的文本框范围等情况。 + int childCount = mEditTextList.getChildCount(); + // 如果编辑文本框的数量只有1个,说明已经是最少的文本框数量了,没有其他文本框可以进行后续的删除相关调整操作了,直接返回,不执行下面的逻辑。 + if (childCount == 1) { + return; + } + + // 从要删除的文本框的下一个索引(`index + 1`)开始,循环遍历剩余的所有编辑文本框(直到`childCount`结束),对每个文本框执行以下操作,目的是调整剩余文本框的索引值,使其保持连续且正确。 + for (int i = index + 1; i < childCount; i++) { + // 通过`mEditTextList`获取当前循环到的子视图(即编辑文本框所在的视图),然后在该子视图中通过`findViewById`方法依据资源ID(`R.id.et_edit_text`)查找对应的编辑文本框, + // 将查找到的编辑文本框转换为`NoteEditText`类型(可能是自定义的继承自`EditText`的文本框类,用于扩展一些特定功能,具体未给出),并调用其`setIndex`方法,传入`i - 1`来重新设置该文本框的索引值, + // 这样可以保证文本框的索引在删除操作后依然是连续且符合逻辑的,便于后续的文本处理等操作基于正确的索引进行。 + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } + + // 通过`mEditTextList`调用`removeViewAt`方法,传入要删除的文本框的索引(`index`),从`mEditTextList`布局容器中移除对应的编辑文本框视图,实现文本框的删除操作,更新界面上显示的编辑文本框布局情况。 + mEditTextList.removeViewAt(index); + NoteEditText edit = null; + // 根据要删除的文本框的索引(`index`)来判断获取哪个剩余文本框用于后续操作,如果`index`等于0,说明删除的是第一个文本框,此时获取`mEditTextList`中的第一个子视图(即剩余的第一个编辑文本框所在的视图), + // 然后在该视图中通过`findViewById`方法依据资源ID(`R.id.et_edit_text`)查找对应的编辑文本框,将其赋值给`edit`变量,用于后续操作。 + if (index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( + R.id.et_edit_text); + } else { + // 如果`index`不等于0,说明删除的不是第一个文本框,此时获取`mEditTextList`中索引为`index - 1`的子视图(即删除操作后,原本在要删除文本框之前的那个编辑文本框所在的视图), + // 然后在该视图中通过`findViewById`方法依据资源ID(`R.id.et_edit_text`)查找对应的编辑文本框,将其赋值给`edit`变量,用于后续操作,这样就获取到了与删除操作相关的合适的剩余编辑文本框对象。 + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( + R.id.et_edit_text); + } + // 获取前面获取到的编辑文本框(`edit`)中的文本长度,赋值给`length`变量,用于后续设置光标位置等操作,确保光标位置能正确定位在合适的位置上。 + int length = edit.length(); + // 调用`edit`(编辑文本框)的`append`方法,传入要删除的文本框中的文本内容(`text`参数),将该文本内容添加到当前获取的编辑文本框中,实现文本内容的转移和合并操作,保证文本内容不会因为文本框删除而丢失。 + edit.append(text); + // 调用`edit`(编辑文本框)的`requestFocus`方法,让该编辑文本框获取焦点,也就是将光标定位到这个文本框中,方便用户继续进行编辑操作,使得编辑流程能够自然衔接,用户体验更好。 + edit.requestFocus(); + // 调用`edit`(编辑文本框)的`setSelection`方法,传入前面获取的文本长度(`length`),将光标位置设置在刚刚添加的文本内容之后,确保光标定位在合理的位置,便于用户继续输入或编辑文本内容。 + edit.setSelection(length); +} + // `onEditTextEnter`方法用于处理在编辑文本框(推测是多个编辑文本框组成的列表,由`mEditTextList`管理)中按下回车键等操作后的逻辑, +// 比如添加新的视图、设置焦点以及调整后续文本框的索引等,以保证编辑界面的显示和操作逻辑的连贯性。 +public void onEditTextEnter(int index, String text) { + /** + * Should not happen, check for debug + */ + // 首先进行一个边界检查,判断传入的索引(`index`)是否大于`mEditTextList`(存放编辑文本框相关视图的列表容器)中包含的子视图数量(也就是编辑文本框的实际数量), + // 如果大于,说明出现了不应该出现的越界情况(正常情况下索引应该在有效范围内),通过`Log.e`方法输出一条错误日志信息,记录“Index out of mEditTextList boundrary, should not happen”(索引超出`mEditTextList`边界,不应出现此情况),方便后续排查问题,了解是否出现了异常的操作导致索引越界问题。 + if (index > mEditTextList.getChildCount()) { + Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + } + + // 调用`getListItem`方法(虽然当前代码中看不到其具体实现,但推测是用于创建一个包含编辑文本框等相关视图元素的列表项视图的方法),传入要添加的文本内容(`text`)和索引(`index`),获取对应的视图对象,赋值给`view`变量,这个视图将被添加到`mEditTextList`中作为新的编辑文本框相关的列表项。 + View view = getListItem(text, index); + // 通过`mEditTextList`调用`addView`方法,将前面获取到的视图(`view`)添加到`mEditTextList`中,指定添加的位置为`index`,这样就在指定的索引位置插入了新的编辑文本框相关的视图,更新了编辑文本框列表的布局和内容。 + mEditTextList.addView(view, index); + // 通过`view`对象调用`findViewById`方法,依据资源ID(`R.id.et_edit_text`)查找对应的编辑文本框,将其转换为`NoteEditText`类型(可能是自定义的继承自`EditText`的文本框类,用于扩展一些特定功能,具体未给出),赋值给`edit`变量,用于后续操作。 + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + // 调用`edit`(编辑文本框)的`requestFocus`方法,让该编辑文本框获取焦点,也就是将光标定位到这个新添加的文本框中,方便用户继续输入内容,使得编辑操作能够自然地在新添加的文本框处继续进行,提升用户编辑体验。 + edit.requestFocus(); + // 调用`edit`(编辑文本框)的`setSelection`方法,传入`0`,将光标位置设置在文本框内容的开头位置(索引为0处),确保光标初始定位在合理的位置,便于用户开始输入新的文本内容。 + edit.setSelection(0); + // 从新添加的文本框的下一个索引(`index + 1`)开始,循环遍历剩余的所有编辑文本框(直到`mEditTextList.getChildCount()`结束),对每个文本框执行以下操作,目的是调整剩余文本框的索引值,使其保持连续且正确。 + for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { + // 通过`mEditTextList`获取当前循环到的子视图(即编辑文本框所在的视图),然后在该子视图中通过`findViewById`方法依据资源ID(`R.id.et_edit_text`)查找对应的编辑文本框, + // 将查找到的编辑文本框转换为`NoteEditText`类型,并调用其`setIndex`方法,传入`i`来重新设置该文本框的索引值,这样可以保证文本框的索引在添加新文本框操作后依然是连续且符合逻辑的,便于后续的文本处理等操作基于正确的索引进行。 + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i); + } +} +// `switchToListMode`方法用于将编辑界面从普通文本编辑模式切换到列表模式,其操作包括清除原有的编辑文本框列表视图、根据传入的文本内容分割并添加新的列表项视图、设置焦点以及切换相关视图的可见性等,以呈现出列表模式的编辑界面效果。 +private void switchToListMode(String text) { + // 通过`mEditTextList`调用`removeAllViews`方法,移除`mEditTextList`中所有的子视图(也就是清除原有的编辑文本框相关的所有视图元素),为重新构建列表模式下的编辑文本框列表做准备,清空之前的布局内容。 + mEditTextList.removeAllViews(); + // 使用`split`方法,以换行符(`\n`)为分隔符,将传入的文本内容(`text`)分割成字符串数组(`items`),每个元素对应列表模式下的一行文本内容,假设传入的文本内容是按照每行表示一个列表项的格式组织的,这样就可以将其解析为各个列表项内容。 + String[] items = text.split("\n"); + int index = 0; + // 遍历分割后的字符串数组(`items`),对每个非空的字符串元素执行以下操作,目的是将每个有效的列表项内容添加到`mEditTextList`中,构建列表模式下的编辑文本框列表。 + for (String item : items) { + if (!TextUtils.isEmpty(item)) { + // 调用`getListItem`方法(虽然当前代码中看不到其具体实现,但推测是用于创建一个包含编辑文本框等相关视图元素的列表项视图的方法),传入当前的列表项文本内容(`item`)和索引(`index`),获取对应的视图对象,然后将其添加到`mEditTextList`中, + // 这样就为每个非空的列表项创建并添加了对应的编辑文本框相关的视图,逐步构建起列表模式下的编辑文本框列表布局。 + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + // 再调用一次`getListItem`方法,传入空字符串(表示添加一个空白的列表项,可能用于方便用户继续添加新的列表项内容等情况)和当前的索引(`index`),获取对应的视图对象,并添加到`mEditTextList`中,完善列表模式下的编辑文本框列表布局,确保最后有一个可编辑的空白项供用户继续操作。 + mEditTextList.addView(getListItem("", index)); + // 通过`mEditTextList`获取索引为`index`(也就是最后添加的那个空白列表项对应的视图)的子视图,然后在该子视图中通过`findViewById`方法依据资源ID(`R.id.et_edit_text`)查找对应的编辑文本框, + // 并调用该编辑文本框的`requestFocus`方法,让这个空白编辑文本框获取焦点,也就是将光标定位到这个空白文本框中,方便用户直接开始输入新的列表项内容,使得列表模式下的编辑操作更加便捷,用户体验更好。 + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); + + // 将原本用于普通文本编辑的`mNoteEditor`(可能是一个`EditText`等文本编辑视图)的可见性设置为`View.GONE`(即隐藏该视图),因为切换到列表模式后,不需要显示这个普通文本编辑视图了,避免界面显示混乱。 + mNoteEditor.setVisibility(View.GONE); + // 将`mEditTextList`(用于列表模式下编辑文本框列表的视图容器)的可见性设置为`View.VISIBLE`(即显示该视图),确保在切换到列表模式后,列表模式下的编辑文本框列表能够正确显示给用户,呈现出列表模式的编辑界面效果。 + mEditTextList.setVisibility(View.VISIBLE); +} +// `getHighlightQueryResult`方法用于根据用户输入的查询内容(`userQuery`),在给定的完整文本(`fullText`)中查找匹配的部分,并将匹配的部分设置特定的背景颜色(高亮显示),最后返回处理后的包含高亮显示效果的`Spannable`对象,方便在界面上展示高亮后的文本内容。 +private Spannable getHighlightQueryResult(String fullText, String userQuery) { + // 创建一个`SpannableString`对象,传入`fullText`是否为`null`的判断结果,如果`fullText`为`null`则传入空字符串,否则传入`fullText`本身,以此初始化一个可设置文本样式(如设置高亮显示等)的`Spannable`类型的字符串对象,赋值给`spannable`变量,用于后续操作。 + SpannableString spannable = new SpannableString(fullText == null? "" : fullText); + // 判断用户输入的查询内容(`userQuery`)是否不为空(即用户有输入查询内容),如果不为空,则进入以下处理逻辑,用于在完整文本中查找匹配的部分并设置高亮显示样式。 + if (!TextUtils.isEmpty(userQuery)) { + // 使用`Pattern`类的`compile`方法,传入用户查询内容(`userQuery`)创建一个正则表达式模式对象(`mPattern`),用于后续通过正则表达式来匹配完整文本中的相关内容,这里假设`userQuery`可以作为有效的正则表达式来进行文本匹配操作(具体根据业务需求和使用场景而定)。 + mPattern = Pattern.compile(userQuery); + // 通过创建好的正则表达式模式对象(`mPattern`)调用`matcher`方法,传入完整文本(`fullText`)创建一个`Matcher`对象(`m`),这个`Matcher`对象用于实际执行文本匹配操作,查找完整文本中与用户查询内容匹配的部分。 + Matcher m = mPattern.matcher(fullText); + int start = 0; + // 使用`while`循环,只要`matcher`对象(`m`)能通过`find`方法找到下一个匹配的部分(从`start`位置开始查找),就进入循环体执行以下操作,目的是逐个找到所有匹配的文本部分并设置高亮显示样式。 + while (m.find(start)) { + // 通过`spannable`对象调用`setSpan`方法,传入一个`BackgroundColorSpan`对象(用于设置文本背景颜色的样式类),这个`BackgroundColorSpan`对象通过调用当前Activity的`getResources`方法获取资源对象,再调用其`getColor`方法获取`R.color.user_query_highlight`对应的颜色资源(可能是用于高亮显示的特定颜色)来创建, + // 同时传入匹配部分的起始位置(`m.start()`)和结束位置(`m.end()`)以及`Spannable.SPAN_INCLUSIVE_EXCLUSIVE`(表示设置的样式应用范围是包含起始位置但不包含结束位置的文本部分,是一种常见的`Spannable`样式设置范围标识),这样就为匹配的文本部分设置了特定的背景颜色,实现了高亮显示效果。 + spannable.setSpan( + new BackgroundColorSpan(this.getResources().getColor( + R.color.user_query_highlight)), m.start(), m.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + // 更新`start`变量的值为当前匹配部分的结束位置(`m.end()`),以便下一次循环从这个位置之后继续查找匹配的部分,确保能遍历完整文本中所有匹配的内容,全部进行高亮显示处理。 + start = m.end(); + } + } + // 最后返回处理好的包含高亮显示效果的`Spannable`对象(`spannable`),这样调用该方法的地方就可以获取到经过高亮处理后的文本内容,用于在界面上展示给用户查看(比如在搜索结果展示等场景下使用)。 + return spannable; +} + + + // 此方法用于获取一个列表项的视图,根据传入的文本内容(item)和索引(index)来设置视图内各子元素的相关属性及样式等。 +private View getListItem(String item, int index) { + // 通过LayoutInflater从当前上下文(this)加载指定布局(R.layout.note_edit_list_item)创建视图,不指定父视图(null)。 + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + // 从加载的视图中找到编辑文本框(NoteEditText类型),用于后续设置文本外观等操作。 + final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + // 根据当前的字体大小资源标识(mFontSizeId)设置编辑文本框的文本外观样式。 + edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + // 从视图中找到复选框(CheckBox类型),用于后续设置其状态改变监听等操作。 + 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); + } + } + }); + + // 判断文本内容是否以特定已选中标记(TAG_CHECKED)开头,若是则设置复选框为选中状态,给编辑文本框添加删除线样式,并去除标记部分文本。 + 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)) { + // 若以未选中标记(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(); + } + + // 为编辑文本框设置文本内容变化监听器(this表示当前类实现了相关监听器接口)。 + edit.setOnTextViewChangeListener(this); + // 设置编辑文本框的索引值。 + edit.setIndex(index); + // 设置编辑文本框的文本内容,经过高亮查询结果处理(根据用户查询内容对文本进行高亮显示相关处理)后的文本。 + edit.setText(getHighlightQueryResult(item, mUserQuery)); + // 返回构建好的列表项视图。 + return view; +} +// 处理文本变化相关逻辑,根据索引及文本是否存在来设置对应列表项中复选框的可见性。 +public void onTextChange(int index, boolean hasText) { + // 判断传入的索引是否超出了编辑文本框列表(mEditTextList)中子视图数量范围,若是则输出错误日志并返回,说明出现异常情况。 + if (index >= mEditTextList.getChildCount()) { + Log.e(TAG, "Wrong index, should not happen"); + return; + } + // 如果文本存在(hasText为true),则设置对应索引的列表项中的复选框可见,否则设置为不可见。 + 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); + } +} +// 处理列表模式改变的相关逻辑,根据新模式(newMode)的值来切换界面显示状态及文本内容等相关操作。 +public void onCheckListModeChanged(int oldMode, int newMode) { + // 如果新模式是列表模式(TextNote.MODE_CHECK_LIST),则调用switchToListMode方法将普通文本编辑器中的文本转换为列表模式展示,传入文本编辑器中的文本内容。 + if (newMode == TextNote.MODE_CHECK_LIST) { + switchToListMode(mNoteEditor.getText().toString()); + } else { + // 如果不是列表模式,先尝试获取工作文本(通过getWorkingText方法),若获取失败,对工作文本做一些处理(去除特定标记等),然后设置文本编辑器的文本内容为经过高亮查询结果处理后的内容, + // 最后隐藏编辑文本框列表,显示普通文本编辑器。 + if (!getWorkingText()) { + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", + "")); + } + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mEditTextList.setVisibility(View.GONE); + mWorkingNote.setVisibility(View.VISIBLE); + } +} +// 获取工作文本的方法,根据当前笔记的列表模式(是否为复选列表模式)来构造不同格式的文本内容,返回是否存在已选中项的布尔值。 +private boolean getWorkingText() { + boolean hasChecked = false; + // 如果是复选列表模式(TextNote.MODE_CHECK_LIST),遍历编辑文本框列表,根据每个列表项中文本内容及复选框状态构造文本,用特定标记表示是否选中,最后设置工作文本内容,同时记录是否存在已选中项。 + 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; +} + + // 用于保存笔记的方法,先获取工作文本内容,然后调用笔记对象的保存方法进行保存,根据保存结果设置返回值及相关结果标识。 +private boolean saveNote() { + // 获取工作文本内容(具体获取逻辑在getWorkingText方法中),为保存笔记做准备。 + getWorkingText(); + // 调用mWorkingNote对象的saveNote方法来实际保存笔记,将保存结果赋值给saved变量。 + boolean saved = mWorkingNote.saveNote(); + if (saved) { + /** + * 注释说明了在从列表视图进入编辑视图有两种情况(打开已有笔记、新建/编辑笔记),保存成功时设置结果为RESULT_OK,用于区分创建/编辑状态,方便后续操作判断。 + */ + setResult(RESULT_OK); + } + // 返回笔记是否保存成功的结果(saved的值)。 + return saved; +} +// 用于将笔记相关内容发送到桌面(创建桌面快捷方式相关操作),先确保笔记已保存到数据库,然后根据笔记ID情况来构建并发送相应的意图以创建快捷方式,若出现问题则提示用户。 +private void sendToDesktop() { + /** + * 注释说明在向桌面发送消息前,要确保当前编辑的笔记已存在数据库中,若不存在(比如新建笔记还没保存时),先调用saveNote方法保存笔记。 + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + if (mWorkingNote.getNoteId() > 0) { + // 创建一个用于发送广播的Intent(sender),后续将在这个Intent中添加创建桌面快捷方式相关的各种信息。 + Intent sender = new Intent(); + // 创建一个意图(shortcutIntent)用于启动NoteEditActivity,设置动作为ACTION_VIEW,表明是查看操作,同时传入笔记的ID作为额外信息。 + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); + shortcutIntent.setAction(Intent.ACTION_VIEW); + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + // 将用于启动NoteEditActivity的意图(shortcutIntent)添加到sender意图中,作为创建桌面快捷方式对应的启动意图。 + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + // 设置桌面快捷方式显示的名称,通过调用makeShortcutIconTitle方法处理笔记内容来生成名称。 + sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, + makeShortcutIconTitle(mWorkingNote.getContent())); + // 设置桌面快捷方式的图标资源,指定为应用内的一个图标资源(R.drawable.icon_app)。 + sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); + // 添加一个额外信息“duplicate”并设为true,具体用途可能与快捷方式相关的一些重复创建等逻辑有关(需结合具体业务看)。 + sender.putExtra("duplicate", true); + // 设置sender意图的动作,用于触发创建桌面快捷方式的相关操作(对应系统中接收此动作的组件来处理创建快捷方式逻辑)。 + sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); + // 弹出一个短暂提示信息告知用户笔记即将进入桌面(具体提示内容对应资源ID的字符串)。 + showToast(R.string.info_note_enter_desktop); + // 发送广播,触发创建桌面快捷方式的操作。 + sendBroadcast(sender); + } else { + /** + * 注释说明当笔记ID不存在时(可能用户没输入有效内容,笔记不值得保存)的情况,此时输出错误日志并提示用户应输入些内容。 + */ + Log.e(TAG, "Send to desktop error"); + showToast(R.string.error_note_empty_for_send_to_desktop); + } +} +// 用于生成桌面快捷方式图标显示的标题内容,会去除笔记内容中特定的标记(如已选中、未选中相关标记),并根据最大长度限制对内容进行截取处理。 +private String makeShortcutIconTitle(String content) { + // 去除笔记内容中已选中相关的标记(TAG_CHECKED)。 + content = content.replace(TAG_CHECKED, ""); + // 去除笔记内容中未选中相关的标记(TAG_UNCHECKED)。 + content = content.replace(TAG_UNCHECKED, ""); + // 根据内容长度与最大长度限制(SHORTCUT_ICON_TITLE_MAX_LEN)比较,若超过则截取前面部分内容作为最终返回的标题,否则直接返回原内容。 + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN? content.substring(0, + SHORTCUT_ICON_TITLE_MAX_LEN) : content; +} +// 用于弹出一个短暂提示信息(Toast),默认显示时长为短时长(具体时长由Toast.LENGTH_SHORT定义),实际调用了双参数的showToast方法来实现。 +private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); +} +// 用于真正弹出一个Toast提示信息,根据传入的资源ID(对应具体提示内容字符串)以及显示时长参数来显示相应的提示信息给用户。 +private void showToast(int resId, int duration) { + // 通过Toast的makeText方法创建一个Toast对象,传入当前上下文(this)、资源ID(resId)以及显示时长(duration),然后调用show方法显示Toast提示信息。 + Toast.makeText(this, resId, duration).show(); +} diff --git a/ui/NoteEditText.java b/ui/NoteEditText.java new file mode 100644 index 0000000..f0826b9 --- /dev/null +++ b/ui/NoteEditText.java @@ -0,0 +1,310 @@ +/* + * 版权声明部分,表明代码的版权归属为The MiCode Open Source Community(米柚开源社区),遵循Apache License 2.0协议, + * 该协议规定了代码使用、分发等方面的权限和限制条件等内容,这里声明了代码在满足协议要求的情况下可被使用。 + * 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. + */ + +// 声明该类所在的包名,属于net.micode.notes.ui包,这有助于在项目中对类进行合理的组织和引用,避免类名冲突等问题。 +package net.micode.notes.ui; +// 导入Android系统的上下文相关类,用于获取应用的各种资源、与系统服务交互等,例如获取字符串资源、启动其他组件等操作都依赖于此。 +import android.content.Context; +// 导入用于表示矩形区域的类,在处理视图的边界、触摸区域等方面可能会用到,比如确定某个触摸点是否在特定的矩形范围内等情况。 +import android.graphics.Rect; +// 导入文本布局相关的类,可通过它获取文本在视图中的行信息、字符偏移量等内容,有助于进行基于文本布局的操作,比如根据触摸位置确定对应的文本位置。 +import android.text.Layout; +// 导入文本选择相关的类,用于设置文本的选中区域,比如设置光标位置、选中一段文本等操作都可以通过它提供的方法来实现。 +import android.text.Selection; +// 导入处理包含样式等复杂文本的接口,用于判断文本是否包含特定样式(如这里后续会涉及的URL样式等)以及获取相关样式元素等操作。 +import android.text.Spanned; +// 导入文本工具类,提供了一些便捷的文本处理方法,例如判断文本是否为空字符串等常用操作,方便代码中对文本状态的判断。 +import android.text.TextUtils; +// 导入用于处理文本中URL样式的类,通过它可以识别文本里的链接样式,并进行相应的操作,比如点击链接跳转等功能实现会用到它。 +import android.text.style.URLSpan; +// 导入Android系统的工具类,主要用于记录日志信息,方便在开发调试阶段查看程序运行情况,定位问题所在。 +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; +// 导入Android系统默认的文本编辑框类,本自定义的NoteEditText类继承自它,以在其基础上扩展更多符合特定需求的功能。 +import android.widget.EditText; + +// 导入项目自定义的资源相关类,通过它可以获取在项目中定义的各种资源,比如字符串资源(这里后续用于获取不同链接类型对应的提示文本资源ID等)。 +import net.micode.notes.R; + +// 导入Java中的HashMap类和Map接口,用于创建键值对形式的集合,这里用来存储不同链接协议与对应的资源ID的映射关系,方便查找使用。 +import java.util.HashMap; +import java.util.Map; +// 自定义的文本编辑框类,继承自Android系统的EditText类,目的是在原生文本编辑框功能基础上进行定制化扩展,以满足特定应用场景(可能是笔记应用中编辑文本的特殊需求)的功能要求。 +public class NoteEditText extends EditText { + // 定义一个静态的、用于日志记录的标签字符串常量,其值为"NoteEditText",在使用Log输出日志时,通过这个标签可以方便地识别出是该类中产生的日志信息,便于调试和问题排查。 + private static final String TAG = "NoteEditText"; + // 用于记录当前这个NoteEditText实例在一组编辑文本框中的索引位置,例如在多个编辑文本框组成的列表里,通过该索引可以区分不同的文本框,方便进行相关操作和管理。 + private int mIndex; + // 用于记录在删除操作之前,文本选择区域的起始位置,后续在处理删除相关逻辑时,可以依据这个位置来判断是否满足删除当前文本框等条件,辅助进行更精准的操作判断。 + private int mSelectionStartBeforeDelete; + + // 定义表示电话链接协议的常量字符串,其值为"tel:",用于在文本中识别是否存在电话链接相关内容,以便后续进行相应的处理(如根据链接类型展示不同提示等)。 + private static final String SCHEME_TEL = "tel:" ; + // 定义表示HTTP网页链接协议的常量字符串,其值为"http:",用于判断文本里是否包含网页链接,进而采取对应的操作逻辑,比如点击链接跳转到网页等。 + private static final String SCHEME_HTTP = "http:" ; + // 定义表示邮件链接协议的常量字符串,其值为"mailto:",用于识别文本中的邮件链接情况,方便实现点击邮件链接启动邮件客户端等相关功能。 + private static final String SCHEME_EMAIL = "mailto:" ; + + // 创建一个静态的HashMap类型的集合,用于存储不同链接协议(如上述的tel、http、mailto等)与对应的资源ID(这些资源ID通常关联着在界面上显示的对应链接类型的提示字符串等资源)之间的映射关系,方便后续根据链接协议快速查找并获取相应的资源显示信息。 + private static final Map sSchemaActionResMap = new HashMap(); + // 静态代码块,用于在类加载时初始化上面定义的sSchemaActionResMap集合,将不同链接协议与对应的资源ID进行关联赋值,使得后续可以直接通过协议字符串查找到对应的资源ID,例如tel协议对应R.string.note_link_tel这个资源ID,以此类推。 + 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); + } + + // 定义一个内部接口,名为OnTextViewChangeListener,用于作为文本编辑框文本变化的监听器,规定了在特定文本编辑操作发生时(如删除、新增文本、文本内容变化等情况)需要外部实现的回调方法, + // 这样外部类(比如包含该编辑文本框的Activity等)可以通过实现这个接口,并将实现对象设置给该编辑文本框,来监听并处理相应的文本变化逻辑,实现业务上的解耦和功能扩展。 + /** + * Call by the {@link NoteEditActivity} to delete or add edit text + */ + public interface OnTextViewChangeListener { + /** + * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens + * and the text is null + */ + // 定义当按下删除键(对应KeyEvent类中的KEYCODE_DEL常量)并且当前文本编辑框中的文本内容为空时需要调用的方法, + // 外部实现该接口的类需要根据具体业务场景来编写这个方法的具体逻辑,比如在笔记应用中可能是从编辑文本框列表里移除当前这个空文本框等操作。 + void onEditTextDelete(int index, String text); + + /** + * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} + * happen + */ + // 定义当按下回车键(对应KeyEvent类中的KEYCODE_ENTER常量)时需要调用的方法,外部实现该接口的类要根据业务需求来实现具体的添加文本框逻辑, + // 例如在笔记应用中可能是创建一个新的编辑文本框并添加到合适的位置(通常是当前文本框之后),方便用户继续输入内容等操作。 + void onEditTextEnter(int index, String text); + + /** + * Hide or show item option when text change + */ + // 定义当文本内容发生变化时需要调用的方法,外部实现该接口的类要依据文本是否有内容等情况来决定相关操作选项(比如上下文菜单里的某些选项、界面上某些与文本相关的按钮等)的显示或隐藏状态,以提供符合当前文本状态的交互界面。 + void onTextChange(int index, boolean hasText); + } + + // 定义一个成员变量,用于存储实现了OnTextViewChangeListener接口的监听器对象,通过调用setOnTextViewChangeListener方法可以将外部实现的监听器对象赋值给它, + // 从而实现文本编辑框文本变化事件与外部处理逻辑的关联,让外部类能够监听并处理文本编辑框的各种文本变化相关情况。 + private OnTextViewChangeListener mOnTextViewChangeListener; +// 构造函数,接收一个Context类型的参数,用于创建NoteEditText实例时传入上下文信息,调用父类(EditText)的构造函数,传入上下文和null作为属性集(通常在简单创建实例且不需要从XML布局中加载属性时使用这种方式), +// 同时初始化当前编辑文本框的索引值mIndex为0,表示默认的初始索引位置。 + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + // 设置当前编辑文本框索引值的方法,外部可以通过调用该方法传入新的索引值来更新mIndex变量,方便在多个编辑文本框组成的场景中,准确地对每个文本框进行定位和管理,例如在列表中调整文本框顺序等操作时会用到。 + public void setIndex(int index) { + mIndex = index; + } + + // 设置文本变化监听器的方法,外部类(如包含该编辑文本框的Activity等)可以通过调用该方法,传入实现了OnTextViewChangeListener接口的对象,将该对象赋值给mOnTextViewChangeListener变量, + // 这样当文本编辑框发生文本删除、新增、内容变化等相关事件时,就能触发对应的回调方法,执行外部类中定义的相应业务逻辑了。 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 构造函数,接收上下文对象(Context)和属性集(AttributeSet)作为参数,调用父类(EditText)的构造函数,传入上下文、属性集以及默认的编辑文本框样式(通过android.R.attr.editTextStyle指定), + // 这种构造方式常用于在XML布局文件中定义该自定义编辑文本框时,加载XML中配置的属性并应用默认样式来初始化文本框实例,使其具有合适的外观和初始属性设置。 + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 构造函数,接收上下文对象(Context)、属性集(AttributeSet)和默认样式定义(defStyle)作为参数,调用父类(EditText)的构造函数,传入相应参数进行初始化, + // 不过当前构造函数中的具体实现部分是自动生成的占位代码(由TODO注释标识,意味着后续可能需要根据具体业务需求进一步完善此处的初始化逻辑,比如根据传入的defStyle进行更多样式相关的设置等)。 + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub + } +// 重写父类(EditText)的onTouchEvent方法,用于处理该编辑文本框上的触摸操作相关逻辑,通过这个方法可以实现根据用户触摸位置来进行文本选择等交互功能,提升用户在文本编辑时的操作体验。 + @Override + public boolean onTouchEvent(MotionEvent event) { + // 根据触摸事件的动作类型(通过event.getAction()方法获取)进行不同的处理逻辑分支,这里主要关注MotionEvent.ACTION_DOWN(即触摸按下动作)的情况, + // 对于其他触摸动作类型(如触摸滑动、触摸抬起等),则默认按照父类(EditText)原有的处理逻辑执行(通过调用super.onTouchEvent(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(); + // 根据经过调整后的触摸点纵坐标(y),通过布局对象的getLineForVertical方法,获取触摸点所在的文本行索引,即确定触摸位置处于文本的第几行,方便后续进一步定位该行内的具体字符位置。 + int line = layout.getLineForVertical(y); + // 根据触摸点所在的文本行索引(line)以及经过调整后的横坐标(x),通过布局对象的getOffsetForHorizontal方法,获取触摸点对应的文本字符在整个文本字符串中的偏移量(也就是字符位置索引), + // 这个偏移量能够准确地指出触摸位置对应的是文本中的哪个字符,为后续设置文本选中区域提供准确的位置信息。 + int off = layout.getOffsetForHorizontal(line, x); + // 通过Selection类的setSelection方法,依据获取到的字符偏移量(off)来设置文本的选中区域,即将文本光标定位到触摸点对应的字符位置,或者如果有长按等操作逻辑,也可以实现选中从触摸点开始的一段文本等效果, + // 方便用户后续进行复制、删除、粘贴等文本编辑操作,提升文本编辑的交互性和便捷性。 + Selection.setSelection(getText(), off); + break; + } + + // 返回调用父类(EditText)的onTouchEvent方法的结果,这样既保证了在处理完特定触摸动作(如这里的ACTION_DOWN)的自定义逻辑后,还能让父类继续处理触摸事件相关的其他默认逻辑, + // 例如触摸事件的传递、与系统其他组件的交互等功能,确保整个触摸事件处理流程的完整性和正确性,避免影响其他相关的触摸操作功能。 + return super.onTouchEvent(event); + } + @Override +// 重写onKeyDown方法,用于处理按键按下的事件 +public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + // 当按下回车键时,如果存在文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 + if (mOnTextViewChangeListener!= null) { + // 直接返回false,这里可能是根据具体业务逻辑决定不做额外处理,直接向上传递该返回值 + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + // 当按下删除键(DEL)时,记录当前删除操作前的文本选择起始位置,以便后续可能的业务逻辑使用 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + // 如果上述case中没有处理或者需要默认行为,调用父类的onKeyDown方法继续处理 + return super.onKeyDown(keyCode, event); +} + +@Override +// 重写onKeyUp方法,用于处理按键抬起的事件 +public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + // 当删除键(DEL)抬起时,如果文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 + if (mOnTextViewChangeListener!= null) { + // 判断如果当前选择起始位置为0 并且 mIndex不等于0(这里mIndex具体含义需看上下文,可能是某种索引标识) + if (0 == mSelectionStartBeforeDelete && mIndex!= 0) { + // 调用监听器的onEditTextDelete方法,传递当前索引(mIndex)以及文本内容(通过getText().toString()获取),并返回true表示已经处理该事件 + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + // 如果监听器为空,打印日志提示监听器未设置 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + // 当回车键抬起时,如果文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 + if (mOnTextViewChangeListener!= null) { + // 获取当前文本选择的起始位置 + int selectionStart = getSelectionStart(); + // 获取从选择起始位置到文本末尾的子字符串,即回车键之后的文本内容 + String text = getText().subSequence(selectionStart, getText().length()).toString(); + // 将文本内容设置为从开头到选择起始位置的子字符串,相当于删除了回车键之后的文本内容 + setText(getText().subSequence(0, selectionStart)); + // 调用监听器的onEditTextEnter方法,传递下一个索引(mIndex + 1)以及回车键之后的文本内容(text) + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + // 如果监听器为空,打印日志提示监听器未设置 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + // 如果上述case中没有处理或者需要默认行为,调用父类的onKeyUp方法继续处理 + return super.onKeyUp(keyCode, event); +} + +@Override +// 重写onFocusChanged方法,用于处理焦点改变的事件 +protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + // 如果文本视图内容改变的监听器(mOnTextViewChangeListener)不为空 + if (mOnTextViewChangeListener!= null) { + // 如果当前失去焦点(!focused)并且文本内容为空(TextUtils.isEmpty(getText())) + if (!focused && TextUtils.isEmpty(getText())) { + // 调用监听器的onTextChange方法,传递当前索引(mIndex)以及表示文本为空的布尔值(false) + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + // 如果不是上述情况(即获得焦点或者文本不为空),调用监听器的onTextChange方法,传递当前索引(mIndex)以及表示文本不为空的布尔值(true) + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + // 调用父类的onFocusChanged方法继续处理焦点改变相关的其他默认逻辑 + super.onFocusChanged(focused, direction, previouslyFocusedRect); +} + +@Override +// 重写onCreateContextMenu方法,用于创建上下文菜单(通常是长按文本等操作弹出的菜单) +protected void onCreateContextMenu(ContextMenu menu) { + // 如果文本内容是Spanned类型(Spanned通常用于包含富文本信息,比如包含链接等格式的文本) + if (getText() instanceof Spanned) { + // 获取当前文本选择的起始位置 + int selStart = getSelectionStart(); + // 获取当前文本选择的结束位置 + int selEnd = getSelectionEnd(); + + // 获取选择范围的最小值(起始和结束位置中较小的那个) + int min = Math.min(selStart, selEnd); + // 获取选择范围的最大值(起始和结束位置中较大的那个) + int max = Math.max(selStart, selEnd); + + // 获取在选择范围内的所有URLSpan类型的对象数组,URLSpan通常用于表示文本中的链接信息 + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + // 遍历sSchemaActionResMap的键集合(这里sSchemaActionResMap具体含义需看上下文,可能是某种资源映射关系) + for (String schema : sSchemaActionResMap.keySet()) { + // 如果URLSpan中的URL包含当前遍历到的模式(schema) + if (urls[0].getURL().indexOf(schema) >= 0) { + // 获取对应的资源ID + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + // 如果没有找到匹配的资源ID,设置默认的资源ID(这里是R.string.note_link_other,具体字符串资源需看项目中的定义) + defaultResId = R.string.note_link_other; + } + + // 向上下文菜单中添加一个菜单项,参数依次为:组ID(0)、菜单项ID(0)、排序顺序(0)、显示的字符串资源ID(defaultResId) + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // 当点击该菜单项时,调用URLSpan的onClick方法,传入当前NoteEditText实例(this),通常用于触发链接跳转等操作 + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + // 调用父类的onCreateContextMenu方法继续处理上下文菜单创建的其他默认逻辑 + super.onCreateContextMenu(menu); +} diff --git a/ui/NoteItemData.java b/ui/NoteItemData.java new file mode 100644 index 0000000..b3e72cf --- /dev/null +++ b/ui/NoteItemData.java @@ -0,0 +1,300 @@ +/* + * 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. + */ + +// 所在包名,表明该类属于net.micode.notes.ui包下,用于UI相关的功能逻辑(推测) +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +// 导入与数据相关的类,可能用于操作笔记数据等功能 +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + +// NoteItemData类,可能用于封装笔记相关的数据项,方便在UI层进行展示和操作等 +public class NoteItemData { + // 定义一个字符串数组,用于指定从数据库查询笔记数据时要获取的列名,对应Notes表中的各个字段 + static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + }; + + // 以下是各个列在PROJECTION数组中的索引位置,方便后续从Cursor中获取对应的值 + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + // 笔记的唯一标识ID + private long mId; + // 提醒日期(可能是设置了提醒功能的笔记对应的提醒时间戳之类的) + private long mAlertDate; + // 背景颜色的ID,用于设置笔记展示的背景颜色(推测) + private int mBgColorId; + // 笔记创建日期的时间戳 + private long mCreatedDate; + // 表示笔记是否有附件,true表示有附件,false表示没有 + private boolean mHasAttachment; + // 笔记最后修改日期的时间戳 + private long mModifiedDate; + // 笔记相关的数量(具体含义需结合业务,可能是子笔记数量之类的) + private int mNotesCount; + // 父级ID,可能表示该笔记所属的文件夹等的ID + private long mParentId; + // 笔记的摘要内容,简短描述笔记的文字信息 + private String mSnippet; + // 笔记的类型,可能有不同分类(如普通笔记、系统笔记等,具体要看Notes类中定义的类型常量) + private int mType; + // 部件(Widget)的ID,可能和在桌面展示相关的部件有关(如果有此功能的话) + private int mWidgetId; + // 部件(Widget)的类型,同样和桌面部件相关(推测) + private int mWidgetType; + // 联系人姓名(如果笔记和联系人相关,比如通话记录对应的笔记等情况) + private String mName; + // 电话号码(如果笔记和通话记录等相关) + private String mPhoneNumber; + + // 以下几个布尔变量用于标记笔记在列表中的位置相关的状态 + private boolean mIsLastItem; + private boolean mIsFirstItem; + private boolean mIsOnlyOneItem; + private boolean mIsOneNoteFollowingFolder; + private boolean mIsMultiNotesFollowingFolder; + + // 构造函数,用于根据传入的Context和Cursor来初始化NoteItemData对象的各个属性 + public NoteItemData(Context context, Cursor cursor) { + // 从Cursor中获取笔记的ID,并赋值给mId属性 + mId = cursor.getLong(ID_COLUMN); + // 从Cursor中获取提醒日期,并赋值给mAlertDate属性 + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + // 从Cursor中获取背景颜色ID,并赋值给mBgColorId属性 + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + // 从Cursor中获取创建日期,并赋值给mCreatedDate属性 + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + // 根据从Cursor中获取的是否有附件的标识(整数),转换为布尔值赋值给mHasAttachment属性 + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0)? true : false; + // 从Cursor中获取修改日期,并赋值给mModifiedDate属性 + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + // 从Cursor中获取笔记数量,并赋值给mNotesCount属性 + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + // 从Cursor中获取父级ID,并赋值给mParentId属性 + mParentId = cursor.getLong(PARENT_ID_COLUMN); + // 从Cursor中获取笔记摘要内容,并赋值给mSnippet属性,同时去除一些特定的标记(TAG_CHECKED和TAG_UNCHECKED,具体含义需看NoteEditActivity类中的定义) + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + // 从Cursor中获取笔记类型,并赋值给mType属性 + mType = cursor.getInt(TYPE_COLUMN); + // 从Cursor中获取部件ID,并赋值给mWidgetId属性 + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + // 从Cursor中获取部件类型,并赋值给mWidgetType属性 + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + // 初始化电话号码为空字符串 + mPhoneNumber = ""; + // 如果父级ID是通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER,具体值需看Notes类定义) + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + // 通过DataUtils工具类,根据笔记ID从ContentResolver中获取电话号码,并赋值给mPhoneNumber属性 + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + // 如果获取到的电话号码不为空 + if (!TextUtils.isEmpty(mPhoneNumber)) { + // 通过Contact类的静态方法,根据电话号码获取联系人姓名,并赋值给mName属性 + mName = Contact.getContact(context, mPhoneNumber); + // 如果获取联系人姓名失败(返回null),则将电话号码作为姓名 + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + // 如果联系人姓名还是为null(可能前面获取过程出现问题等情况),则设置为空字符串 + if (mName == null) { + mName = ""; + } + // 调用checkPostion方法来检查该笔记在列表中的位置相关状态 + checkPostion(cursor); + } + + // 私有方法,用于检查该笔记在列表中的位置相关状态,通过Cursor中的信息来判断 + private void checkPostion(Cursor cursor) { + // 判断是否是列表中的最后一项,根据Cursor的isLast方法结果赋值给mIsLastItem属性 + mIsLastItem = cursor.isLast()? true : false; + // 判断是否是列表中的第一项,根据Cursor的isFirst方法结果赋值给mIsFirstItem属性 + mIsFirstItem = cursor.isFirst()? true : false; + // 判断是否列表中只有一项,通过比较Cursor中的记录数量是否为1来赋值给mIsOnlyOneItem属性 + mIsOnlyOneItem = (cursor.getCount() == 1); + // 初始化是否有多条笔记跟随在文件夹后的标记为false + mIsMultiNotesFollowingFolder = false; + // 初始化是否有一条笔记跟随在文件夹后的标记为false + mIsOneNoteFollowingFolder = false; + + // 如果笔记类型是普通笔记(Notes.TYPE_NOTE)并且不是列表中的第一项 + if (mType == Notes.TYPE_NOTE &&!mIsFirstItem) { + // 获取当前Cursor的位置(索引) + int position = cursor.getPosition(); + // 将Cursor移动到前一条记录(上一条数据) + if (cursor.moveToPrevious()) { + // 如果前一条记录的类型是文件夹类型(Notes.TYPE_FOLDER)或者系统类型(Notes.TYPE_SYSTEM) + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 判断如果总记录数大于当前位置(索引)加1,说明有多条笔记跟随在文件夹后 + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + // 否则说明只有一条笔记跟随在文件夹后 + mIsOneNoteFollowingFolder = true; + } + } + // 将Cursor再移回原来的下一条记录位置(保证Cursor位置状态的正确,避免影响后续操作) + if (!cursor.moveToNext()) { + // 如果无法移回,抛出异常,表示出现了不正常的Cursor移动情况 + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + // 判断是否有一条笔记跟随在文件夹后,对外提供的访问方法 + 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; + } + + // 获取笔记的唯一标识ID,对外提供的访问方法 + public long getId() { + return mId; + } + + // 获取提醒日期,对外提供的访问方法 + public long getAlertDate() { + return mAlertDate; + } + + // 获取创建日期,对外提供的访问方法 + public long getCreatedDate() { + return mCreatedDate; + } + + // 判断笔记是否有附件,对外提供的访问方法 + public boolean hasAttachment() { + return mHasAttachment; + } + + // 获取修改日期,对外提供的访问方法 + public long getModifiedDate() { + return mModifiedDate; + } + + // 获取背景颜色ID,对外提供的访问方法 + public int getBgColorId() { + return mBgColorId; + } + + // 获取父级ID,对外提供的访问方法 + public long getParentId() { + return mParentId; + } + + // 获取笔记数量,对外提供的访问方法 + public int getNotesCount() { + return mNotesCount; + } + + // 获取文件夹ID(这里返回的就是父级ID,可能是为了语义更明确表示是文件夹相关的ID),对外提供的访问方法 + public long getFolderId () { + return mParentId; + } + + // 获取笔记类型,对外提供的访问方法 + public int getType() { + return mType; + } + + // 获取部件类型,对外提供的访问方法 + public int getWidgetType() { + return mWidgetType; + } + + // 获取部件ID,对外提供的访问方法 + public int getWidgetId() { + return mWidgetId; + } + + // 获取笔记摘要内容,对外提供的访问方法 + public String getSnippet() { + return mSnippet; + } + + // 判断笔记是否有提醒设置(根据提醒日期是否大于0来判断),对外提供的访问方法 + public boolean hasAlert() { + return (mAlertDate > 0); + } + + // 判断是否是通话记录相关的笔记(根据父级ID是否是通话记录文件夹ID并且电话号码不为空来判断),对外提供的访问方法 + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER &&!TextUtils.isEmpty(mPhoneNumber)); + } + + // 静态方法,用于从Cursor中获取笔记类型,方便在其他地方直接调用获取类型信息 + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} \ No newline at end of file diff --git a/ui/NotesListActivity.java b/ui/NotesListActivity.java new file mode 100644 index 0000000..8c3a6ba --- /dev/null +++ b/ui/NotesListActivity.java @@ -0,0 +1,1493 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * + * 以下是版权相关声明,说明该代码遵循Apache License 2.0许可证进行开源使用。 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 意味着只有在符合许可证规定的情况下才能使用本文件。 + * 你可以通过以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或者书面同意,否则依据本许可证分发的软件按“原样”提供, + * 不附带任何明示或暗示的保证或条件,关于具体的权限和限制,可查看许可证内容。 + * 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. + */ + +// 包声明,表明该类所属的包名,这里属于net.micode.notes.ui包,通常意味着该类与应用的用户界面相关功能有关 +package net.micode.notes.ui; + +// 导入各种Android系统相关的类,用于实现Activity的基本功能、对话框、小部件管理、异步查询、内容提供器操作等功能 +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; + +// 导入Java标准库中用于处理输入流读取的相关类,可能用于读取文件等操作 +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +// NotesListActivity类继承自Activity,是Android应用中一个用于展示笔记列表的界面类,同时实现了点击和长按列表项的监听器接口 +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // 定义一个常量,用于标识文件夹内笔记列表查询任务的令牌(token),在异步查询机制中作为任务的唯一标识,方便区分不同类型的查询操作 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + // 定义一个常量,用于标识文件夹列表查询任务的令牌(token),同样用于异步查询时区分不同的查询任务 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + // 定义一个常量,作为菜单中“删除文件夹”菜单项的唯一ID,用于在菜单操作中识别该菜单项 + private static final int MENU_FOLDER_DELETE = 0; + // 定义一个常量,作为菜单中“查看文件夹”菜单项的唯一ID,用于在菜单操作中识别该菜单项 + private static final int MENU_FOLDER_VIEW = 1; + // 定义一个常量,作为菜单中“修改文件夹名称”菜单项的唯一ID,用于在菜单操作中识别该菜单项 + private static final int MENU_FOLDER_CHANGE_NAME = 2; + // 定义一个字符串常量,作为存储是否添加应用介绍相关偏好设置的键名,用于在SharedPreferences中进行相应设置的读写操作 + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + // 定义一个枚举类型,用于表示列表编辑的不同状态,包含笔记列表、子文件夹、通话记录文件夹三种情况,方便根据不同状态执行不同的业务逻辑 + private enum ListEditState { + NOTE_LIST, + SUB_FOLDER, + CALL_RECORD_FOLDER + }; + + // 定义一个变量,用于记录当前列表的编辑状态,初始化为NOTE_LIST状态,表示默认处于笔记列表编辑状态 + private ListEditState mState = ListEditState.NOTE_LIST; + // 定义一个变量,用于处理后台数据库查询操作的异步查询处理器,通过它可以在后台线程中执行数据库相关的查询任务,避免阻塞主线程 + private BackgroundQueryHandler mBackgroundQueryHandler; + // 定义一个变量,作为笔记列表的适配器,负责将笔记数据适配到ListView上进行展示,管理数据与视图之间的绑定关系 + private NotesListAdapter mNotesListAdapter; + // 定义一个变量,代表用于展示笔记列表的ListView控件,用户可以在界面上看到笔记列表并与之交互 + private ListView mNotesListView; + // 定义一个变量,代表“新建笔记”按钮,用户点击该按钮可触发新建笔记的操作 + private Button mAddNewNote; + // 定义一个布尔变量,用于标记是否进行事件分发操作,初始值为false,在触摸事件处理逻辑中根据具体情况进行设置和判断 + private boolean mDispatch = false; + // 定义一个整型变量,用于记录触摸事件起始的Y坐标,在处理触摸相关的操作(如滑动等)时,用于计算位置变化等情况 + private int mOriginY = 0; + // 定义一个整型变量,用于记录在分发触摸事件时的Y坐标,同样在触摸事件处理流程中发挥作用,辅助传递准确的触摸位置信息 + private int mDispatchY = 0; + // 定义一个变量,代表标题栏的TextView控件,通常用于显示页面的标题信息,也可能包含其他相关的文本提示等内容 + private TextView mTitleBar; + // 定义一个长整型变量,用于记录当前所在文件夹的ID,通过该ID可以确定当前操作的笔记所属的文件夹,方便进行数据筛选和展示等操作 + private long mCurrentFolderId; + // 定义一个变量,用于获取应用的ContentResolver实例,通过它可以与应用的内容提供器进行交互,实现数据的增删改查等操作 + private ContentResolver mContentResolver; + // 定义一个变量,作为实现了ListView的多选模式监听器和菜单项点击监听器接口的回调类实例,用于处理ListView在多选模式下以及菜单项点击时的相关业务逻辑 + private ModeCallback mModeCallBack; + // 定义一个字符串常量,作为日志输出的标签,方便在调试时通过该标签在Logcat中筛选出该类相关的日志信息,便于排查问题和查看运行状态 + private static final String TAG = "NotesListActivity"; + // 定义一个公共静态常量,用于表示笔记列表视图滚动的速率,具体的单位和实际使用场景需要结合滚动相关的具体逻辑来确定,可能是像素/单位时间等,用于控制列表滚动的速度效果 + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + // 定义一个变量,用于记录当前获取焦点的笔记数据项,通过该对象可以获取和操作当前用户关注的笔记相关的各种属性信息,比如笔记的内容、ID等 + private NoteItemData mFocusNoteDataItem; + // 定义一个静态字符串常量,作为查询普通笔记的选择条件语句,通过匹配笔记的父级ID来筛选相应的笔记,其中的问号部分在实际查询时会传入具体的文件夹ID值进行查询 + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + // 定义一个静态字符串常量,作为查询根文件夹下笔记的选择条件语句,该语句包含了比较复杂的逻辑,不仅涉及笔记类型的判断,还考虑了通话记录文件夹相关的条件,用于准确筛选出根文件夹下符合特定条件的笔记 + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + // 定义一个静态常量,作为请求码,用于标识打开笔记节点的操作,在Activity的onActivityResult方法中,通过该请求码来判断是哪个操作返回的结果,以便进行相应的后续处理 + private final static int REQUEST_CODE_OPEN_NODE = 102; + // 定义一个静态常量,作为请求码,用于标识新建笔记节点的操作,同样在onActivityResult方法中用于区分不同来源的返回结果,执行对应的逻辑 + private final static int REQUEST_CODE_NEW_NODE = 103; + + // 重写Activity的onCreate方法,该方法在Activity创建时被调用,用于进行各种初始化操作 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置该Activity对应的布局文件,这里通过R.layout.note_list找到对应的XML布局资源,将界面布局加载到Activity中 + setContentView(R.layout.note_list); + // 调用initResources方法,初始化Activity中需要使用的各种资源,如视图控件、数据适配器等 + initResources(); + + /** + * 在用户首次使用该应用时插入一个应用介绍,可能是弹出一个引导界面或者添加一条示例笔记等形式, + * 具体实现逻辑在setAppInfoFromRawRes方法中,目的是帮助用户快速了解和使用应用。 + */ + setAppInfoFromRawRes(); + } + // 重写Activity的onActivityResult方法,该方法用于接收来自其他Activity返回的结果 +@Override +protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // 首先判断返回的结果码是否为RESULT_OK,表示操作成功完成,并且请求码是打开笔记节点(REQUEST_CODE_OPEN_NODE)或者新建笔记节点(REQUEST_CODE_NEW_NODE)的请求码 + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + // 如果满足上述条件,调用笔记列表适配器(mNotesListAdapter)的changeCursor方法,并传入null参数。 + // 这可能是为了通知适配器数据发生了变化(比如新的笔记创建或者已有笔记被打开等情况),使其刷新显示内容, + // 传入null可能是让适配器重新获取数据的一种触发方式,具体取决于适配器内部的实现逻辑。 + mNotesListAdapter.changeCursor(null); + } else { + // 如果不满足上述判断条件,即返回结果不符合预期或者不是来自打开或新建笔记节点的操作, + // 则调用父类(Activity)的onActivityResult方法,继续执行默认的结果处理逻辑,确保不会遗漏其他可能的情况。 + super.onActivityResult(requestCode, resultCode, data); + } +} + +// 私有方法,用于从原始资源文件中设置应用信息,可能是创建一个初始的引导笔记之类的功能 +private void setAppInfoFromRawRes() { + // 获取默认的共享偏好设置实例,通过PreferenceManager来获取应用的默认SharedPreferences对象, + // 可以用于读取和存储一些简单的键值对形式的配置信息,比如应用的各种设置选项、首次使用标记等。 + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + // 判断在共享偏好设置中,是否不存在名为PREFERENCE_ADD_INTRODUCTION(其值为"net.micode.notes.introduction")的键对应的布尔值为true的情况, + // 即判断是否还没有添加过应用介绍信息,如果是首次使用或者还未执行过此操作,则进入以下逻辑。 + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + // 创建一个可变的字符串构建器对象,用于逐步构建要设置的应用介绍信息内容,后续会从资源文件中读取相关文本并添加到这里。 + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + // 通过当前上下文(this)获取名为introduction的原始资源文件对应的输入流, + // 这个资源文件应该是存储在项目的res/raw目录下的文本文件之类的资源,用于提供应用介绍的内容。 + in = getResources().openRawResource(R.raw.introduction); + if (in!= null) { + // 创建一个基于输入流(in)的InputStreamReader对象,它将字节流转换为字符流,方便按字符读取文件内容, + // 为后续使用BufferedReader进行高效字符读取做准备。 + InputStreamReader isr = new InputStreamReader(in); + // 创建一个BufferedReader对象,它带有缓冲功能,可以更高效地读取字符流,减少每次读取的系统开销, + // 并提高读取大文件时的性能。 + BufferedReader br = new BufferedReader(isr); + // 创建一个长度为1024的字符数组,用于每次从文件中读取一定量的字符,作为缓冲区, + // 可以根据实际文件大小和性能需求适当调整这个缓冲区大小。 + char[] buf = new char[1024]; + int len = 0; + // 通过循环不断从文件中读取字符到缓冲区buf中,只要读取的字符长度大于0,就表示还有内容可读, + // 然后将读取到的字符添加到字符串构建器sb中,逐步构建完整的文件内容字符串。 + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + // 如果无法成功打开资源文件获取输入流,在日志中输出错误信息,标记为读取介绍文件出错, + // 并直接返回,不再继续后续操作,因为没有有效的文件内容可供使用了。 + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + // 如果在读取文件过程中出现IO异常(比如文件不存在、权限问题等),打印异常的堆栈信息,方便排查问题, + // 然后直接返回,不再继续后续操作,因为出现了异常情况导致无法正常读取文件内容。 + e.printStackTrace(); + return; + } finally { + // 在无论是否出现异常的情况下,都要尝试关闭输入流,释放相关的系统资源,避免资源泄漏。 + // 如果输入流不为null,表示之前成功打开了输入流,就执行关闭操作, + // 如果关闭过程中出现异常,会再次打印异常的堆栈信息,不过不会影响程序的正常流程继续向下执行。 + if (in!= null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + // 创建一个空的工作笔记(WorkingNote)实例,通过WorkingNote类的createEmptyNote静态方法来创建, + // 传入当前上下文(this)、根文件夹的ID(Notes.ID_ROOT_FOLDER,其具体值应该在Notes类中定义,用于标识根文件夹)、 + // 无效的部件(AppWidget)ID(AppWidgetManager.INVALID_APPWIDGET_ID,用于表示不是与某个有效小部件关联的情况)、 + // 无效的部件类型(Notes.TYPE_WIDGET_INVALIDE,同样在Notes类中定义的表示无效部件类型的常量)以及一个颜色相关的值(ResourceParser.RED,具体颜色含义需看ResourceParser类定义), + // 这个工作笔记实例后续会用于存储和操作要添加的应用介绍信息内容。 + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + // 将从资源文件中读取到的内容(存储在字符串构建器sb中,通过toString方法转换为字符串)设置为工作笔记的文本内容, + // 这样就把应用介绍信息填充到了笔记对象中,准备进行保存操作。 + note.setWorkingText(sb.toString()); + // 调用工作笔记(note)的saveNote方法尝试保存这个包含应用介绍信息的笔记, + // 如果保存操作成功(saveNote方法返回true),则表示成功将应用介绍信息以笔记的形式保存到了相应的数据存储中(可能是数据库等), + // 此时将共享偏好设置中名为PREFERENCE_ADD_INTRODUCTION的键对应的布尔值设置为true,表示已经添加过应用介绍信息了, + // 并通过commit方法提交这个设置更改,使其生效。 + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + // 如果保存笔记失败,在日志中输出错误信息,标记为保存介绍笔记出错,然后直接返回,不再继续后续操作, + // 因为未能成功保存应用介绍信息到相应的数据存储中。 + Log.e(TAG, "Save introduction note error"); + return; + } + } +} + +// 重写Activity的onStart方法,该方法在Activity开始可见时被调用,通常用于执行一些需要在界面可见时启动的操作 +@Override +protected void onStart() { + super.onStart(); + // 调用startAsyncNotesListQuery方法,该方法应该是用于启动一个异步的笔记列表查询操作, + // 目的是在Activity启动并可见后,尽快获取笔记数据并展示在界面上,避免阻塞主线程,提升用户体验。 + // 具体的查询逻辑和实现细节应该在startAsyncNotesListQuery方法内部定义(此处未展示完整代码)。 + startAsyncNotesListQuery(); +} + +// 私有方法,用于初始化Activity中需要使用的各种资源,比如获取ContentResolver、设置ListView的相关属性和监听器等 +private void initResources() { + // 获取当前上下文(this)对应的ContentResolver实例,ContentResolver用于与应用的内容提供器进行交互, + // 通过它可以实现对应用内部数据(如数据库中的笔记数据等)的查询、插入、更新、删除等操作,是进行数据操作的重要接口。 + mContentResolver = this.getContentResolver(); + // 创建一个BackgroundQueryHandler实例,传入刚刚获取的ContentResolver对象, + // BackgroundQueryHandler通常用于在后台线程中处理数据库查询相关的操作,避免在主线程执行耗时的查询任务导致界面卡顿, + // 具体的查询处理逻辑应该在BackgroundQueryHandler类内部定义(此处未展示完整代码)。 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + // 将当前所在文件夹的ID设置为根文件夹的ID(Notes.ID_ROOT_FOLDER,其具体值在Notes类中定义,用于标识根文件夹), + // 表示初始状态下操作的是根文件夹下的笔记数据,后续可能会根据用户操作等情况进行更改。 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 通过findViewById方法,根据布局文件中定义的ID(R.id.notes_list)找到对应的ListView控件, + // 这个ListView就是用于展示笔记列表的视图组件,后续会将笔记数据通过适配器绑定到这个ListView上进行展示。 + mNotesListView = (ListView) findViewById(R.id.notes_list); + // 为ListView添加一个页脚视图,通过LayoutInflater从当前上下文(this)加载名为note_list_footer.xml的布局文件并实例化为视图对象, + // 添加页脚视图可以用于展示一些额外的信息或者操作按钮等,比如加载更多、提示信息等, + // 第二个参数传入null表示不需要传递额外的数据给页脚视图,第三个参数false表示这个页脚视图不是可选择的(如果是列表项则可以设置为true)。 + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + // 设置ListView的点击监听器,当用户点击列表中的某个笔记项时,会触发OnListItemClickListener类中定义的逻辑, + // 具体的点击处理逻辑在OnListItemClickListener类内部实现(此处未展示完整代码)。 + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + // 设置ListView的长按监听器为当前类(因为当前类实现了OnItemLongClickListener接口), + // 这样当用户长按列表中的笔记项时,会触发当前类中重写的onItemLongClick方法,用于处理长按相关的操作,比如弹出上下文菜单等。 + mNotesListView.setOnItemLongClickListener(this); + // 创建一个NotesListAdapter实例,传入当前上下文(this),这个适配器用于将笔记数据适配到ListView上进行展示, + // 它会管理数据与视图之间的绑定关系,比如将笔记的各个属性显示在ListView的每一项对应的视图中, + // 具体的适配逻辑在NotesListAdapter类内部定义(此处未展示完整代码)。 + mNotesListAdapter = new NotesListAdapter(this); + // 将创建好的笔记列表适配器(mNotesListAdapter)设置给ListView,使得ListView能够通过这个适配器获取数据并展示笔记列表, + // 这样就建立了数据与视图之间的关联,让用户可以在界面上看到笔记信息。 + mNotesListView.setAdapter(mNotesListAdapter); + // 通过findViewById方法,根据布局文件中定义的ID(R.id.btn_new_note)找到对应的“新建笔记”按钮控件, + // 这个按钮用于用户点击后触发新建笔记的操作,后续会设置相应的点击监听器来处理点击事件。 + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + // 设置“新建笔记”按钮的点击监听器为当前类(因为当前类实现了OnClickListener接口), + // 这样当用户点击这个按钮时,会触发当前类中重写的onClick方法,用于处理新建笔记相关的操作逻辑, + // 具体的点击处理逻辑在当前类重写的onClick方法中实现(此处未展示完整代码)。 + mAddNewNote.setOnClickListener(this); + // 设置“新建笔记”按钮的触摸监听器,通过传入一个新创建的NewNoteOnTouchListener实例来处理触摸按钮相关的操作逻辑, + // 比如触摸滑动、按下抬起等事件的处理,具体的触摸事件处理逻辑在NewNoteOnTouchListener类内部定义(此处未展示完整代码)。 + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + // 初始化用于标记是否进行事件分发的布尔变量mDispatch为false,表示初始状态下不进行事件分发操作, + // 这个变量会在触摸事件处理逻辑中根据具体情况进行更新和判断,用于控制触摸事件的流向等情况。 + mDispatch = false; + // 初始化用于记录在分发触摸事件时的Y坐标的变量mDispatchY为0,用于在触摸事件处理过程中记录和传递准确的触摸位置信息, + // 特别是在涉及触摸滑动等操作时,会根据这个坐标的变化来进行相应的逻辑处理。 + mDispatchY = 0; + // 初始化用于记录触摸事件起始的Y坐标的变量mOriginY为0,同样在触摸事件处理中起到记录起始位置的作用, + // 与mDispatchY配合使用,用于计算触摸操作过程中的位置变化等情况。 + mOriginY = 0; + // 通过findViewById方法,根据布局文件中定义的ID(R.id.tv_title_bar)找到对应的标题栏TextView控件, + // 这个TextView通常用于显示页面的标题信息,也可能包含其他相关的文本提示等内容,具体显示内容会根据应用的逻辑进行设置。 + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + // 设置当前列表的编辑状态为笔记列表状态(ListEditState.NOTE_LIST),表示初始情况下处于普通的笔记列表编辑模式, + // 后续可能会根据用户操作等情况更改为子文件夹、通话记录文件夹等其他编辑状态,不同状态下会有不同的操作逻辑和界面展示效果。 + mState = ListEditState.NOTE_LIST; + // 创建一个ModeCallback实例,这个实例实现了ListView的多选模式监听器和菜单项点击监听器接口, + // 用于处理ListView在多选模式下以及菜单项点击时的相关业务逻辑,比如显示多选操作菜单、处理菜单点击事件等, + // 具体的逻辑在ModeCallback类内部实现(此处未展示完整代码)。 + mModeCallBack = new ModeCallback(); +} + + // 定义一个私有内部类ModeCallback,它实现了ListView.MultiChoiceModeListener(用于处理ListView的多选模式相关事件) +// 和OnMenuItemClickListener(用于处理菜单项点击事件)两个接口,用于处理笔记列表在多选模式下的各种交互逻辑 +private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + // 定义一个成员变量,用于存储下拉菜单实例,这个下拉菜单可能用于展示一些额外的操作选项或者提供快捷操作入口等功能 + private DropdownMenu mDropDownMenu; + // 定义一个成员变量,用于保存当前的ActionMode实例,ActionMode用于管理多选模式下的操作界面以及相关状态等 + private ActionMode mActionMode; + // 定义一个成员变量,用于指向菜单中的“移动”菜单项,方便后续对该菜单项进行显示隐藏以及点击事件处理等操作 + private MenuItem mMoveMenu; + + // 当ListView进入多选模式时,会调用此方法来创建多选模式的操作界面,进行一些初始化设置 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // 通过菜单填充器(getMenuInflater)将名为note_list_options.xml的菜单布局文件加载到传入的菜单(menu)中, + // 这样就为多选模式创建了对应的操作菜单,菜单中的各个菜单项可以在这个布局文件中定义,包含了如删除、移动等操作选项。 + getMenuInflater().inflate(R.menu.note_list_options, menu); + // 找到菜单中ID为R.id.delete的菜单项(通常是“删除”操作菜单项),并设置它的点击监听器为当前类(this), + // 意味着点击这个“删除”菜单项时,会触发当前类中实现的onMenuItemClick方法来处理相应逻辑。 + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + // 获取菜单中ID为R.id.move的菜单项(即“移动”菜单项),并赋值给mMoveMenu变量,方便后续操作。 + mMoveMenu = menu.findItem(R.id.move); + // 判断当前获取焦点的笔记数据项(mFocusNoteDataItem)的父级ID是否等于通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER), + // 或者通过DataUtils工具类获取的用户创建的文件夹数量是否为0,如果满足这两个条件之一,说明当前情况可能不适合进行笔记移动操作。 + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + // 如果不适合移动操作,将“移动”菜单项设置为不可见,避免用户误操作或者操作无效的情况。 + mMoveMenu.setVisible(false); + } else { + // 如果适合移动操作,将“移动”菜单项设置为可见,并设置它的点击监听器为当前类(this), + // 这样点击“移动”菜单项时同样会触发当前类中实现的onMenuItemClick方法来处理移动相关的逻辑。 + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + // 将传入的ActionMode实例保存到成员变量mActionMode中,方便后续在其他方法中对这个多选模式进行操作,比如结束多选模式等。 + mActionMode = mode; + // 调用笔记列表适配器(mNotesListAdapter)的setChoiceMode方法,传入true参数,将笔记列表设置为多选模式, + // 使得ListView可以支持多选操作,用户能够选择多个笔记项进行批量操作。 + mNotesListAdapter.setChoiceMode(true); + // 将ListView的长按可点击属性设置为false,因为进入多选模式后,长按操作通常由多选模式的逻辑来处理, + // 避免长按操作出现重复响应或者冲突的情况。 + mNotesListView.setLongClickable(false); + // 将“新建笔记”按钮(mAddNewNote)的可见性设置为不可见(View.GONE),在多选模式下可能不需要显示该按钮, + // 避免干扰用户对多选操作的注意力以及操作界面的简洁性。 + mAddNewNote.setVisibility(View.GONE); + + // 通过LayoutInflater从NotesListActivity的上下文(this)加载名为note_list_dropdown_menu.xml的布局文件, + // 并实例化为一个视图对象(customView),这个视图将作为自定义的下拉菜单视图,用于展示一些额外的操作选项等。 + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + // 将刚刚创建的自定义视图(customView)设置为当前ActionMode的自定义视图,这样就可以在多选模式的界面上展示这个自定义的下拉菜单视图了。 + mode.setCustomView(customView); + // 创建一个DropdownMenu实例,传入NotesListActivity的上下文(this)、自定义视图中ID为R.id.selection_menu的按钮(通常作为触发下拉菜单展开的按钮), + // 以及名为note_list_dropdown的菜单资源(用于定义下拉菜单中的具体菜单项内容),用于管理和操作这个下拉菜单相关的逻辑。 + 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(), + // 这个操作的目的是切换笔记列表的全选状态,如果当前不是全选则全选所有笔记项,如果当前是全选则取消全选,实现一个快捷的全选/取消全选功能。 + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + // 调用updateMenu方法,用于更新下拉菜单的显示内容,比如根据当前选中的笔记数量更新菜单标题以及相关菜单项的文本和选中状态等。 + updateMenu(); + return true; + } + }); + return true; + } + + // 用于更新下拉菜单的显示内容的私有方法,根据当前选中的笔记数量等情况来调整菜单的标题、菜单项的选中状态和文本等信息 + private void updateMenu() { + // 获取笔记列表适配器(mNotesListAdapter)中当前被选中的笔记数量,通过调用getSelectedCount方法来获取。 + int selectedCount = mNotesListAdapter.getSelectedCount(); + // 通过当前上下文(getResources)获取一个格式化字符串资源,这个字符串资源的格式定义在strings.xml文件中, + // 名为menu_select_title,它可能包含一个占位符用于显示选中的笔记数量,这里将选中数量作为参数传入, + // 得到一个格式化后的字符串,用于设置下拉菜单的标题,直观地展示当前选中的笔记数量情况。 + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + // 在下拉菜单(mDropDownMenu)中查找ID为R.id.action_select_all的菜单项(通常是用于全选/取消全选的菜单项), + // 如果找到了这个菜单项(item不为null),则进行以下操作来更新它的选中状态和文本显示内容。 + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item!= null) { + // 如果笔记列表适配器(mNotesListAdapter)当前处于全选状态(通过调用isAllSelected方法判断), + // 则将这个全选菜单项设置为选中状态(setChecked(true)),并将它的标题文本设置为取消全选的提示文本(R.string.menu_deselect_all), + // 这样用户可以直观地看到当前的全选状态以及点击该菜单项后会执行的操作。 + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + // 如果笔记列表适配器当前不是全选状态,则将这个全选菜单项设置为未选中状态(setChecked(false)), + // 并将它的标题文本设置为全选的提示文本(R.string.menu_select_all),同样方便用户了解操作功能和当前状态。 + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + // 当多选模式的操作菜单需要准备显示时(例如在菜单显示前可能需要根据某些条件动态调整菜单项的可用性等情况),会调用此方法, + // 目前此方法体中没有具体实现逻辑(只是一个占位符,返回false),如果后续有相关需求,可以在这里添加相应的代码来处理准备菜单显示的操作。 + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + // 当用户点击多选模式操作菜单中的菜单项时,会调用此方法来处理相应的点击事件,目前此方法体中没有具体实现逻辑(只是一个占位符,返回false), + // 如果要处理具体的菜单项点击操作逻辑,比如根据不同菜单项执行不同的数据操作、界面更新等,需要在这里添加相应的代码来实现功能。 + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + // 当多选模式结束时(例如用户点击了返回键或者通过代码主动结束多选模式等情况),会调用此方法来进行一些清理和界面恢复操作 + public void onDestroyActionMode(ActionMode mode) { + // 调用笔记列表适配器(mNotesListAdapter)的setChoiceMode方法,传入false参数,将笔记列表从多选模式恢复为普通模式, + // 使得ListView不再支持多选操作,恢复到正常的单选项点击等交互方式。 + mNotesListAdapter.setChoiceMode(false); + // 将ListView的长按可点击属性设置为true,恢复长按操作的响应能力,因为多选模式已经结束,长按操作可以按照常规逻辑进行处理了。 + mNotesListView.setLongClickable(true); + // 将“新建笔记”按钮(mAddNewNote)的可见性设置为可见(View.VISIBLE),在多选模式结束后重新显示该按钮,方便用户继续进行新建笔记等操作。 + mAddNewNote.setVisibility(View.VISIBLE); + } + + // 用于结束当前的多选模式,通过调用保存的ActionMode实例(mActionMode)的finish方法来实现, + // 这会触发onDestroyActionMode等相关方法来进行多选模式结束后的清理和界面恢复操作。 + public void finishActionMode() { + mActionMode.finish(); + } + + // 当ListView中的某个笔记项的选中状态发生改变时(例如用户点击选择或者取消选择某个笔记项),会调用此方法来处理相应的逻辑 + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + // 调用笔记列表适配器(mNotesListAdapter)的setCheckedItem方法,传入笔记项的位置(position)和新的选中状态(checked)参数, + // 用于更新笔记列表适配器中对应笔记项的选中状态记录,确保数据和界面显示的选中状态保持一致。 + mNotesListAdapter.setCheckedItem(position, checked); + // 调用updateMenu方法,更新下拉菜单的显示内容,因为笔记项的选中状态改变了,可能需要相应地更新菜单标题、全选菜单项状态等信息, + // 保持下拉菜单展示的信息与当前选中情况相符。 + updateMenu(); + } + + // 当用户点击菜单项时(无论是多选模式操作菜单还是下拉菜单中的菜单项),会调用此方法来处理相应的点击事件逻辑 + public boolean onMenuItemClick(MenuItem item) { + // 首先判断笔记列表适配器(mNotesListAdapter)中当前被选中的笔记数量是否为0,如果是0,表示没有选中任何笔记项, + // 在这种情况下,弹出一个Toast提示信息,告知用户没有选中任何笔记,显示的文本内容通过getString方法获取strings.xml文件中定义的 + // menu_select_none字符串资源,显示时长为短暂(Toast.LENGTH_SHORT),然后直接返回true,表示已经处理了这个点击事件,避免继续执行其他不必要的逻辑。 + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + // 根据点击的菜单项的ID(item.getItemId())来判断具体是哪个菜单项被点击了,然后执行相应的业务逻辑。 + switch (item.getItemId()) { + case R.id.delete: + // 如果点击的是“删除”菜单项(ID为R.id.delete),创建一个AlertDialog.Builder实例,用于构建一个确认删除的对话框, + // 传入NotesListActivity的上下文(this),以便对话框能够正确显示在当前界面上。 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题文本,通过getString方法获取strings.xml文件中定义的alert_title_delete字符串资源作为标题内容, + // 通常用于提示用户当前操作是删除相关的操作。 + builder.setTitle(getString(R.string.alert_title_delete)); + // 设置对话框的图标,使用Android系统自带的警告图标(android.R.drawable.ic_dialog_alert),增强提示效果,让用户直观地知道这是一个重要操作的提示对话框。 + builder.setIcon(android.R.drawable.ic_dialog_alert); + // 设置对话框的消息内容,通过getString方法获取strings.xml文件中定义的alert_message_delete_notes字符串资源, + // 并传入笔记列表适配器中当前被选中的笔记数量(mNotesListAdapter.getSelectedCount())作为参数进行格式化, + // 这样消息内容可以准确地提示用户将要删除的笔记数量,让用户明确操作的影响范围。 + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + // 设置对话框的“确定”按钮(PositiveButton),传入Android系统自带的确认文本(android.R.string.ok)作为按钮文本, + // 并设置点击监听器,当用户点击“确定”按钮时,会触发这里定义的逻辑,即调用batchDelete方法来执行批量删除选中笔记的操作, + // 具体的批量删除逻辑应该在batchDelete方法中定义(此处未展示完整代码)。 + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + // 设置对话框的“取消”按钮(NegativeButton),传入Android系统自带的取消文本(android.R.string.cancel)作为按钮文本, + // 并设置点击监听器为null,表示点击“取消”按钮时不执行额外的操作,只是关闭对话框,取消当前的删除操作。 + builder.setNegativeButton(android.R.string.cancel, null); + // 最后调用show方法显示这个构建好的确认删除对话框,让用户进行确认操作,确保重要的删除操作是经过用户明确同意的。 + builder.show(); + break; + case R.id.move: + // 如果点击的是“移动”菜单项(ID为R.id.move),调用startQueryDestinationFolders方法, + // 这个方法应该是用于启动查询目标文件夹的操作,比如弹出一个界面让用户选择要将选中的笔记移动到哪个文件夹, + // 具体的查询目标文件夹逻辑应该在startQueryDestinationFolders方法中定义(此处未展示完整代码)。 + startQueryDestinationFolders(); + break; + default: + // 如果点击的菜单项不是上述处理的“删除”或“移动”菜单项,返回false,表示当前类没有处理这个菜单项的点击事件, + // 可能会按照默认的菜单项点击逻辑或者由其他相关的监听器来继续处理这个点击事件。 + return false; + } + return true; + } +} + + // 定义一个私有内部类NewNoteOnTouchListener,它实现了OnTouchListener接口,用于处理“新建笔记”按钮(mAddNewNote)的触摸事件 +private class NewNoteOnTouchListener implements OnTouchListener { + + // 重写OnTouchListener接口中的onTouch方法,该方法会在“新建笔记”按钮被触摸时触发,根据触摸事件的不同动作(如按下、移动、抬起等)执行相应的逻辑 + public boolean onTouch(View v, MotionEvent event) { + // 根据触摸事件(event)的动作类型(通过event.getAction()获取)进行不同的处理,这里使用switch语句来区分不同的动作情况 + switch (event.getAction()) { + // 当触摸事件的动作为ACTION_DOWN,即手指按下按钮时的情况 + case MotionEvent.ACTION_DOWN: { + // 获取当前窗口的默认显示对象,用于获取屏幕相关的信息,比如屏幕的尺寸等 + Display display = getWindowManager().getDefaultDisplay(); + // 通过显示对象获取屏幕的高度,单位通常是像素,用于后续计算触摸位置等相关操作 + int screenHeight = display.getHeight(); + // 获取“新建笔记”按钮(mAddNewNote)的高度,单位同样是像素,用于确定按钮在屏幕中的位置以及触摸区域的判断等 + int newNoteViewHeight = mAddNewNote.getHeight(); + // 计算一个起始位置坐标,这里是屏幕高度减去“新建笔记”按钮的高度,大致表示按钮底部在屏幕中的垂直位置坐标, + // 后续可以基于这个坐标来判断触摸点是否在按钮的有效区域或者透明区域等范围内。 + int start = screenHeight - newNoteViewHeight; + // 计算触摸事件的Y坐标,在起始位置坐标的基础上加上触摸点相对按钮左上角的Y坐标偏移量(通过event.getY()获取), + // 这样得到的eventY就是触摸点在整个屏幕坐标系下相对按钮底部位置的Y坐标值,方便后续的位置判断和逻辑处理。 + int eventY = start + (int) event.getY(); + + /** + * 如果当前列表的编辑状态(mState)是子文件夹状态(ListEditState.SUB_FOLDER), + * 需要减去标题栏(mTitleBar)的高度,因为在这种情况下,触摸位置的计算可能需要考虑标题栏占据的空间, + * 使得触摸坐标的计算更加准确,符合实际的界面布局和交互逻辑。同时也对起始位置start做同样的高度减去操作,保持计算的一致性。 + */ + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + + /** + * HACKME注释部分:这里描述了一种针对“新建笔记”按钮透明部分触摸事件处理的特殊逻辑,这种处理方式不太理想,只是为了满足UI设计师的特定要求。 + * 当点击“新建笔记”按钮的透明部分时,需要将触摸事件分发给按钮后面的列表视图(ListView,即mNotesListView)进行处理。 + * “新建笔记”按钮的透明部分可以用公式 y = -0.12x + 94(单位:像素)以及按钮的上边界来表示,这里的坐标是基于“新建笔记”按钮的左侧边缘为原点的坐标系。 + * 其中的94表示透明部分的最大高度,需要注意的是,如果按钮的背景发生变化,这个公式也需要相应地改变,因为透明部分的形状和范围可能会随之改变。 + */ + if (event.getY() < (event.getX() * (-0.12) + 94)) { + // 获取ListView中的最后一个可见子视图(不包含页脚视图),通过getChildAt方法来获取, + // 传入的参数是ListView的子视图总数减去1(获取最后一个子视图)再减去ListView的页脚视图数量, + // 这样得到的就是列表中实际显示数据的最后一个子视图,用于后续判断触摸点是否在这个子视图的相关范围内。 + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + // 判断获取到的这个子视图不为空,并且子视图的底部坐标大于前面计算的起始位置start, + // 同时子视图的顶部坐标小于起始位置加上透明部分最大高度(start + 94),这意味着触摸点在这个子视图与“新建笔记”按钮透明部分重叠的有效范围内。 + if (view!= null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + // 记录触摸事件起始的Y坐标(相对按钮左上角),通过event.getY()获取并赋值给mOriginY变量, + // 这个坐标用于后续在触摸移动等操作中计算位置变化量。 + mOriginY = (int) event.getY(); + // 将前面计算的基于按钮底部位置的触摸事件Y坐标(已经考虑了各种情况调整后的坐标)赋值给mDispatchY变量, + // 用于在后续触摸事件分发等操作中传递准确的触摸位置信息。 + mDispatchY = eventY; + // 通过event.setLocation方法重新设置触摸事件的坐标位置,将X坐标保持不变(event.getX()), + // Y坐标设置为前面计算的mDispatchY,这样就调整了触摸事件的坐标,使其符合后续分发到ListView的坐标要求。 + event.setLocation(event.getX(), mDispatchY); + // 将mDispatch标记变量设置为true,表示当前需要进行触摸事件的分发操作,后续在触摸移动和抬起等操作中会根据这个标记来执行相应的逻辑。 + mDispatch = true; + // 调用ListView(mNotesListView)的dispatchTouchEvent方法,将调整后的触摸事件(event)分发给ListView进行处理, + // 这样就实现了将“新建笔记”按钮透明部分的触摸事件传递给列表视图,让列表视图能够响应用户在这个特殊区域的触摸操作, + // 并返回ListView对这个触摸事件处理的结果(true表示处理了该事件,false表示未处理)。 + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + // 当触摸事件的动作为ACTION_MOVE,即手指在按钮上滑动时的情况 + case MotionEvent.ACTION_MOVE: { + // 判断mDispatch标记变量是否为true,即前面在ACTION_DOWN阶段是否已经确定需要进行触摸事件的分发操作, + // 如果是,则进行以下触摸事件坐标调整和分发逻辑。 + if (mDispatch) { + // 根据当前触摸点的Y坐标(event.getY())与触摸起始的Y坐标(mOriginY)的差值,更新mDispatchY变量, + // 这样mDispatchY就始终保持着触摸事件在分发过程中的准确Y坐标位置,反映了手指在屏幕上滑动过程中的位置变化。 + mDispatchY += (int) event.getY() - mOriginY; + // 通过event.setLocation方法再次重新设置触摸事件的坐标位置,X坐标保持不变(event.getX()), + // Y坐标设置为更新后的mDispatchY,确保传递给ListView的触摸事件坐标是准确的,符合实际的触摸位置变化情况。 + event.setLocation(event.getX(), mDispatchY); + // 调用ListView(mNotesListView)的dispatchTouchEvent方法,将更新坐标后的触摸事件(event)继续分发给ListView进行处理, + // 使得ListView能够持续响应手指在按钮透明区域滑动过程中的触摸操作,并返回处理结果(true或false)。 + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + // 当触摸事件的动作为默认情况(除了ACTION_DOWN和ACTION_MOVE之外的其他动作,比如手指抬起等情况) + default: { + // 同样先判断mDispatch标记变量是否为true,即之前是否进行了触摸事件的分发操作,如果是,则进行以下收尾操作。 + if (mDispatch) { + // 再次通过event.setLocation方法设置触摸事件的坐标位置,将当前的mDispatchY坐标赋值给触摸事件的Y坐标, + // 确保最后一次传递给ListView的触摸事件坐标是准确的,虽然此时触摸动作已经结束,但可能ListView还需要根据这个最终坐标来处理一些收尾逻辑。 + event.setLocation(event.getX(), mDispatchY); + // 将mDispatch标记变量设置为false,表示触摸事件分发操作结束,后续如果再有触摸操作,需要重新根据ACTION_DOWN阶段的逻辑来判断是否进行分发。 + mDispatch = false; + // 调用ListView(mNotesListView)的dispatchTouchEvent方法,将最终调整后的触摸事件(event)分发给ListView进行处理, + // 让ListView能够处理触摸动作结束时的相关逻辑,比如判断是否触发了点击、滑动结束等操作,并返回处理结果(true或false)。 + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + // 如果触摸事件不属于上述处理的情况(比如触摸点不在按钮透明区域等),返回false,表示当前类没有处理这个触摸事件, + // 可能会按照默认的触摸事件处理逻辑或者由其他相关的监听器来继续处理这个触摸事件。 + return false; + } +}; + + // 启动异步笔记列表查询的方法,用于在后台获取笔记列表数据并更新界面显示 +private void startAsyncNotesListQuery() { + // 根据当前所在文件夹的ID(mCurrentFolderId)来确定查询的选择条件语句(selection)。 + // 如果当前所在文件夹是根文件夹(mCurrentFolderId == Notes.ID_ROOT_FOLDER),则使用根文件夹的查询条件(ROOT_FOLDER_SELECTION), + // 否则使用普通的通过父级ID查询笔记的条件(NORMAL_SELECTION)。这样可以根据不同的文件夹情况准确筛选出要展示的笔记数据。 + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER)? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + // 调用BackgroundQueryHandler(继承自AsyncQueryHandler,用于在后台处理数据库查询操作)的startQuery方法,启动一个异步查询任务。 + // 参数含义如下: + // FOLDER_NOTE_LIST_QUERY_TOKEN:是此次查询任务的令牌(token),用于在查询完成后区分不同类型的查询任务,这里表示是文件夹内笔记列表的查询任务。 + // null:一般用于传递额外的上下文相关信息(cookie),这里暂时传入null,表示没有额外信息需要传递。 + // Notes.CONTENT_NOTE_URI:代表笔记数据的内容URI,用于指定从哪里获取笔记数据,类似于数据库表的地址,指明了要查询的数据来源。 + // NoteItemData.PROJECTION:是一个字符串数组,用于指定要从查询结果中返回的列名,即确定查询出来的数据具体包含哪些字段信息,类似于数据库查询语句中的SELECT子句指定要查询的列。 + // selection:前面根据文件夹情况确定的查询条件语句,用于筛选符合条件的笔记数据,类似数据库查询语句中的WHERE子句。 + // new String[] { String.valueOf(mCurrentFolderId) }:是查询条件语句中占位符(如果有的话)对应的具体值,这里将当前文件夹的ID转换为字符串后作为参数传入,用于替换查询条件中的相应占位符,确保查询的准确性。 + // NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC":是指定查询结果的排序方式,按照笔记的类型(NoteColumns.TYPE)降序排列,若类型相同再按照修改日期(NoteColumns.MODIFIED_DATE)降序排列,确保查询结果按照特定顺序返回,方便界面展示等操作。 + 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"); +} + +// 定义一个私有内部类BackgroundQueryHandler,继承自AsyncQueryHandler,用于在后台线程处理数据库查询相关的操作,并在查询完成后进行相应的后续处理 +private final class BackgroundQueryHandler extends AsyncQueryHandler { + // 构造方法,接收一个ContentResolver对象,调用父类(AsyncQueryHandler)的构造方法,将ContentResolver传递给父类, + // 使得该查询处理器能够通过ContentResolver与内容提供器进行交互,实现对数据库等数据源的查询操作。 + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + // 重写AsyncQueryHandler的onQueryComplete方法,该方法会在异步查询任务完成后被调用,根据不同的查询任务令牌(token)来执行相应的后续处理逻辑 + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + // 根据查询任务令牌(token)进行判断,区分不同类型的查询任务并执行对应的操作。 + switch (token) { + // 如果令牌是FOLDER_NOTE_LIST_QUERY_TOKEN,表示是文件夹内笔记列表的查询任务完成了,执行以下操作。 + case FOLDER_NOTE_LIST_QUERY_TOKEN: + // 调用笔记列表适配器(mNotesListAdapter)的changeCursor方法,传入查询得到的游标(cursor)对象, + // 这样适配器就可以根据新的游标数据更新笔记列表的显示内容,将从数据库中查询到的最新笔记信息展示在界面上。 + mNotesListAdapter.changeCursor(cursor); + break; + // 如果令牌是FOLDER_LIST_QUERY_TOKEN,表示是文件夹列表查询任务完成了,执行以下操作。 + case FOLDER_LIST_QUERY_TOKEN: + // 判断游标(cursor)不为空并且游标中的数据行数大于0,即查询到了有效的文件夹数据,才执行展示文件夹列表菜单的操作(showFolderListMenu方法)。 + if (cursor!= null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + // 如果游标为空或者没有查询到数据,在日志中输出错误信息,标记为查询文件夹失败,方便排查问题,了解为什么没有获取到期望的文件夹数据。 + Log.e(TAG, "Query folder failed"); + } + break; + // 如果是其他未知的令牌,表示不是预期的查询任务完成情况,直接返回,不做任何处理。 + default: + return; + } + } +} + +// 展示文件夹列表菜单的方法,通常会弹出一个对话框,里面包含可供选择的文件夹列表,用户可以选择目标文件夹进行相关操作(比如移动笔记到选择的文件夹等) +private void showFolderListMenu(Cursor cursor) { + // 创建一个AlertDialog.Builder实例,用于构建一个对话框,传入NotesListActivity的上下文(this),以便对话框能够正确显示在当前界面上。 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题文本,通过R.string.menu_title_select_folder获取在strings.xml文件中定义的对应字符串资源作为标题内容, + // 一般用于提示用户当前对话框是用于选择文件夹的操作。 + builder.setTitle(R.string.menu_title_select_folder); + // 创建一个FoldersListAdapter实例,传入当前上下文(this)和查询得到的游标(cursor),这个适配器用于将文件夹数据适配到对话框的列表展示中, + // 使得对话框能够以列表形式展示各个文件夹选项,方便用户选择,具体的适配逻辑在FoldersListAdapter类内部定义(此处未展示完整代码)。 + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + // 设置对话框的列表适配器,将前面创建的文件夹列表适配器(adapter)设置给对话框,这样对话框就会展示文件夹列表内容, + // 并设置点击监听器,当用户点击列表中的某个文件夹选项时,会触发这里定义的逻辑,即调用onClick方法进行相应的操作处理。 + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + // 调用DataUtils工具类的batchMoveToFolder方法,传入ContentResolver(用于操作数据)、笔记列表适配器中当前选中的笔记项的ID数组(mNotesListAdapter.getSelectedItemIds()), + // 以及用户点击选择的文件夹的ID(通过adapter.getItemId(which)获取,which是点击的文件夹在列表中的位置索引), + // 目的是将选中的笔记批量移动到用户选择的文件夹中,具体的移动逻辑在DataUtils类的batchMoveToFolder方法中定义(此处未展示完整代码)。 + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + // 弹出一个Toast提示信息,告知用户笔记移动操作的结果,通过getString方法获取strings.xml文件中定义的format_move_notes_to_folder字符串资源, + // 并传入两个参数:笔记列表适配器中当前选中的笔记数量(mNotesListAdapter.getSelectedCount())以及用户选择的文件夹名称(通过adapter.getFolderName方法获取), + // 这样可以显示一个包含具体移动笔记数量和目标文件夹名称的提示信息,让用户清楚操作的结果,显示时长为短暂(Toast.LENGTH_SHORT)。 + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + // 调用ModeCallback实例(mModeCallBack)的finishActionMode方法,结束当前可能处于的多选模式(比如在多选笔记后进行移动操作的情况), + // 该方法会进行一些界面恢复和相关状态清理的操作,具体逻辑在ModeCallback类的finishActionMode方法中定义(此处未展示完整代码)。 + mModeCallBack.finishActionMode(); + } + }); + // 最后调用show方法显示这个构建好的包含文件夹列表的对话框,让用户能够进行文件夹选择操作。 + builder.show(); +} + +// 创建新笔记的方法,用于启动一个新的Activity,进入笔记编辑界面,让用户可以创建新的笔记内容 +private void createNewNote() { + // 创建一个Intent对象,用于启动一个新的Activity,指定要启动的Activity类为NoteEditActivity.class, + // 这意味着点击“新建笔记”按钮等操作触发此方法后,会跳转到NoteEditActivity这个用于编辑笔记的界面。 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作(Action)为Intent.ACTION_INSERT_OR_EDIT,这个动作通常表示要进行插入或编辑数据的操作, + // 在这里明确了进入NoteEditActivity界面是为了创建新笔记(插入操作)或者编辑已有笔记(虽然此处主要是新建,但可能复用该界面逻辑用于编辑)。 + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 通过Intent的putExtra方法,向即将启动的Activity传递额外的数据,这里传递的是当前所在文件夹的ID(mCurrentFolderId), + // 键名为Notes.INTENT_EXTRA_FOLDER_ID,在NoteEditActivity中可以通过获取这个额外数据来确定新笔记所属的文件夹等相关信息,方便后续操作。 + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + // 调用当前上下文(this)的startActivityForResult方法,传入创建好的Intent和请求码(REQUEST_CODE_NEW_NODE), + // 启动NoteEditActivity,并期望在该Activity操作完成后返回结果,通过请求码可以在onActivityResult方法中区分不同来源的返回结果,进行相应的后续处理。 + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); +} + +// 批量删除笔记的方法,通过异步任务(AsyncTask)在后台执行删除操作,同时根据是否处于同步模式等情况进行不同的处理逻辑 +private void batchDelete() { + // 创建一个继承自AsyncTask的匿名内部类实例,用于在后台线程执行批量删除笔记的操作,并在操作完成后在主线程进行相应的界面更新等后续处理。 + new AsyncTask>() { + // 重写AsyncTask的doInBackground方法,这个方法在后台线程中执行,用于执行耗时的批量删除笔记操作以及相关的数据处理逻辑,返回一个HashSet类型的结果,用于在后续操作中传递一些与小部件(AppWidget)相关的属性信息。 + protected HashSet doInBackground(Void... unused) { + // 获取笔记列表适配器(mNotesListAdapter)中当前选中的小部件相关属性信息集合(HashSet), + // 这些属性可能与笔记关联的小部件显示等情况有关,用于后续在必要时更新小部件的显示等操作。 + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 判断是否不处于同步模式(通过isSyncMode方法判断,此处未展示该方法具体实现,但应该是返回一个布尔值表示是否同步状态), + if (!isSyncMode()) { + // 如果不是同步模式,直接删除选中的笔记,调用DataUtils工具类的batchDeleteNotes方法,传入ContentResolver(用于操作数据)和笔记列表适配器中当前选中的笔记项的ID数组(mNotesListAdapter.getSelectedItemIds()), + // 尝试执行批量删除操作,如果删除成功则继续后续逻辑,如果删除失败,在日志中输出错误信息,标记为删除笔记出错,这里理论上不应该出现错误,方便排查问题。 + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 如果处于同步模式,将选中的笔记移动到回收站文件夹(Notes.ID_TRASH_FOLER,其具体值在Notes类中定义,用于标识回收站文件夹), + // 调用DataUtils工具类的batchMoveToFolder方法,传入ContentResolver、选中的笔记项的ID数组以及回收站文件夹的ID, + // 尝试执行批量移动操作,如果移动失败,在日志中输出错误信息,标记为移动笔记到回收站文件夹出错,同样这里理论上不应该出现错误,方便排查问题。 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + // 将获取到的小部件相关属性信息集合(widgets)作为结果返回,以便在后续的onPostExecute方法中使用这些信息进行相关操作。 + return widgets; + } + + // 重写AsyncTask的onPostExecute方法,这个方法在后台的doInBackground方法执行完成后,在主线程中被调用,用于根据后台操作的结果进行界面更新等后续处理。 + @Override + protected void onPostExecute(HashSet widgets) { + // 判断返回的小部件相关属性信息集合(widgets)不为空,即存在与笔记关联的小部件相关属性信息,才进行以下操作。 + if (widgets!= null) { + // 遍历小部件相关属性信息集合中的每个AppWidgetAttribute对象(widget),进行相应的小部件更新操作。 + for (AppWidgetAttribute widget : widgets) { + // 判断小部件的ID(widget.widgetId)不是无效的小部件ID(AppWidgetManager.INVALID_APPWIDGET_ID),并且小部件的类型(widget.widgetType)也不是无效的小部件类型(Notes.TYPE_WIDGET_INVALIDE), + // 只有满足这两个条件的有效小部件才执行更新操作,避免对无效的小部件进行不必要的操作。 + if (widget.widgetId!= AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType!= Notes.TYPE_WIDGET_INVALIDE) { + // 调用updateWidget方法(此处未展示该方法具体实现,但应该是用于更新小部件显示等相关操作的方法),传入小部件的ID(widget.widgetId)和小部件的类型(widget.widgetType), + // 对符合条件的小部件进行更新,确保小部件的显示内容等与笔记数据的变化保持一致,比如笔记被删除或移动后,小部件显示相应更新。 + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + // 调用ModeCallback实例(mModeCallBack)的finishActionMode方法,结束当前可能处于的多选模式(比如在多选笔记后进行删除操作的情况), + // 进行一些界面恢复和相关状态清理的操作,如恢复ListView的长按可点击属性、重新显示“新建笔记”按钮等,具体逻辑在ModeCallback类的finishActionMode方法中定义(此处未展示完整代码)。 + mModeCallBack.finishActionMode(); + } + }.execute(); +} + + private void deleteFolder(long folderId) { + // 这段代码可能是用于处理文件夹相关操作的逻辑,比如删除文件夹等情况,根据文件夹是否处于同步模式来执行不同的处理方式,并对相关的小部件进行更新 +if (folderId == Notes.ID_ROOT_FOLDER) { + // 如果传入的文件夹ID(folderId)等于根文件夹的ID(Notes.ID_ROOT_FOLDER),则在日志中输出错误信息, + // 提示不应该出现这种情况,因为根文件夹通常不应该被当作普通文件夹进行某些操作(比如删除等),然后直接返回,不再继续后续操作。 + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; +} + +// 创建一个HashSet集合,用于存储要操作的文件夹ID,这里先将传入的folderId添加到集合中,方便后续批量操作时使用这个集合传递参数。 +HashSet ids = new HashSet(); +ids.add(folderId); + +// 通过DataUtils工具类的getFolderNoteWidget方法,获取与指定文件夹(folderId)相关联的小部件属性信息集合(HashSet), +// 这些小部件属性信息可能包含小部件的ID、类型等内容,用于后续在文件夹操作后对相关小部件进行相应的更新操作,确保小部件显示与文件夹数据的变化保持一致。 +HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + +// 判断是否不处于同步模式(通过isSyncMode方法判断,此处未展示该方法具体实现,但应该是返回一个布尔值表示是否同步状态) +if (!isSyncMode()) { + // 如果不是同步模式,直接删除文件夹对应的笔记,调用DataUtils工具类的batchDeleteNotes方法,传入ContentResolver(用于操作数据)和包含文件夹ID的集合(ids), + // 执行批量删除操作,这里可能意味着将该文件夹下的所有笔记都删除,具体的删除逻辑在DataUtils类的batchDeleteNotes方法中定义(此处未展示完整代码)。 + DataUtils.batchDeleteNotes(mContentResolver, ids); +} else { + // 如果处于同步模式,将该文件夹下的笔记移动到回收站文件夹(Notes.ID_TRASH_FOLER,其具体值在Notes类中定义,用于标识回收站文件夹), + // 调用DataUtils工具类的batchMoveToFolder方法,传入ContentResolver、包含文件夹ID的集合(ids)以及回收站文件夹的ID, + // 执行批量移动操作,将相关笔记移动到回收站,具体的移动逻辑在DataUtils类的batchMoveToFolder方法中定义(此处未展示完整代码)。 + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); +} + +// 判断获取到的与文件夹相关的小部件属性信息集合(widgets)不为空,即存在与该文件夹关联的小部件相关属性信息,才进行以下操作,对这些小部件进行更新。 +if (widgets!= null) { + // 遍历小部件属性信息集合中的每个AppWidgetAttribute对象(widget),进行相应的小部件更新操作。 + for (AppWidgetAttribute widget : widgets) { + // 判断小部件的ID(widget.widgetId)不是无效的小部件ID(AppWidgetManager.INVALID_APPWIDGET_ID),并且小部件的类型(widget.widgetType)也不是无效的小部件类型(Notes.TYPE_WIDGET_INVALIDE), + // 只有满足这两个条件的有效小部件才执行更新操作,避免对无效的小部件进行不必要的操作,确保只更新与实际相关且有效的小部件显示等内容。 + if (widget.widgetId!= AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType!= Notes.TYPE_WIDGET_INVALIDE) { + // 调用updateWidget方法(此处未展示该方法具体实现,但应该是用于更新小部件显示等相关操作的方法),传入小部件的ID(widget.widgetId)和小部件的类型(widget.widgetType), + // 对符合条件的小部件进行更新,使其显示内容等与文件夹及笔记数据的变化保持一致,比如文件夹被删除或移动后,小部件显示相应更新。 + updateWidget(widget.widgetId, widget.widgetType); + } + } +} + +// 用于打开一个笔记节点的方法,通常是跳转到笔记编辑界面(NoteEditActivity)并以查看(VIEW)模式打开指定的笔记数据,方便用户查看笔记详情 +private void openNode(NoteItemData data) { + // 创建一个Intent对象,用于启动一个新的Activity,指定要启动的Activity类为NoteEditActivity.class, + // 意味着此操作会跳转到NoteEditActivity这个用于编辑笔记的界面,在这里主要用于查看笔记内容。 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置Intent的动作(Action)为Intent.ACTION_VIEW,明确表示此次进入NoteEditActivity界面是为了查看笔记,而不是进行编辑等其他操作。 + intent.setAction(Intent.ACTION_VIEW); + // 通过Intent的putExtra方法,向即将启动的Activity传递额外的数据,这里传递的是笔记的唯一标识符(data.getId()), + // 键名为Intent.EXTRA_UID,在NoteEditActivity中可以通过获取这个额外数据来确定要查看的具体笔记,方便加载相应的笔记内容进行展示。 + intent.putExtra(Intent.EXTRA_UID, data.getId()); + // 调用当前上下文(this)的startActivityForResult方法,传入创建好的Intent和请求码(REQUEST_CODE_OPEN_NODE), + // 启动NoteEditActivity,并期望在该Activity操作完成后返回结果,通过请求码可以在onActivityResult方法中区分不同来源的返回结果,进行相应的后续处理。 + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); +} + +// 用于打开一个文件夹的方法,根据传入的笔记数据(NoteItemData类型的数据,可能包含文件夹相关信息)来更新当前界面显示的文件夹相关内容,比如切换显示的笔记列表、更新标题栏等 +private void openFolder(NoteItemData data) { + // 将当前所在文件夹的ID(mCurrentFolderId)设置为传入的笔记数据对应的ID(data.getId()), + // 这样后续操作(如查询笔记列表等)就会基于新打开的这个文件夹来进行,确保获取和展示该文件夹下的相关笔记数据。 + mCurrentFolderId = data.getId(); + // 调用startAsyncNotesListQuery方法,启动一个异步的笔记列表查询操作,目的是获取并展示新打开的文件夹下的笔记列表信息, + // 使得界面能够及时更新显示新文件夹对应的笔记内容,具体的查询逻辑在startAsyncNotesListQuery方法中定义(此处未展示完整代码)。 + startAsyncNotesListQuery(); + // 判断如果打开的文件夹ID(data.getId())等于通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER),则将当前列表的编辑状态(mState)设置为通话记录文件夹状态(ListEditState.CALL_RECORD_FOLDER), + // 并且将“新建笔记”按钮(mAddNewNote)的可见性设置为不可见(View.GONE),因为在通话记录文件夹下可能不允许新建笔记等操作,具体根据业务逻辑设定。 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + // 如果不是通话记录文件夹,则将当前列表的编辑状态设置为子文件夹状态(ListEditState.SUB_FOLDER),表示处于普通的子文件夹操作模式。 + mState = ListEditState.SUB_FOLDER; + } + // 再次判断如果打开的文件夹ID等于通话记录文件夹的ID,将标题栏(mTitleBar)的文本内容设置为通话记录文件夹的名称(通过R.string.call_record_folder_name获取在strings.xml文件中定义的对应字符串资源), + // 用于在界面上准确显示当前打开的文件夹名称,方便用户识别。 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + // 如果不是通话记录文件夹,将标题栏的文本内容设置为传入的笔记数据中的摘要信息(data.getSnippet()),同样用于展示当前打开文件夹的相关提示信息,可能是文件夹的简短描述等内容。 + mTitleBar.setText(data.getSnippet()); + } + // 将标题栏(mTitleBar)的可见性设置为可见(View.VISIBLE),确保标题栏在界面上显示出来,展示相应的文件夹名称等信息。 + mTitleBar.setVisibility(View.VISIBLE); +} + +// 实现了OnClickListener接口的onClick方法,用于处理各种点击事件,根据点击的视图(View)的ID来判断具体是哪个控件被点击了,并执行相应的操作逻辑 +public void onClick(View v) { + switch (v.getId()) { + // 如果点击的视图的ID是“新建笔记”按钮的ID(R.id.btn_new_note),则调用createNewNote方法, + // 该方法用于启动一个新的Activity进入笔记编辑界面,让用户可以创建新的笔记内容,具体逻辑在createNewNote方法中定义(此处未展示完整代码)。 + case R.id.btn_new_note: + createNewNote(); + break; + default: + break; + } +} + +// 用于显示软键盘的方法,通过获取系统的输入法服务(InputMethodManager)来强制显示软键盘 +private void showSoftInput() { + // 获取系统的输入法服务,通过getSystemService方法传入Context.INPUT_METHOD_SERVICE参数来获取InputMethodManager实例, + // 这个实例用于管理输入法相关的操作,比如显示、隐藏软键盘等。 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 判断获取到的输入法管理器(inputMethodManager)不为空,即成功获取到输入法服务后,才执行显示软键盘的操作。 + if (inputMethodManager!= null) { + // 调用输入法管理器的toggleSoftInput方法,传入InputMethodManager.SHOW_FORCED(表示强制显示软键盘)和0(表示无额外的标志位参数), + // 触发软键盘显示在屏幕上,方便用户输入内容,比如在需要用户输入文本的编辑框获取焦点时可以调用此方法来显示键盘。 + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } +} + +// 用于隐藏软键盘的方法,通过获取系统的输入法服务(InputMethodManager)并根据指定的视图(View)来隐藏软键盘 +private void hideSoftInput(View view) { + // 获取系统的输入法服务,与前面显示软键盘的获取方式相同,通过getSystemService方法传入Context.INPUT_METHOD_SERVICE参数来获取InputMethodManager实例。 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 调用输入法管理器的hideSoftInputFromWindow方法,传入视图的窗口令牌(view.getWindowToken(),用于标识该视图所在的窗口,输入法管理器通过这个令牌来确定要隐藏键盘的相关窗口)和0(表示无额外的标志位参数), + // 触发隐藏软键盘的操作,使得软键盘从屏幕上消失,通常在不需要用户输入内容或者编辑操作完成后调用此方法来隐藏键盘,提升界面整洁性和用户体验。 + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); +} + +// 用于显示创建或修改文件夹对话框的方法,根据传入的布尔值(create)来确定是创建文件夹还是修改文件夹的操作模式,并进行相应的对话框初始化设置 +private void showCreateOrModifyFolderDialog(final boolean create) { + // 创建一个AlertDialog.Builder实例,用于构建一个对话框,传入当前上下文(this),以便对话框能够正确显示在当前界面上。 + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 通过LayoutInflater从当前上下文(this)加载名为dialog_edit_text.xml的布局文件,并实例化为一个视图对象(view), + // 这个布局文件应该包含了对话框中用于输入文件夹名称等相关的控件内容,比如可能有一个EditText用于输入文件夹名字。 + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + // 从加载的视图(view)中通过findViewById方法找到ID为R.id.et_foler_name的EditText控件,用于后续获取用户输入的文件夹名称等操作, + // 这个EditText就是对话框中供用户输入文件夹名称的文本编辑框。 + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + // 调用showSoftInput方法,显示软键盘,方便用户在弹出的对话框中直接输入文件夹名称,提升操作便捷性。 + showSoftInput(); + // 根据传入的布尔值(create)来判断是创建文件夹还是修改文件夹的操作,如果create为false,表示是修改文件夹操作,执行以下逻辑。 + if (!create) { + // 判断当前获取焦点的笔记数据项(mFocusNoteDataItem)不为空,因为修改文件夹名称可能需要基于已有的某个笔记数据项相关信息来进行, + // 如果为空则无法进行修改操作,在日志中输出错误信息提示数据项为空,然后直接返回,不再继续后续操作。 + if (mFocusNoteDataItem!= null) { + // 如果有有效的笔记数据项,将EditText(etName)的文本内容设置为当前获取焦点的笔记数据项中的摘要信息(mFocusNoteDataItem.getSnippet()), + // 这样可以将原文件夹名称等相关信息预先填充到编辑框中,方便用户修改,具体填充内容根据业务逻辑和数据项中的信息而定。 + etName.setText(mFocusNoteDataItem.getSnippet()); + // 设置对话框的标题文本,通过getString方法获取strings.xml文件中定义的menu_folder_change_name字符串资源作为标题内容, + // 用于提示用户当前对话框是用于修改文件夹名称的操作。 + builder.setTitle(getString(R.string.menu_folder_change_name)); + } else { + Log.e(TAG, "The long click data item is null"); + return; + } + } else { + // 如果create为true,表示是创建文件夹操作,将EditText(etName)的文本内容设置为空字符串,准备让用户输入新的文件夹名称。 + etName.setText(""); + // 设置对话框的标题文本,通过this.getString方法获取strings.xml文件中定义的menu_create_folder字符串资源作为标题内容, + // 用于提示用户当前对话框是用于创建文件夹的操作。 + builder.setTitle(this.getString(R.string.menu_create_folder)); + } + + // 设置对话框的“确定”按钮(PositiveButton),传入Android系统自带的确认文本(android.R.string.ok)作为按钮文本,这里暂时将点击监听器设置为null, + // 可能后续还需要根据具体业务逻辑来添加点击“确定”按钮后的具体操作处理代码,比如验证文件夹名称合法性、创建或修改文件夹等操作。 + builder.setPositiveButton(android.R.string.ok, null); + // 设置对话框的“取消”按钮(NegativeButton),传入Android系统自带的取消文本(android.R.string.cancel)作为按钮文本, + // 并设置点击监听器,当用户点击“取消”按钮时,会触发这里定义的逻辑,即调用hideSoftInput方法隐藏软键盘(传入当前的EditText控件etName),避免软键盘在对话框关闭后仍然显示,影响界面美观和后续操作。 + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etName); + } + }); +} + + // 通过之前创建的AlertDialog.Builder对象(builder)设置要显示的视图(view),然后调用show方法显示对话框,并将显示后的对话框实例赋值给dialog变量, +// 这样后续就可以通过这个dialog变量来操作对话框内的各种控件以及处理相关逻辑。 +final Dialog dialog = builder.setView(view).show(); + +// 从显示的对话框(dialog)中通过findViewById方法查找ID为android.R.id.button1的按钮,在Android系统默认的对话框布局中, +// 这个ID通常对应的是“确定”按钮(PositiveButton),不同的主题或系统版本可能会遵循此约定来标识该按钮,方便后续为其添加点击事件处理逻辑。 +final Button positive = (Button) dialog.findViewById(android.R.id.button1); + +// 为查找到的“确定”按钮(positive)设置点击监听器,当用户点击该按钮时,会触发以下定义的onClick方法内的逻辑。 +positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + // 调用hideSoftInput方法隐藏软键盘,传入当前用于输入文件夹名称的EditText控件(etName),这样做是为了在用户完成对话框操作后, + // 关闭软键盘,避免其继续显示在屏幕上影响界面美观以及后续可能的操作交互,提升用户体验。 + hideSoftInput(etName); + + // 获取EditText(etName)中的文本内容,并转换为字符串形式,该字符串将作为文件夹的名称用于后续的创建或修改文件夹操作判断等逻辑中。 + String name = etName.getText().toString(); + + // 调用DataUtils工具类的checkVisibleFolderName方法,传入ContentResolver(用于与应用的数据提供器交互,实现数据操作)和获取到的文件夹名称(name), + // 该方法用于检查给定的文件夹名称是否已经存在(具体的检查逻辑在DataUtils类的checkVisibleFolderName方法内部实现,此处未展示完整代码), + // 如果检查发现文件夹名称已存在,执行以下操作。 + if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + // 弹出一个Toast提示信息,告知用户文件夹已存在,通过getString方法获取strings.xml文件中定义的folder_exist字符串资源, + // 并将当前的文件夹名称(name)作为参数传入进行格式化,使得提示信息能够明确指出哪个文件夹名称已存在, + // 显示时长设置为较长(Toast.LENGTH_LONG),以便用户能清晰地看到提示内容,知晓当前操作存在的问题。 + Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), + Toast.LENGTH_LONG).show(); + // 将EditText(etName)的光标定位到文本开头(0位置),并选中整个文本内容(etName.length()),这样做方便用户直接修改已存在的文件夹名称, + // 用户无需手动重新定位光标或全选文本,提升了操作的便捷性,使其可以快速对重复的名称进行修改后再次尝试操作。 + etName.setSelection(0, etName.length()); + return; + } + + // 判断当前是否是修改文件夹操作(create为false表示修改操作),如果是修改操作,执行以下逻辑。 + if (!create) { + // 进一步判断获取到的文件夹名称不为空字符串(通过TextUtils.isEmpty方法进行判断,若返回false则表示不为空), + // 只有名称不为空时才进行实际的文件夹信息更新操作,避免更新空名称导致数据异常等情况。 + if (!TextUtils.isEmpty(name)) { + // 创建一个ContentValues对象,它用于存储要更新的数据,以键值对的形式表示,类似于数据库中的插入或更新操作时的数据封装形式, + // 键对应数据库表中的列名,值对应要插入或更新的具体数据内容。 + ContentValues values = new ContentValues(); + // 将文件夹的摘要信息(通常可以理解为文件夹名称等相关描述信息,对应数据库中的NoteColumns.SNIPPET列)设置为新获取的名称(name), + // 通过put方法将列名和对应的值添加到ContentValues对象中,以便后续更新操作使用。 + values.put(NoteColumns.SNIPPET, name); + // 设置文件夹的类型为普通文件夹类型(Notes.TYPE_FOLDER,其具体值在Notes类中定义,用于明确标识该数据为文件夹类型), + // 同样使用put方法将NoteColumns.TYPE列名与对应的文件夹类型值添加到ContentValues对象中,确保数据的类型信息准确更新。 + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + // 设置文件夹的本地修改标志为1,表示该文件夹有本地修改操作发生,这个标志可能用于后续的数据同步等相关逻辑判断, + // 通过put方法将NoteColumns.LOCAL_MODIFIED列名与值1添加到ContentValues对象中,记录文件夹的修改状态信息。 + values.put(NoteColumns.LOCAL_MODIFIED, 1); + + // 通过ContentResolver的update方法来执行实际的文件夹信息更新操作,传入以下参数: + // Notes.CONTENT_NOTE_URI:代表笔记数据的内容URI,它指明了要更新的数据所在的数据源位置,类似于数据库表的地址, + // 告知系统从哪里获取和更新相应的数据,这里明确了是针对笔记相关的数据进行操作。 + // values:前面创建的包含要更新的文件夹相关信息的ContentValues对象,其中封装了要更新的列名及对应的值, + // 系统会根据这些信息来更新相应的数据记录。 + // NoteColumns.ID + "=?":是更新操作的条件语句,用于指定要更新的具体文件夹记录,通过文件夹的ID来进行筛选, + // 这里的问号是占位符,后续需要传入具体的ID值来确定要更新的是哪条文件夹记录。 + // new String[] { String.valueOf(mFocusNoteDataItem.getId()) }:是前面条件语句中占位符对应的具体值, + // 将当前获取焦点的笔记数据项(mFocusNoteDataItem)的ID转换为字符串后作为参数传入,这样就能准确更新对应的文件夹信息了。 + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + // 如果当前是创建文件夹操作(create为true),并且获取到的文件夹名称也不为空字符串,执行以下创建文件夹的逻辑。 + ContentValues values = new ContentValues(); + // 将文件夹的摘要信息(对应NoteColumns.SNIPPET列)设置为新获取的名称(name),通过put方法添加到ContentValues对象中, + // 这就确定了新创建文件夹的名称信息。 + values.put(NoteColumns.SNIPPET, name); + // 设置文件夹的类型为普通文件夹类型(Notes.TYPE_FOLDER),同样使用put方法添加到ContentValues对象中,明确创建的是文件夹类型的数据记录。 + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + + // 通过ContentResolver的insert方法执行实际的文件夹创建操作,传入以下参数: + // Notes.CONTENT_NOTE_URI:代表笔记数据的内容URI,指明了要在哪个数据源位置插入新的文件夹数据记录, + // 告知系统在哪里创建新的文件夹相关信息。 + // values:前面创建的包含要插入的文件夹相关信息的ContentValues对象,其中封装了新文件夹的名称和类型等信息, + // 系统会根据这些信息在指定数据源中插入一条新的文件夹记录。 + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + + // 无论当前是创建还是修改文件夹操作完成后,都调用dialog的dismiss方法关闭对话框,使得界面恢复到正常状态, + // 避免对话框一直显示在屏幕上影响用户后续的操作和界面的整洁性。 + dialog.dismiss(); + } +}); + +// 判断EditText(etName)中的文本内容是否为空字符串(通过TextUtils.isEmpty方法判断),如果为空, +// 则将“确定”按钮(positive)设置为不可用状态(通过setEnabled(false)方法实现),这样可以防止用户在没有输入文件夹名称的情况下点击“确定”按钮, +// 避免无效的操作发生,提升用户操作的准确性和逻辑性。 +if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); +} + +// 以下是为EditText(etName)添加文本变化监听器(TextWatcher)的逻辑,用于实时监听文本内容的变化情况,并根据文本是否为空来动态设置“确定”按钮的可用状态。 +etName.addTextChangedListener(new TextWatcher() { + // 在文本内容即将发生变化前会调用此方法,目前此方法体中没有具体实现逻辑(只是一个占位符,通常如果有需要在文本变化前进行的操作可以在这里添加代码)。 + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // TODO Auto-generated method stub + } + + // 当文本内容发生变化时会调用此方法,在这里根据EditText(etName)中的文本是否为空来动态设置“确定”按钮(positive)的可用状态。 + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + // 在文本内容变化完成后会调用此方法,目前此方法体中没有具体实现逻辑(只是一个占位符,通常如果有需要在文本变化后进行的操作可以在这里添加代码)。 + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + } +}); +// 重写Activity的onBackPressed方法,用于处理用户按下手机返回键时的操作逻辑,根据当前列表的编辑状态(mState)来执行不同的操作。 +@Override +public void onBackPressed() { + switch (mState) { + // 如果当前列表编辑状态是子文件夹状态(SUB_FOLDER),执行以下逻辑。 + case SUB_FOLDER: + // 将当前所在文件夹的ID(mCurrentFolderId)设置为根文件夹的ID(Notes.ID_ROOT_FOLDER),意味着从当前子文件夹返回,回到根文件夹层级。 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 将列表编辑状态(mState)设置为笔记列表状态(ListEditState.NOTE_LIST),表示回到普通的笔记列表展示模式。 + mState = ListEditState.NOTE_LIST; + // 调用startAsyncNotesListQuery方法,启动一个异步的笔记列表查询操作,目的是获取并展示根文件夹下的笔记列表信息, + // 使得界面能够及时更新显示回到根文件夹后的笔记内容,具体的查询逻辑在startAsyncNotesListQuery方法中定义(此处未展示完整代码)。 + startAsyncNotesListQuery(); + // 将标题栏(mTitleBar)的可见性设置为不可见(View.GONE),因为回到根文件夹后可能不需要显示之前子文件夹对应的标题栏内容了, + // 这样可以保持界面的简洁性,符合不同层级下的界面展示需求。 + mTitleBar.setVisibility(View.GONE); + break; + // 如果当前列表编辑状态是通话记录文件夹状态(CALL_RECORD_FOLDER),执行以下逻辑。 + case CALL_RECORD_FOLDER: + // 将当前所在文件夹的ID(mCurrentFolderId)设置为根文件夹的ID(Notes.ID_ROOT_FOLDER),同样是从通话记录文件夹返回,回到根文件夹层级。 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 将列表编辑状态(mState)设置为笔记列表状态(ListEditState.NOTE_LIST),切换回普通的笔记列表展示模式。 + mState = ListEditState.NOTE_LIST; + // 将“新建笔记”按钮(mAddNewNote)的可见性设置为可见(View.VISIBLE),因为回到根文件夹后通常需要显示该按钮,方便用户继续进行新建笔记等操作, + // 而在通话记录文件夹状态下可能之前将其隐藏了(根据业务逻辑设定)。 + mAddNewNote.setVisibility(View.VISIBLE); + // 将标题栏(mTitleBar)的可见性设置为不可见(View.GONE),与前面子文件夹返回时类似,回到根文件夹后不需要显示通话记录文件夹对应的标题栏内容了。 + mTitleBar.setVisibility(View.GONE); + // 调用startAsyncNotesListQuery方法,启动异步笔记列表查询操作,获取并展示根文件夹下的笔记列表信息,更新界面显示。 + startAsyncNotesListQuery(); + break; + // 如果当前列表编辑状态是笔记列表状态(NOTE_LIST),直接调用父类(Activity)的onBackPressed方法, + // 这意味着按照系统默认的返回键处理逻辑进行操作,比如可能会关闭当前Activity或者执行其他默认的返回相关行为,具体取决于Activity的任务栈等相关机制。 + case NOTE_LIST: + super.onBackPressed(); + break; + default: + break; + } +} + + // 此方法用于更新桌面小部件(Widget) +private void updateWidget(int appWidgetId, int appWidgetType) { + // 创建一个意图(Intent),指定动作为更新桌面小部件的动作 + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据传入的小部件类型(appWidgetType)来设置不同的小部件提供类(Widget Provider Class) + 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; + } + + // 将指定的小部件ID添加到意图的额外数据中,这里是以数组形式传递单个小部件ID + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + appWidgetId + }); + + // 发送广播,触发对应的小部件更新操作 + sendBroadcast(intent); + // 设置结果为操作成功(RESULT_OK),并关联对应的意图 + setResult(RESULT_OK, intent); +} + +// 定义一个上下文菜单创建监听器(OnCreateContextMenuListener),用于处理特定视图的上下文菜单创建事件 +private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + // 如果当前有焦点的笔记数据项(mFocusNoteDataItem)不为空 + if (mFocusNoteDataItem!= null) { + // 设置上下文菜单的标题为焦点笔记数据项的摘要信息(片段内容) + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + // 向菜单中添加一个菜单项,用于查看文件夹,菜单项ID为MENU_FOLDER_VIEW,显示文本通过资源字符串获取 + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + // 向菜单中添加一个菜单项,用于删除文件夹,菜单项ID为MENU_FOLDER_DELETE,显示文本通过资源字符串获取 + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + // 向菜单中添加一个菜单项,用于更改文件夹名称,菜单项ID为MENU_FOLDER_CHANGE_NAME,显示文本通过资源字符串获取 + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } + } +}; + +// 当上下文菜单关闭时触发的回调方法 +@Override +public void onContextMenuClosed(Menu menu) { + // 如果笔记列表视图(mNotesListView)不为空,则移除其上下文菜单创建监听器 + if (mNotesListView!= null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + // 调用父类的onContextMenuClosed方法,执行默认的或父类中定义的关闭后逻辑 + super.onContextMenuClosed(menu); +} + +// 当上下文菜单中的某个菜单项被选中时触发的回调方法 +@Override +public boolean onContextItemSelected(MenuItem item) { + // 如果当前有焦点的笔记数据项(mFocusNoteDataItem)为空,则记录错误日志并返回false,表示不处理该事件 + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + // 根据选中菜单项的ID进行不同的操作处理 + switch (item.getItemId()) { + case MENU_FOLDER_VIEW: + // 调用openFolder方法打开对应的文件夹(具体功能由openFolder方法实现) + openFolder(mFocusNoteDataItem); + break; + case MENU_FOLDER_DELETE: + // 创建一个警告对话框构建器(AlertDialog.Builder) + 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)); + // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时调用deleteFolder方法删除文件夹(传入对应文件夹的ID) + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); + } + }); + // 设置对话框的取消按钮(负按钮),点击时不做额外操作(传入null) + builder.setNegativeButton(android.R.string.cancel, null); + // 显示构建好的警告对话框 + builder.show(); + break; + case MENU_FOLDER_CHANGE_NAME: + // 调用showCreateOrModifyFolderDialog方法,传入false参数,用于显示创建或修改文件夹名称的对话框(具体功能由该方法实现) + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + + // 返回true表示已处理该菜单项选择事件 + return true; +} + +// 在准备选项菜单(Options Menu)时触发的回调方法,用于动态设置菜单内容 +@Override +public boolean onPrepareOptionsMenu(Menu menu) { + // 先清空菜单中的所有菜单项 + menu.clear(); + // 根据当前所处的编辑状态(mState)来动态加载不同的菜单布局资源 + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // 根据同步服务(GTaskSyncService)是否正在同步来设置"同步"菜单项的标题文本(显示同步或取消同步) + 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; +} + +// 当选项菜单中的某个菜单项被选中时触发的回调方法,用于处理不同菜单项对应的操作逻辑 +@Override +public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_folder: { + // 调用showCreateOrModifyFolderDialog方法,传入true参数,用于显示创建或修改文件夹的对话框(具体功能由该方法实现) + showCreateOrModifyFolderDialog(true); + break; + } + case R.id.menu_export_text: { + // 调用exportNoteToText方法,用于将笔记导出为文本文件(具体功能由该方法实现) + exportNoteToText(); + break; + } + case R.id.menu_sync: { + // 判断是否处于同步模式(isSyncMode方法的具体逻辑未在代码中体现) + if (isSyncMode()) { + // 如果当前菜单项标题为"同步",则调用startSync方法启动同步服务(GTaskSyncService) + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + GTaskSyncService.startSync(this); + } else { + // 如果当前菜单项标题为"取消同步",则调用cancelSync方法取消同步服务 + GTaskSyncService.cancelSync(this); + } + } else { + // 如果不处于同步模式,则调用startPreferenceActivity方法启动偏好设置相关的活动(具体功能由该方法实现) + startPreferenceActivity(); + } + break; + } + case R.id.menu_setting: { + // 调用startPreferenceActivity方法启动偏好设置相关的活动(具体功能由该方法实现) + startPreferenceActivity(); + break; + } + case R.id.menu_new_note: { + // 调用createNewNote方法创建新的笔记(具体功能由该方法实现) + createNewNote(); + break; + } + case R.id.menu_search: + // 调用onSearchRequested方法,触发搜索相关的操作(具体功能由该方法实现) + onSearchRequested(); + break; + default: + break; + } + return true; +} + + // 当触发搜索请求时调用的方法 +@Override +public boolean onSearchRequested() { + // 调用startSearch方法来启动搜索功能,传入相关参数,这里传入的参数分别表示:搜索关键词为null、不启用语音搜索(false)、应用数据为null、不限制搜索范围(false) + startSearch(null, false, null /* appData */, false); + // 返回true表示已处理搜索请求 + return true; +} + +// 用于将笔记导出为文本文件的方法 +private void exportNoteToText() { + // 获取BackupUtils的单例实例,传入当前的NotesListActivity作为上下文 + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + // 创建一个异步任务(AsyncTask),用于在后台执行导出操作,其参数类型依次为输入参数(这里不需要,所以是Void)、进度参数(这里不需要,所以是Void)、结果参数(返回的是一个整数,用于表示导出操作的状态) + new AsyncTask() { + + // 在后台线程中执行的方法,用于执行实际的导出到文本的操作,返回导出操作的状态码(由BackupUtils的exportToText方法决定) + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + // 在后台操作完成后,在主线程中执行的方法,用于根据返回的结果状态码来进行相应的提示处理 + @Override + protected void onPostExecute(Integer result) { + // 如果结果状态码表示SD卡未挂载 + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + // 创建一个警告对话框构建器(AlertDialog.Builder),传入当前的NotesListActivity作为上下文 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题,通过资源字符串获取对应的文本 + builder.setTitle(NotesListActivity.this.getString(R.string.failed_sdcard_export)); + // 设置对话框的提示信息,通过资源字符串获取对应的文本 + builder.setMessage(NotesListActivity.this.getString(R.string.error_sdcard_unmounted)); + // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时不做额外操作(传入null) + builder.setPositiveButton(android.R.string.ok, null); + // 显示构建好的警告对话框 + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + // 创建一个警告对话框构建器(AlertDialog.Builder),传入当前的NotesListActivity作为上下文 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题,通过资源字符串获取对应的文本 + builder.setTitle(NotesListActivity.this.getString(R.string.success_sdcard_export)); + // 设置对话框的提示信息,通过资源字符串获取对应的格式化文本,其中格式化参数通过BackupUtils实例获取导出的文本文件名和文件目录 + builder.setMessage(NotesListActivity.this.getString( + R.string.format_exported_file_location, backup.getExportedTextFileName(), backup.getExportedTextFileDir())); + // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时不做额外操作(传入null) + builder.setPositiveButton(android.R.string.ok, null); + // 显示构建好的警告对话框 + builder.show(); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + // 创建一个警告对话框构建器(AlertDialog.Builder),传入当前的NotesListActivity作为上下文 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题,通过资源字符串获取对应的文本 + builder.setTitle(NotesListActivity.this.getString(R.string.failed_sdcard_export)); + // 设置对话框的提示信息,通过资源字符串获取对应的文本 + builder.setMessage(NotesListActivity.this.getString(R.string.error_sdcard_export)); + // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时不做额外操作(传入null) + builder.setPositiveButton(android.R.string.ok, null); + // 显示构建好的警告对话框 + builder.show(); + } + } + + }.execute(); +} + +// 判断是否处于同步模式的方法,通过获取同步账户名称(从NotesPreferenceActivity中获取)并检查其去除空格后的长度是否大于0来判断 +private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} + +// 用于启动偏好设置相关活动(NotesPreferenceActivity)的方法 +private void startPreferenceActivity() { + // 获取启动该活动的源Activity,如果当前Activity有父Activity,则使用父Activity,否则使用当前Activity本身 + Activity from = getParent()!= null? getParent() : this; + // 创建一个意图(Intent),指定要启动的目标Activity为NotesPreferenceActivity.class + Intent intent = new Intent(from, NotesPreferenceActivity.class); + // 根据意图启动活动,如果活动已经存在则复用(根据传入的请求码 -1 来决定具体行为,这里暂不详细分析请求码相关细节) + from.startActivityIfNeeded(intent, -1); +} + +// 实现了OnItemClickListener接口的内部类,用于处理列表项点击事件 +private class OnListItemClickListener implements OnItemClickListener { + + // 当列表项被点击时触发的方法 + public void onItemClick(AdapterView parent, View view, int position, long id) { + // 如果被点击的视图是NotesListItem类型的实例 + if (view instanceof NotesListItem) { + // 获取该视图对应的笔记数据项(NoteItemData) + NoteItemData item = ((NotesListItem) view).getItemData(); + // 如果笔记列表适配器(mNotesListAdapter)处于选择模式(例如多选模式等) + if (mNotesListAdapter.isInChoiceMode()) { + // 如果数据项类型是笔记(Notes.TYPE_NOTE) + if (item.getType() == Notes.TYPE_NOTE) { + // 调整位置参数,减去列表头部视图的数量,以获取在实际数据列表中的正确位置 + position = position - mNotesListView.getHeaderViewsCount(); + // 调用回调接口(mModeCallBack)的方法,通知该项的选中状态发生改变,传入相关参数,这里传入的参数表示:视图为null(可能在某些情况下不需要具体视图引用)、调整后的位置、数据项ID、取反当前该项是否已被选中的状态(即切换选中状态) + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + return; + } + + // 根据当前所处的状态(mState)来进行不同的操作处理 + switch (mState) { + case NOTE_LIST: + // 如果数据项类型是文件夹(Notes.TYPE_FOLDER)或者系统类型(Notes.TYPE_SYSTEM) + if (item.getType() == Notes.TYPE_FOLDER || item.getType() == Notes.TYPE_SYSTEM) { + // 调用openFolder方法打开对应的文件夹(具体功能由openFolder方法实现) + openFolder(item); + } else if (item.getType() == Notes.TYPE_NOTE) { + // 调用openNode方法打开对应的笔记节点(具体功能由openNode方法实现) + openNode(item); + } else { + // 如果数据项类型不符合预期,则记录错误日志 + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + // 如果数据项类型是笔记(Notes.TYPE_NOTE) + if (item.getType() == Notes.TYPE_NOTE) { + // 调用openNode方法打开对应的笔记节点(具体功能由openNode方法实现) + openNode(item); + } else { + // 如果数据项类型不符合预期,则记录错误日志 + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + default: + break; + } + } + } + +} + +// 用于启动查询目标文件夹的方法 +private void startQueryDestinationFolders() { + // 构建查询条件字符串(SQL语句中的WHERE子句部分),用于筛选符合特定条件的文件夹记录 + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + // 根据当前所处的编辑状态(mState)来进一步调整查询条件,如果处于NOTE_LIST状态,则使用原查询条件,否则添加额外的条件(与根文件夹相关的条件) + selection = (mState == ListEditState.NOTE_LIST)? selection : + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + // 通过后台查询处理器(mBackgroundQueryHandler)启动一个查询操作,传入相关参数,包括查询令牌(用于标识查询任务)、查询结果回调(这里传入null,可能后续有其他处理方式)、查询的内容URI(指向笔记相关的内容提供器的URI)、查询投影(指定要查询返回的列信息)、构建好的查询条件字符串、查询条件的参数值数组(对应查询条件中?占位符的值)以及排序方式(按照修改日期降序排列) + 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"); +} + +// 当列表项被长按(长按点击)时触发的方法 +public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + // 如果被长按的视图是NotesListItem类型的实例 + if (view instanceof NotesListItem) { + // 获取该视图对应的笔记数据项(NoteItemData),并赋值给mFocusNoteDataItem,用于后续操作(比如上下文菜单等操作可能会用到) + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + // 如果数据项类型是笔记(Notes.TYPE_NOTE)并且笔记列表适配器(mNotesListAdapter)不处于选择模式 + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE &&!mNotesListAdapter.isInChoiceMode()) { + // 尝试启动一个操作模式(例如上下文操作模式等),如果启动成功(返回不为null) + if (mNotesListView.startActionMode(mModeCallBack)!= null) { + // 调用回调接口(mModeCallBack)的方法,通知该项的选中状态变为已选中(传入相关参数,这里传入的参数表示:视图为null(可能在某些情况下不需要具体视图引用)、位置、数据项ID、选中状态为true) + 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)设置上下文菜单创建监听器为之前定义的mFolderOnCreateContextMenuListener,这样长按文件夹时可以弹出相应的上下文菜单 + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + // 返回false,可能表示长按操作未完全消费该事件,具体行为可能取决于调用该方法的上层逻辑 + return false; +} +} diff --git a/ui/NotesListAdapter.java b/ui/NotesListAdapter.java new file mode 100644 index 0000000..bbd52a7 --- /dev/null +++ b/ui/NotesListAdapter.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// 包声明,表明该类所属的包名,用于在项目中进行类的组织和管理 +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +// NotesListAdapter类继承自CursorAdapter,用于适配数据库游标数据到视图,以便在列表等控件中展示笔记相关的数据 +public class NotesListAdapter extends CursorAdapter { + // 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息 + private static final String TAG = "NotesListAdapter"; + // 保存上下文信息,用于后续创建视图、获取资源等操作 + private Context mContext; + // 使用HashMap来记录每个位置(索引)对应的选中状态,键为位置索引(整数),值为是否选中(布尔值) + private HashMap mSelectedIndex; + // 记录笔记的数量,用于一些判断和统计相关操作 + private int mNotesCount; + // 表示当前是否处于选择模式(例如多选模式等),true表示处于选择模式,false表示不在选择模式 + private boolean mChoiceMode; + + // 内部静态类,用于封装桌面小部件(App Widget)的相关属性,这里包含小部件的ID和类型两个属性 + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + // 构造函数,用于初始化NotesListAdapter实例 + public NotesListAdapter(Context context) { + // 调用父类(CursorAdapter)的构造函数,传入上下文和初始游标(这里传入null,可能后续会通过其他方法设置游标) + super(context, null); + // 初始化mSelectedIndex,创建一个新的HashMap用于记录选中状态 + mSelectedIndex = new HashMap(); + // 保存传入的上下文信息 + mContext = context; + // 初始化笔记数量为0 + mNotesCount = 0; + } + + // 该方法用于创建一个新的视图(View),在列表中每个数据项对应的视图需要通过此方法创建,这里返回一个NotesListItem类型的新视图实例,传入当前上下文作为参数来创建视图 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + // 该方法用于将游标中的数据绑定到已创建的视图上,使得视图能够展示对应的数据内容 + @Override + public void bindView(View view, Context context, Cursor cursor) { + // 如果视图是NotesListItem类型的实例 + if (view instanceof NotesListItem) { + // 根据传入的上下文和游标数据创建一个NoteItemData实例,用于获取和解析具体的数据信息 + NoteItemData itemData = new NoteItemData(context, cursor); + // 调用NotesListItem的bind方法,将相关数据和状态传递给视图进行绑定展示,包括上下文、笔记数据项、是否处于选择模式以及当前位置对应的选中状态等信息 + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + // 用于设置指定位置的选中状态,将指定位置和对应的选中状态存入mSelectedIndex中,并通知数据集已发生改变,使得视图能够根据新的选中状态进行更新显示 + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); + } + + // 用于判断当前是否处于选择模式,返回mChoiceMode的值,true表示处于选择模式,false表示不在选择模式 + public boolean isInChoiceMode() { + return mChoiceMode; + } + + // 用于设置选择模式的开关,当设置为选择模式时(传入参数mode为true),先清空之前的选中状态记录(mSelectedIndex),然后更新mChoiceMode的值 + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + // 用于全选或全不选所有笔记数据项(根据传入的checked参数决定),遍历游标中的所有数据项,对于类型为笔记(Notes.TYPE_NOTE)的数据项,调用setCheckedItem方法设置其选中状态 + 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集合,创建一个HashSet用于存储Long类型的ID,遍历mSelectedIndex中所有已选中的位置(键),通过getItemId方法获取对应的数据项ID,并添加到HashSet中,同时对根文件夹ID(Notes.ID_ROOT_FOLDER)进行特殊判断和日志记录(如果出现则认为是错误情况) + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Long id = getItemId(position); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + + return itemSet; + } + + // 用于获取所有已选中的桌面小部件(App Widget)相关属性的集合,创建一个HashSet用于存储AppWidgetAttribute实例,遍历mSelectedIndex中所有已选中的位置(键),通过getItem方法获取对应的数据项游标(Cursor),然后从中解析出小部件的ID和类型信息,封装成AppWidgetAttribute实例添加到HashSet中,同时对无效游标情况进行日志记录和错误处理(如果游标为null则返回null) + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + if (c!= null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + /** + * Don't close cursor here, only the adapter could close it + */ + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + // 用于获取已选中的数据项的数量,获取mSelectedIndex中所有值(布尔值,表示选中状态)的集合,通过迭代器遍历该集合,统计值为true(即选中)的数量并返回 + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (null == values) { + return 0; + } + Iterator iter = values.iterator(); + int count = 0; + while (iter.hasNext()) { + if (true == iter.next()) { + count++; + } + } + return count; + } + + // 用于判断是否所有的数据项都已被选中,通过比较已选中的数据项数量(getSelectedCount方法获取)和总笔记数量(mNotesCount)来判断,当已选中数量不为0且等于总笔记数量时,返回true,表示全选状态,否则返回false + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount!= 0 && checkedCount == mNotesCount); + } + + // 用于判断指定位置的数据项是否被选中,根据mSelectedIndex中对应位置的键值(布尔值)来判断,如果对应位置的值为null,则返回false,表示未选中,否则返回该位置对应的实际选中状态值 + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; + } + return mSelectedIndex.get(position); + } + + // 当数据集内容发生改变时触发的回调方法,这里先调用父类的onContentChanged方法执行默认操作,然后调用calcNotesCount方法重新计算笔记的数量 + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); + } + + // 当游标(Cursor)发生改变时触发的回调方法,这里先调用父类的changeCursor方法执行默认操作,然后调用calcNotesCount方法重新计算笔记的数量 + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + // 私有方法,用于计算笔记的数量,先将mNotesCount重置为0,然后遍历所有数据项(通过getCount方法获取数量),获取对应位置的数据项游标,判断其数据类型是否为笔记(Notes.TYPE_NOTE),如果是则数量加1,同时对无效游标情况进行日志记录和错误处理(如果游标为null则直接返回) + 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++; + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + } + } +} \ No newline at end of file diff --git a/ui/NotesListItem.java b/ui/NotesListItem.java new file mode 100644 index 0000000..f79030b --- /dev/null +++ b/ui/NotesListItem.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// 包声明,表明该类所属的包名,用于在项目中进行类的组织和管理 +package net.micode.notes.ui; + +import android.content.Context; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + +// NotesListItem类继承自LinearLayout,意味着它可以作为一个线性布局容器,用于组织和展示笔记相关的各个子视图组件,呈现出特定的笔记列表项样式 +public class NotesListItem extends LinearLayout { + // 用于显示提醒相关图标的ImageView,比如可能用于显示闹钟图标表示有提醒设置等情况 + private ImageView mAlert; + // 用于显示笔记标题的TextView + private TextView mTitle; + // 用于显示时间相关信息(如修改时间等)的TextView + private TextView mTime; + // 用于显示通话名称相关信息的TextView(可能在特定与通话记录相关的笔记场景下使用) + private TextView mCallName; + // 保存当前笔记列表项对应的笔记数据对象,用于后续获取和展示各项数据 + private NoteItemData mItemData; + // 用于多选等操作时显示的复选框(CheckBox),用于标记该项是否被选中 + private CheckBox mCheckBox; + + // 构造函数,用于初始化NotesListItem实例,传入上下文信息(用于获取资源等操作) + public NotesListItem(Context context) { + super(context); + // 通过inflate方法将布局资源(R.layout.note_item)加载到当前的LinearLayout中,使得该类能够展示对应的布局样式 + inflate(context, R.layout.note_item, this); + // 通过findViewById方法从加载的布局中找到对应的视图组件,以便后续操作和设置数据 + 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); + } + + // 用于将数据绑定到视图组件上,根据传入的不同数据和状态参数来设置各个视图组件的显示内容和可见性等属性 + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 如果处于选择模式(choiceMode为true)并且数据类型是笔记(Notes.TYPE_NOTE) + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + // 将复选框设置为可见,以便用户可以看到并操作选中状态 + mCheckBox.setVisibility(View.VISIBLE); + // 根据传入的checked参数设置复选框的选中状态 + mCheckBox.setChecked(checked); + } else { + // 如果不满足上述条件,则将复选框隐藏起来 + mCheckBox.setVisibility(View.GONE); + } + + // 保存传入的笔记数据对象,方便后续获取数据进行展示等操作 + mItemData = data; + + // 如果数据项的ID是通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER) + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 隐藏通话名称相关的TextView,因为在通话记录文件夹情况下可能不需要显示这个信息 + mCallName.setVisibility(View.GONE); + // 将提醒图标设置为可见,可能用于表示该文件夹有特殊的提醒相关属性或者状态 + mAlert.setVisibility(View.VISIBLE); + // 设置标题的文本外观样式,通过资源ID指定(R.style.TextAppearancePrimaryItem) + 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())); + // 设置提醒图标的资源图片,这里使用通话记录相关的图标(R.drawable.call_record) + mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 如果数据项的父ID是通话记录文件夹的ID,说明该数据项可能是通话记录文件夹下的具体内容 + mCallName.setVisibility(View.VISIBLE); + // 设置通话名称TextView的文本内容为数据中获取的通话名称 + mCallName.setText(data.getCallName()); + // 设置标题的文本外观样式,通过资源ID指定(R.style.TextAppearanceSecondaryItem) + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); + // 设置标题的文本内容为格式化后的摘要信息(通过DataUtils工具类的方法进行格式化) + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 如果数据项有提醒设置(通过hasAlert方法判断) + if (data.hasAlert()) { + // 设置提醒图标的资源图片为闹钟图标(R.drawable.clock),并将其设置为可见 + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + // 如果没有提醒设置,则隐藏提醒图标 + mAlert.setVisibility(View.GONE); + } + } else { + // 对于其他情况(既不是通话记录文件夹本身,也不是其下的具体内容) + mCallName.setVisibility(View.GONE); + // 设置标题的文本外观样式,通过资源ID指定(R.style.TextAppearancePrimaryItem) + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + // 如果数据类型是文件夹(Notes.TYPE_FOLDER) + 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())); + // 如果数据项有提醒设置(通过hasAlert方法判断) + if (data.hasAlert()) { + // 设置提醒图标的资源图片为闹钟图标(R.drawable.clock),并将其设置为可见 + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + // 如果没有提醒设置,则隐藏提醒图标 + mAlert.setVisibility(View.GONE); + } + } + } + + // 设置时间TextView的文本内容,通过DateUtils工具类的方法将数据中的修改时间转换为相对时间格式(例如“几分钟前”“昨天”等相对时间表述)进行展示 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 调用setBackground方法根据数据设置当前列表项的背景样式,具体的背景设置逻辑在该方法中实现 + setBackground(data); + } + + // 私有方法,用于根据笔记数据来设置当前列表项的背景资源,根据数据类型(是否为笔记以及笔记的不同状态等)来选择不同的背景资源进行设置 + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + // 如果数据类型是笔记(Notes.TYPE_NOTE) + if (data.getType() == Notes.TYPE_NOTE) { + // 如果笔记是单独的(isSingle方法判断)或者是某个文件夹下单独跟随的(isOneFollowingFolder方法判断) + if (data.isSingle() || data.isOneFollowingFolder()) { + // 设置背景资源为单个笔记对应的背景资源(通过NoteItemBgResources工具类的方法根据颜色ID获取) + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + // 如果笔记是所在分组的最后一个(isLast方法判断) + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + // 如果笔记是所在分组的第一个(isFirst方法判断)或者是某个文件夹下多个跟随的(isMultiFollowingFolder方法判断) + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + // 其他情况(普通的笔记在中间位置等),设置背景资源为普通笔记对应的背景资源(通过NoteItemBgResources工具类的方法根据颜色ID获取) + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + // 如果数据类型不是笔记(比如是文件夹),则设置背景资源为文件夹对应的背景资源(通过NoteItemBgResources工具类的方法获取) + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + // 用于获取当前列表项对应的笔记数据对象,方便外部调用者获取数据进行其他相关操作 + public NoteItemData getItemData() { + return mItemData; + } +} \ No newline at end of file diff --git a/ui/NotesPreferenceActivity.java b/ui/NotesPreferenceActivity.java new file mode 100644 index 0000000..7217adf --- /dev/null +++ b/ui/NotesPreferenceActivity.java @@ -0,0 +1,519 @@ +/* + * 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; + +// NotesPreferenceActivity类继承自PreferenceActivity,用于展示和管理应用的偏好设置相关界面及功能 +public class NotesPreferenceActivity extends PreferenceActivity { + // 定义偏好设置的名称常量,用于在获取和操作SharedPreferences时作为标识 + 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"; + + // 用于在偏好设置界面中对账户相关的偏好设置项进行分类管理的PreferenceCategory对象 + private PreferenceCategory mAccountCategory; + // 自定义的广播接收器对象,用于接收特定的广播消息(这里可能与同步服务相关的广播有关) + private GTaskReceiver mReceiver; + // 用于保存原始的账户列表信息,可能用于对比账户变化情况等操作 + private Account[] mOriAccounts; + // 用于标记是否添加了新账户的布尔变量,初始值为false + private boolean mHasAddedAccount; + + // onCreate方法在Activity创建时被调用,用于进行初始化相关的操作 + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // 设置ActionBar的显示属性,使得应用图标可以用于导航(通常点击图标可返回上一级界面) + getActionBar().setDisplayHomeAsUpEnabled(true); + + // 从指定的XML资源文件(R.xml.preferences)中加载偏好设置界面的布局和配置信息,构建出偏好设置界面的初始显示内容 + addPreferencesFromResource(R.xml.preferences); + + // 通过键(PREFERENCE_SYNC_ACCOUNT_KEY)从已加载的偏好设置中找到对应的PreferenceCategory对象,用于后续对账户相关设置项的操作 + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + + // 创建一个GTaskReceiver实例,用于接收相关广播消息 + mReceiver = new GTaskReceiver(); + + // 创建一个IntentFilter对象,用于指定要接收的广播动作(这里只关注GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME对应的广播) + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + + // 注册广播接收器,使得该Activity能够接收到符合过滤条件的广播消息 + registerReceiver(mReceiver, filter); + + // 初始化原始账户列表为null,后续可能会获取并赋值实际的账户信息 + mOriAccounts = null; + + // 通过LayoutInflater从当前上下文获取布局Inflater,并加载指定的布局资源(R.layout.settings_header)作为头部视图添加到当前Activity的列表视图中,第三个参数设置为true表示该头部视图在列表滚动时固定显示 + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); + } + + // onResume方法在Activity重新回到前台可见状态时被调用,常用于恢复或更新界面相关的操作以及处理一些业务逻辑 + @Override + protected void onResume() { + super.onResume(); + + // 如果已经添加了新账户(mHasAddedAccount为true) + if (mHasAddedAccount) { + // 获取当前的谷歌账户列表 + Account[] accounts = getGoogleAccounts(); + // 如果原始账户列表不为null且新获取的账户数量大于原始账户数量,说明可能添加了新账户 + if (mOriAccounts!= null && accounts.length > mOriAccounts.length) { + // 遍历新获取的账户列表 + for (Account accountNew : accounts) { + // 标记是否找到匹配的原始账户,初始化为false + boolean found = false; + // 遍历原始账户列表,对比账户名称是否相同 + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + // 如果在原始账户列表中未找到匹配的账户,说明是新添加的账户 + if (!found) { + // 设置同步账户为新添加的这个账户的名称,并跳出循环(只设置一个新账户作为同步账户即可,具体业务逻辑可能如此) + setSyncAccount(accountNew.name); + break; + } + } + } + } + + // 调用refreshUI方法更新界面显示内容,具体更新逻辑在该方法中实现(代码中未完整展示该方法内容) + refreshUI(); + } + // 当Activity被销毁时调用的方法,用于释放相关资源和进行一些清理操作 +@Override +protected void onDestroy() { + // 如果广播接收器(mReceiver)不为空,就注销该广播接收器,避免内存泄漏等问题 + if (mReceiver!= null) { + unregisterReceiver(mReceiver); + } + // 调用父类的onDestroy方法,执行父类中定义的销毁相关的默认操作 + super.onDestroy(); +} + +// 用于加载账户偏好设置相关内容的方法,例如在偏好设置界面中添加账户相关的设置项及设置其显示和点击等行为逻辑 +private void loadAccountPreference() { + // 先移除账户偏好设置分类(mAccountCategory)下的所有已有偏好设置项,确保后续添加的是最新的内容 + mAccountCategory.removeAll(); + + // 创建一个新的Preference实例,用于表示账户相关的偏好设置项 + Preference accountPref = new Preference(this); + // 获取当前的默认同步账户名称(通过调用getSyncAccountName方法) + 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) { + // 如果同步服务(GTaskSyncService)当前没有正在进行同步操作 + if (!GTaskSyncService.isSyncing()) { + // 如果默认账户名称为空字符串,说明是第一次设置账户 + if (TextUtils.isEmpty(defaultAccount)) { + // 弹出选择账户的警告对话框,用于让用户选择要设置的账户(具体逻辑在showSelectAccountAlertDialog方法中) + showSelectAccountAlertDialog(); + } else { + // 如果已经设置过账户了,弹出确认更改账户的警告对话框,提示用户更改账户可能存在的风险(具体逻辑在showChangeAccountConfirmAlertDialog方法中) + showChangeAccountConfirmAlertDialog(); + } + } else { + // 如果同步服务正在同步,通过Toast提示用户当前不能更改账户 + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + // 返回true表示已处理该点击事件 + return true; + } + }); + + // 将创建并设置好的账户偏好设置项添加到账户偏好设置分类中 + mAccountCategory.addPreference(accountPref); +} + +// 用于加载同步按钮相关设置的方法,包括设置按钮的文本、点击事件以及同步状态相关的显示文本等内容 +private void loadSyncButton() { + // 通过findViewById方法找到布局中的同步按钮(Button)实例 + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + // 通过findViewById方法找到布局中的用于显示上次同步时间的TextView实例 + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // 根据同步服务(GTaskSyncService)是否正在同步来设置同步按钮的文本和点击事件逻辑 + // 如果正在同步 + if (GTaskSyncService.isSyncing()) { + // 设置按钮文本为取消同步的文本内容(通过资源字符串获取) + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + // 为按钮设置点击监听器,点击时调用GTaskSyncService的cancelSync方法来取消同步操作(传入当前的NotesPreferenceActivity实例作为上下文) + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + // 如果没有正在同步 + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + // 为按钮设置点击监听器,点击时调用GTaskSyncService的startSync方法来立即启动同步操作(传入当前的NotesPreferenceActivity实例作为上下文) + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + } + // 根据是否设置了同步账户来决定同步按钮是否可用,只有设置了同步账户(同步账户名称不为空字符串)时,按钮才可用 + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // 根据同步服务的状态来设置上次同步时间显示文本及可见性 + // 如果正在同步 + if (GTaskSyncService.isSyncing()) { + // 设置显示文本为同步服务的进度字符串(通过GTaskSyncService的getProgressString方法获取),并将该TextView设置为可见 + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + // 如果没有正在同步,获取上次同步的时间戳(通过调用getLastSyncTime方法) + long lastSyncTime = getLastSyncTime(this); + // 如果上次同步时间戳不为0,说明有上次同步记录 + if (lastSyncTime!= 0) { + // 设置显示文本,通过格式化字符串的方式将时间戳格式化为指定格式的日期时间字符串(先通过资源字符串获取日期时间格式,再进行格式化),并将该TextView设置为可见 + 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 { + // 如果没有上次同步记录,将该TextView设置为不可见 + lastSyncTimeView.setVisibility(View.GONE); + } + } +} + +// 用于刷新整个偏好设置界面的方法,通过调用加载账户偏好设置和加载同步按钮相关的方法来更新界面显示内容 +private void refreshUI() { + loadAccountPreference(); + loadSyncButton(); +} + +// 用于弹出选择账户的警告对话框的方法,在该对话框中展示可供选择的账户列表以及添加账户的入口等内容 +private void showSelectAccountAlertDialog() { + // 创建一个AlertDialog.Builder实例,用于构建警告对话框 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 通过LayoutInflater从当前上下文加载自定义的标题布局(R.layout.account_dialog_title) + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + // 从加载的标题布局中找到对应的标题TextView实例 + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + // 设置标题TextView的文本内容,通过资源字符串获取对应的文本 + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + // 从加载的标题布局中找到对应的副标题TextView实例 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + // 设置副标题TextView的文本内容,通过资源字符串获取对应的文本 + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + // 将自定义的标题视图设置为对话框的标题部分 + dialogBuilder.setCustomTitle(titleView); + // 设置对话框的确认按钮(正按钮),这里先设置为null,后续可能会根据具体逻辑再设置(或者在某些情况下不需要点击操作) + dialogBuilder.setPositiveButton(null, null); + + // 获取当前的谷歌账户列表(通过调用getGoogleAccounts方法) + Account[] accounts = getGoogleAccounts(); + // 获取当前的默认同步账户名称(通过调用getSyncAccountName方法) + String defAccount = getSyncAccountName(this); + + // 将获取的账户列表保存到mOriAccounts变量中,用于后续可能的对比等操作 + mOriAccounts = accounts; + // 标记是否添加了新账户,初始化为false + mHasAddedAccount = false; + + // 如果获取到的账户列表长度大于0,说明有可用的账户可供选择 + if (accounts.length > 0) { + // 创建一个字符序列数组,长度与账户列表长度相同,用于存储账户名称,作为单选列表项展示给用户 + CharSequence[] items = new CharSequence[accounts.length]; + // 创建一个临时的字符序列数组,用于在点击单选列表项时进行账户名称的映射操作(这里其实和items数组内容基本一致,只是为了方便后续代码逻辑理解和编写) + final CharSequence[] itemMapping = items; + // 用于记录默认选中的账户在列表中的索引位置,初始化为 -1,表示没有默认选中项 + int checkedItem = -1; + // 索引变量,用于遍历账户列表并填充items数组以及查找默认选中项的索引 + int index = 0; + // 遍历账户列表 + for (Account account : accounts) { + // 如果当前账户名称与默认同步账户名称相等,说明该账户是当前默认选中的账户,记录其索引位置 + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + // 将账户名称添加到items数组中,作为单选列表项的显示内容 + items[index++] = account.name; + } + // 设置对话框的单选列表项内容、默认选中项索引以及点击监听器,当用户点击某个单选列表项时,调用setSyncAccount方法设置选中的账户作为同步账户,然后关闭对话框,并刷新界面(通过调用refreshUI方法) + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + + // 通过LayoutInflater从当前上下文加载添加账户相关的布局(R.layout.add_account_text),用于在对话框中展示添加账户的入口视图 + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + // 将添加账户的入口视图添加到对话框中 + dialogBuilder.setView(addAccountView); + + // 显示构建好的警告对话框,并获取对话框实例 + final AlertDialog dialog = dialogBuilder.show(); + // 为添加账户的入口视图设置点击监听器,当用户点击时,标记已添加新账户(mHasAddedAccount设置为true),然后启动添加账户的系统设置页面(通过意图指定相关动作和参数),最后关闭当前对话框 + 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实例,用于构建警告对话框 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 通过LayoutInflater从当前上下文加载自定义的标题布局(R.layout.account_dialog_title) + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + // 从加载的标题布局中找到对应的标题TextView实例 + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + // 设置标题TextView的文本内容,通过资源字符串获取对应的格式化文本,其中格式化参数为当前的同步账户名称(通过调用getSyncAccountName方法获取) + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + // 从加载的标题布局中找到对应的副标题TextView实例 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + // 设置副标题TextView的文本内容,通过资源字符串获取对应的警告提示文本 + 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) { + // 如果用户点击的是“更改账户”菜单项(索引为0) + if (which == 0) { + // 弹出选择账户的警告对话框(通过调用showSelectAccountAlertDialog方法),让用户重新选择账户 + showSelectAccountAlertDialog(); + } else if (which == 1) { + // 如果用户点击的是“移除账户”菜单项(索引为1),则调用removeSyncAccount方法移除当前的同步账户,并刷新界面(通过调用refreshUI方法) + removeSyncAccount(); + refreshUI(); + } + } + }); + // 显示构建好的警告对话框 + dialogBuilder.show(); +} + +// 用于获取当前设备上的谷歌账户列表的方法,通过AccountManager来获取指定类型("com.google")的账户信息 +private Account[] getGoogleAccounts() { + // 获取AccountManager实例,传入当前上下文 + AccountManager accountManager = AccountManager.get(this); + // 调用AccountManager的getAccountsByType方法获取类型为"com.google"的账户数组并返回 + return accountManager.getAccountsByType("com.google"); +} + +// 用于设置同步账户的方法,根据传入的账户名称来更新偏好设置中的同步账户相关信息,并进行一些相关的数据清理和提示操作 +private void setSyncAccount(String account) { + // 如果传入的账户与当前获取的同步账户名称不一致(说明要进行账户更改操作) + if (!getSyncAccountName(this).equals(account)) { + // 获取应用的SharedPreferences实例,用于存储和读取偏好设置数据,指定偏好设置名称(PREFERENCE_NAME)和私有访问模式(Context.MODE_PRIVATE) + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + // 获取SharedPreferences的编辑器,用于修改偏好设置中的数据 + SharedPreferences.Editor editor = settings.edit(); + // 如果传入的账户不为null,将其存入偏好设置中对应的键(PREFERENCE_SYNC_ACCOUNT_NAME)下 + if (account!= null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + // 如果传入的账户为null,将同步账户名称设置为空字符串 + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + // 提交对偏好设置的修改,使其生效 + editor.commit(); + + // 调用setLastSyncTime方法将上次同步时间清理(设置为0),表示重新开始同步相关的计时等逻辑 + setLastSyncTime(this, 0); + + // 在一个新线程中执行清理本地与GTask相关信息的操作,创建一个ContentValues实例用于存储要更新的数据 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + // 将笔记相关列(NoteColumns.GTASK_ID)的值设置为空字符串,可能用于清除之前与该账户关联的同步任务相关标识 + values.put(NoteColumns.GTASK_ID, ""); + // 将同步相关列(NoteColumns.SYNC_ID)的值设置为0,可能用于重置同步相关的状态标识 + values.put(NoteColumns.SYNC_ID, 0); + // 通过内容解析器(getContentResolver)更新笔记相关的内容提供器(Notes.CONTENT_NOTE_URI)中的数据,使用设置好的ContentValues进行更新,这里更新条件为null(可能是更新所有符合条件的数据,具体需看内容提供器的实现逻辑) + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + // 通过Toast提示用户账户设置成功,显示的文本通过资源字符串获取,并传入设置的账户名称作为格式化参数 + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } +} + +// 用于移除同步账户的方法,从偏好设置中移除同步账户相关的信息,并清理本地与GTask相关的信息 +private void removeSyncAccount() { + // 获取应用的SharedPreferences实例,用于存储和读取偏好设置数据,指定偏好设置名称(PREFERENCE_NAME)和私有访问模式(Context.MODE_PRIVATE) + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + // 获取SharedPreferences的编辑器,用于修改偏好设置中的数据 + SharedPreferences.Editor editor = settings.edit(); + // 如果偏好设置中包含同步账户名称相关的键(PREFERENCE_SYNC_ACCOUNT_NAME),则移除该键对应的值 + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + // 如果偏好设置中包含上次同步时间相关的键(PREFERENCE_LAST_SYNC_TIME),则移除该键对应的值 + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + // 提交对偏好设置的修改,使其生效 + editor.commit(); + + // 在一个新线程中执行清理本地与GTask相关信息的操作,创建一个ContentValues实例用于存储要更新的数据 + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + // 将笔记相关列(NoteColumns.GTASK_ID)的值设置为空字符串,可能用于清除之前与该账户关联的同步任务相关标识 + values.put(NoteColumns.GTASK_ID, ""); + // 将同步相关列(NoteColumns.SYNC_ID)的值设置为0,可能用于重置同步相关的状态标识 + values.put(NoteColumns.SYNC_ID, 0); + // 通过内容解析器(getContentResolver)更新笔记相关的内容提供器(Notes.CONTENT_NOTE_URI)中的数据,使用设置好的ContentValues进行更新,这里更新条件为null(可能是更新所有符合条件的数据,具体需看内容提供器的实现逻辑) + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); +} + +// 静态方法,用于获取当前设置的同步账户名称,从应用的SharedPreferences中读取对应键(PREFERENCE_SYNC_ACCOUNT_NAME)下存储的值,如果不存在则返回空字符串 +public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); +} + +// 静态方法,用于设置上次同步时间,将指定的时间戳(time)存入应用的SharedPreferences中对应的键(PREFERENCE_LAST_SYNC_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中读取对应键(PREFERENCE_LAST_SYNC_TIME)下存储的时间戳,如果不存在则返回0 +public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); +} + +// 自定义的广播接收器类,继承自BroadcastReceiver,用于接收特定的广播消息并做出相应的响应 +private class GTaskReceiver extends BroadcastReceiver { + + // 当接收到广播消息时调用的方法,在这里进行界面刷新以及根据广播中的同步相关信息更新界面显示内容等操作 + @Override + public void onReceive(Context context, Intent intent) { + // 调用refreshUI方法刷新整个偏好设置界面,更新界面上的各种显示内容(如账户信息、同步按钮状态等) + refreshUI(); + // 如果广播消息中携带的表示是否正在同步的额外数据(通过GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING键获取)为true,说明正在同步 + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + // 通过findViewById方法找到用于显示同步状态的TextView实例 + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + // 设置该TextView的文本内容为广播消息中携带的同步进度相关信息(通过GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG键获取) + syncStatus.setText(intent.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + + } +} + +// 用于处理选项菜单中菜单项被选中时的操作逻辑,例如处理返回按钮等菜单项的点击事件 +public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // 如果点击的是Home按钮(通常是左上角的返回箭头图标,用于返回上一级界面等操作) + case android.R.id.home: + // 创建一个意图(Intent),指定要启动的目标Activity为NotesListActivity.class,即返回笔记列表界面 + Intent intent = new Intent(this, NotesListActivity.class); + // 添加标志位,使得启动的Activity会清除其上的所有其他Activity,实现返回栈的清理,回到笔记列表界面并将其置于栈顶 + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + // 根据意图启动Activity + startActivity(intent); + // 返回true表示已处理该菜单项点击事件 + return true; + default: + // 如果点击的是其他未处理的菜单项,返回false,表示未处理该事件 + return false; + } +} +} diff --git a/widget/NoteWidgetProvider.java b/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..016dbc5 --- /dev/null +++ b/widget/NoteWidgetProvider.java @@ -0,0 +1,178 @@ +/* + * 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.widget; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +// NoteWidgetProvider类是一个抽象类,继承自AppWidgetProvider,用于作为桌面小部件(App Widget)相关功能的基础类,提供了一些通用的小部件操作逻辑以及定义了抽象方法供具体子类实现特定功能 +public abstract class NoteWidgetProvider extends AppWidgetProvider { + // 定义一个字符串数组,用于指定查询数据库时要获取的列信息,这里包含笔记的ID、背景颜色ID以及摘要信息等列 + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + }; + + // 定义常量,用于表示在查询结果中笔记ID所在的列索引位置,方便后续从游标(Cursor)中获取对应的数据 + public static final int COLUMN_ID = 0; + // 定义常量,用于表示在查询结果中背景颜色ID所在的列索引位置,方便后续从游标(Cursor)中获取对应的数据 + public static final int COLUMN_BG_COLOR_ID = 1; + // 定义常量,用于表示在查询结果中摘要信息所在的列索引位置,方便后续从游标(Cursor)中获取对应的数据 + public static final int COLUMN_SNIPPET = 2; + + // 定义用于日志记录的标签,方便在日志输出中识别该类相关的日志信息 + private static final String TAG = "NoteWidgetProvider"; + + // 当桌面小部件被删除时调用的方法,用于处理相关的数据清理等操作 + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + // 创建一个ContentValues实例,用于存储要更新到数据库中的数据 + ContentValues values = new ContentValues(); + // 将笔记相关列(NoteColumns.WIDGET_ID)的值设置为无效的小部件ID(AppWidgetManager.INVALID_APPWIDGET_ID),表示该笔记不再关联被删除的小部件 + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + // 遍历被删除的小部件ID数组 + for (int i = 0; i < appWidgetIds.length; i++) { + // 通过内容解析器(context.getContentResolver())更新笔记相关的内容提供器(Notes.CONTENT_NOTE_URI)中的数据,使用设置好的ContentValues进行更新,更新条件为笔记的小部件ID列等于当前遍历到的被删除小部件的ID + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + } + + // 私有方法,用于获取与指定小部件ID相关的笔记小部件信息,通过查询数据库来获取相应的数据 + private Cursor getNoteWidgetInfo(Context context, int widgetId) { + // 使用内容解析器发起查询操作,查询的内容提供器为Notes.CONTENT_NOTE_URI,指定要获取的列信息为之前定义的PROJECTION数组中的列,查询条件为笔记的小部件ID等于传入的小部件ID且父ID不等于回收站文件夹的ID(Notes.ID_TRASH_FOLER),查询条件的参数通过字符串数组传入,最后一个参数null表示不指定排序方式等额外条件 + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) }, + null); + } + + // 受保护的方法,用于更新桌面小部件的显示内容等信息,调用了另一个重载的update方法,并传入默认参数false + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + } + + // 私有方法,用于实际更新桌面小部件的显示内容等信息,根据传入的小部件ID数组对每个小部件进行相应的更新操作 + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + // 遍历传入的小部件ID数组 + for (int i = 0; i < appWidgetIds.length; i++) { + // 如果小部件ID不是无效的小部件ID(AppWidgetManager.INVALID_APPWIDGET_ID),才进行更新操作 + if (appWidgetIds[i]!= AppWidgetManager.INVALID_APPWIDGET_ID) { + // 获取默认的背景颜色ID,通过ResourceParser工具类的方法获取(具体获取逻辑在该工具类中实现) + int bgId = ResourceParser.getDefaultBgId(context); + // 初始化摘要信息为空字符串,后续会根据实际查询结果进行赋值 + String snippet = ""; + // 创建一个意图(Intent),指定要启动的目标Activity为NoteEditActivity.class,即点击小部件后可能会跳转到的编辑笔记的Activity + Intent intent = new Intent(context, NoteEditActivity.class); + // 设置意图的标志位,使得如果该Activity已经在栈顶,则不会重新创建实例,而是复用已有的实例(例如避免重复打开同一个编辑页面等情况) + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + // 将小部件的ID作为额外数据添加到意图中,方便在目标Activity中获取并使用,通过Notes.INTENT_EXTRA_WIDGET_ID键进行传递 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + // 将小部件的类型作为额外数据添加到意图中,具体的小部件类型由子类实现的getWidgetType方法获取并传入,通过Notes.INTENT_EXTRA_WIDGET_TYPE键进行传递 + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + // 获取与当前小部件ID相关的笔记小部件信息的游标(Cursor),通过调用getNoteWidgetInfo方法查询数据库获取 + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + // 如果游标不为空且游标可以移动到第一条数据(表示查询到了相关数据) + if (c!= null && c.moveToFirst()) { + // 如果查询结果的数量大于1,说明出现了同一个小部件ID关联多条消息的异常情况,记录错误日志并关闭游标,然后直接返回,不进行后续更新操作 + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + // 获取摘要信息列的值,赋值给snippet变量,用于后续在小部件上显示 + snippet = c.getString(COLUMN_SNIPPET); + // 获取背景颜色ID列的值,赋值给bgId变量,用于设置小部件的背景相关显示 + bgId = c.getInt(COLUMN_BG_COLOR_ID); + // 将笔记的ID作为额外数据添加到意图中,通过Intent.EXTRA_UID键传递,方便在目标Activity中根据ID进行相应操作 + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + // 设置意图的动作(Action)为查看(Intent.ACTION_VIEW),表示点击小部件可能执行查看笔记的操作(具体行为由目标Activity根据该动作进一步处理) + intent.setAction(Intent.ACTION_VIEW); + } else { + // 如果游标为空或者没有查询到相关数据,设置摘要信息为资源字符串中对应的提示文本(表示小部件没有关联内容) + snippet = context.getResources().getString(R.string.widget_havenot_content); + // 设置意图的动作(Action)为插入或编辑(Intent.ACTION_INSERT_OR_EDIT),表示点击小部件可能执行新建或编辑笔记的操作(具体行为由目标Activity根据该动作进一步处理) + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + // 如果游标不为空,关闭游标,释放相关资源 + if (c!= null) { + c.close(); + } + + // 创建一个RemoteViews实例,用于构建桌面小部件的远程视图,传入当前上下文的包名和具体的布局ID(由子类实现的getLayoutId方法获取) + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + // 设置小部件中背景图片资源的ID,通过调用抽象方法getBgResourceId传入背景颜色ID来获取对应的资源ID进行设置 + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + // 将背景颜色ID作为额外数据添加到意图中,通过Notes.INTENT_EXTRA_BACKGROUND_ID键传递,方便在目标Activity中根据背景颜色进行相应操作(可能涉及界面显示等方面的适配) + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + + // 根据是否处于隐私模式(privacyMode)来生成不同的PendingIntent实例,用于设置小部件点击事件的响应逻辑 + PendingIntent pendingIntent = null; + if (privacyMode) { + // 如果处于隐私模式,设置小部件上文本显示为隐私模式相关的提示文本(通过资源字符串获取) + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + // 创建一个PendingIntent,用于启动NotesListActivity(可能是进入某种隐私模式下的列表页面等操作),传入当前上下文、小部件ID以及要启动的意图等参数,并设置标志位为PendingIntent.FLAG_UPDATE_CURRENT,表示如果已存在相同的PendingIntent,则更新其额外数据等内容 + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + // 如果不处于隐私模式,设置小部件上文本显示为之前获取或设置的摘要信息(snippet) + rv.setTextViewText(R.id.widget_text, snippet); + // 创建一个PendingIntent,用于启动之前设置的意图(intent,可能是跳转到编辑笔记等相关Activity),传入当前上下文、小部件ID以及要启动的意图等参数,并设置标志位为PendingIntent.FLAG_UPDATE_CURRENT,表示如果已存在相同的PendingIntent,则更新其额外数据等内容 + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + // 设置小部件中文本视图(R.id.widget_text)的点击事件响应PendingIntent,即点击小部件上的文本区域时会触发相应的意图操作 + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + // 通过AppWidgetManager更新指定小部件ID对应的小部件的远程视图,实现小部件显示内容的更新 + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + } + + // 抽象方法,用于获取与指定背景颜色ID对应的背景资源ID,由具体的子类根据实际的资源映射关系来实现该方法,以设置小部件的背景图片等显示相关资源 + protected abstract int getBgResourceId(int bgId); + + // 抽象方法,用于获取小部件对应的布局资源ID,由具体的子类根据不同类型小部件的布局需求来实现该方法,以确定小部件的整体布局样式 + protected abstract int getLayoutId(); + + // 抽象方法,用于获取小部件的类型,由具体的子类根据实际小部件的类型定义来实现该方法,以便在更新等操作中传递准确的小部件类型信息 + protected abstract int getWidgetType(); +} \ No newline at end of file diff --git a/widget/NoteWidgetProvider_2x.java b/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..561657e --- /dev/null +++ b/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,53 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// NoteWidgetProvider_2x类继承自NoteWidgetProvider抽象类,是针对特定尺寸(可能是 2x 尺寸,具体需结合应用场景理解)桌面小部件的具体实现类,用于实现该尺寸小部件的特定功能,比如布局、背景资源以及类型等相关设置。 +public class NoteWidgetProvider_2x extends NoteWidgetProvider { + // 重写父类的onUpdate方法,该方法在桌面小部件需要更新时被调用(例如小部件添加到桌面、系统触发小部件更新等情况) + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的update方法来执行小部件的更新操作,将当前上下文、AppWidgetManager实例以及小部件ID数组传递给父类方法,由父类中定义的通用更新逻辑来处理更新相关事宜,例如设置小部件的显示内容、点击事件等(具体逻辑在父类的update方法中) + super.update(context, appWidgetManager, appWidgetIds); + } + + // 重写父类的抽象方法getLayoutId,用于返回该尺寸小部件对应的布局资源ID,这里返回的是R.layout.widget_2x,表示使用名为widget_2x的布局文件来展示该小部件的外观样式 + @Override + protected int getLayoutId() { + return R.layout.widget_2x; + } + + // 重写父类的抽象方法getBgResourceId,用于根据传入的背景颜色ID获取对应的背景资源ID,通过调用ResourceParser.WidgetBgResources工具类的相关方法(getWidget2xBgResource)来获取适合该尺寸小部件的背景资源ID,实现了根据不同背景颜色ID来设置小部件背景图片等显示资源的功能 + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + } + + // 重写父类的抽象方法getWidgetType,用于返回该小部件的类型,这里返回的是Notes.TYPE_WIDGET_2X,表示该小部件属于特定的2x类型(具体类型含义可能由应用中对不同尺寸或功能小部件的分类定义决定),以便在小部件相关的操作和逻辑中能准确区分不同类型的小部件 + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; + } +} \ No newline at end of file diff --git a/widget/NoteWidgetProvider_4x.java b/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..9446326 --- /dev/null +++ b/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,52 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +// NoteWidgetProvider_4x类继承自NoteWidgetProvider抽象类,是专门针对某种特定的4x尺寸(可能是桌面小部件尺寸规格,具体取决于应用定义)桌面小部件功能实现的类,通过重写抽象类中的相关方法来定制该尺寸小部件的具体行为和外观展示等内容。 +public class NoteWidgetProvider_4x extends NoteWidgetProvider { + // 重写父类的onUpdate方法,该方法会在桌面小部件需要更新时被调用,例如小部件首次添加到桌面、系统定时触发小部件更新或者应用内有相关更新操作触发等情况。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // 调用父类的update方法来执行具体的更新操作,将当前上下文、AppWidgetManager实例以及小部件ID数组作为参数传递过去,由父类中通用的更新逻辑来处理诸如设置小部件显示内容、配置点击事件等更新相关的操作(父类update方法中已定义了这些通用逻辑)。 + super.update(context, appWidgetManager, appWidgetIds); + } + + // 重写父类的抽象方法getLayoutId,用于返回该4x尺寸小部件对应的布局资源ID,这里返回R.layout.widget_4x,表示该小部件将会使用名为widget_4x的布局文件来构建其界面显示样式,该布局文件中定义了小部件各个元素的布局结构、样式等信息。 + protected int getLayoutId() { + return R.layout.widget_4x; + } + + // 重写父类的抽象方法getBgResourceId,用于根据传入的背景颜色ID获取对应的背景资源ID,通过调用ResourceParser.WidgetBgResources工具类中的getWidget4xBgResource方法来获取适合该4x尺寸小部件的背景资源ID,以此实现根据不同的背景颜色需求来准确设置小部件的背景图片等显示相关资源,使得小部件的外观能根据不同情况进行相应变化。 + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + } + + // 重写父类的抽象方法getWidgetType,用于返回该小部件的类型标识,这里返回Notes.TYPE_WIDGET_4X,表示该小部件属于特定的4x类型(在应用中可能依据不同尺寸、功能等对小部件进行了分类定义,通过这个类型标识来区分),方便在整个桌面小部件相关的逻辑处理中(如更新、交互等操作)准确识别该小部件的类型并进行针对性的处理。 + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; + } +} \ No newline at end of file diff --git a/小米便签开源代码的泛读报告.docx b/小米便签开源代码的泛读报告.docx new file mode 100644 index 0000000..8e6a1b5 Binary files /dev/null and b/小米便签开源代码的泛读报告.docx differ