diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java index d982351..0872c4c 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java @@ -29,7 +29,7 @@ import android.util.Log; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.CallNote; import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.ui.NotesRecyclerViewAdapter.AppWidgetAttribute; import java.util.ArrayList; import java.util.HashSet; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/GridSpacingItemDecoration.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/GridSpacingItemDecoration.java new file mode 100644 index 0000000..1a7d15d --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/GridSpacingItemDecoration.java @@ -0,0 +1,68 @@ +/* + * 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.graphics.Rect; +import android.view.View; +import androidx.recyclerview.widget.RecyclerView; + +/** + * 网格布局间距装饰类 + *
+ * 为网格布局的RecyclerView添加统一的间距,确保每个网格项之间有合适的间隔。 + * 支持是否包含边缘间距的配置。 + *
+ */ +public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { + private int spanCount; + private int spacing; + private boolean includeEdge; + + /** + * 构造函数 + * @param spanCount 网格列数 + * @param spacing 间距大小(像素) + * @param includeEdge 是否包含边缘间距 + */ + public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) { + this.spanCount = spanCount; + this.spacing = spacing; + this.includeEdge = includeEdge; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + int position = parent.getChildAdapterPosition(view); + int column = position % spanCount; + + if (includeEdge) { + outRect.left = spacing - column * spacing / spanCount; + outRect.right = (column + 1) * spacing / spanCount; + + if (position < spanCount) { + outRect.top = spacing; + } + outRect.bottom = spacing; + } else { + outRect.left = column * spacing / spanCount; + outRect.right = spacing - (column + 1) * spacing / spanCount; + if (position >= spanCount) { + outRect.top = spacing; + } + } + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutManagerController.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutManagerController.java new file mode 100644 index 0000000..0024c82 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutManagerController.java @@ -0,0 +1,294 @@ +/* + * 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.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +/** + * 布局管理器控制类 + *+ * 负责管理RecyclerView的布局切换,提供平滑的布局过渡效果。 + * 支持线性布局、网格布局和瀑布流布局的无缝切换。 + * 保存用户的布局偏好设置,确保应用重启后保持布局状态。 + *
+ */ +public class LayoutManagerController { + private static final String TAG = "LayoutManagerController"; + private static final String PREF_LAYOUT_TYPE = "layout_type"; + private static final String PREF_GRID_SPAN_COUNT = "grid_span_count"; + private static final String PREF_ITEM_SPACING = "item_spacing"; + + private Context mContext; + private RecyclerView mRecyclerView; + private RecyclerView.LayoutManager mCurrentLayoutManager; + private LayoutType mCurrentLayoutType; + private SharedPreferences mPreferences; + private LayoutChangeListener mListener; + + /** + * 布局变化监听器接口 + */ + public interface LayoutChangeListener { + void onLayoutChanged(LayoutType newLayoutType); + void onLayoutChangeFailed(Exception e); + } + + /** + * 构造函数 + * @param context 上下文 + * @param recyclerView RecyclerView实例 + * @param listener 布局变化监听器 + */ + public LayoutManagerController(Context context, RecyclerView recyclerView, LayoutChangeListener listener) { + mContext = context; + mRecyclerView = recyclerView; + mListener = listener; + mPreferences = PreferenceManager.getDefaultSharedPreferences(context); + + // 加载保存的布局类型 + String savedLayoutKey = mPreferences.getString(PREF_LAYOUT_TYPE, LayoutType.LINEAR.getKey()); + mCurrentLayoutType = LayoutType.fromKey(savedLayoutKey); + + Log.d(TAG, "LayoutManagerController initialized with layout: " + mCurrentLayoutType.getDisplayName()); + } + + /** + * 初始化布局 + */ + public void initializeLayout() { + switchLayout(mCurrentLayoutType, false); + } + + /** + * 切换布局 + * @param layoutType 目标布局类型 + * @param animate 是否播放动画 + * @return 切换是否成功 + */ + public boolean switchLayout(LayoutType layoutType, boolean animate) { + if (layoutType == mCurrentLayoutType) { + Log.d(TAG, "Already in " + layoutType.getDisplayName() + " mode"); + return true; + } + + long startTime = System.currentTimeMillis(); + + try { + // 保存滚动位置 + int scrollPosition = 0; + if (mRecyclerView.getLayoutManager() != null) { + if (mRecyclerView.getLayoutManager() instanceof LinearLayoutManager) { + LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager(); + scrollPosition = layoutManager.findFirstVisibleItemPosition(); + } else if (mRecyclerView.getLayoutManager() instanceof GridLayoutManager) { + GridLayoutManager layoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager(); + scrollPosition = layoutManager.findFirstVisibleItemPosition(); + } else if (mRecyclerView.getLayoutManager() instanceof StaggeredGridLayoutManager) { + StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) mRecyclerView.getLayoutManager(); + int[] positions = layoutManager.findFirstVisibleItemPositions(null); + scrollPosition = positions.length > 0 ? positions[0] : 0; + } + } + + // 移除所有装饰 + int decorationCount = mRecyclerView.getItemDecorationCount(); + for (int i = decorationCount - 1; i >= 0; i--) { + mRecyclerView.removeItemDecorationAt(i); + } + + // 创建新的布局管理器 + RecyclerView.LayoutManager newLayoutManager = createLayoutManager(layoutType); + + // 应用新布局 + if (animate) { + mRecyclerView.setLayoutManager(newLayoutManager); + } else { + mRecyclerView.setLayoutManager(newLayoutManager); + } + + // 添加间距装饰 + addItemDecoration(layoutType); + + // 保存布局偏好 + saveLayoutPreference(layoutType); + + // 恢复滚动位置 + restoreScrollPosition(scrollPosition); + + mCurrentLayoutManager = newLayoutManager; + mCurrentLayoutType = layoutType; + + long duration = System.currentTimeMillis() - startTime; + Log.d(TAG, "Layout switched to " + layoutType.getDisplayName() + " in " + duration + "ms"); + + // 通知监听器 + if (mListener != null) { + mListener.onLayoutChanged(layoutType); + } + + return true; + + } catch (Exception e) { + Log.e(TAG, "Failed to switch layout: " + e.getMessage(), e); + + // 通知监听器失败 + if (mListener != null) { + mListener.onLayoutChangeFailed(e); + } + + return false; + } + } + + /** + * 创建布局管理器 + * @param layoutType 布局类型 + * @return 布局管理器实例 + */ + private RecyclerView.LayoutManager createLayoutManager(LayoutType layoutType) { + int spanCount = getGridSpanCount(); + switch (layoutType) { + case LINEAR: + return new LinearLayoutManager(mContext); + case GRID: + return new GridLayoutManager(mContext, spanCount); + case STAGGERED: + return new StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL); + default: + return new LinearLayoutManager(mContext); + } + } + + /** + * 添加间距装饰 + * @param layoutType 布局类型 + */ + private void addItemDecoration(LayoutType layoutType) { + int spacing = getItemSpacing(); + + switch (layoutType) { + case LINEAR: + mRecyclerView.addItemDecoration(new NoteItemDecoration(mContext)); + break; + case GRID: + mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(getGridSpanCount(), spacing, true)); + break; + case STAGGERED: + mRecyclerView.addItemDecoration(new StaggeredGridSpacingItemDecoration(getGridSpanCount(), spacing, true)); + break; + } + } + + /** + * 恢复滚动位置 + * @param position 滚动位置 + */ + private void restoreScrollPosition(int position) { + if (mRecyclerView.getAdapter() != null && position >= 0 && position < mRecyclerView.getAdapter().getItemCount()) { + mRecyclerView.scrollToPosition(position); + } + } + + /** + * 保存布局偏好 + * @param layoutType 布局类型 + */ + private void saveLayoutPreference(LayoutType layoutType) { + mPreferences.edit() + .putString(PREF_LAYOUT_TYPE, layoutType.getKey()) + .apply(); + } + + /** + * 获取网格列数 + * @return 网格列数 + */ + public int getGridSpanCount() { + return mPreferences.getInt(PREF_GRID_SPAN_COUNT, 2); + } + + /** + * 设置网格列数 + * @param spanCount 网格列数 + */ + public void setGridSpanCount(int spanCount) { + if (spanCount < 1) spanCount = 1; + if (spanCount > 4) spanCount = 4; + + mPreferences.edit() + .putInt(PREF_GRID_SPAN_COUNT, spanCount) + .apply(); + + // 重新应用布局 + switchLayout(mCurrentLayoutType, true); + } + + /** + * 获取项目间距 + * @return 项目间距(像素) + */ + public int getItemSpacing() { + return mPreferences.getInt(PREF_ITEM_SPACING, 16); + } + + /** + * 设置项目间距 + * @param spacing 项目间距(像素) + */ + public void setItemSpacing(int spacing) { + if (spacing < 0) spacing = 0; + if (spacing > 48) spacing = 48; + + mPreferences.edit() + .putInt(PREF_ITEM_SPACING, spacing) + .apply(); + + // 重新应用布局 + switchLayout(mCurrentLayoutType, true); + } + + /** + * 获取当前布局类型 + * @return 当前布局类型 + */ + public LayoutType getCurrentLayoutType() { + return mCurrentLayoutType; + } + + /** + * 获取下一个布局类型(循环切换) + * @return 下一个布局类型 + */ + public LayoutType getNextLayoutType() { + LayoutType[] types = LayoutType.values(); + int currentIndex = 0; + for (int i = 0; i < types.length; i++) { + if (types[i] == mCurrentLayoutType) { + currentIndex = i; + break; + } + } + return types[(currentIndex + 1) % types.length]; + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutSettingsDialog.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutSettingsDialog.java new file mode 100644 index 0000000..e443472 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutSettingsDialog.java @@ -0,0 +1,199 @@ +/* + * 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.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.SeekBar; +import android.widget.Spinner; +import android.widget.TextView; +import net.micode.notes.R; + +/** + * 布局设置对话框 + *+ * 提供布局类型选择、网格列数和项目间距的配置界面。 + * 支持实时预览布局效果。 + *
+ */ +public class LayoutSettingsDialog { + private Context mContext; + private LayoutManagerController mLayoutManagerController; + private AlertDialog mDialog; + private Spinner mLayoutTypeSpinner; + private SeekBar mGridColumnsSeekBar; + private SeekBar mItemSpacingSeekBar; + private TextView mGridColumnsValue; + private TextView mItemSpacingValue; + + /** + * 构造函数 + * @param context 上下文 + * @param layoutManagerController 布局管理器 + */ + public LayoutSettingsDialog(Context context, LayoutManagerController layoutManagerController) { + mContext = context; + mLayoutManagerController = layoutManagerController; + } + + /** + * 显示布局设置对话框 + */ + public void show() { + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setTitle(R.string.layout_settings_title); + + View dialogView = LayoutInflater.from(mContext).inflate(R.layout.layout_settings_dialog, null); + builder.setView(dialogView); + + initViews(dialogView); + setupListeners(); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + saveSettings(); + } + }); + + builder.setNegativeButton(android.R.string.cancel, null); + + mDialog = builder.create(); + mDialog.show(); + } + + /** + * 初始化视图 + * @param dialogView 对话框视图 + */ + private void initViews(View dialogView) { + mLayoutTypeSpinner = (Spinner) dialogView.findViewById(R.id.layout_type_spinner); + mGridColumnsSeekBar = (SeekBar) dialogView.findViewById(R.id.grid_columns_seekbar); + mItemSpacingSeekBar = (SeekBar) dialogView.findViewById(R.id.item_spacing_seekbar); + mGridColumnsValue = (TextView) dialogView.findViewById(R.id.grid_columns_value); + mItemSpacingValue = (TextView) dialogView.findViewById(R.id.item_spacing_value); + + // 设置布局类型选项 + ArrayAdapter+ * 定义笔记列表支持的所有布局类型,包括线性布局、网格布局和瀑布流布局。 + * 每种布局类型都有对应的显示名称和描述。 + *
+ */ +public enum LayoutType { + LINEAR("linear", "列表布局", "传统的垂直列表布局,适合大量笔记浏览"), + GRID("grid", "网格布局", "网格排列布局,适合快速浏览和预览"), + STAGGERED("staggered", "瀑布流布局", "错落有致的布局,适合不同长度的笔记展示"); + + private final String key; + private final String displayName; + private final String description; + + LayoutType(String key, String displayName, String description) { + this.key = key; + this.displayName = displayName; + this.description = description; + } + + public String getKey() { + return key; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + public static LayoutType fromKey(String key) { + for (LayoutType type : values()) { + if (type.key.equals(key)) { + return type; + } + } + return LINEAR; + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteItemDecoration.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteItemDecoration.java new file mode 100644 index 0000000..8011a60 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteItemDecoration.java @@ -0,0 +1,73 @@ +/* + * 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.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.view.View; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.ItemDecoration; +import net.micode.notes.R; + +/** + * 笔记列表项装饰 + *+ * 为RecyclerView添加分隔线效果,替代ListView的divider属性。 + *
+ */ +public class NoteItemDecoration extends ItemDecoration { + private Drawable mDivider; + private int mDividerHeight; + + /** + * 构造函数 + * @param context 上下文 + */ + public NoteItemDecoration(Context context) { + mDivider = context.getResources().getDrawable(R.drawable.list_divider); + if (mDivider != null) { + mDividerHeight = 1; + } else { + mDividerHeight = mDivider.getIntrinsicHeight(); + } + } + + @Override + public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { + int left = parent.getPaddingLeft(); + int right = parent.getWidth() - parent.getPaddingRight(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams(); + + int top = child.getBottom() + params.bottomMargin; + int bottom = top + mDividerHeight; + + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + outRect.bottom = mDividerHeight; + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteViewHolder.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteViewHolder.java new file mode 100644 index 0000000..29bb5ad --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteViewHolder.java @@ -0,0 +1,54 @@ +/* + * 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.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + +import net.micode.notes.R; + +/** + * 笔记ViewHolder + *+ * RecyclerView的ViewHolder实现,持有笔记列表项的所有视图引用。 + * 避免重复findViewById,提升列表滚动性能。 + *
+ */ +public class NoteViewHolder extends RecyclerView.ViewHolder { + public ImageView mAlert; + public TextView mTitle; + public TextView mTime; + public TextView mCallName; + public CheckBox mCheckBox; + + /** + * 构造函数 + * @param itemView 列表项的根视图 + */ + public NoteViewHolder(View itemView) { + super(itemView); + // 查找所有子视图(只执行一次) + mAlert = (ImageView) itemView.findViewById(R.id.iv_alert_icon); + mTitle = (TextView) itemView.findViewById(R.id.tv_title); + mTime = (TextView) itemView.findViewById(R.id.tv_time); + mCallName = (TextView) itemView.findViewById(R.id.tv_name); + mCheckBox = (CheckBox) itemView.findViewById(android.R.id.checkbox); + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java index 6ff1b90..ff680a4 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -55,11 +55,14 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.Button; import android.widget.EditText; -import android.widget.ListView; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.DefaultItemAnimator; + import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; @@ -68,7 +71,7 @@ import net.micode.notes.model.WorkingNote; import net.micode.notes.tool.BackupUtils; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser; -import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.ui.NotesRecyclerViewAdapter.AppWidgetAttribute; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; @@ -96,7 +99,7 @@ import java.util.HashSet; * @see NotesListAdapter * @see GTaskSyncService */ -public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { +public class NotesListActivity extends Activity implements OnClickListener, NotesRecyclerViewAdapter.OnItemLongClickListener { // 笔记列表查询令牌 private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; @@ -133,10 +136,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private BackgroundQueryHandler mBackgroundQueryHandler; // 笔记列表适配器 - private NotesListAdapter mNotesListAdapter; + private NotesRecyclerViewAdapter mNotesListAdapter; // 笔记列表视图 - private ListView mNotesListView; + private RecyclerView mNotesRecyclerView; + + // 布局管理器 + private LinearLayoutManager mLayoutManager; + + // 布局切换控制器 + private LayoutManagerController mLayoutManagerController; // 新建笔记按钮 private Button mAddNewNote; @@ -216,7 +225,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { - mNotesListAdapter.changeCursor(null); + mNotesListAdapter.swapCursor(null); } else { super.onActivityResult(requestCode, resultCode, data); } @@ -293,13 +302,43 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mContentResolver = this.getContentResolver(); mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); mCurrentFolderId = Notes.ID_ROOT_FOLDER; - mNotesListView = (ListView) findViewById(R.id.notes_list); - mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), - null, false); - mNotesListView.setOnItemClickListener(new OnListItemClickListener()); - mNotesListView.setOnItemLongClickListener(this); - mNotesListAdapter = new NotesListAdapter(this); - mNotesListView.setAdapter(mNotesListAdapter); + mNotesRecyclerView = (RecyclerView) findViewById(R.id.notes_list); + mLayoutManager = new LinearLayoutManager(this); + mNotesRecyclerView.setLayoutManager(mLayoutManager); + + // 创建适配器 + mNotesListAdapter = new NotesRecyclerViewAdapter(this); + mNotesRecyclerView.setAdapter(mNotesListAdapter); + + // 设置动画 + DefaultItemAnimator animator = new DefaultItemAnimator(); + animator.setAddDuration(300); + animator.setRemoveDuration(300); + animator.setMoveDuration(300); + animator.setChangeDuration(300); + mNotesRecyclerView.setItemAnimator(animator); + + // 初始化布局切换控制器 + mLayoutManagerController = new LayoutManagerController(this, mNotesRecyclerView, + new LayoutManagerController.LayoutChangeListener() { + @Override + public void onLayoutChanged(LayoutType newLayoutType) { + showToast("已切换到" + newLayoutType.getDisplayName()); + } + + @Override + public void onLayoutChangeFailed(Exception e) { + showToast("布局切换失败: " + e.getMessage()); + } + }); + + // 应用保存的布局 + mLayoutManagerController.initializeLayout(); + + // 设置点击和长按监听 + mNotesListAdapter.setOnItemClickListener(new OnListItemClickListener()); + mNotesListAdapter.setOnItemLongClickListener(this); + mAddNewNote = (Button) findViewById(R.id.btn_new_note); mAddNewNote.setOnClickListener(this); mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); @@ -314,9 +353,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt /** * 多选模式回调类 * - * 实现ListView.MultiChoiceModeListener接口,处理多选模式的创建、销毁和项选中状态变化 + * 实现ActionMode.Callback接口,处理多选模式的创建、销毁和项选中状态变化 */ - private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private class ModeCallback implements ActionMode.Callback, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; private MenuItem mMoveMenu; @@ -341,7 +380,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } mActionMode = mode; mNotesListAdapter.setChoiceMode(true); - mNotesListView.setLongClickable(false); mAddNewNote.setVisibility(View.GONE); View customView = LayoutInflater.from(NotesListActivity.this).inflate( @@ -422,7 +460,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt */ public void onDestroyActionMode(ActionMode mode) { mNotesListAdapter.setChoiceMode(false); - mNotesListView.setLongClickable(true); mAddNewNote.setVisibility(View.VISIBLE); } @@ -510,15 +547,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * also change. This is very bad, just for the UI designer's strong requirement. */ if (event.getY() < (event.getX() * (-0.12) + 94)) { - View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - - mNotesListView.getFooterViewsCount()); + View view = mNotesRecyclerView.getChildAt(mNotesRecyclerView.getChildCount() - 1); if (view != null && view.getBottom() > start && (view.getTop() < (start + 94))) { mOriginY = (int) event.getY(); mDispatchY = eventY; event.setLocation(event.getX(), mDispatchY); mDispatch = true; - return mNotesListView.dispatchTouchEvent(event); + return mNotesRecyclerView.dispatchTouchEvent(event); } } break; @@ -527,7 +563,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt if (mDispatch) { mDispatchY += (int) event.getY() - mOriginY; event.setLocation(event.getX(), mDispatchY); - return mNotesListView.dispatchTouchEvent(event); + return mNotesRecyclerView.dispatchTouchEvent(event); } break; } @@ -535,7 +571,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt if (mDispatch) { event.setLocation(event.getX(), mDispatchY); mDispatch = false; - return mNotesListView.dispatchTouchEvent(event); + return mNotesRecyclerView.dispatchTouchEvent(event); } break; } @@ -594,7 +630,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt protected void onQueryComplete(int token, Object cookie, Cursor cursor) { switch (token) { case FOLDER_NOTE_LIST_QUERY_TOKEN: - mNotesListAdapter.changeCursor(cursor); + mNotesListAdapter.swapCursor(cursor); break; case FOLDER_LIST_QUERY_TOKEN: if (cursor != null && cursor.getCount() > 0) { @@ -979,9 +1015,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt @Override public void onContextMenuClosed(Menu menu) { - if (mNotesListView != null) { - mNotesListView.setOnCreateContextMenuListener(null); - } super.onContextMenuClosed(menu); } @@ -1100,6 +1133,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt case R.id.menu_search: onSearchRequested(); break; + case R.id.menu_switch_layout: + showLayoutSettingsDialog(); + break; default: break; } @@ -1168,6 +1204,28 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }.execute(); } + /** + * 显示布局设置对话框 + *+ * 打开布局设置对话框,允许用户选择布局类型、网格列数和项目间距。 + *
+ */ + private void showLayoutSettingsDialog() { + LayoutSettingsDialog dialog = new LayoutSettingsDialog(this, mLayoutManagerController); + dialog.show(); + } + + /** + * 显示Toast提示 + *+ * 显示简短的提示信息,自动消失。 + *
+ * @param message 提示信息 + */ + private void showToast(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + /** * 检查是否处于同步模式 *@@ -1202,22 +1260,21 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * *
*/ - private class OnListItemClickListener implements OnItemClickListener { + private class OnListItemClickListener implements NotesRecyclerViewAdapter.OnItemClickListener { /** * 列表项点击事件处理 * - * @param parent 父视图 * @param view 被点击的视图 * @param position 列表项位置 * @param id 列表项ID */ - public void onItemClick(AdapterView> parent, View view, int position, long id) { - if (view instanceof NotesListItem) { - NoteItemData item = ((NotesListItem) view).getItemData(); + public void onItemClick(View view, int position, long id) { + Cursor cursor = (Cursor) mNotesListAdapter.getItem(position); + if (cursor != null) { + NoteItemData item = new NoteItemData(NotesListActivity.this, cursor); if (mNotesListAdapter.isInChoiceMode()) { if (item.getType() == Notes.TYPE_NOTE) { - position = position - mNotesListView.getHeaderViewsCount(); mModeCallBack.onItemCheckedStateChanged(null, position, id, !mNotesListAdapter.isSelectedItem(position)); } @@ -1279,24 +1336,25 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt /** * 列表项长按事件处理 * - * @param parent 父视图 * @param view 被长按的视图 * @param position 列表项位置 * @param id 列表项ID * @return true表示事件已处理 */ - public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) { - if (view instanceof NotesListItem) { - mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + public boolean onItemLongClick(View view, int position, long id) { + Cursor cursor = (Cursor) mNotesListAdapter.getItem(position); + if (cursor != null) { + mFocusNoteDataItem = new NoteItemData(NotesListActivity.this, cursor); if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { - if (mNotesListView.startActionMode(mModeCallBack) != null) { + if (startActionMode(mModeCallBack) != null) { mModeCallBack.onItemCheckedStateChanged(null, position, id, true); - mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } else { Log.e(TAG, "startActionMode fails"); } } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { - mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + registerForContextMenu(view); + openContextMenu(view); } } return false; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerViewAdapter.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerViewAdapter.java new file mode 100644 index 0000000..2711272 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerViewAdapter.java @@ -0,0 +1,465 @@ +/* + * 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.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import androidx.recyclerview.widget.RecyclerView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser.NoteItemBgResources; + +import java.util.HashSet; + +/** + * 笔记RecyclerView适配器 + * + * 这个类继承自RecyclerView.Adapter,用于将数据库中的笔记数据绑定到RecyclerView中显示。 + * 它支持笔记的选择模式、批量操作以及与桌面小部件的关联。 + * 相比原NotesListAdapter,使用RecyclerView带来更好的性能和动画支持。 + * + * 主要功能: + * 1. 将笔记数据绑定到NoteViewHolder视图 + * 2. 支持多选模式和批量选择操作 + * 3. 获取选中的笔记ID和关联的桌面小部件信息 + * 4. 统计笔记数量和选中数量 + * 5. 支持局部刷新和内置动画 + * + * @see NoteViewHolder + * @see NoteItemData + */ +public class NotesRecyclerViewAdapter extends RecyclerView.Adapter+ * 为瀑布流布局的RecyclerView添加统一的间距,确保每个网格项之间有合适的间隔。 + * 支持是否包含边缘间距的配置。 + *
+ */ +public class StaggeredGridSpacingItemDecoration extends RecyclerView.ItemDecoration { + private int spanCount; + private int spacing; + private boolean includeEdge; + + /** + * 构造函数 + * @param spanCount 网格列数 + * @param spacing 间距大小(像素) + * @param includeEdge 是否包含边缘间距 + */ + public StaggeredGridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) { + this.spanCount = spanCount; + this.spacing = spacing; + this.includeEdge = includeEdge; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + StaggeredGridLayoutManager.LayoutParams params = + (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); + int spanIndex = params.getSpanIndex(); + + if (includeEdge) { + outRect.left = spacing - spanIndex * spacing / spanCount; + outRect.right = (spanIndex + 1) * spacing / spanCount; + + if (params.getViewAdapterPosition() < spanCount) { + outRect.top = spacing; + } + outRect.bottom = spacing; + } else { + outRect.left = spanIndex * spacing / spanCount; + outRect.right = spacing - (spanIndex + 1) * spacing / spanCount; + if (params.getViewAdapterPosition() >= spanCount) { + outRect.top = spacing; + } + } + } +} diff --git a/src/Notesmaster/app/src/main/res/drawable/list_divider.xml b/src/Notesmaster/app/src/main/res/drawable/list_divider.xml new file mode 100644 index 0000000..4a9645b --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/list_divider.xml @@ -0,0 +1,20 @@ + + + +