新增富文本编辑功能,优化原代码中的搜索功能 #7

Merged
ptgkr64sc merged 1 commits from ranhao_branch into master 4 weeks ago

@ -26,6 +26,8 @@ import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
@ -188,8 +190,12 @@ public class NotesProvider extends ContentProvider {
try {
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
Cursor rawCursor = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
// 包装Cursor以处理HTML内容
if (rawCursor != null) {
c = new HtmlCursorWrapper(rawCursor);
}
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
@ -203,6 +209,240 @@ public class NotesProvider extends ContentProvider {
return c;
}
/**
* CursorHTML
* HTML
*/
private class HtmlCursorWrapper implements Cursor {
private Cursor mCursor;
public HtmlCursorWrapper(Cursor cursor) {
mCursor = cursor;
}
@Override
public int getCount() {
return mCursor.getCount();
}
@Override
public int getPosition() {
return mCursor.getPosition();
}
@Override
public boolean move(int offset) {
return mCursor.move(offset);
}
@Override
public boolean moveToPosition(int position) {
return mCursor.moveToPosition(position);
}
@Override
public boolean moveToFirst() {
return mCursor.moveToFirst();
}
@Override
public boolean moveToLast() {
return mCursor.moveToLast();
}
@Override
public boolean moveToNext() {
return mCursor.moveToNext();
}
@Override
public boolean moveToPrevious() {
return mCursor.moveToPrevious();
}
@Override
public boolean isFirst() {
return mCursor.isFirst();
}
@Override
public boolean isLast() {
return mCursor.isLast();
}
@Override
public boolean isBeforeFirst() {
return mCursor.isBeforeFirst();
}
@Override
public boolean isAfterLast() {
return mCursor.isAfterLast();
}
@Override
public int getColumnIndex(String columnName) {
return mCursor.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
return mCursor.getColumnIndexOrThrow(columnName);
}
@Override
public String getColumnName(int columnIndex) {
return mCursor.getColumnName(columnIndex);
}
@Override
public String[] getColumnNames() {
return mCursor.getColumnNames();
}
@Override
public int getColumnCount() {
return mCursor.getColumnCount();
}
@Override
public byte[] getBlob(int columnIndex) {
return mCursor.getBlob(columnIndex);
}
@Override
public String getString(int columnIndex) {
String value = mCursor.getString(columnIndex);
// 处理搜索结果中的HTML内容
if (value != null && (
columnIndex == mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1) ||
columnIndex == mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2))
) {
// 将HTML转换为普通文本
return Html.fromHtml(value).toString();
}
return value;
}
@Override
public short getShort(int columnIndex) {
return mCursor.getShort(columnIndex);
}
@Override
public int getInt(int columnIndex) {
return mCursor.getInt(columnIndex);
}
@Override
public long getLong(int columnIndex) {
return mCursor.getLong(columnIndex);
}
@Override
public float getFloat(int columnIndex) {
return mCursor.getFloat(columnIndex);
}
@Override
public double getDouble(int columnIndex) {
return mCursor.getDouble(columnIndex);
}
@Override
public int getType(int columnIndex) {
return mCursor.getType(columnIndex);
}
@Override
public boolean isNull(int columnIndex) {
return mCursor.isNull(columnIndex);
}
@Override
public void deactivate() {
mCursor.deactivate();
}
@Override
public boolean requery() {
return mCursor.requery();
}
@Override
public void close() {
mCursor.close();
}
@Override
public boolean isClosed() {
return mCursor.isClosed();
}
@Override
public void registerContentObserver(android.database.ContentObserver observer) {
mCursor.registerContentObserver(observer);
}
@Override
public void unregisterContentObserver(android.database.ContentObserver observer) {
mCursor.unregisterContentObserver(observer);
}
@Override
public void registerDataSetObserver(android.database.DataSetObserver observer) {
mCursor.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(android.database.DataSetObserver observer) {
mCursor.unregisterDataSetObserver(observer);
}
@Override
public void setNotificationUri(android.content.ContentResolver cr, Uri uri) {
mCursor.setNotificationUri(cr, uri);
}
@Override
public Uri getNotificationUri() {
return mCursor.getNotificationUri();
}
@Override
public boolean getWantsAllOnMoveCalls() {
return mCursor.getWantsAllOnMoveCalls();
}
@Override
public void setExtras(Bundle extras) {
mCursor.setExtras(extras);
}
@Override
public Bundle getExtras() {
return mCursor.getExtras();
}
@Override
public Bundle respond(Bundle extras) {
return mCursor.respond(extras);
}
@Override
public void copyStringToBuffer(int columnIndex, android.database.CharArrayBuffer buffer) {
String value = getString(columnIndex);
if (value != null) {
char[] data = value.toCharArray();
if (buffer.data == null || buffer.data.length < data.length) {
buffer.data = new char[data.length];
}
System.arraycopy(data, 0, buffer.data, 0, data.length);
buffer.sizeCopied = data.length;
}
}
}
/**
*
* @param uri URI

@ -0,0 +1,109 @@
/*
* Copyright (c) 2026, 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.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.ui.NotesPreferenceActivity;
/**
*
*/
public class ElderModeUtils {
/**
*
*/
private static final float ELDER_MODE_FONT_SCALE = 1.1f;
/**
*
*
* @param context
* @return
*/
public static boolean isElderModeEnabled(Context context) {
// 使用默认的SharedPreferences因为CheckBoxPreference会自动保存到这里
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getBoolean(NotesPreferenceActivity.PREFERENCE_ELDER_MODE_KEY, false);
}
/**
* View
*
* @param context
* @param view View
*/
public static void applyElderMode(Context context, View view) {
if (!isElderModeEnabled(context)) {
return;
}
// 只对用户输入的内容应用字体增大,系统信息保持不变
if (view instanceof TextView) {
TextView textView = (TextView) view;
int id = textView.getId();
// 只增大用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(textView.getTextSize() * ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
applyElderMode(context, viewGroup.getChildAt(i));
}
}
}
/**
*
*
* @param context
* @param view View
*/
public static void clearElderMode(Context context, View view) {
if (!isElderModeEnabled(context)) {
return;
}
// 只对用户输入的内容应用字体恢复,系统信息保持不变
if (view instanceof TextView) {
TextView textView = (TextView) view;
int id = textView.getId();
// 只恢复用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(textView.getTextSize() / ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
clearElderMode(context, viewGroup.getChildAt(i));
}
}
}
}

@ -30,11 +30,16 @@ import android.content.SharedPreferences;
import android.graphics.Paint;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@ -298,7 +303,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(mWorkingNote.getContent()); // 切换到列表模式
} else {
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
// 加载富文本内容
String content = mWorkingNote.getContent();
CharSequence htmlContent = Html.fromHtml(content != null ? content : "");
mNoteEditor.setText(getHighlightQueryResult(htmlContent, mUserQuery));
mNoteEditor.setSelection(mNoteEditor.getText().length()); // 光标移到最后
}
@ -643,6 +651,18 @@ public class NoteEditActivity extends Activity implements OnClickListener,
case R.id.menu_delete_remind:
mWorkingNote.setAlertDate(0, false); // 删除提醒
break;
case R.id.menu_bold:
applyRichTextStyle(android.graphics.Typeface.BOLD);
break;
case R.id.menu_italic:
applyRichTextStyle(android.graphics.Typeface.ITALIC);
break;
case R.id.menu_underline:
applyUnderlineStyle();
break;
case R.id.menu_strikethrough:
applyStrikethroughStyle();
break;
default:
break;
}
@ -846,9 +866,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
/**
* Spannable
*/
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
private Spannable getHighlightQueryResult(CharSequence fullText, String userQuery) {
SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
if (!TextUtils.isEmpty(userQuery)) {
if (!TextUtils.isEmpty(userQuery) && !TextUtils.isEmpty(fullText)) {
mPattern = Pattern.compile(userQuery);
Matcher m = mPattern.matcher(fullText);
int start = 0;
@ -900,7 +920,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 设置编辑文本变化监听器
edit.setOnTextViewChangeListener(this);
edit.setIndex(index); // 设置索引
edit.setText(getHighlightQueryResult(item, mUserQuery)); // 设置文本(支持高亮)
// 列表模式下不使用富文本,直接设置文本
edit.setText(item); // 设置文本
return view;
}
@ -954,17 +975,22 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (!TextUtils.isEmpty(edit.getText())) {
// 根据复选框状态添加不同的标记
if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) {
sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n");
sb.append(TAG_CHECKED).append(" " ).append(edit.getText()).append("\n" );
hasChecked = true;
} else {
sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n");
sb.append(TAG_UNCHECKED).append(" " ).append(edit.getText()).append("\n" );
}
}
}
mWorkingNote.setWorkingText(sb.toString());
} else {
// 普通模式:直接获取编辑框文本
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
// 普通模式将富文本转换为HTML格式保存
if (mNoteEditor.getText() instanceof Spanned) {
String htmlContent = Html.toHtml((Spanned) mNoteEditor.getText());
mWorkingNote.setWorkingText(htmlContent);
} else {
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
}
}
return hasChecked;
}
@ -986,6 +1012,152 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return saved;
}
/**
*
*/
private void applyRichTextStyle(int style) {
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
// 列表模式下,获取当前焦点的编辑框
for (int i = 0; i < mEditTextList.getChildCount(); i++) {
NoteEditText editText = (NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text);
if (editText.hasFocus()) {
applyStyleToEditText(editText, style);
break;
}
}
} else {
// 普通模式下,直接应用到主编辑框
applyStyleToEditText(mNoteEditor, style);
}
}
/**
* 线
*/
private void applyUnderlineStyle() {
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
// 列表模式下,获取当前焦点的编辑框
for (int i = 0; i < mEditTextList.getChildCount(); i++) {
NoteEditText editText = (NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text);
if (editText.hasFocus()) {
applyUnderlineToEditText(editText);
break;
}
}
} else {
// 普通模式下,直接应用到主编辑框
applyUnderlineToEditText(mNoteEditor);
}
}
/**
* 线
*/
private void applyStrikethroughStyle() {
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
// 列表模式下,获取当前焦点的编辑框
for (int i = 0; i < mEditTextList.getChildCount(); i++) {
NoteEditText editText = (NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text);
if (editText.hasFocus()) {
applyStrikethroughToEditText(editText);
break;
}
}
} else {
// 普通模式下,直接应用到主编辑框
applyStrikethroughToEditText(mNoteEditor);
}
}
/**
* EditText
*/
private void applyStyleToEditText(EditText editText, int style) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start != end) {
SpannableString spannable = new SpannableString(editText.getText());
StyleSpan[] existingSpans = spannable.getSpans(start, end, StyleSpan.class);
// 检查是否已经应用了相同的样式
boolean hasStyle = false;
for (StyleSpan span : existingSpans) {
if (span.getStyle() == style) {
hasStyle = true;
break;
}
}
if (hasStyle) {
// 如果已经应用了相同的样式,则移除
for (StyleSpan span : existingSpans) {
if (span.getStyle() == style) {
spannable.removeSpan(span);
}
}
} else {
// 如果没有应用相同的样式,则添加
spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
}
editText.setText(spannable);
editText.setSelection(end);
}
}
/**
* EditText线
*/
private void applyUnderlineToEditText(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start != end) {
SpannableString spannable = new SpannableString(editText.getText());
UnderlineSpan[] existingSpans = spannable.getSpans(start, end, UnderlineSpan.class);
if (existingSpans.length > 0) {
// 如果已经应用了下划线,则移除
for (UnderlineSpan span : existingSpans) {
spannable.removeSpan(span);
}
} else {
// 如果没有应用下划线,则添加
spannable.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
}
editText.setText(spannable);
editText.setSelection(end);
}
}
/**
* EditText线
*/
private void applyStrikethroughToEditText(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start != end) {
SpannableString spannable = new SpannableString(editText.getText());
StrikethroughSpan[] existingSpans = spannable.getSpans(start, end, StrikethroughSpan.class);
if (existingSpans.length > 0) {
// 如果已经应用了删除线,则移除
for (StrikethroughSpan span : existingSpans) {
spannable.removeSpan(span);
}
} else {
// 如果没有应用删除线,则添加
spannable.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
}
editText.setText(spannable);
editText.setSelection(end);
}
}
/**
*
*/

@ -18,6 +18,7 @@ package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.text.Html;
import android.text.TextUtils;
import net.micode.notes.data.Contact;
@ -102,6 +103,10 @@ public class NoteItemData {
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
mSnippet = cursor.getString(SNIPPET_COLUMN);
// 将HTML格式转换为普通文本解决富文本编辑后显示乱码的问题
if (!TextUtils.isEmpty(mSnippet)) {
mSnippet = Html.fromHtml(mSnippet).toString();
}
// 移除摘要中的复选框标记(已勾选和未勾选)
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");

@ -81,12 +81,9 @@ public class NotesPreferenceActivity extends PreferenceActivity {
/* 使用应用图标作为导航按钮 */
getActionBar().setDisplayHomeAsUpEnabled(true);
// 检查并设置密保问题
// 检查并设置密保问题(只在首次设置时显示)
if (!hasSecurityQuestionsSet()) {
showSetSecurityQuestionsDialog();
} else {
// 验证密保问题才能进入设置
showVerifySecurityQuestionsDialog();
}
// 从XML文件加载偏好设置
@ -355,8 +352,8 @@ public class NotesPreferenceActivity extends PreferenceActivity {
securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// 显示修改密保问题的对话框
showChangeSecurityQuestionsDialog();
// 验证当前密保才能修改
showVerifySecurityQuestionsForModificationDialog();
return true;
}
});
@ -420,6 +417,58 @@ public class NotesPreferenceActivity extends PreferenceActivity {
// 显示对话框
builder.show();
}
/**
*
*/
private void showVerifySecurityQuestionsForModificationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("验证身份");
builder.setIcon(android.R.drawable.ic_lock_idle_lock);
builder.setMessage("请回答您的密保问题以验证身份,验证成功后才能修改密保");
// 创建布局
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(40, 20, 40, 20);
// 创建姓名输入框
final EditText nameInput = new EditText(this);
nameInput.setHint("请输入姓名");
layout.addView(nameInput);
// 创建生日输入框
final EditText birthdayInput = new EditText(this);
birthdayInput.setHint("请输入生日 (YYYY-MM-DD)");
layout.addView(birthdayInput);
builder.setView(layout);
// 设置确定按钮
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String name = nameInput.getText().toString();
String birthday = birthdayInput.getText().toString();
if (verifySecurityQuestions(name, birthday)) {
// 验证成功,显示修改密保对话框
Toast.makeText(NotesPreferenceActivity.this, "验证成功", Toast.LENGTH_SHORT).show();
showChangeSecurityQuestionsDialog();
} else {
// 验证失败,显示提示
Toast.makeText(NotesPreferenceActivity.this, "验证失败,请重新输入", Toast.LENGTH_SHORT).show();
// 重新显示验证对话框
showVerifySecurityQuestionsForModificationDialog();
}
}
});
// 设置取消按钮
builder.setNegativeButton("取消", null);
// 显示对话框
builder.show();
}
/**
*

Loading…
Cancel
Save