From de008ccc92ffe1a2d1a86673bbf69262222a40dd Mon Sep 17 00:00:00 2001 From: dhc <2213979464@qq.com> Date: Thu, 9 Jan 2025 23:35:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/AlarmAlertActivity.java | 202 +++++++++++++++++++++ ui/AlarmInitReceiver.java | 88 +++++++++ ui/AlarmReceiver.java | 40 +++++ ui/DateTimePicker.java | 245 +++++++++++++++++++++++++ ui/DateTimePickerDialog.java | 125 +++++++++++++ ui/DropdownMenu.java | 0 ui/FoldersListAdapter.java | 101 +++++++++++ ui/NoteEditActivity.java | 269 ++++++++++++++++++++++++++++ ui/NoteEditText.java | 259 +++++++++++++++++++++++++++ ui/NoteItemData.java | 227 ++++++++++++++++++++++++ ui/NotesListActivity.java | 305 ++++++++++++++++++++++++++++++++ ui/NotesListAdapter.java | 240 +++++++++++++++++++++++++ ui/NotesListItem.java | 179 +++++++++++++++++++ ui/NotesPreferenceActivity.java | 147 +++++++++++++++ 14 files changed, 2427 insertions(+) create mode 100644 ui/AlarmAlertActivity.java create mode 100644 ui/AlarmInitReceiver.java create mode 100644 ui/AlarmReceiver.java create mode 100644 ui/DateTimePicker.java create mode 100644 ui/DateTimePickerDialog.java create mode 100644 ui/DropdownMenu.java create mode 100644 ui/FoldersListAdapter.java create mode 100644 ui/NoteEditActivity.java create mode 100644 ui/NoteEditText.java create mode 100644 ui/NoteItemData.java create mode 100644 ui/NotesListActivity.java create mode 100644 ui/NotesListAdapter.java create mode 100644 ui/NotesListItem.java create mode 100644 ui/NotesPreferenceActivity.java diff --git a/ui/AlarmAlertActivity.java b/ui/AlarmAlertActivity.java new file mode 100644 index 0000000..f4bcf56 --- /dev/null +++ b/ui/AlarmAlertActivity.java @@ -0,0 +1,202 @@ +/* + * 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创建时调用的方法,进行一系列初始化操作 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置Activity无标题栏,去除默认的标题显示 + requestWindowFeature(Window.FEATURE_NO_TITLE); + + 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 { + // 从意图的数据部分解析出笔记的ID,假设数据路径中第二个元素是笔记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) { + // 如果解析数据出现异常,打印异常堆栈信息,并直接返回,不进行后续操作 + e.printStackTrace(); + return; + } + + // 创建MediaPlayer对象,用于后续播放声音 + mPlayer = new MediaPlayer(); + // 检查该笔记是否在笔记数据库中可见(通过DataUtils工具类的方法判断),如果可见则展示操作对话框并播放闹钟声音,否则直接结束该Activity + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); + } + } + + // 判断屏幕是否处于开启状态的方法,通过获取PowerManager服务并调用其isScreenOn方法来判断 + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + // 用于播放闹钟提醒声音的方法 + private void playAlarmSound() { + // 获取系统默认的闹钟铃声的Uri + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + // 获取系统中静音模式影响的音频流设置值 + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + // 根据静音模式设置情况来设置MediaPlayer的音频流类型,如果静音模式影响到闹钟音频流,则使用相应设置,否则使用默认的闹钟音频流类型 + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM))!= 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + + try { + // 设置MediaPlayer的数据源为获取到的铃声Uri + mPlayer.setDataSource(this, url); + // 准备MediaPlayer,使其处于可播放状态(可能涉及加载音频资源等操作) + mPlayer.prepare(); + // 设置循环播放,让闹钟声音一直响,直到被停止 + mPlayer.setLooping(true); + // 启动MediaPlayer,开始播放闹钟声音 + mPlayer.start(); + } catch (IllegalArgumentException e) { + // 捕获并打印参数异常相关的堆栈信息,如果出现此类异常 + e.printStackTrace(); + } catch (SecurityException e) { + // 捕获并打印安全相关异常的堆栈信息,如果出现此类异常 + e.printStackTrace(); + } catch (IllegalStateException e) { + // 捕获并打印非法状态相关异常的堆栈信息,如果出现此类异常 + e.printStackTrace(); + } catch (IOException e) { + // 捕获并打印I/O相关异常的堆栈信息,如果出现此类异常 + e.printStackTrace(); + } + } + + // 用于显示操作对话框的方法,对话框中包含提醒相关的信息以及操作按钮等 + private void showActionDialog() { + 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); + } + // 显示对话框,并设置对话框关闭时的监听器(实现了OnDismissListener接口) + dialog.show().setOnDismissListener(this); + } + + // 实现OnClickListener接口的方法,处理对话框按钮点击事件 + public void onClick(DialogInterface dialog, int which) { + switch (which) { + // 如果点击的是“进入”按钮(一般对应负向按钮,具体由设置顺序决定) + case DialogInterface.BUTTON_NEGATIVE: + // 创建一个启动NoteEditActivity的意图,设置动作为查看(ACTION_VIEW),并传入笔记的ID作为额外数据 + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 启动NoteEditActivity,让用户可以查看或编辑对应的笔记内容 + startActivity(intent); + break; + default: + break; + } + } + + // 实现OnDismissListener接口的方法,当对话框关闭时调用 + public void onDismiss(DialogInterface dialog) { + // 停止播放闹钟声音,并释放MediaPlayer相关资源 + stopAlarmSound(); + // 结束当前的AlarmAlertActivity,完成整个闹钟提醒相关的操作流程 + finish(); + } + + // 用于停止播放闹钟声音并释放MediaPlayer资源的方法 + 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..1f1eff4 --- /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 + }; + + // 定义列索引常量,方便后续从游标(Cursor)中获取对应列的数据,这里分别对应ID列和提醒日期列的索引 + private static final int COLUMN_ID = 0; + private static final int COLUMN_ALERTED_DATE = 1; + + // 重写BroadcastReceiver的onReceive方法,当该广播接收器接收到匹配的广播时会被调用,在此方法中执行具体的闹钟初始化逻辑 + @Override + public void onReceive(Context context, Intent intent) { + // 获取当前的系统时间(以毫秒为单位),用于后续与笔记的提醒日期进行比较筛选 + long currentDate = System.currentTimeMillis(); + + // 通过ContentResolver查询数据库,获取满足特定条件的笔记记录游标(Cursor) + // 查询条件是提醒日期大于当前日期且笔记类型为普通笔记(Notes.TYPE_NOTE)的记录,只获取前面定义的投影列信息 + 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); + + // 如果游标不为空,说明查询到了符合条件的笔记记录 + if (c!= null) { + // 将游标移动到第一条记录位置,如果有记录则进入循环处理每条记录 + if (c.moveToFirst()) { + do { + // 从游标中获取当前笔记记录的提醒日期(以毫秒为单位的时间戳) + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + + // 创建一个用于触发闹钟提醒的意图(Intent),指定其对应的广播接收类为AlarmReceiver + Intent sender = new Intent(context, AlarmReceiver.class); + // 设置意图的数据部分,将笔记的ID附加到对应的内容URI上,用于标识具体是哪个笔记的闹钟提醒 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + + // 创建一个PendingIntent,用于包装前面的sender意图,使得可以在合适的时间由系统触发该意图对应的广播 + // 这里使用的请求码为0,具体的触发情况等由后续设置闹钟时关联使用 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统的AlarmManager服务,用于设置闹钟相关操作 + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + + // 通过AlarmManager设置闹钟,使用RTC_WAKEUP模式(表示在指定的绝对时间唤醒设备来触发闹钟提醒) + // 指定提醒的时间为前面获取的笔记提醒日期,关联的PendingIntent就是前面创建的用于触发广播的那个 + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + // 关闭游标,释放相关资源,避免内存泄漏等问题 + c.close(); + } + } +} \ No newline at end of file diff --git a/ui/AlarmReceiver.java b/ui/AlarmReceiver.java new file mode 100644 index 0000000..71492a3 --- /dev/null +++ b/ui/AlarmReceiver.java @@ -0,0 +1,40 @@ +/* + * 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,BroadcastReceiver用于接收系统发送的广播消息,在Android中常用于实现各种系统事件的响应逻辑,例如这里用于响应闹钟触发相关的广播。 +public class AlarmReceiver extends BroadcastReceiver { + + // 重写BroadcastReceiver的onReceive方法,该方法会在接收到与之匹配的广播时被调用,在此类中主要用于启动与闹钟提醒相关的Activity(即AlarmAlertActivity)。 + @Override + public void onReceive(Context context, Intent intent) { + // 通过Intent的setClass方法,将原本传入的意图(intent)的目标类修改为AlarmAlertActivity.class,也就是将广播意图重新定向到用于展示闹钟提醒详细信息的Activity。 + intent.setClass(context, AlarmAlertActivity.class); + + // 给意图添加FLAG_ACTIVITY_NEW_TASK标志,这是因为BroadcastReceiver接收到广播时所处的上下文环境可能没有与之关联的任务栈(Task), + // 添加该标志可以确保启动的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..67d9b0c --- /dev/null +++ b/ui/DateTimePicker.java @@ -0,0 +1,245 @@ +/* + * 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 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; + +// DateTimePicker类继承自FrameLayout,它是一个自定义的视图组件,用于实现日期和时间的选择功能,用户可以通过该组件方便地设置年、月、日、时、分以及选择12/24小时制等。 +public class DateTimePicker extends FrameLayout { + + // 定义默认的启用状态,默认为启用(true),用于控制整个DateTimePicker组件及其内部子控件的可交互状态。 + private static final boolean DEFAULT_ENABLE_STATE = true; + + // 定义半天的小时数,用于12小时制相关的时间计算和判断,例如区分上午和下午的时间范围。 + private static final int HOURS_IN_HALF_DAY = 12; + // 定义一天的总小时数,用于24小时制相关的操作和判断。 + private static final int HOURS_IN_ALL_DAY = 24; + // 定义一周的天数,用于日期选择器(如显示一周内的日期选项等)相关的操作。 + private static final int DAYS_IN_ALL_WEEK = 7; + // 定义日期选择器(NumberPicker)的最小值,通常从0开始,表示一周内日期选项的最小索引。 + private static final int DATE_SPINNER_MIN_VAL = 0; + // 定义日期选择器(NumberPicker)的最大值,根据一周7天,最大值为6(索引从0开始)。 + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + // 定义24小时制视图下小时选择器(NumberPicker)的最小值,即0点。 + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + // 定义24小时制视图下小时选择器(NumberPicker)的最大值,即23点。 + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + // 定义12小时制视图下小时选择器(NumberPicker)的最小值,通常为1(12小时制习惯从1开始计数)。 + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + // 定义12小时制视图下小时选择器(NumberPicker)的最大值,即12点。 + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + // 定义分钟选择器(NumberPicker)的最小值,即0分钟。 + private static final int MINUT_SPINNER_MIN_VAL = 0; + // 定义分钟选择器(NumberPicker)的最大值,即59分钟。 + private static final int MINUT_SPINNER_MAX_VAL = 59; + // 定义上午/下午(AM/PM)选择器(NumberPicker)的最小值,通常为0(对应AM)。 + private static final int AMPM_SPINNER_MIN_VAL = 0; + // 定义上午/下午(AM/PM)选择器(NumberPicker)的最大值,通常为1(对应PM)。 + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // 用于显示日期的NumberPicker控件,用户可以通过它选择具体的日期(以一周内的某一天来表示)。 + private final NumberPicker mDateSpinner; + // 用于显示小时的NumberPicker控件,根据设置的12/24小时制,显示相应范围的小时选项供用户选择。 + private final NumberPicker mHourSpinner; + // 用于显示分钟的NumberPicker控件,提供0到59分钟的选项供用户选择。 + private final NumberPicker mMinuteSpinner; + // 用于显示上午/下午(AM/PM)的NumberPicker控件,在12小时制下用于区分时间段,仅在非24小时制时可见。 + private final NumberPicker mAmPmSpinner; + // Calendar对象,用于存储和操作当前选择的日期和时间信息,方便进行各种日期时间的计算和设置。 + private Calendar mDate; + + // 用于存储一周内各天显示名称的字符串数组,例如“周一”“周二”等,会根据系统设置进行本地化显示。 + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + // 用于标记当前时间是否处于上午(AM),根据小时数等情况进行更新,用于12小时制下的显示和逻辑判断。 + private boolean mIsAm; + + // 用于标记是否处于24小时制视图,true表示24小时制,false表示12小时制,决定了小时选择器和上午/下午选择器的显示及相关逻辑。 + private boolean mIs24HourView; + + // 用于存储组件的启用状态,初始化为默认的启用状态,通过相应方法可以修改并控制组件及其内部控件的可操作性。 + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + // 用于标记是否处于初始化阶段,在初始化过程中一些逻辑处理可能与正常使用阶段有所不同,避免不必要的重复操作或异常情况。 + private boolean mInitialising; + + // 定义一个接口类型的变量,用于设置日期时间改变时的回调监听器,外部类可以实现该接口来监听用户在DateTimePicker上操作导致的日期时间变化情况。 + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + // 内部类,实现了NumberPicker.OnValueChangeListener接口,用于监听日期选择器(mDateSpinner)的值变化事件。 + // 当用户在日期选择器上选择了不同的日期时,会触发该监听器的onValueChange方法,进而更新内部的日期信息(mDate)以及相关的显示控件,并通知外部监听器日期时间已改变。 + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 根据新选择的日期与旧日期的差值,调整内部的Calendar对象(mDate)的日期信息,实现日期的变更。 + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + // 更新日期显示相关的控件,确保界面上显示的日期信息是最新的。 + updateDateControl(); + // 调用onDateTimeChanged方法,通知外部监听器日期时间已发生改变,触发相应的回调逻辑(如果有设置监听器的话)。 + onDateTimeChanged(); + } + }; + + // 内部类,实现了NumberPicker.OnValueChangeListener接口,用于监听小时选择器(mHourSpinner)的值变化事件。 + // 当用户在小时选择器上选择了不同的小时值时,会触发该监听器的onValueChange方法,根据12/24小时制等不同情况进行日期、时间以及相关显示控件的更新,并通知外部监听器日期时间已改变。 + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + // 在12小时制下,如果当前是下午(!mIsAm)且从11点(HOURS_IN_HALF_DAY - 1)切换到12点(HOURS_IN_HALF_DAY),则日期需要加一天,标记日期已改变。 + 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; + } + // 在12小时制下,如果当前是上午(mIsAm)且从12点(HOURS_IN_HALF_DAY)切换到11点(HOURS_IN_HALF_DAY - 1),则日期需要减一天,标记日期已改变。 + else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + // 如果是从11点切换到12点或者从12点切换到11点,还需要切换上午/下午(AM/PM)的标记,并更新对应的显示控件。 + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm =!mIsAm; + updateAmPmControl(); + } + } else { + // 在24小时制下,如果从23点(HOURS_IN_ALL_DAY - 1)切换到0点,则日期需要加一天,标记日期已改变。 + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } + // 在24小时制下,如果从0点切换到23点,则日期需要减一天,标记日期已改变。 + else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + // 根据当前选择的小时值以及上午/下午标记(在12小时制下),计算出用于设置到内部Calendar对象(mDate)的小时数,并进行设置。 + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + // 通知外部监听器日期时间已发生改变,触发相应的回调逻辑(如果有设置监听器的话)。 + onDateTimeChanged(); + // 如果日期发生了改变,更新当前的年、月、日信息到组件内部的Calendar对象(mDate)中,确保整体日期时间信息的一致性。 + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + // 内部类,实现了NumberPicker.OnValueChangeListener接口,用于监听分钟选择器(mMinuteSpinner)的值变化事件。 + // 当用户在分钟选择器上选择了不同的分钟值时,会触发该监听器的onValueChange方法,根据分钟值的变化情况进行日期、时间以及相关显示控件的更新,并通知外部监听器日期时间已改变。 + 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; + // 如果从最大分钟值(59)切换到最小分钟值(0),则小时数需要加1,表示时间向后推移了一小时。 + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } + // 如果从最小分钟值(0)切换到最大分钟值(59),则小时数需要减1,表示时间向前推移了一小时。 + else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + } + if (offset!= 0) { + // 根据分钟值变化导致的小时数偏移,调整内部的Calendar对象(mDate)的小时信息,实现时间的变更。 + 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(); + } + } + // 设置内部的Calendar对象(mDate)的分钟信息为用户选择的新分钟值。 + mDate.set(Calendar.MINUTE, newVal); + // 通知外部监听器日期时间已发生改变,触发相应的回调逻辑(如果有设置监听器的话)。 + onDateTimeChanged(); + } + }; + + // 内部类,实现了NumberPicker.OnValueChangeListener接口,用于监听上午/下午(AM/PM)选择器(mAmPmSpinner)的值变化事件。 + // 当用户在上午/下午选择器上切换了选项时,会触发该监听器的onValueChange方法,相应地调整内部的日期时间信息以及更新相关显示控件,并通知外部监听器日期时间已改变。 + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mIsAm =!mIsAm; + // 如果切换到上午(AM),则将小时数减去12小时(从下午时间转换到上午时间对应的小时调整)。 + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } + // 如果切换到下午(PM),则将小时数加上12小时(从上午时间转换到下午时间对应的小时调整)。 + else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + // 更新上午/下午显示相关的控件,确保界面上显示的信息与内部状态一致。 + updateAmPmControl(); + // 通知外部监听器日期时间已发生改变,触发相应的回调逻辑(如果有设置监听器的话)。 + onDateTimeChanged(); + } + }; + + // 定义一个接口,用于外部类实现,以便在DateTimePicker的日期时间发生改变时接收到通知并进行相应的处理。 + public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); + } + + // 构造函数,创建一个DateTimePicker实例,使用当前系统时间作为初始时间进行初始化。 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + // 构造函数,创建一个DateTimePicker实例,并使用指定的日期时间(以毫秒为单位的时间戳)作为初始时间进行初始化。 + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + // 构造函数,创建一个DateTimePicker实例,使用指定的日期时间(以毫秒为单位的时间戳)和是否为24小时制作为参数进行初始化,这是最完整的初始化构造函数。 + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + // 获取一个Calendar实例,用于存储和操作日期时间信息,初始化为当前时间(如果没有传入特定日期时间的话)。 + mDate = Calendar.getInstance(); + mInitialising = true; + // 根据当前小时数判断是否处于上午(AM),用于初始化上午/下午标记(如果当前小时大于等于12,则为下午,即!mIsAm)。 + mIsAm = getCurrentHourOfDay() >= HOURS \ No newline at end of file diff --git a/ui/DateTimePickerDialog.java b/ui/DateTimePickerDialog.java new file mode 100644 index 0000000..aacca4c --- /dev/null +++ b/ui/DateTimePickerDialog.java @@ -0,0 +1,125 @@ +/* + * 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 java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +// DateTimePickerDialog类继承自AlertDialog,同时实现了OnClickListener接口,它是一个自定义的对话框类,用于展示日期和时间选择的界面,并在用户操作后返回选择的日期时间结果,方便用户在应用中进行日期时间的设置操作。 +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + // Calendar对象,用于存储当前在对话框中显示和操作的日期时间信息,初始化为当前系统时间,后续会根据用户在日期时间选择器中的操作进行更新。 + private Calendar mDate = Calendar.getInstance(); + // 用于标记是否处于24小时制视图,决定了日期时间选择器以及对话框标题中时间显示的格式(12小时制或24小时制)。 + private boolean mIs24HourView; + // 定义一个接口类型的变量,用于设置当用户在对话框中点击确定按钮并设置好日期时间后触发的回调监听器,外部类可实现该接口来处理用户选择的日期时间结果。 + private OnDateTimeSetListener mOnDateTimeSetListener; + // 用于显示日期和时间选择界面的DateTimePicker实例,用户通过它来实际操作选择具体的年、月、日、时、分等信息。 + private DateTimePicker mDateTimePicker; + + // 定义一个接口,供外部类实现,当用户在对话框中完成日期时间设置并点击确定按钮后,会调用该接口中的方法,将对话框实例以及选择的日期时间(以毫秒为单位的时间戳)传递给外部类进行后续处理。 + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + // 构造函数,创建一个DateTimePickerDialog实例,传入上下文(Context)以及初始的日期时间(以毫秒为单位的时间戳)参数,用于初始化对话框的相关属性和显示内容。 + public DateTimePickerDialog(Context context, long date) { + super(context); + // 创建一个DateTimePicker实例,用于在对话框中展示日期时间选择的界面,它内部包含了各种选择器(如日期、小时、分钟等选择器)供用户操作。 + mDateTimePicker = new DateTimePicker(context); + // 将创建的DateTimePicker视图设置为对话框的主要显示内容,使得对话框展示出日期时间选择的界面给用户。 + setView(mDateTimePicker); + + // 为DateTimePicker设置日期时间改变的监听器,当用户在DateTimePicker上操作改变了日期、时间等信息时,会触发该监听器的onDateTimeChanged方法。 + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + // 根据用户在DateTimePicker上选择的新日期时间信息,更新内部存储的Calendar对象(mDate)的年、月、日、时、分等字段,使其保持与用户选择一致。 + 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方法,根据更新后的日期时间信息来更新对话框的标题,使其显示当前选择的日期时间。 + updateTitle(mDate.getTimeInMillis()); + } + }); + + // 设置内部存储的Calendar对象(mDate)的时间为传入的初始日期时间,并将秒数设置为0,确保初始时间的准确性和一致性。 + mDate.setTimeInMillis(date); + mDate.set(Calendar.SECOND, 0); + + // 将DateTimePicker的当前显示日期时间设置为与内部存储的Calendar对象(mDate)一致,保证初始界面显示的时间是传入的初始时间。 + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + + // 设置对话框的“确定”按钮文本(从资源文件中获取对应的字符串)以及点击该按钮的监听器为当前类实例(因为实现了OnClickListener接口),当用户点击“确定”按钮时会触发onClick方法进行相应处理。 + setButton(context.getString(R.string.datetime_dialog_ok), this); + + // 设置对话框的“取消”按钮文本(从资源文件中获取对应的字符串)以及点击该按钮的监听器为null,意味着点击“取消”按钮时不做额外的特定操作(通常会关闭对话框)。 + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + + // 根据系统当前的时间格式设置(是否为24小时制)来初始化对话框的时间显示格式,调用set24HourView方法进行设置,同时会影响DateTimePicker内部时间选择器的显示格式。 + set24HourView(DateFormat.is24HourFormat(this.getContext())); + + // 初次更新对话框的标题,使其显示初始的日期时间信息,调用updateTitle方法根据当前的日期时间来设置合适的标题内容。 + updateTitle(mDate.getTimeInMillis()); + } + + // 用于设置对话框是否采用24小时制视图的方法,通过传入的布尔值参数来更新内部的mIs24HourView标记,并相应地影响日期时间选择器以及对话框标题中时间显示的格式。 + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + // 用于设置当用户在对话框中点击确定按钮后触发的回调监听器的方法,外部类可以通过传入实现了OnDateTimeSetListener接口的实例来处理用户选择的日期时间结果。 + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + // 私有方法,用于更新对话框的标题内容,根据传入的日期时间(以毫秒为单位的时间戳)以及当前设置的时间显示格式(24小时制或12小时制)等信息,生成合适的标题字符串并设置给对话框。 + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | // 设置显示年份的格式标志 + DateUtils.FORMAT_SHOW_DATE | // 设置显示日期(月、日等)的格式标志 + DateUtils.FORMAT_SHOW_TIME; // 设置显示时间(时、分等)的格式标志 + + // 根据是否为24小时制视图,添加对应的时间格式标志,用于控制时间在标题中的显示格式(24小时制或12小时制)。 + flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; + + // 使用DateUtils工具类的formatDateTime方法,根据传入的上下文、日期时间以及格式标志等信息,生成格式化后的日期时间字符串,并设置为对话框的标题内容。 + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + // 实现OnClickListener接口的方法,当用户点击对话框中的按钮(如“确定”按钮)时会被调用,根据按钮的点击情况以及是否设置了OnDateTimeSetListener监听器,进行相应的操作,例如将用户选择的日期时间结果传递给外部监听器进行处理。 + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener!= null) { + // 如果设置了OnDateTimeSetListener监听器,调用其OnDateTimeSet方法,将当前对话框实例以及用户选择的日期时间(通过内部存储的Calendar对象获取时间戳)传递给外部类进行后续处理。 + 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..e69de29 diff --git a/ui/FoldersListAdapter.java b/ui/FoldersListAdapter.java new file mode 100644 index 0000000..c4110f9 --- /dev/null +++ b/ui/FoldersListAdapter.java @@ -0,0 +1,101 @@ +/* + * 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.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + +// FoldersListAdapter类继承自CursorAdapter,它是一个用于将数据库游标(Cursor)中的数据适配到ListView等可展示列表视图组件上的适配器类, +// 在这里主要用于处理文件夹相关数据的展示,比如将文件夹的名称等信息展示在列表中。 +public class FoldersListAdapter extends CursorAdapter { + + // 定义查询数据库时使用的投影(即要查询的列),用于获取文件夹相关信息,这里包含文件夹的ID和摘要(可能用于显示文件夹名称等相关用途)两列。 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义列索引常量,方便后续从游标(Cursor)中获取对应列的数据,这里分别对应ID列和名称相关列(通过SNIPPET列来获取文件夹名称等情况)的索引。 + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + // 构造函数,用于创建FoldersListAdapter实例,接收上下文(Context)和数据库游标(Cursor)作为参数, + // 调用父类(CursorAdapter)的构造函数进行初始化,将传入的游标与适配器关联起来,以便后续处理数据展示相关操作。 + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + } + + // 重写CursorAdapter的抽象方法newView,该方法的作用是创建一个新的视图(View)用于展示游标中的每一项数据, + // 在这里返回一个自定义的FolderListItem实例,它继承自LinearLayout,用于展示文件夹相关信息的具体布局。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + // 重写CursorAdapter的抽象方法bindView,该方法用于将游标中当前位置的数据绑定到指定的视图(View)上,进行具体的数据展示设置, + // 比如设置文本内容到对应的TextView等控件中,在这里根据游标中的数据判断文件夹名称的显示内容,并调用FolderListItem的bind方法进行展示设置。 + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + // 判断如果当前游标中获取的文件夹ID等于根文件夹ID(Notes.ID_ROOT_FOLDER,具体含义由Notes类定义), + // 则显示特定的表示根文件夹的字符串(从资源文件中获取对应的文本),否则显示游标中对应列(NAME_COLUMN)获取到的文件夹名称。 + 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)和列表中的位置(position)参数, + // 根据位置获取对应的游标(Cursor),然后按照前面的逻辑判断并返回相应的文件夹名称,方便外部调用获取具体的文件夹名称信息。 + 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); + } + + // 内部私有类,继承自LinearLayout,用于定义展示文件夹信息的具体布局和相关操作逻辑,它是每个列表项的具体视图实现。 + private class FolderListItem extends LinearLayout { + + // 用于显示文件夹名称的TextView控件,通过在构造函数中查找布局文件中的对应控件实例进行后续操作。 + private TextView mName; + + // 构造函数,接收上下文(Context)参数,调用父类(LinearLayout)的构造函数进行初始化, + // 并通过inflate方法将指定的布局文件(R.layout.folder_list_item)填充到当前视图中,然后获取布局中的TextView控件实例。 + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + // 自定义的方法,用于将传入的文件夹名称设置到对应的TextView(mName)控件上,进行具体的文本显示设置,使得列表项能正确展示文件夹名称信息。 + 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..25f6aa4 --- /dev/null +++ b/ui/NoteEditActivity.java @@ -0,0 +1,269 @@ +/* + * 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.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// NoteEditActivity类继承自Activity,它是用于编辑笔记的主要Activity,实现了多个接口来处理用户交互、笔记设置变更以及文本内容变化等相关逻辑, +// 提供了丰富的功能,如文本编辑、样式设置、提醒设置、分享、删除笔记等操作。 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 内部类,用于持有笔记头部相关视图控件的引用,方便在外部类中对这些控件进行统一操作和管理,例如设置文本、控制显示隐藏等。 + private class HeadViewHolder { + public TextView tvModified; + public ImageView ivAlertIcon; + public TextView tvAlertDate; + public ImageView ibSetBgColor; + } + + // 用于存储背景颜色选择按钮(如黄色、红色等按钮)与对应的颜色资源ID的映射关系,方便根据按钮ID获取对应的颜色资源标识,以进行背景颜色相关的操作。 + private static final Map sBgSelectorBtnsMap = new HashMap(); + 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); + } + + // 用于存储背景颜色选择按钮对应的选中状态指示器(如黄色按钮选中时对应的黄色选中指示器视图)与颜色资源ID的映射关系, + // 便于根据颜色资源ID找到对应的选中指示器视图并控制其显示隐藏,以反馈当前选中的背景颜色。 + private static final Map sBgSelectorSelectionMap = new HashMap(); + static { + sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); + sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select); + sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select); + sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select); + sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); + } + + // 用于存储字体大小选择按钮(如大字体、小字体等按钮)与对应的字体大小资源ID的映射关系,方便根据按钮ID获取相应的字体大小设置,以进行字体大小相关的操作。 + private static final Map sFontSizeBtnsMap = new HashMap(); + static { + sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); + sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL); + sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM); + sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); + } + + // 用于存储字体大小选择按钮对应的选中状态指示器(如大字体按钮选中时对应的大字体选中指示器视图)与字体大小资源ID的映射关系, + // 便于根据字体大小资源ID找到对应的选中指示器视图并控制其显示隐藏,以反馈当前选中的字体大小。 + private static final Map sFontSelectorSelectionMap = new HashMap(); + static { + sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + // 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息,便于调试和查看运行情况。 + private static final String TAG = "NoteEditActivity"; + + // 用于持有笔记头部视图相关控件的实例,通过初始化赋值后,可方便地在其他方法中操作这些控件,比如设置修改日期、提醒日期等文本显示。 + private HeadViewHolder mNoteHeaderHolder; + // 笔记头部视图的整体布局视图,用于设置背景等相关样式操作,例如根据笔记的相关设置改变其背景资源显示。 + private View mHeadViewPanel; + // 用于显示和选择笔记背景颜色的视图,包含多个背景颜色选择按钮等,通过控制其显示隐藏以及按钮点击事件来实现背景颜色的选择功能。 + private View mNoteBgColorSelector; + // 用于显示和选择字体大小的视图,包含多个字体大小选择按钮等,通过点击按钮进行字体大小的切换操作,并更新相关文本的显示样式。 + private View mFontSizeSelector; + // 用于编辑笔记文本内容的EditText控件,用户在该控件中输入、修改笔记的具体文字内容,同时也会根据其他设置(如字体大小等)更新显示样式。 + private EditText mNoteEditor; + // 包含笔记编辑区域(mNoteEditor)的整体布局视图,用于设置其背景等相关样式,使其与笔记的整体风格和设置保持一致。 + private View mNoteEditorPanel; + // 代表正在编辑的笔记对象,封装了笔记的各种属性(如文本内容、背景颜色、提醒设置等)以及相关的操作方法(如保存、加载等),是与笔记数据交互的核心对象。 + private WorkingNote mWorkingNote; + // 用于获取和操作应用的共享偏好设置(SharedPreferences),可以保存和读取一些用户的个性化设置信息,例如字体大小偏好设置等。 + private SharedPreferences mSharedPrefs; + // 用于记录当前选中的字体大小资源ID,通过读取共享偏好设置初始化,并在字体大小切换时更新,以确保文本编辑区域能正确应用对应的字体大小样式。 + private int mFontSizeId; + + // 用于在共享偏好设置中标识字体大小偏好设置项的键名,通过该键可以准确地存取对应的字体大小设置值。 + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + + // 定义快捷方式图标标题的最大长度,用于创建桌面快捷方式时,截取合适长度的笔记内容作为快捷方式的显示标题,避免标题过长显示不美观。 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + + // 定义表示选中状态的标记字符串,用于在处理列表模式下的笔记内容时,标识某一项是否被选中,例如在复选框列表形式的笔记中表示该项已选中。 + public static final String TAG_CHECKED = String.valueOf('\u221A'); + // 定义表示未选中状态的标记字符串,用于在处理列表模式下的笔记内容时,标识某一项是否未被选中,例如在复选框列表形式的笔记中表示该项未选中。 + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); + + // 用于在列表模式下展示笔记内容的线性布局,当笔记处于列表模式(如复选框列表形式)时,每个列表项会添加到该布局中进行展示,方便用户以列表形式查看和编辑笔记内容。 + private LinearLayout mEditTextList; + + // 用于存储用户的搜索查询字符串,当从搜索结果进入笔记编辑页面时,会携带该查询内容,可用于在笔记内容中高亮显示匹配的查询部分等相关操作。 + private String mUserQuery; + // 用于编译用户查询字符串为正则表达式模式,方便后续在笔记内容中进行匹配查找,以实现如高亮显示查询结果等功能。 + private Pattern mPattern; + + // Activity创建时调用的方法,用于进行一些初始化操作,如设置Activity的布局内容,根据传入的Intent初始化活动状态等,是整个Activity生命周期的起始方法。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置该Activity对应的布局文件为R.layout.note_edit,该布局文件定义了笔记编辑页面的整体UI结构,包含各种视图控件的布局和样式等。 + this.setContentView(R.layout.note_edit); + + // 如果是首次创建该Activity(savedInstanceState为null)且无法成功初始化活动状态(initActivityState方法返回false), + // 则直接结束该Activity,不再进行后续操作,因为可能缺少必要的启动参数等导致无法正常展示编辑页面。 + if (savedInstanceState == null &&!initActivityState(getIntent())) { + finish(); + return; + } + // 调用初始化资源的方法,用于获取布局中的各个视图控件实例,并进行一些相关的初始设置,例如设置点击监听器等。 + initResources(); + } + + /** + * 当前Activity在内存不足时可能会被系统销毁。当再次被用户启动时,需要恢复之前的状态, + * 该方法会在这种情况下被调用,用于从保存的状态中恢复Activity的相关数据和设置。 + */ + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + // 如果保存的状态不为空且包含特定的额外数据(通过Intent.EXTRA_UID标识),则尝试重新初始化活动状态, + // 若初始化失败则结束该Activity,若成功则表示从之前被销毁的状态中恢复过来了,并在日志中记录恢复信息。 + 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"); + } + } + + // 用于根据传入的Intent初始化Activity的状态,根据Intent的不同动作(ACTION_VIEW或ACTION_INSERT_OR_EDIT等)以及携带的额外数据, + // 进行相应的操作,如加载已有笔记、创建新笔记等,并返回初始化是否成功的标志,是决定Activity能否正常展示编辑功能的关键方法。 + private boolean initActivityState(Intent intent) { + // 初始化为空,表示尚未确定要编辑的笔记对象,后续根据Intent的具体情况进行赋值和加载操作。 + mWorkingNote = null; + // 判断Intent的动作是否为查看(ACTION_VIEW),如果是查看已有笔记的情况,则进行以下操作。 + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + // 获取Intent中传递的笔记ID,如果没有传递则默认为0,后续会根据该ID来查找并加载对应的笔记数据。 + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + // 如果Intent中包含搜索相关的额外数据(SearchManager.EXTRA_DATA_KEY),说明是从搜索结果进入编辑页面, + // 则根据传递的搜索数据获取笔记ID,并获取用户的搜索查询字符串,用于后续在笔记中展示搜索匹配情况等操作。 + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + + // 通过DataUtils工具类方法检查该笔记ID对应的笔记是否在笔记数据库中可见(是否存在且符合类型要求等), + // 如果不存在,则跳转到笔记列表页面(NotesListActivity),并显示提示信息告知用户笔记不存在,然后结束当前编辑Activity。 + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); + finish(); + return false; + } else { + // 如果笔记存在,则通过WorkingNote的加载方法,根据笔记ID加载对应的笔记对象到mWorkingNote中, + // 如果加载失败则记录错误日志并结束当前编辑Activity,若加载成功则继续后续的初始化设置。 + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + finish(); + return false; + } + } + // 设置软键盘的显示模式,使其初始状态为隐藏,并且当软键盘弹出时,调整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())) { + // 处理创建新笔记或编辑已有笔记的情况,以下是获取创建或编辑笔记所需的各种额外数据,如文件夹ID、小部件相关信息、背景资源ID等。 + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + + // 尝试解析通话记录相关的笔记内容,如果传入的通话日期(callDate)和电话号码(phoneNumber)都不为空, + // 则根据这两个信息查找是否已存在对应的笔记,如果存在则加载该笔记,若不存在则创建一个空的笔记并转换为通话记录笔记格式,填充相应的通话数据。 + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate!= 0 && phoneNumber!= null) { + if (TextUtils.isEmpty(phoneNumber)) { + Log.w(TAG, "The call record number is null"); + } + long noteId = 0; + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), + phoneNumber, callDate)) > 0) { + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load call note failed with note id" + noteId); \ No newline at end of file diff --git a/ui/NoteEditText.java b/ui/NoteEditText.java new file mode 100644 index 0000000..f04b165 --- /dev/null +++ b/ui/NoteEditText.java @@ -0,0 +1,259 @@ +/* + * 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.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +// NoteEditText类继承自EditText,它是一个自定义的文本编辑视图类,在基础的文本编辑功能之上,添加了一些与特定业务逻辑相关的功能, +// 比如处理不同类型链接的点击、根据按键操作触发相应的文本变更回调等功能,用于在笔记编辑场景下更灵活地处理文本输入与交互操作。 +public class NoteEditText extends EditText { + + // 用于日志记录的标签,方便在日志输出中识别该类相关的日志信息,便于调试和查看运行情况。 + private static final String TAG = "NoteEditText"; + // 用于记录当前该文本编辑视图在整体布局中的索引位置,例如在列表模式下多个文本编辑视图组成列表时,标识其顺序位置,方便进行相关操作和逻辑处理。 + private int mIndex; + // 用于记录在按下删除键(KEYCODE_DEL)之前文本的选择起始位置,以便后续判断是否执行特定的删除逻辑,比如在特定条件下触发删除整个文本编辑视图等操作。 + private int mSelectionStartBeforeDelete; + + // 定义表示电话号码链接的协议头字符串,用于识别文本中是否包含电话号码类型的链接内容,后续可根据此进行相应的处理,如点击跳转拨号等操作。 + private static final String SCHEME_TEL = "tel:"; + // 定义表示超文本链接(HTTP协议)的协议头字符串,用于识别文本中是否包含网页链接内容,以便进行如点击打开网页等相关操作。 + private static final String SCHEME_HTTP = "http:"; + // 定义表示电子邮件链接的协议头字符串,用于识别文本中是否包含邮件链接内容,方便进行如点击启动邮件客户端等相应处理。 + private static final String SCHEME_EMAIL = "mailto:"; + + // 用于存储不同链接协议(如tel、http、mailto等)与对应的资源ID(通常用于显示给用户的操作提示字符串资源)的映射关系, + // 方便根据链接类型获取相应的提示文本资源,例如在弹出的菜单中显示合适的操作选项名称。 + private static final Map sSchemaActionResMap = new HashMap(); + 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); + } + + // 定义一个接口,用于与外部进行交互,当文本编辑视图发生特定的文本变更事件(如删除、添加文本、文本内容改变等情况)时, + // 会回调该接口中的相应方法,供外部类(如包含该文本编辑视图的Activity)进行相关逻辑处理,实现对文本编辑操作的监听和响应。 + public interface OnTextViewChangeListener { + /** + * 当按下删除键({@link KeyEvent#KEYCODE_DEL})且文本内容为空时,触发该方法,用于删除当前的文本编辑视图相关文本内容, + * 外部类可根据索引等信息进行相应的布局和数据更新操作。 + */ + void onEditTextDelete(int index, String text); + + /** + * 当按下回车键({@link KeyEvent#KEYCODE_ENTER})时,触发该方法,用于在当前文本编辑视图之后添加新的文本编辑视图及相应文本内容, + * 外部类可以根据传递的索引和文本信息进行布局添加等操作,实现类似换行添加新内容的效果。 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本内容发生改变时,触发该方法,用于根据文本是否为空来决定隐藏或显示相关的菜单项等操作,例如根据文本有无隐藏或显示编辑相关的功能选项。 + */ + void onTextChange(int index, boolean hasText); + } + + // 用于持有实现了OnTextViewChangeListener接口的实例,外部类通过设置该实例来监听文本编辑视图的相关文本变更事件,以便进行对应的业务逻辑处理。 + private OnTextViewChangeListener mOnTextViewChangeListener; + + // 构造函数,接收上下文(Context)参数,调用父类(EditText)的另一个构造函数进行初始化,同时将索引(mIndex)初始化为0, + // 该构造函数通常在代码中通过 `new` 关键字创建实例时使用,用于简单创建一个文本编辑视图实例并设置初始索引值。 + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + // 用于设置当前文本编辑视图在整体布局中的索引位置的方法,外部类可以通过调用该方法来更新索引值,确保在进行相关操作时能准确识别每个文本编辑视图的顺序和位置。 + public void setIndex(int index) { + mIndex = index; + } + + // 用于设置实现了OnTextViewChangeListener接口的监听器实例的方法,外部类通过传入实现了该接口的对象, + // 使得文本编辑视图能够在特定文本变更事件发生时回调对应的接口方法,实现对文本编辑操作的监听和响应。 + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + // 构造函数,接收上下文(Context)和属性集(AttributeSet)参数,调用父类(EditText)的对应构造函数进行初始化, + // 并采用系统默认的编辑文本样式(android.R.attr.editTextStyle)来设置文本编辑视图的外观和基本属性, + // 该构造函数通常在布局文件中通过 XML 配置创建该视图实例时被调用,以解析并应用布局中定义的相关属性设置。 + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + // 构造函数,接收上下文(Context)、属性集(AttributeSet)和默认样式(defStyle)参数,调用父类(EditText)的对应构造函数进行初始化, + // 该构造函数提供了更灵活的样式定制功能,可根据传入的默认样式参数来设置文本编辑视图的外观和属性,不过这里暂未做额外的自定义初始化逻辑(TODO 部分)。 + 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) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // 获取触摸点的原始 X 坐标(相对于文本编辑视图左上角)。 + int x = (int) event.getX(); + // 获取触摸点的原始 Y 坐标(相对于文本编辑视图左上角)。 + int y = (int) event.getY(); + // 调整 X 坐标,减去文本编辑视图的左内边距,使其坐标基于文本内容区域的左上角。 + x -= getTotalPaddingLeft(); + // 调整 Y 坐标,减去文本编辑视图的上内边距,使其坐标基于文本内容区域的左上角。 + y -= getTotalPaddingTop(); + // 进一步调整 X 坐标,加上文本编辑视图的水平滚动偏移量,以处理滚动情况下的正确位置计算。 + x += getScrollX(); + // 进一步调整 Y 坐标,加上文本编辑视图的垂直滚动偏移量,以处理滚动情况下的正确位置计算。 + y += getScrollY(); + + // 获取文本编辑视图的文本布局对象,用于后续根据坐标计算文本位置相关操作。 + Layout layout = getLayout(); + // 根据触摸点的垂直坐标(Y坐标)获取对应的文本行号,确定触摸点所在的文本行。 + int line = layout.getLineForVertical(y); + // 根据触摸点所在的文本行以及水平坐标(X坐标)获取对应的文本偏移量,即确定触摸点在该行文本中的具体位置偏移量。 + int off = layout.getOffsetForHorizontal(line, x); + // 根据计算得到的文本偏移量,设置文本的选择位置,即将光标定位到触摸点对应的文本位置处,方便用户进行后续的文本编辑操作。 + Selection.setSelection(getText(), off); + break; + } + + // 调用父类(EditText)的onTouchEvent方法,确保其他默认的触摸事件处理逻辑也能正常执行,例如处理长按弹出复制粘贴等菜单操作等。 + return super.onTouchEvent(event); + } + + // 重写父类(EditText)的按键按下事件处理方法(onKeyDown),用于捕获特定按键按下的操作, + // 这里主要针对回车键(KEYCODE_ENTER)和删除键(KEYCODE_DEL)进行了相关逻辑处理,例如记录一些必要的状态信息等。 + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + // 如果设置了OnTextViewChangeListener监听器,返回false,可能是为了让后续的默认回车键处理逻辑(如换行等)先不执行, + // 而是等待在按键抬起(onKeyUp)时由自定义的逻辑来处理回车键相关操作,比如添加新的文本编辑视图等。 + if (mOnTextViewChangeListener!= null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + // 当按下删除键时,记录当前文本的选择起始位置,用于后续在按键抬起时判断是否执行特定的删除操作,例如删除整个文本编辑视图等情况。 + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + // 调用父类(EditText)的onKeyDown方法,确保其他默认的按键按下事件处理逻辑也能正常执行,例如处理其他按键的功能(如方向键移动光标等)。 + return super.onKeyDown(keyCode, event); + } + + // 重写父类(EditText)的按键抬起事件处理方法(onKeyUp),用于在特定按键抬起时执行相应的业务逻辑, + // 这里针对删除键(KEYCODE_DEL)和回车键(KEYCODE_ENTER)进行了详细的操作处理,根据不同情况触发相应的文本变更回调方法等。 + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener!= null) { + // 判断如果在按下删除键之前文本的选择起始位置为0(意味着可能要删除整个文本内容)且当前文本编辑视图的索引不为0(不是第一个视图), + // 则调用OnTextViewChangeListener接口的onEditTextDelete方法,通知外部类进行相应的删除操作,例如从布局中移除该文本编辑视图等, + // 并返回true表示该按键事件已被处理,阻止父类的默认删除键抬起处理逻辑(通常是删除单个字符等操作)执行。 + if (0 == mSelectionStartBeforeDelete && mIndex!= 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + // 如果没有设置OnTextViewChangeListener监听器,则在日志中记录提示信息,便于调试发现问题,因为此时无法执行相应的自定义删除逻辑。 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener!= null) { + // 获取当前文本的选择起始位置,用于后续截取要添加到新文本编辑视图中的文本内容。 + int selectionStart = getSelectionStart(); + // 截取从选择起始位置到文本末尾的内容作为要添加到新文本编辑视图中的文本,通过调用subSequence方法获取子串并转换为字符串类型。 + String text = getText().subSequence(selectionStart, length()).toString(); + // 将当前文本编辑视图中的文本内容更新为从开头到选择起始位置的部分,相当于把要添加到新视图的文本分离出来, + // 后续会通过onEditTextEnter方法将该文本添加到新创建的文本编辑视图中。 + setText(getText().subSequence(0, selectionStart)); + // 调用OnTextViewChangeListener接口的onEditTextEnter方法,通知外部类添加新的文本编辑视图及相应文本内容, + // 传入当前索引加1作为新视图的索引,表示添加在当前视图之后,以及截取的文本内容作为新视图的初始文本。 + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + // 如果没有设置OnTextViewChangeListener监听器,则在日志中记录提示信息,便于调试发现问题,因为此时无法执行相应的自定义回车键相关逻辑。 + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + // 调用父类(EditText)的onKeyUp方法,确保其他默认的按键抬起事件处理逻辑也能正常执行,例如处理按键抬起后的一些状态重置等操作。 + return super.onKeyUp(keyCode, event); + } + + // 重写父类(EditText)的焦点改变事件处理方法(onFocusChanged),用于在文本编辑视图的焦点发生改变(获得焦点或失去焦点)时执行相应的逻辑, + // 这里根据焦点状态以及文本内容是否为空,调用OnTextViewChangeListener接口的onTextChange方法通知外部类进行相关操作,例如显示或隐藏某些菜单项等。 + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener!= null) { + // 如果失去焦点且文本内容为空,调用OnTextViewChangeListener接口的onTextChange方法,通知外部类进行相应操作, + // 例如隐藏与文本编辑相关的功能选项等,传入当前索引以及表示文本为空的标志(false)。 + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + // 如果获得焦点或者文本内容不为空,调用OnTextViewChangeListener接口的onTextChange方法,通知外部类进行相应操作, + // 例如显示与文本编辑相关的功能选项等,传入当前索引以及表示文本不为空的标志(true)。 + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + // 调用父类(EditText)的onFocusChanged方法,确保其他默认的焦点改变事件处理逻辑也能正常执行,例如处理焦点改变后的一些界面更新等操作。 + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + // 重写父类(EditText)的创建上下文菜单方法(onCreateContextMenu),用于在长按文本编辑视图弹出上下文菜单时,添加自定义的菜单项及相应的点击处理逻辑, + // 这里主要针对文本中包含的链接(URLSpan类型)进行处理,根据链接类型添加对应的操作菜单项,例如点击电话号码链接可拨打电话等操作。 + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + // 获取当前文本选择的起始位置,用于后续确定选中的文本范围来查找其中包含的链接信息。 + int selStart = getSelectionStart(); + // 获取当前文本选择的结束位置,用于后续确定选中的文本范围来查找其中包含的链接信息。 + int selEnd = getSelectionEnd(); + + // 获取选择起始位置和结束位置中的较小值,确保后续范围判断的正确性,避免出现起始位置大于结束位置的情况。 + int min = Math.min(selStart, selEnd); + // 获取选择起始位置和结束位置中的较大值,确保后续范围判断的正确性,避免出现起始位置大于结束位置的情况。 + int max = Math.max(selStart, selEnd); + + // 从文本的选中范围(由min和max确定)内获取所有的URLSpan类型的链接对象数组,这些链接对象代表了文本中包含的各种链接信息, + // 后续会 \ No newline at end of file diff --git a/ui/NoteItemData.java b/ui/NoteItemData.java new file mode 100644 index 0000000..14d3022 --- /dev/null +++ b/ui/NoteItemData.java @@ -0,0 +1,227 @@ +/* + * 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.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类用于封装笔记相关的数据信息,它从数据库游标(Cursor)中获取各项笔记数据,并提供了一系列方法来对外提供这些数据以及判断笔记在列表中的相关位置等情况, +// 方便在展示笔记列表等场景下对笔记的各项属性进行操作和判断。 +public class NoteItemData { + + // 定义查询数据库时使用的投影(即要查询的列),用于获取笔记相关的多个属性信息,这些列对应了笔记表中的各个字段, + // 通过指定这些列,可以从数据库中获取需要的数据来构建NoteItemData实例,代表笔记的各项详细情况。 + 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, + }; + + // 定义列索引常量,方便后续从游标(Cursor)中准确获取对应列的数据,每个常量对应着PROJECTION数组中列的顺序位置, + // 通过这些索引可以清晰地从游标中提取出笔记的各个属性值,如ID、提醒日期、背景颜色ID等信息。 + 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,从游标中获取,对应着笔记在展示时所使用的背景颜色资源标识,通过该ID可以设置笔记在界面上的背景颜色样式。 + private int mBgColorId; + // 笔记的创建日期(以时间戳形式表示),从游标中获取,可用于展示笔记的创建时间信息,例如在列表中显示笔记创建的先后顺序等情况。 + private long mCreatedDate; + // 用于标记笔记是否有附件,从游标中获取对应的数据并转换为布尔值,方便判断笔记是否关联了其他附件资源,如图片、文件等,以决定是否展示附件相关的提示或操作入口。 + private boolean mHasAttachment; + // 笔记的最后修改日期(以时间戳形式表示),从游标中获取,常用于展示笔记的更新情况,例如提示用户笔记最近一次修改的时间等,便于用户了解笔记的变动历史。 + private long mModifiedDate; + // 与笔记相关的数量信息(具体含义可能根据业务场景而定,也许是关联的子笔记数量等情况),从游标中获取,可用于一些统计或展示相关的逻辑处理,例如显示某个文件夹下笔记的总数等。 + private int mNotesCount; + // 笔记所属的父级ID(如所属文件夹的ID等),从游标中获取,用于构建笔记的层级关系,方便进行分类管理和展示,例如在文件夹视图中确定笔记归属的文件夹。 + private long mParentId; + // 笔记的摘要信息(通常是笔记内容的简短描述,可能是开头部分内容等),从游标中获取,并进行一些预处理(去除特定标记字符串)后存储, + // 可用于在列表中简要展示笔记内容,让用户快速了解笔记大致情况。 + private String mSnippet; + // 笔记的类型标识,从游标中获取,用于区分不同类型的笔记(如普通笔记、系统笔记、文件夹等不同类型),根据类型可以进行不同的业务逻辑处理和展示方式。 + private int mType; + // 笔记关联的小部件ID(如果有的话),从游标中获取,用于处理笔记与桌面小部件之间的关联关系,例如更新小部件显示内容时根据该ID找到对应的笔记数据。 + private int mWidgetId; + // 笔记关联的小部件类型标识,从游标中获取,用于区分不同类型的小部件(可能对应不同的布局、功能等),以便在与小部件交互时进行相应的适配操作。 + private int mWidgetType; + // 通话记录相关笔记对应的联系人姓名,若笔记属于通话记录类型(根据父级ID判断),则通过相关工具方法尝试获取对应的联系人姓名,若获取不到则使用电话号码代替, + // 用于在展示通话记录笔记时显示更友好的联系人信息,方便用户识别。 + private String mName; + // 通话记录相关笔记对应的电话号码,若笔记属于通话记录类型(根据父级ID判断),则通过相关工具方法尝试获取对应的电话号码,用于展示通话记录相关的关键信息以及后续可能的拨打电话等操作。 + private String mPhoneNumber; + + // 用于标记该笔记数据对应的笔记是否是列表中的最后一项,通过游标判断是否处于最后位置来设置该标记,方便在列表展示等场景下进行边界判断和相关样式处理。 + private boolean mIsLastItem; + // 用于标记该笔记数据对应的笔记是否是列表中的第一项,通过游标判断是否处于第一位置来设置该标记,同样便于在列表展示等场景下进行边界判断和相关样式处理。 + private boolean mIsFirstItem; + // 用于标记该笔记数据对应的笔记是否是列表中唯一的一项,通过判断游标中的记录总数是否为1来设置该标记,可用于一些特殊的展示逻辑,例如只有一个笔记时的全屏展示等情况。 + private boolean mIsOnlyOneItem; + // 用于标记该笔记是否是某个文件夹下仅跟随的一个笔记,通过检查笔记类型以及前后位置关系等来判断,在列表展示和操作逻辑中可用于区分不同的排列情况。 + private boolean mIsOneNoteFollowingFolder; + // 用于标记该笔记是否是某个文件夹下跟随的多个笔记之一,通过检查笔记类型以及前后位置关系等来判断,同样有助于在列表展示和操作逻辑中对笔记的排列情况进行准确判断和处理。 + private boolean mIsMultiNotesFollowingFolder; + + // 构造函数,用于创建NoteItemData实例,接收上下文(Context)和数据库游标(Cursor)作为参数, + // 从游标中按照定义的列索引提取各项笔记数据,并进行一些必要的预处理和位置相关情况的判断,以初始化该实例的各个属性。 + public NoteItemData(Context context, Cursor cursor) { + // 从游标中获取笔记的ID,并赋值给对应的成员变量,作为该笔记的唯一标识。 + mId = cursor.getLong(ID_COLUMN); + // 从游标中获取笔记的提醒日期,并赋值给对应的成员变量,用于后续提醒相关的逻辑判断。 + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + // 从游标中获取笔记的背景颜色ID,并赋值给对应的成员变量,以便后续设置笔记的背景颜色样式。 + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + // 从游标中获取笔记的创建日期,并赋值给对应的成员变量,可用于展示笔记的创建时间等信息。 + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + // 从游标中获取笔记是否有附件的标识数据,转换为布尔值后赋值给对应的成员变量,用于判断笔记是否关联了附件资源。 + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0)? true : false; + // 从游标中获取笔记的最后修改日期,并赋值给对应的成员变量,用于展示笔记的更新情况等。 + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + // 从游标中获取与笔记相关的数量信息,并赋值给对应的成员变量,具体用途根据业务场景而定,比如统计相关逻辑。 + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + // 从游标中获取笔记所属的父级ID,并赋值给对应的成员变量,用于构建笔记的层级关系。 + mParentId = cursor.getLong(PARENT_ID_COLUMN); + // 从游标中获取笔记的摘要信息,先赋值给对应的成员变量,然后进行预处理,去除特定的标记字符串(如列表模式下的选中、未选中标记),以便更好地展示摘要内容。 + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + // 从游标中获取笔记的类型标识,并赋值给对应的成员变量,用于区分不同类型的笔记进行相应的业务逻辑处理。 + mType = cursor.getInt(TYPE_COLUMN); + // 从游标中获取笔记关联的小部件ID,并赋值给对应的成员变量,用于处理笔记与小部件之间的关联关系。 + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + // 从游标中获取笔记关联的小部件类型标识,并赋值给对应的成员变量,用于区分不同类型的小部件进行相应的适配操作。 + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + // 初始化电话号码为空字符串,后续根据笔记是否属于通话记录类型来尝试获取对应的电话号码信息。 + mPhoneNumber = ""; + // 判断如果笔记所属的父级ID是通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER),则尝试通过DataUtils工具类方法, + // 根据笔记ID从内容解析器(getContentResolver)获取对应的电话号码信息,并赋值给mPhoneNumber成员变量。 + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + // 如果获取到的电话号码不为空(即成功获取到了电话号码),则通过Contact类的方法尝试获取对应的联系人姓名, + // 如果获取到的联系人姓名为null(可能不存在对应的联系人记录等情况),则使用电话号码作为显示名称赋值给mName成员变量。 + if (!TextUtils.isEmpty(mPhoneNumber)) { + mName = Contact.getContact(context, mPhoneNumber); + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + // 如果最终获取到的联系人姓名仍为null(例如前面获取过程出现问题或者本身就没有对应联系人),则将姓名设置为空字符串,确保成员变量有合理的默认值。 + if (mName == null) { + mName = ""; + } + + // 调用方法检查该笔记在列表中的位置相关情况,设置对应的位置标记成员变量,以便后续在列表展示等场景下进行相关判断和处理。 + checkPostion(cursor); + } + + // 私有方法,用于检查当前笔记在游标所代表的列表中的位置相关情况,通过游标提供的方法判断是否是第一、最后一项,以及是否是某个文件夹下的唯一笔记、 + // 仅跟随一个笔记或者跟随多个笔记等情况,并设置相应的成员变量标记,方便在其他方法中对外提供这些位置判断信息。 + private void checkPostion(Cursor cursor) { + // 通过游标判断是否处于最后位置,将结果赋值给mIsLastItem成员变量,标记该笔记是否是列表中的最后一项。 + mIsLastItem = cursor.isLast()? true : false; + // 通过游标判断是否处于第一位置,将结果赋值给mIsFirstItem成员变量,标记该笔记是否是列表中的第一项。 + mIsFirstItem = cursor.isFirst()? true : false; + // 通过判断游标中的记录总数是否为1,将结果赋值给mIsOnlyOneItem成员变量,标记该笔记是否是列表中唯一的一项。 + mIsOnlyOneItem = (cursor.getCount() == 1); + // 初始化为false,表示默认该笔记不是某个文件夹下跟随多个笔记的情况,后续根据具体判断逻辑进行更新。 + mIsMultiNotesFollowingFolder = false; + // 初始化为false,表示默认该笔记不是某个文件夹下仅跟随一个笔记的情况,后续根据具体判断逻辑进行更新。 + mIsOneNoteFollowingFolder = false; + + // 如果当前笔记的类型是普通笔记类型(Notes.TYPE_NOTE)且不是列表中的第一项(因为要判断前面的记录情况),则进行以下位置相关的判断逻辑。 + if (mType == Notes.TYPE_NOTE &&!mIsFirstItem) { + // 获取当前游标所处的位置索引,用于后续与前后位置的记录进行比较判断。 + int position = cursor.getPosition(); + // 将游标移动到前一条记录位置,以便检查前一条记录的类型等信息来判断与当前笔记的关系。 + if (cursor.moveToPrevious()) { + // 判断前一条记录的类型是否是文件夹类型或者系统类型,如果是,则表示当前笔记可能是跟随在某个文件夹后面的笔记情况。 + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 判断游标中的记录总数是否大于当前位置索引加1(即当前位置后面还有记录),如果是,则表示当前笔记是某个文件夹下跟随的多个笔记之一, + // 设置mIsMultiNotesFollowingFolder成员变量为true,标记该情况。 + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + // 如果后面没有其他记录了,则表示当前笔记是某个文件夹下仅跟随的一个笔记,设置mIsOneNoteFollowingFolder成员变量为true,标记该情况。 + mIsOneNoteFollowingFolder = true; + } + } + // 将游标再移回原来的位置(即当前笔记所在的位置),确保游标位置的正确性,不影响后续其他操作对游标的使用。 + if (!cursor.moveToNext()) { + 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; + } + + // 用于判断该笔记是否是列表中的第一项,返回对应的成员变量标记值,外部可通过调用该方法来获取此位置判断信息, + // 在列表展示等 \ No newline at end of file diff --git a/ui/NotesListActivity.java b/ui/NotesListActivity.java new file mode 100644 index 0000000..448d31c --- /dev/null +++ b/ui/NotesListActivity.java @@ -0,0 +1,305 @@ +// 内部类,实现了ListView.MultiChoiceModeListener和OnMenuItemClickListener接口,用于处理ListView的多选模式相关的各种操作逻辑, +// 比如创建多选模式下的菜单、处理菜单项点击、选项状态改变以及退出多选模式等操作,通过与NotesListAdapter等配合实现对笔记的批量操作功能。 +private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + // 当多选模式创建时调用该方法,用于初始化多选模式下的菜单、设置菜单项的可见性和点击监听器等操作, + // 例如根据笔记所在文件夹等情况决定“移动”菜单项是否可见,同时设置自定义的视图用于展示下拉菜单等内容。 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // 填充多选模式下的菜单布局(R.menu.note_list_options)到传入的菜单对象中,使得菜单显示相应的选项。 + getMenuInflater().inflate(R.menu.note_list_options, menu); + // 为“删除”菜单项设置点击监听器,当点击该菜单项时,会触发此内部类中对应的点击处理逻辑(通过实现OnMenuItemClickListener接口)。 + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + mMoveMenu = menu.findItem(R.id.move); + // 如果当前长按选中的笔记数据项所在的父文件夹是通话记录文件夹,或者用户创建的文件夹数量为0,那么隐藏“移动”菜单项, + // 因为在这些情况下可能不适合或无法进行移动操作。 + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + // 为“移动”菜单项设置点击监听器,以便后续处理点击该菜单项时的相应逻辑。 + mMoveMenu.setOnMenuItemClickListener(this); + } + mActionMode = mode; + // 通知笔记列表适配器进入多选模式,以便适配器进行相应的数据和视图状态更新,例如标记选中的项等。 + mNotesListAdapter.setChoiceMode(true); + // 设置ListView不再响应长按事件,因为此时已经进入了多选模式,长按相关逻辑由多选模式的操作来处理。 + mNotesListView.setLongClickable(false); + // 隐藏“新建笔记”按钮,在多选模式下通常不需要该按钮显示,避免操作冲突等情况。 + mAddNewNote.setVisibility(View.GONE); + + // 通过LayoutInflater加载一个自定义的视图(R.layout.note_list_dropdown_menu)用于作为多选模式下的自定义视图, + // 这个视图可能包含一些额外的交互元素,比如下拉菜单等。 + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + // 将自定义视图设置到ActionMode中,使其在界面上显示出来,替代默认的ActionMode样式。 + mode.setCustomView(customView); + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + // 为下拉菜单设置点击监听器,当点击下拉菜单中的菜单项时,会执行相应的逻辑,这里用于处理全选/反选操作以及更新菜单显示状态。 + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // 调用笔记列表适配器的方法,实现全选或反选已选笔记的功能,根据当前的选中状态进行切换。 + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + // 更新菜单相关的显示状态,比如更新全选按钮的文本和选中状态等。 + updateMenu(); + return true; + } + + }); + return true; + } + + // 更新菜单相关显示状态的私有方法,比如根据已选笔记的数量更新下拉菜单的标题,以及更新全选菜单项的选中状态和文本等, + // 使得菜单显示能准确反映当前的多选情况。 + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // 获取格式化后的字符串资源,用于设置下拉菜单的标题,显示当前选中的笔记数量等信息。 + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item!= null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + // 在准备多选模式下的菜单时调用(每次菜单显示前可能会调用此方法来更新菜单项状态等),目前此方法未实现具体逻辑,直接返回false, + // 可根据实际需求后续添加相应代码来动态调整菜单的准备工作,比如根据不同条件禁用或启用某些菜单项等。 + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + // 处理多选模式下菜单项点击事件的方法,根据点击的菜单项执行相应的操作逻辑,目前此方法还未完整实现具体的业务逻辑, + // 后续需根据不同菜单项的功能需求添加代码来实现诸如删除、移动所选笔记等操作。 + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + // 当多选模式销毁时调用的方法,用于清理相关的状态和设置,比如通知笔记列表适配器退出多选模式,恢复ListView的长按响应功能, + // 以及重新显示“新建笔记”按钮等,确保界面回到正常的交互状态。 + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + // 结束当前的多选模式,调用ActionMode的finish方法来关闭多选模式相关的界面和操作状态, + // 通常在完成批量操作或者取消操作时调用此方法来退出多选模式。 + public void finishActionMode() { + mActionMode.finish(); + } + + // 当多选模式下笔记的选中状态发生改变时调用的方法,用于通知笔记列表适配器更新对应笔记项的选中状态, + // 并调用updateMenu方法来更新菜单的显示状态,以保持界面与数据状态的一致性。 + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + // 处理菜单项点击事件的方法,根据点击的具体菜单项执行相应的业务逻辑,比如点击“删除”菜单项时弹出确认对话框进行笔记删除操作, + // 点击“移动”菜单项时启动查询目标文件夹的操作等,实现对所选笔记的各种批量操作功能。 + public boolean onMenuItemClick(MenuItem item) { + // 如果没有选中任何笔记项(即选中数量为0),则弹出提示Toast告知用户需要先选择笔记,然后直接返回,不执行后续逻辑。 + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + switch (item.getItemId()) { + case R.id.delete: + // 创建一个AlertDialog.Builder用于构建确认删除的对话框,设置对话框的标题、图标、提示信息等内容。 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + // 点击确认按钮后,调用batchDelete方法执行批量删除所选笔记的操作。 + batchDelete(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + // 点击“移动”菜单项时,调用startQueryDestinationFolders方法启动查询目标文件夹的操作, + // 以便后续将所选笔记移动到指定的文件夹中。 + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; + } +} + +// 内部类,实现了OnTouchListener接口,用于处理“新建笔记”按钮的触摸事件,比如判断触摸位置是否在按钮的特定透明区域, +// 以及根据触摸动作(按下、移动、抬起等)进行相应的事件分发和处理逻辑,例如将触摸事件分发给ListView等操作。 +private class NewNoteOnTouchListener implements OnTouchListener { + + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // 获取设备屏幕的默认显示对象,用于获取屏幕相关的尺寸信息,比如屏幕高度等。 + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + int eventY = start + (int) event.getY(); + /** + * Minus TitleBar's height + * 如果当前处于子文件夹状态,需要减去标题栏的高度,因为坐标计算等可能需要基于去除标题栏后的区域来进行, + * 以更准确地判断触摸位置等情况。 + */ + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + /** + * HACKME:When click the transparent part of "New Note" button, dispatch + * the event to the list view behind this button. The transparent part of + * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) + * and the line top of the button. The coordinate based on left of the "New + * Note" button. The 94 represents maximum height of the transparent part. + * Notice that, if the background of the button changes, the formula should + * also change. This is very bad, just for the UI designer's strong requirement. + * 以下是一段临时解决办法(HACKME)的代码逻辑,当点击“新建笔记”按钮的透明部分时,将触摸事件分发给按钮后面的ListView进行处理。 + * 透明部分的范围通过一个公式(y = -0.12x + 94,单位为像素,坐标基于按钮左侧)以及按钮顶部边界来界定, + * 并且提示如果按钮背景改变,该公式也需要相应更改,这种处理方式不太理想,只是为了满足UI设计师的特定要求。 + */ + if (event.getY() < (event.getX() * (-0.12) + 94)) { + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + if (view!= null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (mDispatch) { + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + default: { + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + return false; + } + +}; + +// 启动异步查询笔记列表数据的方法,根据当前所在文件夹的ID来构建合适的查询条件,然后通过异步查询处理实例(mBackgroundQueryHandler) +// 发起对笔记列表数据的查询操作,确保在后台线程进行数据库查询,不阻塞主线程,提高应用的响应性能。 +private void startAsyncNotesListQuery() { + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER)? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[]{ + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); +} + +// 内部类,继承自AsyncQueryHandler,用于在后台线程处理数据库查询操作,并在查询完成后通过回调方法处理相应的结果, +// 根据不同的查询令牌(token)区分是查询笔记列表还是文件夹列表等情况,然后执行对应的后续操作,比如更新适配器数据等。 +private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN: + // 如果是查询笔记列表的操作完成,调用笔记列表适配器的方法,传入查询得到的游标(Cursor)数据, + // 以便适配器更新界面上显示的笔记列表内容,反映最新的数据情况。 + mNotesListAdapter.changeCursor(cursor); + break; + case FOLDER_LIST_QUERY_TOKEN: + if (cursor!= null && cursor.getCount() > 0) { + // 如果是查询文件夹列表操作完成且查询结果不为空(即有文件夹数据),则调用showFolderListMenu方法展示文件夹列表菜单, + // 方便用户选择操作的目标文件夹等。 + showFolderListMenu(cursor); + } else { + Log.e(TAG, "Query folder failed"); + } + break; + default: + return; + } + } +} + +// 展示文件夹列表菜单的方法,通过AlertDialog.Builder构建一个包含文件夹列表的对话框,使用FoldersListAdapter作为适配器来展示文件夹数据, +// 并为对话框的列表项点击事件设置监听器,当用户点击某个文件夹时,执行将所选笔记移动到该文件夹的操作,并弹出相应的提示Toast告知用户操作结果, +// 最后关闭多选模式相关的操作界面。 +private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); + } + }); + builder.show(); +} + +// 创建新笔记的方法,构建一个启动NoteEditActivity的Intent,设置相应的动作(Intent.ACTION_INSERT_OR_EDIT)以及传递当前所在文件夹的ID作为额外参数, +// 然后通过startActivityForResult方法启动该Activity,以便在新建笔记操作完成后能获取返回结果并进行相应的数据更新等处理。 +private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); +} + +// 批量删除所选笔记的方法,通过AsyncTask在后台线程执行删除操作,根据是否处于同步模式(通过isSyncMode方法判断)来决定是直接删除笔记还是将笔记移动到回收站文件夹, +// 在操作完成后(onPostExecute方法中),如果涉及到与小部件相关的数据更新(比如所选笔记关联了小部件),则调用updateWidget方法更新相应的小部件显示内容, +// 最后关闭多选模式相关的操作界面。 +private void batchDelete() { + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + if (!isSyncMode()) { + // 如果未处于同步模式,直接调用DataUtils的方法批量删除所选笔记,这里 \ No newline at end of file diff --git a/ui/NotesListAdapter.java b/ui/NotesListAdapter.java new file mode 100644 index 0000000..6368e77 --- /dev/null +++ b/ui/NotesListAdapter.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * 按照许可要求使用此文件,否则不允许使用。 + * 可以通过以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,软件依据许可证分发是“按现状”分发, + * 不附带任何明示或暗示的保证或条件。请查看许可证了解具体权限和限制。 + */ + +// 所在包声明,表明该类属于笔记应用(net.micode.notes)的用户界面(ui)相关模块。 +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +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,是专门用于适配笔记列表数据与视图(如ListView)的适配器类, +// 负责处理笔记数据展示、管理笔记项的选中状态等相关操作逻辑,是笔记列表界面展示与数据交互的重要组成部分。 +public class NotesListAdapter extends CursorAdapter { + // 定义一个静态的字符串常量,作为日志输出时的标签,方便在查看日志时快速识别与该适配器相关的日志信息,利于调试与问题排查。 + private static final String TAG = "NotesListAdapter"; + // 用于保存上下文对象,通过构造函数传入,在整个适配器的生命周期内提供与应用环境相关的资源访问、视图创建等操作所需的上下文环境。 + private Context mContext; + // 使用HashMap来存储笔记项在列表中的选中状态,键(Integer类型)为笔记项在列表中的位置索引,值(Boolean类型)表示对应位置的笔记项是否被选中, + // 以此来跟踪用户在笔记列表中的多选操作情况,方便后续根据选中状态进行相应的数据处理和界面更新。 + private HashMap mSelectedIndex; + // 记录笔记的数量(仅统计实际的笔记类型数据项数量,不包含文件夹等其他非笔记类型的数据), + // 该数量会在数据内容变化(如游标更新、数据重新加载)时通过重新计算得到,用于辅助判断如全选等相关业务逻辑。 + private int mNotesCount; + // 用于标识当前适配器是否处于多选模式的布尔变量,通过设置该变量来控制适配器在不同模式下的数据处理、视图显示等行为逻辑, + // 例如在多选模式下需正确显示已选笔记项的选中状态、提供相应的批量操作菜单等。 + private boolean mChoiceMode; + + // 内部静态类,用于封装与应用小部件(Widget)相关的属性信息,主要包含小部件的唯一标识ID和小部件的类型信息, + // 在涉及笔记与小部件关联的相关操作中,用于传递和处理小部件的相关属性数据。 + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + // 构造方法,接收一个Context上下文对象,调用父类(CursorAdapter)的构造方法并传入上下文以及初始化为null的游标对象, + // 同时初始化用于记录选中状态的HashMap,保存传入的上下文对象,并将笔记数量初始化为0,完成适配器的初始化设置。 + public NotesListAdapter(Context context) { + super(context, null); + mSelectedIndex = new HashMap(); + mContext = context; + mNotesCount = 0; + } + + // 重写CursorAdapter的抽象方法,用于创建新的视图对象,该方法在需要为新的笔记项创建视图进行展示时被调用, + // 此处简单地返回一个NotesListItem类型的视图实例,通常NotesListItem是自定义的视图类,用于具体实现笔记内容在界面上的展示布局等细节逻辑。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + // 重写CursorAdapter的抽象方法,负责将数据绑定到已创建的视图上,使其能够正确展示笔记信息, + // 首先判断传入的视图是否是NotesListItem类型,如果是,则从游标中解析出笔记数据并封装成NoteItemData对象, + // 然后调用NotesListItem的bind方法,将上下文、笔记数据对象、当前的多选模式状态以及该笔记项是否被选中的状态传递进去, + // 从而实现将数据与视图进行绑定,确保笔记信息准确显示在对应的视图上。 + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + // 用于设置指定位置笔记项的选中状态的公有方法,接收笔记项在列表中的位置索引(int类型)和要设置的选中状态(布尔值)作为参数, + // 将对应的位置和选中状态存入mSelectedIndex这个HashMap中,之后调用notifyDataSetChanged方法通知适配器数据集已发生变化, + // 触发视图的刷新操作,使得界面上该笔记项的选中状态显示能够根据新设置的状态进行更新,例如改变选中项的背景颜色等视觉效果。 + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); + } + + // 用于判断当前适配器是否处于多选模式的公有方法,直接返回表示多选模式状态的成员变量mChoiceMode的值, + // 外部代码可以通过调用该方法来了解适配器当前所处的交互模式状态,进而执行与之对应的不同操作逻辑,例如在多选模式下显示特定的操作菜单等。 + public boolean isInChoiceMode() { + return mChoiceMode; + } + + // 用于设置适配器的多选模式状态的公有方法,接收一个布尔值参数,当进入多选模式时(传入true), + // 先清空之前记录的所有笔记项选中状态(通过调用mSelectedIndex的clear方法),然后将mChoiceMode变量设置为传入的模式值, + // 此后适配器会根据新的模式状态调整相关的数据处理和视图交互逻辑,例如更新界面上笔记项的选中显示效果等。 + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + // 用于实现全选或全不选所有笔记项的公有方法,接收一个布尔值参数,根据该参数决定是将所有笔记项设置为选中还是取消选中状态, + // 首先获取当前适配器关联的游标对象(通过调用getCursor方法),然后遍历游标中的所有笔记项(通过getCount方法获取笔记项总数,并移动游标到每个位置), + // 对于每个位置的笔记项,通过NoteItemData.getNoteType方法判断其类型是否为笔记(与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类型的集合用于存储笔记项的ID, + // 遍历mSelectedIndex这个记录选中状态的HashMap,对于值为true(即被选中)的项,通过调用getItemId方法获取其对应的笔记项ID, + // 接着进行合法性判断,如果获取到的ID等于Notes.ID_ROOT_FOLDER(通常根文件夹ID不应作为笔记项被选中,此处作为一种异常情况的判断), + // 则在日志中输出相应提示信息(使用Log.d方法输出调试级别的日志),否则将合法的ID添加到HashSet集合中,最后返回该包含所有选中笔记项ID的集合, + // 方便外部代码基于这些ID进行批量操作,如批量删除、批量移动等操作时确定具体操作的笔记对象。 + 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; + } + + // 用于获取所有被选中笔记项关联的小部件属性集合的公有方法,创建一个HashSet类型的集合用于存储小部件相关属性对象, + // 遍历mSelectedIndex这个记录选中状态的HashMap,对于值为true(即被选中)的项,通过调用getItem方法获取对应的游标对象(代表选中的笔记项数据), + // 如果游标不为空,则创建一个AppWidgetAttribute对象,从游标数据中解析出小部件的ID和类型信息(通过NoteItemData类的相关方法获取)并赋值给该对象, + // 然后将其添加到HashSet集合中,最后返回包含所有选中笔记项关联小部件属性的集合,该集合可用于后续与小部件相关的批量更新等操作, + // 注意按照代码中的注释说明,此处不会关闭游标,游标关闭操作由适配器统一管理,以确保数据访问的正确性和一致性。 + 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中所有值的集合(即所有笔记项的选中状态值集合,Collection类型), + // 如果该集合为空(表示没有任何笔记项设置了选中状态),则直接返回0;否则通过迭代器(Iterator)遍历该集合, + // 统计值为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方法获取当前被选中笔记项的数量, + // 然后判断如果选中数量不为0(即至少有一个笔记项被选中)且选中数量等于笔记的总数量(通过比较与mNotesCount的值),则返回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); + } + + // 重写父类(CursorAdapter)的方法,当数据集的内容发生变化时(例如数据库中的笔记数据有更新、插入或删除等操作导致游标所关联的数据改变)会被调用, + // 在此方法中,先调用父类的onContentChanged方法执行默认的内容改变处理逻辑,然后调用calcNotesCount私有方法重新计算笔记的总数量, + // 以确保后续基于笔记数量的相关业务逻辑(如全选判断等)能够基于最新的数据情况进行正确操作。 + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); + } + + // 重写父类(CursorAdapter)的方法,用于更改适配器所使用的游标对象(通常意味着数据来源发生了变化,比如重新从数据库查询获取了新的笔记数据), + // 先调用父类的changeCursor方法完成游标切换的默认操作,然后调用calcNotesCount私有方法重新计算笔记的总数量, + // 使得适配器能够基于新的游标数据更新界面展示,并保证相关数据统计逻辑的准确性,确保后续各种基于笔记数据的操作和显示都是基于最新的有效数据。 + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + // 私有方法,用于计算笔记的总数量,首先将mNotesCount初始化为0,然后遍历游标中的所有笔记项(通过getCount方法获取笔记项总数,并移动游标到每个位置), + // 对于每个笔记项,通过NoteItemData.getNoteType方法判断其类型是否为笔记(与Notes.TYPE_NOTE进行比较),如果是笔记类型,则将笔记总数量加1, + // 在遍历过程中,如果遇到游标为无效的情况(即游标为null),则通过Log.e方法输出错误日志信息,并直接返回,不再继续计算笔记数量, + // 以此保证数量统计的准确性是基于有效的游标数据进行的,避免因无效游标导致的错误计算或异常情况。 + 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..f0c873d --- /dev/null +++ b/ui/NotesListItem.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * 按照许可要求使用此文件,否则不允许使用。 + * 可以通过以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,软件依据许可证分发是“按现状”分发, + * 不附带任何明示或暗示的保证或条件。请查看许可证了解具体权限和限制。 + */ + +// 所在包声明,表明该类属于笔记应用(net.micode.notes)的用户界面(ui)相关模块。 +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控件,通过调用DateUtils工具类将时间戳转换为相对时间格式(如“几分钟前”“昨天”等)进行展示,方便用户了解笔记的更新情况。 + private TextView mTime; + // 用于显示通话记录相关名称的TextView控件,在涉及通话记录的笔记项中展示通话对方名称等信息,在其他类型笔记项中通常设置为不可见。 + private TextView mCallName; + // 用于存储当前列表项对应的笔记数据对象,方便在其他方法中获取笔记的详细信息来进行展示和逻辑处理,比如获取笔记类型、是否有提醒等属性。 + private NoteItemData mItemData; + // 用于表示笔记项是否被选中的CheckBox控件,在多选模式下根据笔记项的选中状态显示勾选与否,非多选模式下则隐藏该控件,用于用户进行多选操作的可视化交互。 + private CheckBox mCheckBox; + + // 构造方法,接收一个Context上下文对象,调用父类(LinearLayout)的构造方法传入上下文,然后通过inflate方法加载指定的布局文件(R.layout.note_item)到当前视图中, + // 最后通过findViewById方法找到布局文件中对应的各个子视图控件(如ImageView、TextView、CheckBox等)并赋值给相应的成员变量,完成视图的初始化工作。 + public NotesListItem(Context context) { + super(context); + inflate(context, R.layout.note_item, this); + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + // 用于绑定数据和设置视图显示状态的方法,接收上下文对象、笔记数据对象、是否处于多选模式的布尔值以及当前笔记项是否被选中的布尔值作为参数, + // 根据这些参数来调整视图中各个控件的显示内容和可见性等状态,使得笔记项能够正确展示对应笔记的数据信息以及符合当前交互模式(多选或非多选)的外观效果。 + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 如果处于多选模式且当前笔记数据对象的类型为普通笔记(Notes.TYPE_NOTE),则将CheckBox控件设置为可见,并根据传入的选中状态参数设置其勾选状态, + // 以便用户能看到该笔记项是否已被选中;否则将CheckBox控件隐藏,例如在非多选模式或者对于非笔记类型的数据项不需要显示选中状态。 + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + mCheckBox.setVisibility(View.GONE); + } + + // 将传入的笔记数据对象赋值给成员变量mItemData,方便后续在其他方法中获取该笔记的详细信息进行处理,例如设置背景等操作需要用到笔记相关属性。 + mItemData = data; + + // 如果当前笔记数据对象的ID等于通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER),说明当前列表项对应的是通话记录文件夹, + // 则进行如下设置: + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 隐藏用于显示通话名称的TextView控件,因为通话记录文件夹不需要显示通话对方名称。 + mCallName.setVisibility(View.GONE); + // 设置提醒图标ImageView为可见,用于展示通话记录文件夹对应的特定图标(通常用于区分不同类型的文件夹等)。 + mAlert.setVisibility(View.VISIBLE); + // 设置标题TextView的文本外观样式为主要项的样式(通过资源ID指定,R.style.TextAppearancePrimaryItem),使其在视觉上突出显示。 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + // 设置标题TextView的文本内容,拼接通话记录文件夹的名称字符串(通过资源字符串获取)以及包含的文件数量信息(通过格式化字符串展示,调用getString和format_folder_files_count方法), + // 让用户直观了解通话记录文件夹的基本情况。 + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + // 设置提醒图标ImageView的资源图片为通话记录对应的图标(R.drawable.call_record),使其显示正确的图标样式来代表通话记录文件夹。 + mAlert.setImageResource(R.drawable.call_record); + } + // 如果当前笔记数据对象的父ID等于通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER),说明当前列表项对应的是通话记录文件夹下的笔记, + // 则进行如下设置: + else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 显示用于显示通话名称的TextView控件,并设置其文本内容为笔记数据中获取的通话对方名称,以便展示通话相关的具体信息。 + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + // 设置标题TextView的文本外观样式为次要项的样式(通过资源ID指定,R.style.TextAppearanceSecondaryItem),使其与主要项(如文件夹标题等)在视觉上有所区分。 + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); + // 设置标题TextView的文本内容为格式化后的笔记摘要信息(通过DataUtils工具类的getFormattedSnippet方法获取格式化后的摘要内容),展示笔记的主要内容摘要。 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + // 如果笔记数据对象表示有提醒设置(通过hasAlert方法判断),则设置提醒图标ImageView为可见,并设置其图片资源为提醒对应的图标(R.drawable.clock),展示提醒状态; + // 否则将提醒图标ImageView隐藏,表示无提醒设置。 + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + // 如果当前笔记数据对象既不是通话记录文件夹也不是其下的笔记(即其他普通类型的笔记或文件夹等情况),则进行如下设置: + else { + // 隐藏用于显示通话名称的TextView控件,因为不需要展示通话相关信息。 + mCallName.setVisibility(View.GONE); + // 设置标题TextView的文本外观样式为主要项的样式(通过资源ID指定,R.style.TextAppearancePrimaryItem),使其在视觉上突出显示。 + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + // 如果笔记数据对象的类型是文件夹(Notes.TYPE_FOLDER),则设置标题TextView的文本内容为文件夹的摘要信息(通过getSnippet方法获取)拼接包含的文件数量信息(通过格式化字符串展示), + // 展示文件夹名称及其中包含的笔记数量情况;同时将提醒图标ImageView隐藏,因为文件夹通常不需要展示提醒相关图标。 + if (data.getType() == Notes.TYPE_FOLDER) { + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + mAlert.setVisibility(View.GONE); + } + // 如果笔记数据对象的类型是普通笔记(非文件夹类型),则设置标题TextView的文本内容为格式化后的笔记摘要信息(通过DataUtils工具类的getFormattedSnippet方法获取格式化后的摘要内容),展示笔记的主要内容摘要, + // 并且根据笔记是否有提醒设置(通过hasAlert方法判断)来决定提醒图标ImageView的显示与否及显示的图标资源(有提醒则显示提醒图标,无提醒则隐藏),与前面处理类似。 + else { + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + + // 设置用于显示时间的TextView控件的文本内容,通过DateUtils工具类的getRelativeTimeSpanString方法将笔记数据中的最后修改时间戳转换为相对时间格式的字符串进行展示, + // 方便用户直观了解笔记的最近更新时间情况。 + 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方法判断), + // 则设置当前列表项的背景资源为对应单独笔记的背景资源(通过NoteItemBgResources工具类的getNoteBgSingleRes方法获取资源ID并设置),使其有特定的视觉效果。 + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } + // 如果笔记是列表中的最后一个笔记(通过isLast方法判断),则设置当前列表项的背景资源为对应最后一个笔记的背景资源(通过NoteItemBgResources工具类的getNoteBgLastRes方法获取资源ID并设置),展示相应的外观效果。 + else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } + // 如果笔记是列表中的第一个笔记(通过isFirst方法判断)或者是某个文件夹后的多个笔记中的第一个(通过isMultiFollowingFolder方法判断), + // 则设置当前列表项的背景资源为对应第一个笔记的背景资源(通过NoteItemBgResources工具类的getNoteBgFirstRes方法获取资源ID并设置),呈现对应的视觉样式。 + else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } + // 如果笔记不属于上述几种特殊位置情况(即普通的中间笔记),则设置当前列表项的背景资源为普通笔记的默认背景资源(通过NoteItemBgResources工具类的getNoteBgNormalRes方法获取资源ID并设置),保持统一的视觉效果。 + else { + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } + // 如果笔记数据对象的类型不是普通笔记(即文件夹等其他类型),则统一设置当前列表项的背景资源为文件夹对应的背景资源(通过NoteItemBgResources工具类的getFolderBgRes方法获取资源ID并设置), + // 使得文件夹在界面上有统一的外观样式与普通笔记区分开来。 + else { + 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..b959b3a --- /dev/null +++ b/ui/NotesPreferenceActivity.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * 按照许可要求使用此文件,否则不允许使用。 + * 可以通过以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 除非适用法律要求或书面同意,软件依据许可证分发是“按现状”分发, + * 不附带任何明示或暗示的保证或条件。请查看许可证了解具体权限和限制。 + */ + +// 所在包声明,表明该类属于笔记应用(net.micode.notes)的用户界面(ui)相关模块。 +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,借助DateUtils将时间戳转换为相对时间格式(如“几分钟前”等)展示,便于用户掌握笔记更新情况。 + private TextView mTime; + // 针对通话记录相关笔记,用于显示通话对方名称的TextView,在其他类型笔记中通常设为不可见。 + private TextView mCallName; + // 存储当前列表项对应的笔记数据对象,方便后续获取笔记详细属性来处理展示逻辑等操作。 + private NoteItemData mItemData; + // 多选模式下用于表示笔记项是否被选中的CheckBox,在相应模式下依据选中状态显示勾选情况,非多选模式则隐藏,实现多选交互的可视化。 + private CheckBox mCheckBox; + + // 构造方法,接收上下文对象,初始化视图相关操作: + // 1. 调用父类(LinearLayout)构造方法传入上下文。 + // 2. 通过inflate加载指定布局文件(R.layout.note_item)到当前视图。 + // 3. 利用findViewById找到布局内各子视图控件并赋值给对应成员变量,完成视图初始化。 + public NotesListItem(Context context) { + super(context); + inflate(context, R.layout.note_item, this); + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + // 绑定数据与设置视图显示状态的方法,根据传入参数调整各控件显示内容和可见性,确保正确展示笔记信息并适配交互模式外观: + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + // 根据多选模式及笔记类型决定CheckBox的显示与勾选状态: + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + mCheckBox.setVisibility(View.GONE); + } + + // 保存传入的笔记数据对象,供后续操作使用。 + mItemData = data; + + // 依据笔记数据对象的不同情况设置各控件显示内容和可见性: + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 当前为通话记录文件夹时的设置: + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + // 当前为通话记录文件夹下笔记时的设置: + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } else { + // 其他普通笔记或文件夹类型笔记时的通用设置: + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + if (data.getType() == Notes.TYPE_FOLDER) { + // 若是文件夹类型,设置标题文本包含摘要及文件数量信息,隐藏提醒图标。 + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + mAlert.setVisibility(View.GONE); + } else { + // 若是普通笔记类型,设置标题为格式化摘要内容,并根据提醒设置决定提醒图标的显示。 + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + + // 设置时间文本为相对时间格式字符串,展示笔记最后修改时间。 + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 根据笔记数据设置列表项背景样式,增强不同类型笔记的视觉区分度。 + setBackground(data); + } + + // 私有方法,依据笔记数据对象来设置当前列表项的背景资源样式,按照笔记类型及在列表中的位置等属性选用合适背景资源: + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + if (data.getType() == Notes.TYPE_NOTE) { + // 根据普通笔记在列表中的不同位置情况设置相应背景资源: + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + // 非普通笔记(如文件夹)统一设置对应的文件夹背景资源。 + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + // 对外提供获取当前列表项对应的笔记数据对象的方法,方便外部代码获取笔记详细信息用于其他逻辑处理或数据传递。 + public NoteItemData getItemData() { + return mItemData; + } +} \ No newline at end of file