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 layoutAdapter = new ArrayAdapter<>( + mContext, android.R.layout.simple_spinner_item, LayoutType.values()); + mLayoutTypeSpinner.setAdapter(layoutAdapter); + + // 设置当前值 + LayoutType currentLayout = mLayoutManagerController.getCurrentLayoutType(); + mLayoutTypeSpinner.setSelection(currentLayout.ordinal()); + + int gridColumns = mLayoutManagerController.getGridSpanCount(); + mGridColumnsSeekBar.setProgress(gridColumns - 1); + mGridColumnsValue.setText(String.valueOf(gridColumns)); + + int itemSpacing = mLayoutManagerController.getItemSpacing(); + mItemSpacingSeekBar.setProgress(itemSpacing / 2); + mItemSpacingValue.setText(String.valueOf(itemSpacing)); + + // 根据布局类型启用/禁用控件 + updateControlStates(currentLayout); + } + + /** + * 设置监听器 + */ + private void setupListeners() { + mLayoutTypeSpinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + LayoutType selectedLayout = (LayoutType) parent.getItemAtPosition(position); + updateControlStates(selectedLayout); + } + + @Override + public void onNothingSelected(android.widget.AdapterView parent) { + } + }); + + mGridColumnsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int columns = progress + 1; + mGridColumnsValue.setText(String.valueOf(columns)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + + mItemSpacingSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int spacing = progress * 2; + mItemSpacingValue.setText(String.valueOf(spacing)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + } + + /** + * 更新控件状态 + * @param layoutType 布局类型 + */ + private void updateControlStates(LayoutType layoutType) { + switch (layoutType) { + case LINEAR: + mGridColumnsSeekBar.setEnabled(false); + mItemSpacingSeekBar.setEnabled(false); + break; + case GRID: + case STAGGERED: + mGridColumnsSeekBar.setEnabled(true); + mItemSpacingSeekBar.setEnabled(true); + break; + } + } + + /** + * 保存设置 + */ + private void saveSettings() { + LayoutType selectedLayout = (LayoutType) mLayoutTypeSpinner.getSelectedItem(); + int gridColumns = mGridColumnsSeekBar.getProgress() + 1; + int itemSpacing = mItemSpacingSeekBar.getProgress() * 2; + + mLayoutManagerController.setGridSpanCount(gridColumns); + mLayoutManagerController.setItemSpacing(itemSpacing); + + if (selectedLayout != mLayoutManagerController.getCurrentLayoutType()) { + mLayoutManagerController.switchLayout(selectedLayout, true); + } + + mDialog.dismiss(); + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutType.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutType.java new file mode 100644 index 0000000..a94eec4 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/LayoutType.java @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * 布局类型枚举 + *

+ * 定义笔记列表支持的所有布局类型,包括线性布局、网格布局和瀑布流布局。 + * 每种布局类型都有对应的显示名称和描述。 + *

+ */ +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 { + private static final String TAG = "NotesRecyclerViewAdapter"; + + private Context mContext; + private Cursor mCursor; + private SparseArray mSelectedIndex; + private int mNotesCount; + private boolean mChoiceMode; + private OnItemClickListener mOnItemClickListener; + private OnItemLongClickListener mOnItemLongClickListener; + + /** + * 桌面小部件属性类 + * + * 用于存储桌面小部件的ID和类型信息 + */ + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + } + + /** + * 点击监听器接口 + */ + public interface OnItemClickListener { + void onItemClick(View view, int position, long id); + } + + /** + * 长按监听器接口 + */ + public interface OnItemLongClickListener { + boolean onItemLongClick(View view, int position, long id); + } + + /** + * 构造器 + * + * 初始化笔记列表适配器,创建选中状态Map和计数器 + * + * @param context 应用上下文,不能为 null + */ + public NotesRecyclerViewAdapter(Context context) { + mSelectedIndex = new SparseArray<>(); + mContext = context; + mNotesCount = 0; + } + + /** + * 创建ViewHolder + * + * @param parent 父视图组 + * @param viewType 视图类型 + * @return 新创建的NoteViewHolder对象 + */ + @Override + public NoteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(mContext) + .inflate(R.layout.note_item, parent, false); + final NoteViewHolder holder = new NoteViewHolder(view); + + // 设置点击监听 + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mOnItemClickListener != null) { + int position = holder.getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + long id = getItemId(position); + mOnItemClickListener.onItemClick(v, position, id); + } + } + } + }); + + // 设置长按监听 + view.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + if (mOnItemLongClickListener != null) { + int position = holder.getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + long id = getItemId(position); + return mOnItemLongClickListener.onItemLongClick(v, position, id); + } + } + return false; + } + }); + + return holder; + } + + /** + * 绑定数据到ViewHolder + * + * 将数据库游标中的数据绑定到已存在的ViewHolder上 + * + * @param holder 需要绑定数据的ViewHolder + * @param position 列表项位置 + */ + @Override + public void onBindViewHolder(NoteViewHolder holder, int position) { + if (mCursor != null && mCursor.moveToPosition(position)) { + NoteItemData itemData = new NoteItemData(mContext, mCursor); + + // 绑定数据到视图 + bindViewHolder(holder, itemData, position); + } + } + + /** + * 绑定ViewHolder数据 + * + * @param holder ViewHolder对象 + * @param data 笔记数据 + * @param position 位置 + */ + private void bindViewHolder(NoteViewHolder holder, NoteItemData data, int position) { + // 处理复选框 + if (mChoiceMode && data.getType() == Notes.TYPE_NOTE) { + holder.mCheckBox.setVisibility(View.VISIBLE); + holder.mCheckBox.setChecked(isSelectedItem(position)); + } else { + holder.mCheckBox.setVisibility(View.GONE); + } + + // 处理提醒图标 + if (data.hasAlert()) { + holder.mAlert.setImageResource(R.drawable.clock); + holder.mAlert.setVisibility(View.VISIBLE); + } else { + holder.mAlert.setVisibility(View.GONE); + } + + // 处理标题 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + holder.mCallName.setVisibility(View.GONE); + holder.mAlert.setVisibility(View.VISIBLE); + holder.mTitle.setTextAppearance(mContext, R.style.TextAppearancePrimaryItem); + holder.mTitle.setText(mContext.getString(R.string.call_record_folder_name) + + mContext.getString(R.string.format_folder_files_count, data.getNotesCount())); + holder.mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + holder.mCallName.setVisibility(View.VISIBLE); + holder.mCallName.setText(data.getCallName()); + holder.mTitle.setTextAppearance(mContext, R.style.TextAppearanceSecondaryItem); + holder.mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + } else { + holder.mCallName.setVisibility(View.GONE); + holder.mTitle.setTextAppearance(mContext, R.style.TextAppearancePrimaryItem); + + if (data.getType() == Notes.TYPE_FOLDER) { + holder.mTitle.setText(data.getSnippet() + + mContext.getString(R.string.format_folder_files_count, + data.getNotesCount())); + holder.mAlert.setVisibility(View.GONE); + } else { + holder.mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + holder.mAlert.setImageResource(R.drawable.clock); + holder.mAlert.setVisibility(View.VISIBLE); + } else { + holder.mAlert.setVisibility(View.GONE); + } + } + } + + // 设置时间 + holder.mTime.setText(android.text.format.DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // 设置背景 + setBackground(holder.itemView, data); + } + + /** + * 根据笔记项的位置和类型设置合适的背景资源 + * @param view 列表项视图 + * @param data 笔记数据 + */ + private void setBackground(View view, NoteItemData data) { + int id = data.getBgColorId(); + if (data.getType() == Notes.TYPE_NOTE) { + int bgRes; + if (data.isSingle() || data.isOneFollowingFolder()) { + bgRes = NoteItemBgResources.getNoteBgSingleRes(id); + } else if (data.isLast()) { + bgRes = NoteItemBgResources.getNoteBgLastRes(id); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + bgRes = NoteItemBgResources.getNoteBgFirstRes(id); + } else { + bgRes = NoteItemBgResources.getNoteBgNormalRes(id); + } + view.setBackgroundResource(bgRes); + } else { + view.setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + /** + * 获取列表项数量 + * + * @return 列表项数量,如果游标为null则返回0 + */ + @Override + public int getItemCount() { + return mCursor != null ? mCursor.getCount() : 0; + } + + /** + * 获取指定位置的列表项ID + * + * @param position 列表项位置 + * @return 列表项ID,如果游标为null或位置无效则返回0 + */ + public long getItemId(int position) { + if (mCursor != null && mCursor.moveToPosition(position)) { + return mCursor.getLong(mCursor.getColumnIndexOrThrow(Notes.NoteColumns.ID)); + } + return 0; + } + + /** + * 获取指定位置的列表项 + * + * @param position 列表项位置 + * @return 列表项对象,如果游标为null或位置无效则返回null + */ + public Object getItem(int position) { + if (mCursor != null && mCursor.moveToPosition(position)) { + return mCursor; + } + return null; + } + + /** + * 设置指定位置的选中状态 + * + * @param position 列表项位置,从0开始 + * @param checked 是否选中 + */ + public void setCheckedItem(int position, boolean checked) { + mSelectedIndex.put(position, checked); + notifyItemChanged(position); + } + + /** + * 判断是否处于选择模式 + * + * @return 如果处于选择模式返回true,否则返回false + */ + public boolean isInChoiceMode() { + return mChoiceMode; + } + + /** + * 设置选择模式 + * + * @param mode true表示进入选择模式,false表示退出选择模式 + */ + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + notifyDataSetChanged(); + } + + /** + * 全选或取消全选所有笔记 + * + * @param checked true表示全选,false表示取消全选 + */ + public void selectAll(boolean checked) { + if (mCursor != null) { + for (int i = 0; i < getItemCount(); i++) { + if (mCursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(mCursor) == Notes.TYPE_NOTE) { + mSelectedIndex.put(i, checked); + } + } + } + notifyDataSetChanged(); + } + } + + /** + * 获取所有选中项的笔记ID集合 + * + * @return 包含所有选中笔记ID的HashSet集合,如果没有选中项则返回空集合 + */ + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet<>(); + for (int i = 0; i < mSelectedIndex.size(); i++) { + int key = mSelectedIndex.keyAt(i); + if (mSelectedIndex.get(key)) { + long id = getItemId(key); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + return itemSet; + } + + /** + * 获取所有选中项关联的桌面小部件集合 + * + * @return 包含所有选中笔记关联的桌面小部件属性的HashSet集合,如果游标无效则返回null + */ + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet<>(); + for (int i = 0; i < mSelectedIndex.size(); i++) { + int key = mSelectedIndex.keyAt(i); + if (mSelectedIndex.get(key)) { + Cursor c = (Cursor) getItem(key); + if (c != null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + /** + * 获取选中项的数量 + * + * @return 选中项的数量,如果没有选中项则返回0 + */ + public int getSelectedCount() { + int count = 0; + for (int i = 0; i < mSelectedIndex.size(); i++) { + if (mSelectedIndex.get(mSelectedIndex.keyAt(i))) { + count++; + } + } + return count; + } + + /** + * 判断是否已全选所有笔记 + * + * @return 如果所有笔记都被选中且至少有一个笔记则返回true,否则返回false + */ + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); + } + + /** + * 判断指定位置的项是否被选中 + * + * @param position 列表项位置,从0开始 + * @return 如果该项被选中返回true,否则返回false + */ + public boolean isSelectedItem(int position) { + return mSelectedIndex.get(position, false); + } + + /** + * 更换游标 + * + * @param newCursor 新的数据库游标 + */ + public Cursor swapCursor(Cursor newCursor) { + Cursor oldCursor = mCursor; + mCursor = newCursor; + calcNotesCount(); + notifyDataSetChanged(); + return oldCursor; + } + + /** + * 计算笔记数量 + */ + private void calcNotesCount() { + mNotesCount = 0; + if (mCursor != null) { + for (int i = 0; i < getItemCount(); i++) { + if (mCursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(mCursor) == Notes.TYPE_NOTE) { + mNotesCount++; + } + } + } + } + } + + /** + * 设置点击监听器 + * + * @param listener 点击监听器 + */ + public void setOnItemClickListener(OnItemClickListener listener) { + mOnItemClickListener = listener; + } + + /** + * 设置长按监听器 + * + * @param listener 长按监听器 + */ + public void setOnItemLongClickListener(OnItemLongClickListener listener) { + mOnItemLongClickListener = listener; + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/StaggeredGridSpacingItemDecoration.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/StaggeredGridSpacingItemDecoration.java new file mode 100644 index 0000000..8f73ff8 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/StaggeredGridSpacingItemDecoration.java @@ -0,0 +1,70 @@ +/* + * 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; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +/** + * 瀑布流布局间距装饰类 + *

+ * 为瀑布流布局的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 @@ + + + + + + + diff --git a/src/Notesmaster/app/src/main/res/layout/layout_settings_dialog.xml b/src/Notesmaster/app/src/main/res/layout/layout_settings_dialog.xml new file mode 100644 index 0000000..907ec21 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/layout_settings_dialog.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Notesmaster/app/src/main/res/layout/note_list.xml b/src/Notesmaster/app/src/main/res/layout/note_list.xml index 697c792..58fb839 100644 --- a/src/Notesmaster/app/src/main/res/layout/note_list.xml +++ b/src/Notesmaster/app/src/main/res/layout/note_list.xml @@ -39,15 +39,14 @@ android:textColor="#FFEAD1AE" android:textSize="@dimen/text_font_size_medium" /> - + android:overScrollMode="never" + android:scrollbars="vertical" />