From 97d4a685bac5eec37f92965826264d98b6cc00a3 Mon Sep 17 00:00:00 2001 From: chroe <454604461@qq.com> Date: Sun, 25 Jan 2026 19:08:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=8C=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=92=8C=E6=A0=87=E7=AD=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/net/micode/notes/data/Notes.java | 6 + .../notes/data/NotesDatabaseHelper.java | 21 +- .../net/micode/notes/ui/NoteEditActivity.java | 247 +++++++++++++-- .../net/micode/notes/ui/NoteEditText.java | 195 ++++++++++++ .../net/micode/notes/ui/NoteItemData.java | 18 ++ .../micode/notes/ui/NotesListActivity.java | 281 ++++++++++++++++-- .../net/micode/notes/ui/NotesListItem.java | 26 +- src/main/res/drawable/ic_format_bold.xml | 3 + src/main/res/drawable/ic_format_clear.xml | 9 + .../res/drawable/ic_format_color_fill.xml | 4 + .../res/drawable/ic_format_color_text.xml | 4 + src/main/res/drawable/ic_format_italic.xml | 3 + .../res/drawable/ic_format_underlined.xml | 3 + src/main/res/layout/note_edit.xml | 69 +++++ src/main/res/layout/note_list.xml | 11 + src/main/res/menu/note_list_options.xml | 6 + src/main/res/values/strings.xml | 6 + 17 files changed, 860 insertions(+), 52 deletions(-) create mode 100644 src/main/res/drawable/ic_format_bold.xml create mode 100644 src/main/res/drawable/ic_format_clear.xml create mode 100644 src/main/res/drawable/ic_format_color_fill.xml create mode 100644 src/main/res/drawable/ic_format_color_text.xml create mode 100644 src/main/res/drawable/ic_format_italic.xml create mode 100644 src/main/res/drawable/ic_format_underlined.xml diff --git a/src/main/java/net/micode/notes/data/Notes.java b/src/main/java/net/micode/notes/data/Notes.java index 901fd1f..2475b1a 100644 --- a/src/main/java/net/micode/notes/data/Notes.java +++ b/src/main/java/net/micode/notes/data/Notes.java @@ -259,6 +259,12 @@ public class Notes { *

类型 : INTEGER

*/ public static final int TITLE_MAX_LENGTH = 50; + + /** + * 便签标签 + *

类型 : TEXT

+ */ + public static final String TAGS = "tags"; } /** diff --git a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java index 7600a45..0d6067d 100644 --- a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java @@ -36,7 +36,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "note.db"; // 数据库版本号 - private static final int DB_VERSION = 8; + private static final int DB_VERSION = 9; // 数据库表名定义 public interface TABLE { @@ -94,7 +94,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.IS_PINNED + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.PIN_PRIORITY + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.IS_LOCKED + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.LOCK_PASSWORD + " TEXT NOT NULL DEFAULT ''" + + NoteColumns.LOCK_PASSWORD + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TAGS + " TEXT NOT NULL DEFAULT ''" + ")"; // 创建数据表的SQL语句 @@ -436,6 +437,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { upgradeToV8(db); oldVersion++; } + + if (oldVersion == 8) { + upgradeToV9(db); + oldVersion++; + } if (reCreateTriggers) { reCreateNoteTableTriggers(db); @@ -546,4 +552,15 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TITLE + " TEXT NOT NULL DEFAULT ''"); } + + /** + * 将数据库从v8升级到v9 + * 此版本升级添加了标签字段,用于支持便签标签功能 + * @param db SQLite数据库实例 + */ + private void upgradeToV9(SQLiteDatabase db) { + // 为笔记表添加标签字段 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TAGS + + " TEXT NOT NULL DEFAULT ''"); + } } diff --git a/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/main/java/net/micode/notes/ui/NoteEditActivity.java index 402f220..97fa85e 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -27,12 +27,17 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Color; import android.graphics.Paint; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; +import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -120,6 +125,11 @@ public class NoteEditActivity extends Activity implements OnClickListener, public View titleArea; // 标题区域 public TextView tvWordCountLabel; // 字数统计标签 public TextView tvWordCount; // 字数统计数字 + public ImageButton ibBold; // 加粗按钮 + public ImageButton ibItalic; // 斜体按钮 + public ImageButton ibUnderline; // 下划线按钮 + public ImageButton ibHighlight; // 高亮按钮 + public ImageButton ibTextColor; // 文本颜色按钮 } /** @@ -174,7 +184,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, private View mHeadViewPanel; // 头部视图面板 private View mNoteBgColorSelector; // 背景颜色选择器 private View mFontSizeSelector; // 字体大小选择器 - private EditText mNoteEditor; // 便签编辑器 + private NoteEditText mNoteEditor; // 便签编辑器 private View mNoteEditorPanel; // 编辑器面板 private WorkingNote mWorkingNote; // 当前工作便签 private SharedPreferences mSharedPrefs; // 共享偏好设置 @@ -402,7 +412,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, switchToListMode(mWorkingNote.getContent()); } else { // 普通文本模式,显示高亮搜索结果 - mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + String content = mWorkingNote.getContent(); + + // 直接使用getHighlightQueryResult方法获取处理后的Spannable对象 + Spannable spannedContent = getHighlightQueryResult(content, mUserQuery); + mNoteEditor.setText(spannedContent); + // 将光标定位到文本末尾 mNoteEditor.setSelection(mNoteEditor.getText().length()); } @@ -598,9 +613,21 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteHeaderHolder.titleArea = findViewById(R.id.note_title_area); mNoteHeaderHolder.tvWordCountLabel = (TextView) findViewById(R.id.tv_word_count_label); mNoteHeaderHolder.tvWordCount = (TextView) findViewById(R.id.tv_word_count); + + // 初始化富文本工具栏按钮 + mNoteHeaderHolder.ibBold = (ImageButton) findViewById(R.id.btn_bold); + mNoteHeaderHolder.ibBold.setOnClickListener(this); + mNoteHeaderHolder.ibItalic = (ImageButton) findViewById(R.id.btn_italic); + mNoteHeaderHolder.ibItalic.setOnClickListener(this); + mNoteHeaderHolder.ibUnderline = (ImageButton) findViewById(R.id.btn_underline); + mNoteHeaderHolder.ibUnderline.setOnClickListener(this); + mNoteHeaderHolder.ibHighlight = (ImageButton) findViewById(R.id.btn_highlight); + mNoteHeaderHolder.ibHighlight.setOnClickListener(this); + mNoteHeaderHolder.ibTextColor = (ImageButton) findViewById(R.id.btn_text_color); + mNoteHeaderHolder.ibTextColor.setOnClickListener(this); // 初始化编辑器 - mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditor = (NoteEditText) findViewById(R.id.note_edit_view); mNoteEditorPanel = findViewById(R.id.sv_note_edit); // 初始化背景颜色选择器 @@ -718,7 +745,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, /** * 点击事件处理 *

- * 处理UI组件的点击事件,包括设置背景色、选择背景色、选择字体大小 + * 处理UI组件的点击事件,包括设置背景色、选择背景色、选择字体大小、富文本格式化等 *

* * @param v 被点击的视图 @@ -738,7 +765,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } else if (id == R.id.btn_set_bg_color) { mNoteBgColorSelector.setVisibility(View.VISIBLE); findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - - View.VISIBLE); + View.VISIBLE); } else if (sBgSelectorBtnsMap.containsKey(id)) { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.GONE); @@ -759,6 +786,21 @@ public class NoteEditActivity extends Activity implements OnClickListener, mFontSizeSelector.setVisibility(View.GONE); } else if (id == R.id.add_img_btn) { insertImage(); + } else if (id == R.id.btn_bold) { + // 处理加粗按钮点击 + mNoteEditor.toggleBold(); + } else if (id == R.id.btn_italic) { + // 处理斜体按钮点击 + mNoteEditor.toggleItalic(); + } else if (id == R.id.btn_underline) { + // 处理下划线按钮点击 + mNoteEditor.toggleUnderline(); + } else if (id == R.id.btn_highlight) { + // 处理高亮按钮点击 + showColorPickerDialog(true); + } else if (id == R.id.btn_text_color) { + // 处理文本颜色按钮点击,显示颜色选择对话框 + showColorPickerDialog(false); } } @@ -1160,21 +1202,55 @@ public class NoteEditActivity extends Activity implements OnClickListener, } private Spannable getHighlightQueryResult(String fullText, String userQuery) { - SpannableStringBuilder builder = new SpannableStringBuilder(fullText == null ? "" : fullText); + // 确保fullText不为null + if (fullText == null) { + return new SpannableString(""); + } + + // 先将纯文本转换为SpannableString + SpannableStringBuilder builder = new SpannableStringBuilder(fullText); + + // 只处理HTML格式的内容,避免重复处理 + if (fullText.contains("<") && fullText.contains(">")) { + try { + // 如果是HTML格式,先转换为Spanned对象 + Spanned spannedText = Html.fromHtml(fullText); + builder = new SpannableStringBuilder(spannedText); + } catch (Exception e) { + Log.e(TAG, "Error parsing HTML: " + e.getMessage()); + // 如果HTML解析失败,使用纯文本 + builder = new SpannableStringBuilder(fullText); + } + } + + // 处理搜索高亮 if (!TextUtils.isEmpty(userQuery)) { - mPattern = Pattern.compile(userQuery); - Matcher m = mPattern.matcher(fullText); - int start = 0; - while (m.find(start)) { - builder.setSpan( - new BackgroundColorSpan(this.getResources().getColor( - R.color.user_query_highlight)), m.start(), m.end(), - Spannable.SPAN_INCLUSIVE_EXCLUSIVE); - start = m.end(); + try { + mPattern = Pattern.compile(userQuery, Pattern.CASE_INSENSITIVE); + Matcher m = mPattern.matcher(builder.toString()); + int start = 0; + while (m.find(start)) { + builder.setSpan( + new BackgroundColorSpan(this.getResources().getColor( + R.color.user_query_highlight)), m.start(), m.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + start = m.end(); + } + } catch (Exception e) { + Log.e(TAG, "Error highlighting search query: " + e.getMessage()); + } + } + + // 只在有图片标记时才处理图片转换 + if (fullText.contains("[IMAGE:")) { + try { + // 使用统一的图片处理逻辑 + processImageConversion(builder, builder.toString()); + } catch (Exception e) { + Log.e(TAG, "Error processing images: " + e.getMessage()); } } - // 使用统一的图片处理逻辑 - processImageConversion(builder, builder.toString()); + return builder; } @@ -1203,9 +1279,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); } + // 对于checklist模式,将HTML转换为纯文本 + String plainText = Html.fromHtml(item).toString(); + edit.setOnTextViewChangeListener(this); edit.setIndex(index); - edit.setText(getHighlightQueryResult(item, mUserQuery)); + edit.setText(getHighlightQueryResult(plainText, mUserQuery)); return view; } @@ -1223,12 +1302,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, public void onCheckListModeChanged(int oldMode, int newMode) { if (newMode == TextNote.MODE_CHECK_LIST) { - switchToListMode(mNoteEditor.getText().toString()); + // 从富文本模式切换到checklist模式,将HTML转换为纯文本 + String plainText = Html.fromHtml(mNoteEditor.getText().toString()).toString(); + switchToListMode(plainText); } else { if (!getWorkingText()) { mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", "")); } + // 从checklist模式切换回富文本模式,将纯文本转换为HTML mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); mEditTextList.setVisibility(View.GONE); mNoteEditor.setVisibility(View.VISIBLE); @@ -1253,7 +1335,26 @@ public class NoteEditActivity extends Activity implements OnClickListener, } mWorkingNote.setWorkingText(sb.toString()); } else { - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + // 处理富文本内容,特殊处理ImageSpan + Editable editable = mNoteEditor.getEditableText(); + if (editable != null && editable instanceof Spanned) { + Spanned spanned = (Spanned) editable; + + // 检查是否包含ImageSpan + ImageSpan[] imageSpans = spanned.getSpans(0, spanned.length(), ImageSpan.class); + if (imageSpans.length > 0) { + // 对于包含图片的内容,使用原始文本格式存储,保留[IMAGE:path]标记 + mWorkingNote.setWorkingText(spanned.toString()); + } else { + // 对于不包含图片的内容,转换为HTML格式存储 + String htmlContent = Html.toHtml(spanned); + mWorkingNote.setWorkingText(htmlContent); + } + } else { + // 对于普通文本,转换为HTML格式存储 + String htmlContent = Html.toHtml(mNoteEditor.getText()); + mWorkingNote.setWorkingText(htmlContent); + } } updateWordCount(); return hasChecked; @@ -1272,7 +1373,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, } content = sb.toString(); } else { - content = mNoteEditor.getText().toString(); + // 对于富文本模式,获取纯文本内容进行字数统计 + Spanned spannedContent = mNoteEditor.getText(); + content = spannedContent.toString(); } // 过滤掉所有 [IMAGE:图片路径] 格式的图片标记,只统计纯文本字符数 @@ -1487,6 +1590,108 @@ public class NoteEditActivity extends Activity implements OnClickListener, } openImagePicker(); } + + /** + * 显示颜色选择对话框 + * @param isHighlight 是否为高亮模式,true为高亮,false为文本颜色 + */ + private void showColorPickerDialog(final boolean isHighlight) { + // 定义常用颜色数组 + final int[] colors = new int[] { + Color.BLACK, Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, + Color.CYAN, Color.MAGENTA, Color.GRAY, Color.DKGRAY, Color.LTGRAY, + Color.WHITE, Color.rgb(255, 165, 0), // Orange + Color.rgb(128, 0, 128), // Purple + Color.rgb(0, 128, 128), // Teal + Color.rgb(255, 192, 203) // Pink + }; + + // 创建颜色选择器对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(isHighlight ? R.string.menu_highlight : R.string.menu_text_color); + + // 创建颜色选择网格布局 + LinearLayout colorPickerLayout = new LinearLayout(this); + colorPickerLayout.setOrientation(LinearLayout.VERTICAL); + colorPickerLayout.setPadding(20, 20, 20, 20); + + // 创建3行5列的颜色选择网格 + int columns = 5; + int rows = (int) Math.ceil((double) colors.length / columns); + + for (int i = 0; i < rows; i++) { + LinearLayout rowLayout = new LinearLayout(this); + rowLayout.setOrientation(LinearLayout.HORIZONTAL); + rowLayout.setWeightSum(columns); + + for (int j = 0; j < columns; j++) { + final int index = i * columns + j; + if (index < colors.length) { + ImageView colorButton = new ImageView(this); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + 0, 60, 1); + params.setMargins(5, 5, 5, 5); + colorButton.setLayoutParams(params); + + // 创建带边框的颜色选择按钮 + // 内层是颜色填充,外层是黑色边框 + GradientDrawable colorDrawable = new GradientDrawable(); + colorDrawable.setColor(colors[index]); + + GradientDrawable borderDrawable = new GradientDrawable(); + borderDrawable.setColor(Color.TRANSPARENT); + borderDrawable.setStroke(2, Color.BLACK); + borderDrawable.setShape(GradientDrawable.RECTANGLE); + + // 创建LayerDrawable来叠加两个Drawable + Drawable[] layers = {borderDrawable, colorDrawable}; + LayerDrawable layerDrawable = new LayerDrawable(layers); + + // 设置内边距,留出边框空间 + int padding = 2; + layerDrawable.setLayerInset(1, padding, padding, padding, padding); + + colorButton.setBackground(layerDrawable); + colorButton.setScaleType(ImageView.ScaleType.CENTER); + colorButton.setClickable(true); + colorButton.setFocusable(true); + + // 添加点击事件 + colorButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + // 应用选择的颜色 + if (isHighlight) { + // 高亮模式 + mNoteEditor.toggleHighlight(colors[index]); + } else { + // 文本颜色模式 + mNoteEditor.toggleTextColor(colors[index]); + } + } + }); + + rowLayout.addView(colorButton); + } + } + + colorPickerLayout.addView(rowLayout); + } + + builder.setView(colorPickerLayout); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + + // 创建对话框并设置为不可取消,只能通过取消按钮关闭 + AlertDialog dialog = builder.create(); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } private void openImagePicker() { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); diff --git a/src/main/java/net/micode/notes/ui/NoteEditText.java b/src/main/java/net/micode/notes/ui/NoteEditText.java index 610623c..abc88cf 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditText.java +++ b/src/main/java/net/micode/notes/ui/NoteEditText.java @@ -17,12 +17,19 @@ package net.micode.notes.ui; import android.content.Context; +import android.graphics.Color; import android.graphics.Rect; import android.text.Layout; import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.util.Log; import android.view.ContextMenu; @@ -303,4 +310,192 @@ public class NoteEditText extends EditText { } super.onCreateContextMenu(menu); } + + /** + * 切换文本的加粗格式 + */ + public void toggleBold() { + applyStyle(android.graphics.Typeface.BOLD); + } + + /** + * 切换文本的斜体格式 + */ + public void toggleItalic() { + applyStyle(android.graphics.Typeface.ITALIC); + } + + /** + * 切换文本的下划线格式 + */ + public void toggleUnderline() { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start == end) { + return; + } + + Spannable str = getText(); + UnderlineSpan[] spans = str.getSpans(start, end, UnderlineSpan.class); + + if (spans.length > 0) { + // 如果已经有下划线,移除 + for (UnderlineSpan span : spans) { + str.removeSpan(span); + } + } else { + // 如果没有下划线,添加 + str.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + + setText(str); + setSelection(end); + } + + /** + * 切换文本的高亮格式 + */ + public void toggleHighlight() { + toggleHighlight(Color.YELLOW); + } + + /** + * 切换文本的高亮格式 + * @param color 高亮颜色值 + */ + public void toggleHighlight(int color) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start == end) { + return; + } + + Spannable str = getText(); + BackgroundColorSpan[] spans = str.getSpans(start, end, BackgroundColorSpan.class); + + // 检查是否已经应用了相同颜色的高亮 + boolean hasSameColor = false; + for (BackgroundColorSpan span : spans) { + if (span.getBackgroundColor() == color) { + hasSameColor = true; + break; + } + } + + // 移除所有高亮 + for (BackgroundColorSpan span : spans) { + str.removeSpan(span); + } + + // 如果没有相同颜色的高亮,则添加新颜色的高亮 + if (!hasSameColor) { + str.setSpan(new BackgroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + } + + /** + * 切换文本颜色 + * @param color 文本颜色值 + */ + public void toggleTextColor(int color) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start == end) { + return; + } + + Spannable str = getText(); + ForegroundColorSpan[] spans = str.getSpans(start, end, ForegroundColorSpan.class); + + // 检查是否已经应用了相同颜色 + boolean hasSameColor = false; + for (ForegroundColorSpan span : spans) { + if (span.getForegroundColor() == color) { + hasSameColor = true; + break; + } + } + + // 移除所有文本颜色 + for (ForegroundColorSpan span : spans) { + str.removeSpan(span); + } + + // 如果没有相同颜色,则添加新颜色 + if (!hasSameColor) { + str.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + } + + /** + * 清除文本格式 + */ + public void clearFormatting() { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start == end) { + return; + } + + Spannable str = getText(); + + // 移除所有格式 + StyleSpan[] styleSpans = str.getSpans(start, end, StyleSpan.class); + for (StyleSpan span : styleSpans) { + str.removeSpan(span); + } + + UnderlineSpan[] underlineSpans = str.getSpans(start, end, UnderlineSpan.class); + for (UnderlineSpan span : underlineSpans) { + str.removeSpan(span); + } + + BackgroundColorSpan[] backgroundSpans = str.getSpans(start, end, BackgroundColorSpan.class); + for (BackgroundColorSpan span : backgroundSpans) { + str.removeSpan(span); + } + + ForegroundColorSpan[] foregroundSpans = str.getSpans(start, end, ForegroundColorSpan.class); + for (ForegroundColorSpan span : foregroundSpans) { + str.removeSpan(span); + } + + setText(str); + setSelection(end); + } + + /** + * 应用文本样式 + * @param style 样式常量 + */ + private void applyStyle(int style) { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start == end) { + return; + } + + Spannable str = getText(); + StyleSpan[] spans = str.getSpans(start, end, StyleSpan.class); + + boolean hasStyle = false; + for (StyleSpan span : spans) { + if (span.getStyle() == style) { + str.removeSpan(span); + hasStyle = true; + } + } + + if (!hasStyle) { + str.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + + setText(str); + setSelection(end); + } } diff --git a/src/main/java/net/micode/notes/ui/NoteItemData.java b/src/main/java/net/micode/notes/ui/NoteItemData.java index 7d2876f..0c6442b 100644 --- a/src/main/java/net/micode/notes/ui/NoteItemData.java +++ b/src/main/java/net/micode/notes/ui/NoteItemData.java @@ -50,6 +50,7 @@ public class NoteItemData { NoteColumns.WIDGET_TYPE, NoteColumns.IS_LOCKED, NoteColumns.TITLE, + NoteColumns.TAGS, }; /** @@ -116,6 +117,10 @@ public class NoteItemData { * 标题列索引 */ private static final int TITLE_COLUMN = 15; + /** + * 标签列索引 + */ + private static final int TAGS_COLUMN = 16; /** * 笔记ID @@ -181,6 +186,10 @@ public class NoteItemData { * 标题 */ private String mTitle; + /** + * 标签 + */ + private String mTags; /** * 联系人姓名 */ @@ -236,6 +245,7 @@ public class NoteItemData { mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); mIsLocked = (cursor.getInt(IS_LOCKED_COLUMN) > 0); mTitle = cursor.getString(TITLE_COLUMN); + mTags = cursor.getString(TAGS_COLUMN); mPhoneNumber = ""; // 如果是通话记录文件夹,获取电话号码和联系人姓名 @@ -497,4 +507,12 @@ public class NoteItemData { public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } + + /** + * 获取标签 + * @return 标签 + */ + public String getTags() { + return mTags; + } } diff --git a/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/main/java/net/micode/notes/ui/NotesListActivity.java index 7b221d3..7c7df77 100644 --- a/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -291,6 +291,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mSearchEditText = (EditText) findViewById(R.id.search_edit_text); mSearchClear = (ImageView) findViewById(R.id.search_clear); mSearchButton = (ImageView) findViewById(R.id.search_button); + ImageView mSearchTagsButton = (ImageView) findViewById(R.id.search_tags_button); + + // 设置标签选择按钮点击事件 + mSearchTagsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showTagsSelectorDialog(); + } + }); // 设置搜索文本变化监听,恢复实时搜索功能 mSearchEditText.addTextChangedListener(new TextWatcher() { @@ -392,6 +401,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } pinMenu.setOnMenuItemClickListener(this); } + // 添加标签设置菜单项 + MenuItem tagsMenu = menu.findItem(R.id.tags); + if (tagsMenu != null) { + tagsMenu.setOnMenuItemClickListener(this); + } mActionMode = mode; mNotesListAdapter.setChoiceMode(true); mNotesListView.setLongClickable(false); @@ -483,6 +497,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt startQueryDestinationFolders(); } else if (item.getItemId() == R.id.pin) { togglePinStatus(); + } else if (item.getItemId() == R.id.tags) { + showTagsDialog(); } else { return false; } @@ -552,39 +568,93 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }; + // 当前选中的标签,用于筛选 + private String mSelectedTag = ""; + private void startAsyncNotesListQuery() { String selection; String[] selectionArgs; + // 构建基础查询条件 + String baseSelection = ""; + ArrayList argsList = new ArrayList<>(); + // 根据是否有搜索关键词构建不同的查询条件 if (!TextUtils.isEmpty(mSearchQuery)) { // 有搜索关键词的情况 if (mCurrentFolderId == Notes.ID_ROOT_FOLDER) { // 根文件夹:需要将搜索条件应用到两个部分 - selection = "((" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=? AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ?))" - + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0 AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ?)))"; - selectionArgs = new String[] { - String.valueOf(mCurrentFolderId), - "%" + mSearchQuery + "%", - "%" + mSearchQuery + "%", - "%" + mSearchQuery + "%", - "%" + mSearchQuery + "%" - }; + baseSelection = "((" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=? AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ? OR " + NoteColumns.TAGS + " LIKE ?))" + + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0 AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ? OR " + NoteColumns.TAGS + " LIKE ?)))"; + argsList.add(String.valueOf(mCurrentFolderId)); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); } else { // 普通文件夹 - selection = NORMAL_SELECTION + " AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ?)"; - selectionArgs = new String[] { - String.valueOf(mCurrentFolderId), - "%" + mSearchQuery + "%", - "%" + mSearchQuery + "%" - }; + baseSelection = NORMAL_SELECTION + " AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ? OR " + NoteColumns.TAGS + " LIKE ?)"; + argsList.add(String.valueOf(mCurrentFolderId)); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); } } else { // 没有搜索关键词的情况 - selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; - selectionArgs = new String[] { String.valueOf(mCurrentFolderId) }; + baseSelection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; + argsList.add(String.valueOf(mCurrentFolderId)); } + // 添加标签筛选条件 + if (!TextUtils.isEmpty(mSelectedTag)) { + if (mCurrentFolderId == Notes.ID_ROOT_FOLDER) { + // 根文件夹需要特殊处理 + String tagCondition = " AND " + NoteColumns.TAGS + " LIKE ?"; + + // 根据是否有搜索关键词构建不同的标签筛选条件 + if (!TextUtils.isEmpty(mSearchQuery)) { + // 有搜索关键词的情况 + baseSelection = "((" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=? AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ? OR " + NoteColumns.TAGS + " LIKE ?)" + tagCondition + ")" + + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0 AND (" + NoteColumns.TITLE + " LIKE ? OR " + NoteColumns.SNIPPET + " LIKE ? OR " + NoteColumns.TAGS + " LIKE ?)" + tagCondition + "))"; + + // 重新构建参数列表 + argsList.clear(); + argsList.add(String.valueOf(mCurrentFolderId)); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSelectedTag + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSearchQuery + "%"); + argsList.add("%" + mSelectedTag + "%"); + } else { + // 没有搜索关键词的情况 + baseSelection = "((" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?" + tagCondition + ")" + + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0" + tagCondition + "))"; + + // 重新构建参数列表 + argsList.clear(); + argsList.add(String.valueOf(mCurrentFolderId)); + argsList.add("%" + mSelectedTag + "%"); + argsList.add("%" + mSelectedTag + "%"); + } + } else { + // 普通文件夹 + if (baseSelection.contains("AND")) { + baseSelection += " AND " + NoteColumns.TAGS + " LIKE ?"; + } else { + baseSelection += " WHERE " + NoteColumns.TAGS + " LIKE ?"; + } + argsList.add("%" + mSelectedTag + "%"); + } + } + + selection = baseSelection; + selectionArgs = argsList.toArray(new String[argsList.size()]); + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs, NoteColumns.IS_PINNED + " DESC," + NoteColumns.PIN_PRIORITY + " DESC," + @@ -1107,4 +1177,181 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } return false; } + + /** + * 显示标签设置对话框 + */ + private void showTagsDialog() { + // 查询所有唯一标签 + Cursor cursor = mContentResolver.query( + Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.TAGS }, + NoteColumns.TAGS + " <> '' AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + null, + NoteColumns.TAGS + " ASC" + ); + + // 提取所有唯一标签 + HashSet tagSet = new HashSet<>(); + if (cursor != null) { + while (cursor.moveToNext()) { + String tags = cursor.getString(0); + if (tags != null && !tags.isEmpty()) { + // 处理多标签情况,使用逗号分隔 + String[] tagArray = tags.split(","); + for (String tag : tagArray) { + tag = tag.trim(); + if (!tag.isEmpty()) { + tagSet.add(tag); + } + } + } + } + cursor.close(); + } + + // 创建对话框 + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etTags = (EditText) view.findViewById(R.id.et_foler_name); + etTags.setHint(R.string.hint_tags); + + // 设置当前标签 + if (mFocusNoteDataItem != null && mFocusNoteDataItem.getTags() != null) { + etTags.setText(mFocusNoteDataItem.getTags()); + } + + builder.setTitle(getString(R.string.menu_tags)); + builder.setView(view); + + // 如果有现有标签,添加标签选择按钮 + if (!tagSet.isEmpty()) { + final ArrayList tagList = new ArrayList<>(tagSet); + + builder.setNeutralButton("选择标签", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 显示标签选择对话框 + AlertDialog.Builder tagSelectorBuilder = new AlertDialog.Builder(NotesListActivity.this); + tagSelectorBuilder.setTitle("选择标签"); + tagSelectorBuilder.setItems(tagList.toArray(new String[0]), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String selectedTag = tagList.get(which); + String currentTags = etTags.getText().toString().trim(); + + // 检查标签是否已存在 + if (currentTags.isEmpty()) { + etTags.setText(selectedTag); + } else { + // 检查标签是否已存在 + String[] existingTags = currentTags.split(","); + boolean tagExists = false; + for (String tag : existingTags) { + if (tag.trim().equals(selectedTag)) { + tagExists = true; + break; + } + } + if (!tagExists) { + etTags.setText(currentTags + ", " + selectedTag); + } + } + } + }); + tagSelectorBuilder.show(); + } + }); + } + + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etTags); + } + }); + + final Dialog dialog = builder.show(); + final Button positive = (Button) dialog.findViewById(android.R.id.button1); + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + hideSoftInput(etTags); + String tags = etTags.getText().toString().trim(); + + // 更新标签 + ContentValues values = new ContentValues(); + values.put(NoteColumns.TAGS, tags); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, + NoteColumns.ID + "=?", + new String[] { String.valueOf(mFocusNoteDataItem.getId()) }); + + dialog.dismiss(); + mModeCallBack.finishActionMode(); + startAsyncNotesListQuery(); + } + }); + } + + /** + * 显示标签选择对话框,用于搜索筛选 + */ + private void showTagsSelectorDialog() { + // 查询所有唯一标签 + Cursor cursor = mContentResolver.query( + Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.TAGS }, + NoteColumns.TAGS + " <> '' AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + null, + NoteColumns.TAGS + " ASC" + ); + + if (cursor == null) { + Toast.makeText(this, getString(R.string.error_sdcard_unmounted), Toast.LENGTH_SHORT).show(); + return; + } + + // 提取所有唯一标签 + HashSet tagSet = new HashSet<>(); + while (cursor.moveToNext()) { + String tags = cursor.getString(0); + if (tags != null && !tags.isEmpty()) { + // 处理多标签情况,使用逗号分隔 + String[] tagArray = tags.split(","); + for (String tag : tagArray) { + tag = tag.trim(); + if (!tag.isEmpty()) { + tagSet.add(tag); + } + } + } + } + cursor.close(); + + // 将标签转换为数组 + final ArrayList tagList = new ArrayList<>(tagSet); + tagList.add(0, "全部"); // 添加"全部"选项 + + // 创建对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.menu_tags)); + builder.setItems(tagList.toArray(new String[0]), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + // 选择了"全部",清除标签筛选 + mSelectedTag = ""; + } else { + // 选择了某个标签 + mSelectedTag = tagList.get(which); + } + // 刷新便签列表 + startAsyncNotesListQuery(); + } + }); + + builder.show(); + } } diff --git a/src/main/java/net/micode/notes/ui/NotesListItem.java b/src/main/java/net/micode/notes/ui/NotesListItem.java index 3bf7ff6..738835d 100644 --- a/src/main/java/net/micode/notes/ui/NotesListItem.java +++ b/src/main/java/net/micode/notes/ui/NotesListItem.java @@ -136,20 +136,22 @@ public class NotesListItem extends LinearLayout { data.getNotesCount())); mAlert.setVisibility(View.GONE); } else { - // 普通笔记 - String title = data.getTitle(); - if (title != null && !title.isEmpty()) { - // 如果有标题,显示标题 - mTitle.setText(title); - } else { - // 如果没有标题,显示正文前20个字符 - String snippet = data.getSnippet(); - if (snippet != null && snippet.length() > 20) { - mTitle.setText(snippet.substring(0, 20) + "..."); + // 普通笔记 + String title = data.getTitle(); + if (title != null && !title.isEmpty()) { + // 如果有标题,显示标题 + mTitle.setText(title); } else { - mTitle.setText(snippet); + // 如果没有标题,显示正文前20个字符 + String snippet = data.getSnippet(); + // 将HTML转换为纯文本,移除HTML标签 + String plainText = android.text.Html.fromHtml(snippet).toString(); + if (plainText != null && plainText.length() > 20) { + mTitle.setText(plainText.substring(0, 20) + "..."); + } else { + mTitle.setText(plainText); + } } - } // 设置提醒图标 if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); diff --git a/src/main/res/drawable/ic_format_bold.xml b/src/main/res/drawable/ic_format_bold.xml new file mode 100644 index 0000000..6440c7e --- /dev/null +++ b/src/main/res/drawable/ic_format_bold.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_clear.xml b/src/main/res/drawable/ic_format_clear.xml new file mode 100644 index 0000000..0729ed2 --- /dev/null +++ b/src/main/res/drawable/ic_format_clear.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_color_fill.xml b/src/main/res/drawable/ic_format_color_fill.xml new file mode 100644 index 0000000..cd39dfe --- /dev/null +++ b/src/main/res/drawable/ic_format_color_fill.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_color_text.xml b/src/main/res/drawable/ic_format_color_text.xml new file mode 100644 index 0000000..2e504e0 --- /dev/null +++ b/src/main/res/drawable/ic_format_color_text.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_italic.xml b/src/main/res/drawable/ic_format_italic.xml new file mode 100644 index 0000000..dc90219 --- /dev/null +++ b/src/main/res/drawable/ic_format_italic.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_format_underlined.xml b/src/main/res/drawable/ic_format_underlined.xml new file mode 100644 index 0000000..330e631 --- /dev/null +++ b/src/main/res/drawable/ic_format_underlined.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/res/layout/note_edit.xml b/src/main/res/layout/note_edit.xml index 971e984..b529e4d 100644 --- a/src/main/res/layout/note_edit.xml +++ b/src/main/res/layout/note_edit.xml @@ -143,6 +143,75 @@ android:background="#E0E0E0" /> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3c7037e..177b8fd 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -50,6 +50,8 @@ Move to folder Pin Unpin + Tags + Enter tags %d selected Nothing selected, the operation is invalid Select all @@ -206,6 +208,10 @@ Title has reached maximum length Bold Italic + Underline + Highlight + Text Color + Clear Format Normal Insert Image Undo