// 内部类,实现了ListView.MultiChoiceModeListener和OnMenuItemClickListener接口,用于处理ListView的多选模式相关的各种操作逻辑, // 比如创建多选模式下的菜单、处理菜单项点击、选项状态改变以及退出多选模式等操作,通过与NotesListAdapter等配合实现对笔记的批量操作功能。 private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; private MenuItem mMoveMenu; // 当多选模式创建时调用该方法,用于初始化多选模式下的菜单、设置菜单项的可见性和点击监听器等操作, // 例如根据笔记所在文件夹等情况决定“移动”菜单项是否可见,同时设置自定义的视图用于展示下拉菜单等内容。 public boolean onCreateActionMode(ActionMode mode, Menu menu) { // 填充多选模式下的菜单布局(R.menu.note_list_options)到传入的菜单对象中,使得菜单显示相应的选项。 getMenuInflater().inflate(R.menu.note_list_options, menu); // 为“删除”菜单项设置点击监听器,当点击该菜单项时,会触发此内部类中对应的点击处理逻辑(通过实现OnMenuItemClickListener接口)。 menu.findItem(R.id.delete).setOnMenuItemClickListener(this); mMoveMenu = menu.findItem(R.id.move); // 如果当前长按选中的笔记数据项所在的父文件夹是通话记录文件夹,或者用户创建的文件夹数量为0,那么隐藏“移动”菜单项, // 因为在这些情况下可能不适合或无法进行移动操作。 if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER || DataUtils.getUserFolderCount(mContentResolver) == 0) { mMoveMenu.setVisible(false); } else { mMoveMenu.setVisible(true); // 为“移动”菜单项设置点击监听器,以便后续处理点击该菜单项时的相应逻辑。 mMoveMenu.setOnMenuItemClickListener(this); } mActionMode = mode; // 通知笔记列表适配器进入多选模式,以便适配器进行相应的数据和视图状态更新,例如标记选中的项等。 mNotesListAdapter.setChoiceMode(true); // 设置ListView不再响应长按事件,因为此时已经进入了多选模式,长按相关逻辑由多选模式的操作来处理。 mNotesListView.setLongClickable(false); // 隐藏“新建笔记”按钮,在多选模式下通常不需要该按钮显示,避免操作冲突等情况。 mAddNewNote.setVisibility(View.GONE); // 通过LayoutInflater加载一个自定义的视图(R.layout.note_list_dropdown_menu)用于作为多选模式下的自定义视图, // 这个视图可能包含一些额外的交互元素,比如下拉菜单等。 View customView = LayoutInflater.from(NotesListActivity.this).inflate( R.layout.note_list_dropdown_menu, null); // 将自定义视图设置到ActionMode中,使其在界面上显示出来,替代默认的ActionMode样式。 mode.setCustomView(customView); mDropDownMenu = new DropdownMenu(NotesListActivity.this, (Button) customView.findViewById(R.id.selection_menu), R.menu.note_list_dropdown); // 为下拉菜单设置点击监听器,当点击下拉菜单中的菜单项时,会执行相应的逻辑,这里用于处理全选/反选操作以及更新菜单显示状态。 mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { // 调用笔记列表适配器的方法,实现全选或反选已选笔记的功能,根据当前的选中状态进行切换。 mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); // 更新菜单相关的显示状态,比如更新全选按钮的文本和选中状态等。 updateMenu(); return true; } }); return true; } // 更新菜单相关显示状态的私有方法,比如根据已选笔记的数量更新下拉菜单的标题,以及更新全选菜单项的选中状态和文本等, // 使得菜单显示能准确反映当前的多选情况。 private void updateMenu() { int selectedCount = mNotesListAdapter.getSelectedCount(); // 获取格式化后的字符串资源,用于设置下拉菜单的标题,显示当前选中的笔记数量等信息。 String format = getResources().getString(R.string.menu_select_title, selectedCount); mDropDownMenu.setTitle(format); MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); if (item!= null) { if (mNotesListAdapter.isAllSelected()) { item.setChecked(true); item.setTitle(R.string.menu_deselect_all); } else { item.setChecked(false); item.setTitle(R.string.menu_select_all); } } } // 在准备多选模式下的菜单时调用(每次菜单显示前可能会调用此方法来更新菜单项状态等),目前此方法未实现具体逻辑,直接返回false, // 可根据实际需求后续添加相应代码来动态调整菜单的准备工作,比如根据不同条件禁用或启用某些菜单项等。 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } // 处理多选模式下菜单项点击事件的方法,根据点击的菜单项执行相应的操作逻辑,目前此方法还未完整实现具体的业务逻辑, // 后续需根据不同菜单项的功能需求添加代码来实现诸如删除、移动所选笔记等操作。 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // TODO Auto-generated method stub return false; } // 当多选模式销毁时调用的方法,用于清理相关的状态和设置,比如通知笔记列表适配器退出多选模式,恢复ListView的长按响应功能, // 以及重新显示“新建笔记”按钮等,确保界面回到正常的交互状态。 public void onDestroyActionMode(ActionMode mode) { mNotesListAdapter.setChoiceMode(false); mNotesListView.setLongClickable(true); mAddNewNote.setVisibility(View.VISIBLE); } // 结束当前的多选模式,调用ActionMode的finish方法来关闭多选模式相关的界面和操作状态, // 通常在完成批量操作或者取消操作时调用此方法来退出多选模式。 public void finishActionMode() { mActionMode.finish(); } // 当多选模式下笔记的选中状态发生改变时调用的方法,用于通知笔记列表适配器更新对应笔记项的选中状态, // 并调用updateMenu方法来更新菜单的显示状态,以保持界面与数据状态的一致性。 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { mNotesListAdapter.setCheckedItem(position, checked); updateMenu(); } // 处理菜单项点击事件的方法,根据点击的具体菜单项执行相应的业务逻辑,比如点击“删除”菜单项时弹出确认对话框进行笔记删除操作, // 点击“移动”菜单项时启动查询目标文件夹的操作等,实现对所选笔记的各种批量操作功能。 public boolean onMenuItemClick(MenuItem item) { // 如果没有选中任何笔记项(即选中数量为0),则弹出提示Toast告知用户需要先选择笔记,然后直接返回,不执行后续逻辑。 if (mNotesListAdapter.getSelectedCount() == 0) { Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), Toast.LENGTH_SHORT).show(); return true; } switch (item.getItemId()) { case R.id.delete: // 创建一个AlertDialog.Builder用于构建确认删除的对话框,设置对话框的标题、图标、提示信息等内容。 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(getString(R.string.alert_title_delete)); builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(getString(R.string.alert_message_delete_notes, mNotesListAdapter.getSelectedCount())); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // 点击确认按钮后,调用batchDelete方法执行批量删除所选笔记的操作。 batchDelete(); } }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); break; case R.id.move: // 点击“移动”菜单项时,调用startQueryDestinationFolders方法启动查询目标文件夹的操作, // 以便后续将所选笔记移动到指定的文件夹中。 startQueryDestinationFolders(); break; default: return false; } return true; } } // 内部类,实现了OnTouchListener接口,用于处理“新建笔记”按钮的触摸事件,比如判断触摸位置是否在按钮的特定透明区域, // 以及根据触摸动作(按下、移动、抬起等)进行相应的事件分发和处理逻辑,例如将触摸事件分发给ListView等操作。 private class NewNoteOnTouchListener implements OnTouchListener { public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { // 获取设备屏幕的默认显示对象,用于获取屏幕相关的尺寸信息,比如屏幕高度等。 Display display = getWindowManager().getDefaultDisplay(); int screenHeight = display.getHeight(); int newNoteViewHeight = mAddNewNote.getHeight(); int start = screenHeight - newNoteViewHeight; int eventY = start + (int) event.getY(); /** * Minus TitleBar's height * 如果当前处于子文件夹状态,需要减去标题栏的高度,因为坐标计算等可能需要基于去除标题栏后的区域来进行, * 以更准确地判断触摸位置等情况。 */ if (mState == ListEditState.SUB_FOLDER) { eventY -= mTitleBar.getHeight(); start -= mTitleBar.getHeight(); } /** * HACKME:When click the transparent part of "New Note" button, dispatch * the event to the list view behind this button. The transparent part of * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) * and the line top of the button. The coordinate based on left of the "New * Note" button. The 94 represents maximum height of the transparent part. * Notice that, if the background of the button changes, the formula should * also change. This is very bad, just for the UI designer's strong requirement. * 以下是一段临时解决办法(HACKME)的代码逻辑,当点击“新建笔记”按钮的透明部分时,将触摸事件分发给按钮后面的ListView进行处理。 * 透明部分的范围通过一个公式(y = -0.12x + 94,单位为像素,坐标基于按钮左侧)以及按钮顶部边界来界定, * 并且提示如果按钮背景改变,该公式也需要相应更改,这种处理方式不太理想,只是为了满足UI设计师的特定要求。 */ if (event.getY() < (event.getX() * (-0.12) + 94)) { View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - mNotesListView.getFooterViewsCount()); 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); } } break; } case MotionEvent.ACTION_MOVE: { if (mDispatch) { mDispatchY += (int) event.getY() - mOriginY; event.setLocation(event.getX(), mDispatchY); return mNotesListView.dispatchTouchEvent(event); } break; } default: { if (mDispatch) { event.setLocation(event.getX(), mDispatchY); mDispatch = false; return mNotesListView.dispatchTouchEvent(event); } break; } } return false; } }; // 启动异步查询笔记列表数据的方法,根据当前所在文件夹的ID来构建合适的查询条件,然后通过异步查询处理实例(mBackgroundQueryHandler) // 发起对笔记列表数据的查询操作,确保在后台线程进行数据库查询,不阻塞主线程,提高应用的响应性能。 private void startAsyncNotesListQuery() { String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER)? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[]{ String.valueOf(mCurrentFolderId) }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } // 内部类,继承自AsyncQueryHandler,用于在后台线程处理数据库查询操作,并在查询完成后通过回调方法处理相应的结果, // 根据不同的查询令牌(token)区分是查询笔记列表还是文件夹列表等情况,然后执行对应的后续操作,比如更新适配器数据等。 private final class BackgroundQueryHandler extends AsyncQueryHandler { public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { switch (token) { case FOLDER_NOTE_LIST_QUERY_TOKEN: // 如果是查询笔记列表的操作完成,调用笔记列表适配器的方法,传入查询得到的游标(Cursor)数据, // 以便适配器更新界面上显示的笔记列表内容,反映最新的数据情况。 mNotesListAdapter.changeCursor(cursor); break; case FOLDER_LIST_QUERY_TOKEN: if (cursor!= null && cursor.getCount() > 0) { // 如果是查询文件夹列表操作完成且查询结果不为空(即有文件夹数据),则调用showFolderListMenu方法展示文件夹列表菜单, // 方便用户选择操作的目标文件夹等。 showFolderListMenu(cursor); } else { Log.e(TAG, "Query folder failed"); } break; default: return; } } } // 展示文件夹列表菜单的方法,通过AlertDialog.Builder构建一个包含文件夹列表的对话框,使用FoldersListAdapter作为适配器来展示文件夹数据, // 并为对话框的列表项点击事件设置监听器,当用户点击某个文件夹时,执行将所选笔记移动到该文件夹的操作,并弹出相应的提示Toast告知用户操作结果, // 最后关闭多选模式相关的操作界面。 private void showFolderListMenu(Cursor cursor) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(R.string.menu_title_select_folder); final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); builder.setAdapter(adapter, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); Toast.makeText( NotesListActivity.this, getString(R.string.format_move_notes_to_folder, mNotesListAdapter.getSelectedCount(), adapter.getFolderName(NotesListActivity.this, which)), Toast.LENGTH_SHORT).show(); mModeCallBack.finishActionMode(); } }); builder.show(); } // 创建新笔记的方法,构建一个启动NoteEditActivity的Intent,设置相应的动作(Intent.ACTION_INSERT_OR_EDIT)以及传递当前所在文件夹的ID作为额外参数, // 然后通过startActivityForResult方法启动该Activity,以便在新建笔记操作完成后能获取返回结果并进行相应的数据更新等处理。 private void createNewNote() { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } // 批量删除所选笔记的方法,通过AsyncTask在后台线程执行删除操作,根据是否处于同步模式(通过isSyncMode方法判断)来决定是直接删除笔记还是将笔记移动到回收站文件夹, // 在操作完成后(onPostExecute方法中),如果涉及到与小部件相关的数据更新(比如所选笔记关联了小部件),则调用updateWidget方法更新相应的小部件显示内容, // 最后关闭多选模式相关的操作界面。 private void batchDelete() { new AsyncTask>() { protected HashSet doInBackground(Void... unused) { HashSet widgets = mNotesListAdapter.getSelectedWidget(); if (!isSyncMode()) { // 如果未处于同步模式,直接调用DataUtils的方法批量删除所选笔记,这里