搜索功能

pull/27/head
蒋天翔 4 weeks ago
parent 19721c364f
commit 269d287c71

@ -0,0 +1,67 @@
package net.micode.notes.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
public class SearchHistoryManager {
private static final String PREF_NAME = "search_history";
private static final String KEY_HISTORY = "history_list";
private static final int MAX_HISTORY_SIZE = 10;
private final SharedPreferences mPrefs;
public SearchHistoryManager(Context context) {
mPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public List<String> getHistory() {
String json = mPrefs.getString(KEY_HISTORY, "");
List<String> list = new ArrayList<>();
if (TextUtils.isEmpty(json)) {
return list;
}
try {
JSONArray array = new JSONArray(json);
for (int i = 0; i < array.length(); i++) {
list.add(array.getString(i));
}
} catch (JSONException e) {
e.printStackTrace();
}
return list;
}
public void addHistory(String keyword) {
if (TextUtils.isEmpty(keyword)) return;
List<String> history = getHistory();
// Remove existing to move to top
history.remove(keyword);
history.add(0, keyword);
// Limit size
if (history.size() > MAX_HISTORY_SIZE) {
history = history.subList(0, MAX_HISTORY_SIZE);
}
saveHistory(history);
}
public void removeHistory(String keyword) {
List<String> history = getHistory();
if (history.remove(keyword)) {
saveHistory(history);
}
}
public void clearHistory() {
mPrefs.edit().remove(KEY_HISTORY).apply();
}
private void saveHistory(List<String> history) {
JSONArray array = new JSONArray(history);
mPrefs.edit().putString(KEY_HISTORY, array.toString()).apply();
}
}

@ -0,0 +1,188 @@
package net.micode.notes.ui;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.SearchHistoryManager;
import java.util.ArrayList;
import java.util.List;
public class NoteSearchActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, NoteSearchAdapter.OnItemClickListener {
private SearchView mSearchView;
private RecyclerView mRecyclerView;
private TextView mTvNoResult;
private NoteSearchAdapter mAdapter;
private NotesRepository mRepository;
private SearchHistoryManager mHistoryManager;
private TextView mBtnShowHistory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_search);
mRepository = new NotesRepository(getContentResolver());
mHistoryManager = new SearchHistoryManager(this);
initViews();
// Initial state: search is empty, show history button if there is history, or just show list
// Requirement: "history option below search bar"
showHistoryOption();
}
private void initViews() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
toolbar.setNavigationOnClickListener(v -> finish());
mSearchView = findViewById(R.id.search_view);
mSearchView.setOnQueryTextListener(this);
mSearchView.setFocusable(true);
mSearchView.setIconified(false);
mSearchView.requestFocusFromTouch();
mBtnShowHistory = findViewById(R.id.btn_show_history);
mBtnShowHistory.setOnClickListener(v -> showHistoryList());
mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new NoteSearchAdapter(this, this);
mRecyclerView.setAdapter(mAdapter);
mTvNoResult = findViewById(R.id.tv_no_result);
}
private void showHistoryOption() {
// Show the "History" button, hide the list
mBtnShowHistory.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
mTvNoResult.setVisibility(View.GONE);
}
private void showHistoryList() {
List<String> history = mHistoryManager.getHistory();
if (history.isEmpty()) {
// If no history, maybe show a toast or empty state?
// But for now, let's just show the empty list which is fine
}
List<Object> data = new ArrayList<>(history);
mAdapter.setData(data, null);
mBtnShowHistory.setVisibility(View.GONE); // Hide button when showing list
mTvNoResult.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
private void performSearch(String query) {
if (TextUtils.isEmpty(query)) {
showHistoryOption();
return;
}
// Hide history button when searching
mBtnShowHistory.setVisibility(View.GONE);
mRepository.searchNotes(query, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> result) {
runOnUiThread(() -> {
List<Object> data = new ArrayList<>(result);
mAdapter.setData(data, query);
if (data.isEmpty()) {
mTvNoResult.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
} else {
mTvNoResult.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
});
}
@Override
public void onError(Exception error) {
runOnUiThread(() -> {
Toast.makeText(NoteSearchActivity.this, "Search failed: " + error.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
}
@Override
public boolean onQueryTextSubmit(String query) {
if (!TextUtils.isEmpty(query)) {
mHistoryManager.addHistory(query);
performSearch(query);
mSearchView.clearFocus(); // Hide keyboard
}
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
if (TextUtils.isEmpty(newText)) {
showHistoryOption();
} else {
performSearch(newText);
}
return true;
}
@Override
public void onNoteClick(NotesRepository.NoteInfo note) {
// Save history when user clicks a result
String query = mSearchView.getQuery().toString();
if (!TextUtils.isEmpty(query)) {
mHistoryManager.addHistory(query);
}
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, note.getId());
// Pass search keyword for highlighting in editor
// NoteEditActivity uses SearchManager.EXTRA_DATA_KEY for ID and USER_QUERY for keyword
intent.putExtra(android.app.SearchManager.EXTRA_DATA_KEY, String.valueOf(note.getId()));
intent.putExtra(android.app.SearchManager.USER_QUERY, mSearchView.getQuery().toString());
startActivity(intent);
}
@Override
public void onHistoryClick(String keyword) {
mSearchView.setQuery(keyword, true);
}
@Override
public void onHistoryDelete(String keyword) {
mHistoryManager.removeHistory(keyword);
// Refresh history view if we are currently showing history (search box is empty)
if (TextUtils.isEmpty(mSearchView.getQuery())) {
showHistoryList();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mRepository != null) {
mRepository.shutdown();
}
}
}

@ -0,0 +1,180 @@
package net.micode.notes.ui;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NoteSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HISTORY = 1;
private static final int TYPE_NOTE = 2;
private Context mContext;
private List<Object> mDataList;
private String mSearchKeyword;
private OnItemClickListener mListener;
public interface OnItemClickListener {
void onNoteClick(NotesRepository.NoteInfo note);
void onHistoryClick(String keyword);
void onHistoryDelete(String keyword);
}
public NoteSearchAdapter(Context context, OnItemClickListener listener) {
mContext = context;
mListener = listener;
mDataList = new ArrayList<>();
}
public void setData(List<Object> data, String keyword) {
mDataList = data;
mSearchKeyword = keyword;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
Object item = mDataList.get(position);
if (item instanceof String) {
return TYPE_HISTORY;
} else if (item instanceof NotesRepository.NoteInfo) {
return TYPE_NOTE;
}
return super.getItemViewType(position);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == TYPE_HISTORY) {
View view = LayoutInflater.from(mContext).inflate(R.layout.search_history_item, parent, false);
return new HistoryViewHolder(view);
} else {
View view = LayoutInflater.from(mContext).inflate(R.layout.note_item, parent, false);
return new NoteViewHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HistoryViewHolder) {
String keyword = (String) mDataList.get(position);
((HistoryViewHolder) holder).bind(keyword);
} else if (holder instanceof NoteViewHolder) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) mDataList.get(position);
((NoteViewHolder) holder).bind(note);
}
}
@Override
public int getItemCount() {
return mDataList.size();
}
class HistoryViewHolder extends RecyclerView.ViewHolder {
TextView tvKeyword;
ImageView ivDelete;
public HistoryViewHolder(View itemView) {
super(itemView);
tvKeyword = itemView.findViewById(R.id.tv_history_keyword);
ivDelete = itemView.findViewById(R.id.iv_delete_history);
}
public void bind(final String keyword) {
tvKeyword.setText(keyword);
itemView.setOnClickListener(v -> {
if (mListener != null) mListener.onHistoryClick(keyword);
});
ivDelete.setOnClickListener(v -> {
if (mListener != null) mListener.onHistoryDelete(keyword);
});
}
}
class NoteViewHolder extends RecyclerView.ViewHolder {
ImageView ivTypeIcon;
TextView tvTitle;
TextView tvTime;
TextView tvName;
ImageView ivAlertIcon;
CheckBox checkbox;
public NoteViewHolder(View itemView) {
super(itemView);
ivTypeIcon = itemView.findViewById(R.id.iv_type_icon);
tvTitle = itemView.findViewById(R.id.tv_title);
tvTime = itemView.findViewById(R.id.tv_time);
tvName = itemView.findViewById(R.id.tv_name);
ivAlertIcon = itemView.findViewById(R.id.iv_alert_icon);
checkbox = itemView.findViewById(android.R.id.checkbox);
}
public void bind(final NotesRepository.NoteInfo note) {
// 设置标题和高亮
// NoteInfo.title defaults to snippet if title is empty, so it's safe to use title
if (!TextUtils.isEmpty(mSearchKeyword)) {
tvTitle.setText(getHighlightText(note.title, mSearchKeyword));
} else {
tvTitle.setText(note.title);
}
// 设置时间
tvTime.setText(android.text.format.DateUtils.getRelativeTimeSpanString(note.modifiedDate));
// 设置背景(如果 NoteInfo 中有背景ID
// 注意NoteInfo 中 bgColorId 是整型ID需要转换为资源ID
// 这里为了简单,暂不设置复杂的背景,或者使用默认背景
// 点击事件
itemView.setOnClickListener(v -> {
if (mListener != null) mListener.onNoteClick(note);
});
// 隐藏不需要的视图
ivTypeIcon.setVisibility(View.GONE);
tvName.setVisibility(View.GONE);
checkbox.setVisibility(View.GONE);
ivAlertIcon.setVisibility(View.GONE);
}
}
private Spannable getHighlightText(String text, String keyword) {
if (text == null) text = "";
SpannableString spannable = new SpannableString(text);
if (!TextUtils.isEmpty(keyword)) {
Pattern pattern = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
spannable.setSpan(
new BackgroundColorSpan(0x40FFFF00), // 半透明黄色
matcher.start(),
matcher.end(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
return spannable;
}
}

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.Material3.Light">
<!-- 搜索框 -->
<androidx.appcompat.widget.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionSearch|flagNoExtractUi"
app:iconifiedByDefault="false"
app:queryHint="@string/search_hint" />
</androidx.appcompat.widget.Toolbar>
<!-- 历史记录按钮 -->
<TextView
android:id="@+id/btn_show_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:padding="16dp"
android:text="@string/search_history_title"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:drawablePadding="8dp"
android:visibility="gone"
app:drawableEndCompat="@android:drawable/arrow_down_float" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 搜索结果列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:scrollbars="vertical" />
<!-- 无结果提示 -->
<TextView
android:id="@+id/tv_no_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/search_no_results"
android:visibility="gone"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_recent_history"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@null" />
<TextView
android:id="@+id/tv_history_keyword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary" />
<ImageView
android:id="@+id/iv_delete_history"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@string/menu_delete" />
</LinearLayout>
Loading…
Cancel
Save