From 1979288d96819022bd4377f196c3e65dae4a8f9e Mon Sep 17 00:00:00 2001 From: zzy <2858538334@qq,com> Date: Mon, 30 Dec 2024 22:32:55 +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 --- AlarmAlertActivity.java | 159 +++++++++++++ AlarmInitReceiver.java | 75 ++++++ AlarmReceiver.java | 35 +++ DateTimePicker.java | 222 +++++++++++++++++ DropdownMenu.java | 63 +++++ FoldersListAdapter.java | 89 +++++++ NoteEditActivity.java | 515 ++++++++++++++++++++++++++++++++++++++++ NoteEditText.java | 304 ++++++++++++++++++++++++ 8 files changed, 1462 insertions(+) create mode 100644 AlarmAlertActivity.java create mode 100644 AlarmInitReceiver.java create mode 100644 AlarmReceiver.java create mode 100644 DateTimePicker.java create mode 100644 DropdownMenu.java create mode 100644 FoldersListAdapter.java create mode 100644 NoteEditActivity.java create mode 100644 NoteEditText.java diff --git a/AlarmAlertActivity.java b/AlarmAlertActivity.java new file mode 100644 index 0000000..b13107a --- /dev/null +++ b/AlarmAlertActivity.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.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。 +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; // 便签的ID。 + private String mSnippet; // 便签的预览文本。 + private static final int SNIPPET_PREW_MAX_LEN = 60; // 预览文本的最大长度。 + MediaPlayer mPlayer; // 用于播放提醒声音的 MediaPlayer。 + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); // 请求无标题栏的窗口特性。 + + final Window win = getWindow(); + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); // 锁屏时显示窗口。 + + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 屏幕关闭时点亮屏幕。 + } + + Intent intent = getIntent(); // 获取启动该 Activity 的 Intent。 + + try { + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); // 从 Intent 中获取便签 ID。 + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); // 获取便签预览文本。 + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; // 预览文本超出最大长度时截断。 + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + mPlayer = new MediaPlayer(); // 初始化 MediaPlayer。 + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); // 显示操作对话框。 + playAlarmSound(); // 播放提醒声音。 + } else { + finish(); // 如果便签不可见,则结束 Activity。 + } + } + + // 检查屏幕是否开启。 + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + // 播放提醒声音。 + private void playAlarmSound() { + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // 显示操作对话框。 + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); + dialog.setMessage(mSnippet); + dialog.setPositiveButton(R.string.notealert_ok, this); + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); + } + + // DialogInterface.OnClickListener 的实现,处理对话框按钮点击事件。 + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + // DialogInterface.OnDismissListener 的实现,处理对话框消失事件。 + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + // 停止提醒声音。 + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } +} \ No newline at end of file diff --git a/AlarmInitReceiver.java b/AlarmInitReceiver.java new file mode 100644 index 0000000..c1e7d08 --- /dev/null +++ b/AlarmInitReceiver.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; // 指定该类所在的包路径 + +import android.app.AlarmManager; // 引入AlarmManager类,用于设置闹钟 +import android.app.PendingIntent; // 引入PendingIntent类,表示延迟的Intent +import android.content.BroadcastReceiver; // 引入BroadcastReceiver类,用于接收广播 +import android.content.ContentUris; // 引入ContentUris类,用于生成URI +import android.content.Context; // 引入Context类,用于访问系统资源 +import android.content.Intent; // 引入Intent类,用于启动组件 +import android.database.Cursor; // 引入Cursor类,用于数据库查询 + +import net.micode.notes.data.Notes; // 引入Notes类,处理笔记数据 +import net.micode.notes.data.Notes.NoteColumns; // 引入NoteColumns类,表示Notes表的列 + +public class AlarmInitReceiver extends BroadcastReceiver { // 定义一个继承自BroadcastReceiver的类 + + // 定义查询需要的列 + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, // 获取笔记的ID列 + NoteColumns.ALERTED_DATE // 获取提醒时间列 + }; + + // 列索引常量 + private static final int COLUMN_ID = 0; // 第0列是ID列 + private static final int COLUMN_ALERTED_DATE = 1; // 第1列是提醒时间列 + + @Override + public void onReceive(Context context, Intent intent) { // 重写onReceive方法,接收广播 + long currentDate = System.currentTimeMillis(); // 获取当前的系统时间(以毫秒为单位) + + // 查询数据库,获取需要提醒的笔记,且提醒时间大于当前时间 + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, // 查询URI为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 sender = new Intent(context, AlarmReceiver.class); // 创建一个新的Intent,用于发送广播 + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); // 设置Intent的数据(笔记的ID) + + // 创建PendingIntent,表示在指定时间执行AlarmReceiver广播 + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统的AlarmManager服务 + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + // 设置闹钟,使用RTC_WAKEUP模式,当到达指定的提醒时间时唤醒设备并触发PendingIntent + alarmManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); // 如果查询结果中还有下一条记录,则继续处理 + } + c.close(); // 关闭Cursor + } + } +} diff --git a/AlarmReceiver.java b/AlarmReceiver.java new file mode 100644 index 0000000..c692e17 --- /dev/null +++ b/AlarmReceiver.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +// AlarmReceiver 类继承自 BroadcastReceiver,用于接收便签提醒的广播事件。 +public class AlarmReceiver extends BroadcastReceiver { + // onReceive 方法是 BroadcastReceiver 的回调方法,当接收到广播时被调用。 + @Override + public void onReceive(Context context, Intent intent) { + // 设置 intent 的目标类为 AlarmAlertActivity。 + intent.setClass(context, AlarmAlertActivity.class); + // 为 intent 添加 FLAG_ACTIVITY_NEW_TASK 标志,使得在新的任务栈中启动 Activity。 + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // 在给定的上下文中启动 AlarmAlertActivity。 + context.startActivity(intent); + } +} \ No newline at end of file diff --git a/DateTimePicker.java b/DateTimePicker.java new file mode 100644 index 0000000..52692cf --- /dev/null +++ b/DateTimePicker.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import net.micode.notes.R; + + +import android.content.Context; +import android.text.format.DateFormat; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.NumberPicker; + +// DateTimePicker 类继承自 FrameLayout,用于提供一个日期时间选择器。 +public class DateTimePicker extends FrameLayout { + + // 默认启用状态。 + private static final boolean DEFAULT_ENABLE_STATE = true; + + // 半天的小时数和全天的小时数。 + private static final int HOURS_IN_HALF_DAY = 12; + private static final int HOURS_IN_ALL_DAY = 24; + // 一周的天数。 + private static final int DAYS_IN_ALL_WEEK = 7; + // 日期选择器的最小值和最大值。 + private static final int DATE_SPINNER_MIN_VAL = 0; + private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1; + // 24小时制和12小时制下小时选择器的最小值和最大值。 + private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0; + private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23; + private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1; + private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12; + // 分钟选择器的最小值和最大值。 + private static final int MINUT_SPINNER_MIN_VAL = 0; + private static final int MINUT_SPINNER_MAX_VAL = 59; + // 上下午选择器的最小值和最大值。 + private static final int AMPM_SPINNER_MIN_VAL = 0; + private static final int AMPM_SPINNER_MAX_VAL = 1; + + // 日期、小时、分钟和上下午的 NumberPicker 控件。 + private final NumberPicker mDateSpinner; + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mAmPmSpinner; + // 用于存储日期的 Calendar 对象。 + private Calendar mDate; + + // 日期显示值数组。 + private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; + + // 表示是否为上午的布尔值。 + private boolean mIsAm; + + // 表示是否为24小时视图的布尔值。 + private boolean mIs24HourView; + + // 控件的启用状态。 + private boolean mIsEnabled = DEFAULT_ENABLE_STATE; + + // 正在初始化的布尔值。 + private boolean mInitialising; + + // 日期时间改变监听器。 + private OnDateTimeChangedListener mOnDateTimeChangedListener; + + // 日期改变监听器。 + private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新日期并通知日期时间改变。 + mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); + updateDateControl(); + onDateTimeChanged(); + } + }; + + // 小时改变监听器。 + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新小时并通知日期时间改变。 + // 处理12小时制下上下午的转换。 + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } else { + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + onDateTimeChanged(); + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + // 分钟改变监听器。 + private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新分钟并通知日期时间改变。 + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + int offset = 0; + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + } + if (offset != 0) { + mDate.add(Calendar.HOUR_OF_DAY, offset); + mHourSpinner.setValue(getCurrentHour()); + updateDateControl(); + int newHour = getCurrentHourOfDay(); + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + updateAmPmControl(); + } else { + mIsAm = true; + updateAmPmControl(); + } + } + mDate.set(Calendar.MINUTE, newVal); + onDateTimeChanged(); + } + }; + + // 上下午改变监听器。 + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 更新上下午并通知日期时间改变。 + mIsAm = !mIsAm; + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + updateAmPmControl(); + onDateTimeChanged(); + } + }; + + // 日期时间改变监听器接口。 + public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); + } + + // DateTimePicker 的构造函数。 + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + inflate(context, R.layout.datetime_picker, this); + + // 初始化日期、小时、分钟和上下午的 NumberPicker 控件。 + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); + mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); + mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + + // \ No newline at end of file diff --git a/DropdownMenu.java b/DropdownMenu.java new file mode 100644 index 0000000..fe6e47a --- /dev/null +++ b/DropdownMenu.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; // 定义类所在的包路径 + +import android.content.Context; // 引入Context类,用于访问应用程序的上下文 +import android.view.Menu; // 引入Menu类,用于操作菜单 +import android.view.MenuItem; // 引入MenuItem类,用于菜单项的处理 +import android.view.View; // 引入View类,用于视图的操作 +import android.view.View.OnClickListener; // 引入OnClickListener接口,用于处理点击事件 +import android.widget.Button; // 引入Button类,用于显示按钮 +import android.widget.PopupMenu; // 引入PopupMenu类,用于显示弹出菜单 +import android.widget.PopupMenu.OnMenuItemClickListener; // 引入PopupMenu的菜单项点击监听器 + +public class DropdownMenu { + private Button mButton; // 用于显示的按钮,点击后弹出菜单 + private PopupMenu mPopupMenu; // 弹出菜单的实例 + private Menu mMenu; // 菜单项的集合 + + // 构造函数,初始化DropdownMenu,设置按钮及其弹出菜单 + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; // 获取传入的按钮对象 + mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮的背景图标为下拉菜单图标 + mPopupMenu = new PopupMenu(context, mButton); // 初始化PopupMenu,指定按钮作为弹出菜单的锚点 + mMenu = mPopupMenu.getMenu(); // 获取PopupMenu中的Menu对象 + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 使用传入的menuId加载菜单资源 + mButton.setOnClickListener(new OnClickListener() { // 为按钮设置点击事件监听器 + public void onClick(View v) { + mPopupMenu.show(); // 当按钮被点击时,显示弹出菜单 + } + }); + } + + // 设置菜单项点击事件监听器 + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); // 设置PopupMenu的菜单项点击监听器 + } + } + + // 根据菜单项的ID查找菜单项 + public MenuItem findItem(int id) { + return mMenu.findItem(id); // 在菜单中查找具有指定ID的菜单项 + } + + // 设置按钮的文本 + public void setTitle(CharSequence title) { + mButton.setText(title); // 设置按钮的文本为传入的title + } +} diff --git a/FoldersListAdapter.java b/FoldersListAdapter.java new file mode 100644 index 0000000..94bb583 --- /dev/null +++ b/FoldersListAdapter.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.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,用于显示文件夹列表。 +public class FoldersListAdapter extends CursorAdapter { + // 定义查询数据库时需要的列。 + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + // 定义列索引常量。 + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + // FoldersListAdapter 的构造函数。 + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + } + + // 新建视图的方法,返回一个 FolderListItem 实例。 + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + // 绑定视图与数据的方法。 + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); + } + } + + // 根据位置获取文件夹名称的方法。 + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + } + + // FolderListItem 是一个内部类,表示列表中的单个文件夹项。 + private class FolderListItem extends LinearLayout { + private TextView mName; // 用于显示文件夹名称的 TextView。 + + // FolderListItem 的构造函数。 + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + // 绑定文件夹名称的方法。 + public void bind(String name) { + mName.setText(name); + } + } +} \ No newline at end of file diff --git a/NoteEditActivity.java b/NoteEditActivity.java new file mode 100644 index 0000000..075ccd6 --- /dev/null +++ b/NoteEditActivity.java @@ -0,0 +1,515 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// NoteEditActivity 是一个用于编辑便签的 Activity。 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + // 内部类,用于持有头部视图的控件引用。 + private class HeadViewHolder { + public TextView tvModified; // 修改日期的 TextView。 + public ImageView ivAlertIcon; // 提醒图标的 ImageView。 + public TextView tvAlertDate; // 提醒日期的 TextView。 + public ImageView ibSetBgColor; // 设置背景颜色的 ImageView。 + } + + // 背景颜色选择按钮与颜色 ID 的映射。 + 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 的映射。 + 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 的映射。 + 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 的映射。 + 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"; + + // 头部视图的 Holder。 + private HeadViewHolder mNoteHeaderHolder; + + // 头部视图面板、背景颜色选择器和字体大小选择器。 + private View mHeadViewPanel; + + private View mNoteBgColorSelector; + + private View mFontSizeSelector; + + // 编辑器和编辑器面板。 + private EditText mNoteEditor; + + private View mNoteEditorPanel; + + // 正在编辑的便签。 + private WorkingNote mWorkingNote; + + // SharedPreferences 和字体大小 ID。 + private SharedPreferences mSharedPrefs; + private int mFontSizeId; + + // SharedPreferences 中字体大小的键。 + 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; + + // onCreate 方法,初始化 Activity。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.setContentView(R.layout.note_edit); + + if (savedInstanceState == null && !initActivityState(getIntent())) { + finish(); + return; + } + initResources(); + } + + // onRestoreInstanceState 方法,恢复 Activity 状态。 + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + if (!initActivityState(intent)) { + finish(); + return; + } + Log.d(TAG, "Restoring from killed activity"); + } + } + + // initActivityState 方法,初始化 Activity 状态。 + private boolean initActivityState(Intent intent) { + // 省略部分代码... + } + + // onResume 方法,恢复 Activity 时调用。 + @Override + protected void onResume() { + super.onResume(); + initNoteScreen(); + } + + // initNoteScreen 方法,初始化便签屏幕。 + private void initNoteScreen() { + // 省略部分代码... + } + + // onNewIntent 方法,处理新的 Intent。 + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); + } + + // onSaveInstanceState 方法,保存 Activity 状态。 + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // 省略部分代码... + } + + // dispatchTouchEvent 方法,分发触摸事件。 + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + // 省略部分代码... + } + + // inRangeOfView 方法,判断触摸事件是否在视图范围内。 + private boolean inRangeOfView(View view, MotionEvent ev) { + // 省略部分代码... + } + + // initResources 方法,初始化资源。 + private void initResources() { + // 省略部分代码... + } + + // onPause 方法,暂停 Activity 时调用。 + @Override + protected void onPause() { + super.onPause(); + if(saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + clearSettingState(); + } + + // updateWidget 方法,更新小部件。 + private void updateWidget() { + // 省略部分代码... + } +} +/** + * 当点击某个视图时调用此方法。 + * @param v 被点击的视图。 + */ +public void onClick(View v) { + int id = v.getId(); // 获取被点击视图的ID。 + // 如果被点击的视图是设置背景颜色的按钮,则显示背景颜色选择器。 + if (id == R.id.btn_set_bg_color) { + mNoteBgColorSelector.setVisibility(View.VISIBLE); + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(-View.VISIBLE); + } else if (sBgSelectorBtnsMap.containsKey(id)) { // 如果被点击的是背景颜色按钮。 + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.GONE); + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); // 更改背景颜色ID。 + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(id)) { // 如果被点击的是字体大小按钮。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + mFontSizeId = sFontSizeBtnsMap.get(id); // 更改字体大小ID。 + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); // 保存新的字体大小偏好。 + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + // 如果便签处于清单模式,更新清单视图;否则,更新文本外观。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + mFontSizeSelector.setVisibility(View.GONE); + } +} + +/** + * 当按下返回键时调用。 + */ +@Override +public void onBackPressed() { + if(clearSettingState()) { + return; + } + saveNote(); + super.onBackPressed(); +} + +/** + * 清除设置状态,例如隐藏背景颜色选择器和字体大小选择器。 + * @return 如果设置状态被清除,则返回true,否则返回false。 + */ +private boolean clearSettingState() { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return false; +} + +/** + * 当背景颜色改变时调用。 + */ +public void onBackgroundColorChanged() { + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(View.VISIBLE); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); +} + +/** + * 准备选项菜单。 + * @param menu 选项菜单。 + * @return 如果菜单已准备,则返回true,否则返回false。 + */ +@Override +public boolean onPrepareOptionsMenu(Menu menu) { + if (isFinishing()) { + return true; + } + clearSettingState(); + menu.clear(); + // 根据便签的文件夹ID决定加载哪个菜单。 + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); + } else { + getMenuInflater().inflate(R.menu.note_edit, menu); + } + // 根据便签的属性更新菜单项。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); + } else { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); + } + if (mWorkingNote.hasClockAlert()) { + menu.findItem(R.id.menu_alert).setVisible(false); + } else { + menu.findItem(R.id.menu_delete_remind).setVisible(false); + } + return true; +} + +/** + * 处理选项菜单项的选择。 + * @param item 被选择的菜单项。 + * @return 如果项被处理,则返回true,否则返回false。 + */ +@Override +public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_note: + createNewNote(); + break; + case R.id.menu_delete: + // 显示删除便签的确认对话框。 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_note)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteCurrentNote(); + finish(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.menu_font_size: + mFontSizeSelector.setVisibility(View.VISIBLE); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + break; + case R.id.menu_list_mode: + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? + TextNote.MODE_CHECK_LIST : 0); + break; + case R.id.menu_share: + getWorkingText(); + sendTo(this, mWorkingNote.getContent()); + break; + case R.id.menu_send_to_desktop: + sendToDesktop(); + break; + case R.id.menu_alert: + setReminder(); + break; + case R.id.menu_delete_remind: + mWorkingNote.setAlertDate(0, false); + break; + default: + break; + } + return true; +} + +/** + * 设置便签的提醒。 + */ +private void setReminder() { + DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + public void OnDateTimeSet(AlertDialog dialog, long date) { + mWorkingNote.setAlertDate(date, true); + } + }); + d.show(); +} + +/** + * 分享便签到支持{@link Intent#ACTION_SEND}动作和{text/plain}类型的应用。 + * @param context 上下文。 + * @param info 要分享的便签内容。 + */ +private void sendTo(Context context, String info) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, info); + intent.setType("text/plain"); + context.startActivity(intent); +} + +/** + * 创建一个新的便签。 + */ +private void createNewNote() { + // 在创建新便签之前保存当前正在编辑的便签。 + saveNote(); + finish(); + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + startActivity(intent); +} + +/** + * 删除当前便签。 + */ +private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "错误的便签ID,不应该发生"); + } + if (!isSyncMode()) { + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "删除便签错误"); + } + } else { + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "将便签移动到废纸篓文件夹错误,不应该发生"); + } + } + } + mWorkingNote.markDeleted(true); +} + +/** + * 检查应用是否处于同步模式。 + * @return 如果处于同步模式,则返回true,否则返回false。 + */ +private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; +} + +/** + * 当时钟提醒改变时调用。 + * @param date 新的提醒日期。 + * @param set 如果设置了提醒,则为true;如果取消了提醒,则为false。 + */ +public void onClockAlertChanged(long date, boolean set) { + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + if (mWorkingNote.getNoteId() > 0) { + Intent intent = new Intent(this, AlarmReceiver.class); + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + showAlertHeader(); + if(!set) { + alarmManager.cancel(pendingIntent); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + Log.e(TAG, "时钟提醒设置错误"); + showToast(R.string.error_note_empty_for_clock); + } +} + +/** + * 当小部件改变时调用。 + */ +public void onWidgetChanged() { + updateWidget(); +} + +/** + * 处理在清单中删除文本项。 + * @param index 要删除项的索引。 + * @param text 项的文本。 + */ diff --git a/NoteEditText.java b/NoteEditText.java new file mode 100644 index 0000000..0e34903 --- /dev/null +++ b/NoteEditText.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + private int mIndex; + private int mSelectionStartBeforeDelete; + + private static final String SCHEME_TEL = "tel:" ; + private static final String SCHEME_HTTP = "http:" ; + private static final String SCHEME_EMAIL = "mailto:" ; + + 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); + } + + /** + * Call by the {@link NoteEditActivity} to delete or add edit text + */ + public interface OnTextViewChangeListener { + /** + * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens + * and the text is null + */ + void onEditTextDelete(int index, String text); + + /** + * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} + * happen + */ + void onEditTextEnter(int index, String text); + + /** + * Hide or show item option when text change + */ + void onTextChange(int index, boolean hasText); + } + + /* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; // 定义类所在的包路径 + +// 引入相关的 Android 类 +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; // 引入资源文件中的R类 + +import java.util.HashMap; +import java.util.Map; + +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; // 定义日志标签,用于调试输出 + private int mIndex; // 用于表示当前编辑文本的索引 + private int mSelectionStartBeforeDelete; // 用于记录删除前的文本选择开始位置 + + // 定义常见的URI Schemes + private static final String SCHEME_TEL = "tel:" ; + private static final String SCHEME_HTTP = "http:" ; + private static final String SCHEME_EMAIL = "mailto:" ; + + // 定义一个映射关系,存储不同URI Scheme对应的资源ID + private static final Map sSchemaActionResMap = new HashMap(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接对应的资源ID + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网站链接对应的资源ID + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 邮件链接对应的资源ID + } + + /** + * 在 {@link NoteEditActivity} 中调用,用于删除或添加编辑文本 + */ + public interface OnTextViewChangeListener { + /** + * 当发生删除事件(例如按下删除键)且文本为空时,删除当前编辑文本 + */ + void onEditTextDelete(int index, String text); + + /** + * 当发生回车事件(例如按下回车键)时,添加编辑文本 + */ + void onEditTextEnter(int index, String text); + + /** + * 当文本发生变化时,隐藏或显示项的选项 + */ + void onTextChange(int index, boolean hasText); + } +} +// 定义一个文本视图变化监听器的接口 +private OnTextViewChangeListener mOnTextViewChangeListener; + +// NoteEditText的构造函数,当从AttributeSet创建时调用 +public NoteEditText(Context context) { + super(context, null); + mIndex = 0; +} + +// 设置当前文本编辑框的索引 +public void setIndex(int index) { + mIndex = index; +} + +// 设置文本视图变化监听器 +public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; +} + +// NoteEditText的构造函数,当从AttributeSet和 defStyle创建时调用 +public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); +} + +// NoteEditText的构造函数,当从AttributeSet、defStyle和 defStyle创建时调用 +public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub +} + +// 处理触摸事件,用于实现自定义的文本选择行为 +@Override +public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + Selection.setSelection(getText(), off); + break; + } + + return super.onTouchEvent(event); +} + +// 处理按键事件,例如Enter和Delete键 +@Override +public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); +} + +// 处理按键释放事件,例如Enter和Delete键 +@Override +public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener != null) { + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); +} + +// 处理焦点变化事件,用于在文本编辑框失去焦点时通知监听器 +@Override +protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); +} + +// 创建上下文菜单,用于实现长按文本时弹出的菜单 +@Override +protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + if(urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // goto a new intent + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); +}