diff --git a/src/ui/NotesListActivity.java b/src/ui/NotesListActivity.java index e843aec..bda637d 100644 --- a/src/ui/NotesListActivity.java +++ b/src/ui/NotesListActivity.java @@ -135,9 +135,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private final static int REQUEST_CODE_OPEN_NODE = 102; private final static int REQUEST_CODE_NEW_NODE = 103; + // 在Activity创建时调用 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // 设置Activity的布局文件为note_list.xml setContentView(R.layout.note_list); initResources(); @@ -147,32 +149,45 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt setAppInfoFromRawRes(); } + // 在Activity接收到其他Activity返回的结果时调用 @Override 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); } else { + // 否则,调用父类的方法处理结果 super.onActivityResult(requestCode, resultCode, data); } } + // 从raw资源文件中读取应用的介绍信息,并保存为一个笔记 private void setAppInfoFromRawRes() { + // 获取SharedPreferences对象,用于存储设置信息 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + // 如果没有添加过介绍信息,那么执行以下操作 if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + // 用于拼接介绍信息的文本 StringBuilder sb = new StringBuilder(); InputStream in = null; try { + // 从raw资源文件中打开介绍信息的输入流 in = getResources().openRawResource(R.raw.introduction); if (in != null) { + // 用于读取输入流 InputStreamReader isr = new InputStreamReader(in); + // 用于缓冲输入流 BufferedReader br = new BufferedReader(isr); + // 用于存储读取到的字符 char [] buf = new char[1024]; int len = 0; + // 循环读取输入流中的字符,直到结束 while ((len = br.read(buf)) > 0) { sb.append(buf, 0, len); } } else { + // 如果输入流为空,那么打印错误日志并返回 Log.e(TAG, "Read introduction file error"); return; } @@ -182,6 +197,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } finally { if(in != null) { try { + // 关闭输入流 in.close(); } catch (IOException e) { // TODO Auto-generated catch block @@ -190,10 +206,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 创建一个空的工作笔记对象,设置其所属文件夹,小部件ID,类型和颜色为默认值 WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, ResourceParser.RED); + // 设置工作笔记的文本为StringBuilder对象中的内容 note.setWorkingText(sb.toString()); + // 保存工作笔记到数据库中 if (note.saveNote()) { sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); } else { @@ -203,43 +222,71 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 在Activity的生命周期中开始时调用 @Override protected void onStart() { super.onStart(); + // 开始异步查询笔记列表 startAsyncNotesListQuery(); } + // 用来初始化一些资源 private void initResources() { + // 获取内容解析器,用来操作数据库 mContentResolver = this.getContentResolver(); + // 创建一个后台查询处理器,用来异步查询数据 mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + // 设置当前文件夹的ID为根文件夹的ID 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); + // 获取添加新笔记的按钮,用来创建新笔记 mAddNewNote = (Button) findViewById(R.id.btn_new_note); + // 给添加新笔记的按钮设置一个点击监听器,用来响应点击事件 mAddNewNote.setOnClickListener(this); + // 给添加新笔记的按钮设置一个触摸监听器,用来响应触摸事件 mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + // 初始化一个标志位,用来判断是否需要分发触摸事件 mDispatch = false; + // 用来记录分发触摸事件时的Y坐标 mDispatchY = 0; + // 用来记录触摸事件开始时的Y坐标 mOriginY = 0; + // 获取标题栏视图,用来显示标题 mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + // 设置当前的列表编辑状态为普通状态 mState = ListEditState.NOTE_LIST; + // 创建一个模式回调对象,用来处理上下文菜单的操作 mModeCallBack = new ModeCallback(); } + // 用户长按列表视图的某一项时,弹出一个上下文菜单,让用户可以对选中的笔记进行一些操作 private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; private MenuItem mMoveMenu; - + // 用户长按一个笔记时触发的,用于创建一个操作模式(ActionMode), + // 在这个模式下,用户可以对笔记进行一些操作,比如删除或移动 public boolean onCreateActionMode(ActionMode mode, Menu menu) { + //从资源文件中加载菜单项 getMenuInflater().inflate(R.menu.note_list_options, menu); + //找到删除菜单项,并设置点击监听器 menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + //找到移动菜单项,并保存为一个成员变量 mMoveMenu = menu.findItem(R.id.move); + //判断当前笔记是否属于通话记录文件夹,或者用户没有创建任何自定义文件夹, + //如果是,则隐藏移动菜单项,否则显示移动菜单项,并设置点击监听器 if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER || DataUtils.getUserFolderCount(mContentResolver) == 0) { mMoveMenu.setVisible(false); @@ -247,17 +294,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mMoveMenu.setVisible(true); mMoveMenu.setOnMenuItemClickListener(this); } + //保存操作模式为一个成员变量 mActionMode = mode; + //设置笔记列表适配器的选择模式为true,表示可以多选笔记 mNotesListAdapter.setChoiceMode(true); + //设置笔记列表视图的长按可点击属性为false,表示不再响应长按事件 mNotesListView.setLongClickable(false); + //隐藏添加新笔记的按钮 mAddNewNote.setVisibility(View.GONE); + //从布局文件中加载一个自定义视图,用于显示一个下拉菜单按钮 View customView = LayoutInflater.from(NotesListActivity.this).inflate( R.layout.note_list_dropdown_menu, null); + //将自定义视图设置为操作模式的自定义视图 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()); @@ -266,62 +322,85 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } }); + //返回true表示创建操作模式成功 return true; } + // 用于更新菜单项的状态和标题,根据当前选择的笔记的数量和是否全选 private void updateMenu() { + //获取当前选择的笔记的数量 int selectedCount = mNotesListAdapter.getSelectedCount(); // Update dropdown menu 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); } } } + // 在操作模式准备显示之前调用,用于修改菜单项的状态或可见性, + // 返回true表示修改了菜单项,返回false表示没有修改 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } + // 在用户点击操作模式中的菜单项时调用,用于处理相应的逻辑, + // 返回true表示处理了点击事件,返回false表示没有处理 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // TODO Auto-generated method stub return false; } + // 在操作模式结束时调用,用于恢复笔记列表的正常状态 public void onDestroyActionMode(ActionMode mode) { + //设置笔记列表适配器的选择模式为false,表示不再可以多选笔记 mNotesListAdapter.setChoiceMode(false); + //设置笔记列表视图的长按可点击属性为true,表示可以响应长按事件 mNotesListView.setLongClickable(true); + //显示添加新笔记的按钮 mAddNewNote.setVisibility(View.VISIBLE); } + // 用于结束操作模式,调用操作模式的finish方法 public void finishActionMode() { mActionMode.finish(); } + // 在用户改变某个笔记的选择状态时调用,用于更新笔记列表适配器和菜单项的状态 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + //设置笔记列表适配器中指定位置的笔记的选择状态 mNotesListAdapter.setCheckedItem(position, checked); + //更新菜单项的状态和标题 updateMenu(); } + // 在用户点击操作模式中的菜单项时调用,用于处理相应的逻辑 public boolean onMenuItemClick(MenuItem item) { + //如果当前没有选择任何笔记,则弹出一个提示信息,并返回true表示处理了点击事件 if (mNotesListAdapter.getSelectedCount() == 0) { Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), Toast.LENGTH_SHORT).show(); return true; } + //根据菜单项的id,执行不同的操作 switch (item.getItemId()) { case R.id.delete: + //创建一个对话框对象,用于提示用户是否确定删除所选笔记 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, @@ -330,10 +409,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { + //如果用户点击确定按钮,则调用批量删除的方法 batchDelete(); } }); builder.setNegativeButton(android.R.string.cancel, null); + //显示对话框 builder.show(); break; case R.id.move: @@ -346,19 +427,27 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 用于处理添加新笔记按钮的触摸事件 private class NewNoteOnTouchListener implements OnTouchListener { - + // 用于处理触摸事件,参数v是被触摸的视图,参数event是触摸事件对象 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; + //计算触摸事件的y坐标,即按钮起始位置加上触摸事件相对于按钮的y坐标 int eventY = start + (int) event.getY(); /** * Minus TitleBar's height */ + //如果当前是子文件夹模式,则需要减去标题栏的高度,因为标题栏会遮挡部分按钮 if (mState == ListEditState.SUB_FOLDER) { eventY -= mTitleBar.getHeight(); start -= mTitleBar.getHeight(); @@ -372,43 +461,63 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * 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. */ + // 如果触摸事件发生在屏幕的左上角 if (event.getY() < (event.getX() * (-0.12) + 94)) { + // 获取列表视图中最后一个可见的项目 View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - mNotesListView.getFooterViewsCount()); + // 如果项目不为空,并且它的底部在起始位置下方,它的顶部在起始位置加上94的上方 if (view != null && view.getBottom() > start && (view.getTop() < (start + 94))) { + // 保存触摸事件的原始y坐标 mOriginY = (int) event.getY(); + // 设置分发y坐标为当前y坐标 mDispatchY = eventY; + // 设置触摸事件的位置为分发坐标 event.setLocation(event.getX(), mDispatchY); + // 设置一个标志,表示触摸事件应该被分发给列表视图 mDispatch = true; + // 将触摸事件分发给列表视图,并返回结果 return mNotesListView.dispatchTouchEvent(event); } } break; } + // 如果触摸事件是一个移动动作 case MotionEvent.ACTION_MOVE: { + // 如果标志设置为将触摸事件分发给列表视图 if (mDispatch) { + // 更新分发y坐标,加上当前y坐标和原始y坐标的差值 mDispatchY += (int) event.getY() - mOriginY; + // 设置触摸事件的位置为分发坐标 event.setLocation(event.getX(), mDispatchY); + // 将触摸事件分发给列表视图,并返回结果 return mNotesListView.dispatchTouchEvent(event); } break; } default: { + // 如果标志设置为将触摸事件分发给列表视图 if (mDispatch) { + // 设置触摸事件的位置为分发坐标 event.setLocation(event.getX(), mDispatchY); + // 重置标志为false mDispatch = false; + // 将触摸事件分发给列表视图,并返回结果 return mNotesListView.dispatchTouchEvent(event); } break; } } + // 如果没有满足以上任何条件,返回false return false; } }; + // 用来异步查询笔记列表 private void startAsyncNotesListQuery() { + // 设置查询条件,如果当前文件夹的id是根文件夹的id,就用根文件夹的选择条件,否则用普通的选择条件 String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, @@ -417,21 +526,31 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } + // 用来处理异步查询的结果 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: + // 改变笔记列表适配器的游标,更新数据 mNotesListAdapter.changeCursor(cursor); break; + // 如果是查询文件夹列表的标识符 case FOLDER_LIST_QUERY_TOKEN: + // 如果游标不为空,并且有数据 if (cursor != null && cursor.getCount() > 0) { + // 显示文件夹列表菜单,传入游标 showFolderListMenu(cursor); } else { + // 否则,打印错误日志,表示查询文件夹 Log.e(TAG, "Query folder failed"); } break; @@ -441,13 +560,21 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 用来显示文件夹列表菜单 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) { + // 调用数据工具类的方法,批量将选中的笔记移动到点击的文件夹中, + // 传入内容解析器,选中的笔记的id和点击的文件夹的id DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); Toast.makeText( @@ -455,58 +582,85 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt getString(R.string.format_move_notes_to_folder, mNotesListAdapter.getSelectedCount(), adapter.getFolderName(NotesListActivity.this, which)), + // 显示一个短暂的提示信息,表示移动了多少个笔记到哪个文件夹 Toast.LENGTH_SHORT).show(); + // 结束操作模式,取消选择 mModeCallBack.finishActionMode(); } }); + // 显示对话框 builder.show(); } + // 用来创建一个新的笔记 private void createNewNote() { + // 创建一个意图,传入当前活动和笔记编辑活动的类对象 Intent intent = new Intent(this, NoteEditActivity.class); + // 设置意图的动作为插入或编辑 intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 将当前文件夹的id作为额外数据放入意图中 intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + // 启动一个新的活动,并期望返回结果,传入意图和请求码 this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } + // 用来批量删除笔记 private void batchDelete() { + // 创建一个异步任务,传入空参数,空进度和小部件属性的集合 new AsyncTask>() { + // 在后台线程执行的方法,传入空参数 protected HashSet doInBackground(Void... unused) { + // 获取选中的小部件的属性集合 HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 如果不是同步模式 if (!isSyncMode()) { // if not synced, delete notes directly + // 调用数据工具类的方法,直接删除选中的笔记,传入内容解析器和选中的笔记的id if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter .getSelectedItemIds())) { } else { + // 否则,打印错误日志,表示删除笔记出错,不应该发生 Log.e(TAG, "Delete notes error, should not happens"); } } else { // in sync mode, we'll move the deleted note into the trash // folder + // 如果是同步模式,调用数据工具类的方法,将选中的笔记移动到回收站文件夹中, + // 传入内容解析器,选中的笔记的id和回收站文件夹的id if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + // 如果失败,打印错误日志,表示移动笔记到回收站文件夹出错,不应该发生 Log.e(TAG, "Move notes to trash folder error, should not happens"); } } + // 返回小部件属性集合 return widgets; } + // 在主线程执行的方法,传入小部件属性集合 @Override protected void onPostExecute(HashSet widgets) { + // 如果小部件属性集合不为空 if (widgets != null) { + // 遍历每个小部件属性 for (AppWidgetAttribute widget : widgets) { + // 如果小部件的id和类型都有效 if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + // 更新小部件,传入小部件的id和类型 updateWidget(widget.widgetId, widget.widgetType); } } } + // 结束操作模式,取消选择 mModeCallBack.finishActionMode(); } - }.execute(); + }.execute(); // 执行异步任务 } + // 用于删除文件夹 private void deleteFolder(long folderId) { + // 如果文件夹id是根文件夹的id,打印错误信息并返回 if (folderId == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Wrong folder id, should not happen " + folderId); return; @@ -514,6 +668,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt HashSet ids = new HashSet(); ids.add(folderId); + // 获取文件夹中的笔记小部件 HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); if (!isSyncMode()) { @@ -525,6 +680,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } if (widgets != null) { for (AppWidgetAttribute widget : widgets) { + // 如果小部件id和类型有效,更新小部件 if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { updateWidget(widget.widgetId, widget.widgetType); @@ -533,32 +689,44 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 打开一个笔记 private void openNode(NoteItemData data) { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_VIEW); + // 传递笔记的id作为额外参数 intent.putExtra(Intent.EXTRA_UID, data.getId()); + // 启动笔记编辑活动,并期待返回结果 this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } + // 打开一个文件夹,并显示其中的笔记列表 private void openFolder(NoteItemData data) { + // 设置当前文件夹的id mCurrentFolderId = data.getId(); + // 异步查询笔记列表 startAsyncNotesListQuery(); + // 判断文件夹的类型,设置状态和视图 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + // 如果是通话记录文件夹,设置状态为CALL_RECORD_FOLDER,并隐藏新建笔记按钮 mState = ListEditState.CALL_RECORD_FOLDER; mAddNewNote.setVisibility(View.GONE); } else { + // 如果是其他文件夹,设置状态为SUB_FOLDER mState = ListEditState.SUB_FOLDER; } + // 设置标题栏的文本,如果是通话记录文件夹,显示固定的字符串,否则显示文件夹的名称 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mTitleBar.setText(R.string.call_record_folder_name); } else { mTitleBar.setText(data.getSnippet()); } + // 显示标题栏 mTitleBar.setVisibility(View.VISIBLE); } - + // 处理点击事件 public void onClick(View v) { switch (v.getId()) { + // 如果点击的是新建笔记按钮,调用createNewNote方法 case R.id.btn_new_note: createNewNote(); break; @@ -567,89 +735,134 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 显示软键盘 private void showSoftInput() { + // 获取输入法管理器对象 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { + // 强制显示软键盘 inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } } + // 隐藏软键盘 private void hideSoftInput(View view) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } + //定义一个方法,根据参数create来显示创建或修改文件夹的对话框 private void showCreateOrModifyFolderDialog(final boolean create) { + //创建一个AlertDialog.Builder对象,用于构建对话框 final AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 从布局文件中加载一个View对象,用于显示对话框的内容 View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + //从View对象中获取一个EditText对象,用于输入文件夹的名称 final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); showSoftInput(); + //判断create参数是否为false,如果是,表示要修改文件夹的名称 if (!create) { + //判断mFocusNoteDataItem是否不为空,如果是,表示有选中的文件夹 if (mFocusNoteDataItem != null) { + //设置EditText对象的文本为选中文件夹的名称 etName.setText(mFocusNoteDataItem.getSnippet()); + //设置对话框的标题为“修改文件夹名称” builder.setTitle(getString(R.string.menu_folder_change_name)); } else { + //否则,打印一条错误日志,并返回 Log.e(TAG, "The long click data item is null"); return; } } else { + //否则,表示要创建文件夹 + //设置EditText对象的文本为空 etName.setText(""); + //设置对话框的标题为“新文件夹” builder.setTitle(this.getString(R.string.menu_create_folder)); } + //设置对话框的确定按钮,点击后不做任何操作(需要在后面重写点击事件) builder.setPositiveButton(android.R.string.ok, null); + //设置对话框的取消按钮,点击后调用一个方法,隐藏软键盘,并传入EditText对象作为参数 builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { hideSoftInput(etName); } }); + // 设置对话框的视图,并显示 final Dialog dialog = builder.setView(view).show(); + // 根据ID找到正面按钮,然后将其转换为Button类型 final Button positive = (Button)dialog.findViewById(android.R.id.button1); + // 设置positive按钮的点击事件 positive.setOnClickListener(new OnClickListener() { public void onClick(View v) { + // 隐藏软键盘 hideSoftInput(etName); + // 获取输入框中的文件夹名 String name = etName.getText().toString(); + // 检查文件夹名是否已存在 if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + // 如果已存在,弹出提示信息 Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), Toast.LENGTH_LONG).show(); + // 选中输入框中的所有文本 etName.setSelection(0, etName.length()); + // 返回,不执行后续操作 return; } + // 如果不是创建模式,而是修改模式 if (!create) { + // 如果输入框中有内容 if (!TextUtils.isEmpty(name)) { + // 创建一个ContentValues对象,用于存放更新的数据 ContentValues values = new ContentValues(); + // 将文件夹名作为便签片段存入values values.put(NoteColumns.SNIPPET, name); + // 将便签类型设为文件夹类型存入values values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + // 将本地修改标志设为1存入values values.put(NoteColumns.LOCAL_MODIFIED, 1); + // 根据便签ID更新数据库中的数据 mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] { String.valueOf(mFocusNoteDataItem.getId()) }); } } else if (!TextUtils.isEmpty(name)) { + // 如果是创建模式,并且输入框中有内容 + // 创建一个ContentValues对象,用于存放插入的数据 ContentValues values = new ContentValues(); + // 将文件夹名作为便签片段存入values values.put(NoteColumns.SNIPPET, name); + // 将便签类型设为文件夹类型存入values values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + // 插入一条新的数据到数据库中 mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); } + // 关闭对话框 dialog.dismiss(); } }); + // 如果输入框中没有内容,禁用positive按钮 if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } /** * When the name edit text is null, disable the positive button */ + // 为输入框添加文本变化的监听器 etName.addTextChangedListener(new TextWatcher() { + // 在文本变化之前的回调方法 public void beforeTextChanged(CharSequence s, int start, int count, int after) { // TODO Auto-generated method stub } + // 在文本变化之前的回调方法 public void onTextChanged(CharSequence s, int start, int before, int count) { + // 如果输入框中没有内容,禁用positive按钮 if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } else { @@ -657,6 +870,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 在文本变化之后的回调方法 public void afterTextChanged(Editable s) { // TODO Auto-generated method stub @@ -664,23 +878,32 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }); } + // 当用户按下返回键时执行 @Override public void onBackPressed() { switch (mState) { case SUB_FOLDER: + //如果当前状态是子文件夹,那么返回到根文件夹 mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; + //开始异步查询笔记列表 startAsyncNotesListQuery(); + //隐藏标题栏 mTitleBar.setVisibility(View.GONE); break; case CALL_RECORD_FOLDER: + //如果当前状态是通话记录文件夹,那么返回到根文件夹 mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; + //显示添加新笔记的按钮 mAddNewNote.setVisibility(View.VISIBLE); + //隐藏标题栏 mTitleBar.setVisibility(View.GONE); + //开始异步查询笔记列表 startAsyncNotesListQuery(); break; case NOTE_LIST: + //如果当前状态是笔记列表,那么直接调用父类的方法 super.onBackPressed(); break; default: @@ -688,29 +911,42 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + // 用来更新小部件 private void updateWidget(int appWidgetId, int appWidgetType) { + //创建一个更新小部件的意图 Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (appWidgetType == Notes.TYPE_WIDGET_2X) { + //如果小部件类型是2x,那么设置意图的类为NoteWidgetProvider_2x intent.setClass(this, NoteWidgetProvider_2x.class); } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { + //如果小部件类型是4x,那么设置意图的类为NoteWidgetProvider_4x intent.setClass(this, NoteWidgetProvider_4x.class); } else { + //如果小部件类型不支持,那么打印错误日志并返回 Log.e(TAG, "Unspported widget type"); return; } + //将小部件的id放入意图中 intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { appWidgetId }); + //发送广播更新小部件 sendBroadcast(intent); + //设置结果为成功,并将意图返回给调用者 setResult(RESULT_OK, intent); } + // 定义一个私有的最终变量,用于创建上下文菜单的监听器 private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + // 重写onCreateContextMenu方法,用于在长按文件夹时弹出菜单 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + // 如果当前聚焦的笔记数据项不为空 if (mFocusNoteDataItem != null) { + // 设置菜单的标题为笔记的摘要 menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + // 添加菜单项,分别为查看文件夹、删除文件夹和修改文件夹名称,指定id和顺序 menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); @@ -718,38 +954,53 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } }; + //当上下文菜单关闭时,执行以下操作 @Override public void onContextMenuClosed(Menu menu) { + //如果笔记列表视图不为空,就取消设置上下文菜单监听器 if (mNotesListView != null) { mNotesListView.setOnCreateContextMenuListener(null); } + //调用父类的方法 super.onContextMenuClosed(menu); } + //当上下文菜单中的某一项被选择时,执行以下操作 @Override public boolean onContextItemSelected(MenuItem item) { + //如果长按的数据项为空,就打印错误日志并返回false if (mFocusNoteDataItem == null) { Log.e(TAG, "The long click data item is null"); return false; } + //根据选择的菜单项的id,执行不同的操作 switch (item.getItemId()) { + //如果选择了查看文件夹,就打开对应的文件夹 case MENU_FOLDER_VIEW: openFolder(mFocusNoteDataItem); break; + //如果选择了删除文件夹,就弹出一个对话框,询问用户是否确定删除 case MENU_FOLDER_DELETE: AlertDialog.Builder builder = new AlertDialog.Builder(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_folder)); + //设置对话框的确定按钮和点击事件 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { + //删除文件夹 deleteFolder(mFocusNoteDataItem.getId()); } }); + //设置对话框的取消按钮和点击事件 builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); + builder.show();//显示对话框 break; + //如果选择了修改文件夹名称,就显示一个创建或修改文件夹的对话框 case MENU_FOLDER_CHANGE_NAME: showCreateOrModifyFolderDialog(false); break; @@ -757,103 +1008,158 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt break; } + //返回true表示处理了上下文菜单事件 return true; } + // 重写onPrepareOptionsMenu方法,用于在准备选项菜单时根据不同的状态显示不同的菜单 @Override public boolean onPrepareOptionsMenu(Menu menu) { + // 清空菜单 menu.clear(); + // 如果当前状态是笔记列表 if (mState == ListEditState.NOTE_LIST) { + // 从note_list.xml文件中加载菜单项 getMenuInflater().inflate(R.menu.note_list, menu); // set sync or sync_cancel menu.findItem(R.id.menu_sync).setTitle( GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); } else if (mState == ListEditState.SUB_FOLDER) { + // 如果当前状态是子文件夹 + // 从sub_folder.xml文件中加载菜单项 getMenuInflater().inflate(R.menu.sub_folder, menu); } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + // 如果当前状态是通话记录文件夹 + // 从call_record_folder.xml文件中加载菜单项 getMenuInflater().inflate(R.menu.call_record_folder, menu); } else { + // 如果当前状态不属于以上任何一种 + // 打印错误日志,显示错误的状态 Log.e(TAG, "Wrong state:" + mState); } + // 返回true表示显示菜单 return true; } + // 重写onOptionsItemSelected方法,用于处理选项菜单的点击事件 @Override public boolean onOptionsItemSelected(MenuItem item) { + // 根据菜单项的id进行分支判断 switch (item.getItemId()) { + // 如果点击了新建文件夹菜单项 case R.id.menu_new_folder: { + // 显示创建或修改文件夹的对话框,传入true表示是创建模式 showCreateOrModifyFolderDialog(true); break; } + // 如果点击了导出文本菜单项 case R.id.menu_export_text: { + // 调用导出笔记到文本的方法 exportNoteToText(); break; } + // 如果点击了同步菜单项 case R.id.menu_sync: { + // 如果当前是同步模式 if (isSyncMode()) { + // 如果菜单项的标题是同步 if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + // 调用GTaskSyncService的startSync方法,开始同步 GTaskSyncService.startSync(this); } else { + // 如果菜单项的标题是取消同步 + // 调用GTaskSyncService的cancelSync方法,取消同步 GTaskSyncService.cancelSync(this); } } else { + // 如果当前不是同步模式 + // 调用启动偏好设置活动的方法 startPreferenceActivity(); } break; } case R.id.menu_setting: { + // 如果点击了设置菜单项 + // 调用启动偏好设置活动的方法 startPreferenceActivity(); break; } case R.id.menu_new_note: { + // 如果点击了新建笔记菜单项 + // 调用创建新笔记的方法 createNewNote(); break; } case R.id.menu_search: + // 如果点击了搜索菜单项 + // 调用onSearchRequested方法,启动搜索界面 onSearchRequested(); break; default: break; } + // 返回true表示处理了菜单项的点击事件 return true; } + // 用于启动搜索界面 @Override public boolean onSearchRequested() { + // 调用startSearch方法,传入null表示不指定搜索的初始值, + // false表示不显示搜索提示,null表示不传递额外的数据,false表示不使用全局搜索 startSearch(null, false, null /* appData */, false); + // 返回true表示处理了搜索请求 return true; } + // 用于到处笔记到文本文件 private void exportNoteToText() { + // 获取一个BackupUtils的实例,传入当前活动的上下文 final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + // 创建一个异步任务,用于在后台线程执行导出操作 new AsyncTask() { + // 重写doInBackground方法,用于执行导出操作,并返回一个整数表示导出的结果 @Override protected Integer doInBackground(Void... unused) { return backup.exportToText(); } + // 重写onPostExecute方法,用于在主线程处理导出的结果 @Override protected void onPostExecute(Integer result) { + // 如果导出的结果是SD卡未挂载 if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + // 创建一个对话框构造器,传入当前活动的上下文 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题为导出失败 builder.setTitle(NotesListActivity.this .getString(R.string.failed_sdcard_export)); + // 设置对话框的内容为SD卡未挂载的错误信息 builder.setMessage(NotesListActivity.this .getString(R.string.error_sdcard_unmounted)); + // 设置对话框的确定按钮,不指定点击事件 builder.setPositiveButton(android.R.string.ok, null); - builder.show(); + builder.show();// 显示对话框 } else if (result == BackupUtils.STATE_SUCCESS) { + // 如果导出的结果是成功 + // 创建一个对话框构造器,传入当前活动的上下文 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题为导出成功 builder.setTitle(NotesListActivity.this .getString(R.string.success_sdcard_export)); + // 设置对话框的内容为导出文件的名称和位置 builder.setMessage(NotesListActivity.this.getString( R.string.format_exported_file_location, backup .getExportedTextFileName(), backup.getExportedTextFileDir())); + // 设置对话框的确定按钮,不指定点击事件 builder.setPositiveButton(android.R.string.ok, null); builder.show(); } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + // 如果导出的结果是系统错误 + // 创建一个对话框构造器,传入当前活动的上下文 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题为导出失败 builder.setTitle(NotesListActivity.this .getString(R.string.failed_sdcard_export)); builder.setMessage(NotesListActivity.this @@ -863,49 +1169,79 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } - }.execute(); + }.execute();// 调用execute方法,启动异步任务 } + // 用于判断是否是同步模式 private boolean isSyncMode() { + // 调用NotesPreferenceActivity的getSyncAccountName方法,获取同步账户的名称,并去除两端的空格, + // 如果长度大于0,说明有同步账户,返回true,否则返回false return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + // 用于启动偏好设置活动 private void startPreferenceActivity() { + // 获取当前活动的父活动,如果没有父活动,则使用当前活动 Activity from = getParent() != null ? getParent() : this; + // 创建一个意图对象,指定从当前活动或父活动跳转到NotesPreferenceActivity Intent intent = new Intent(from, NotesPreferenceActivity.class); + // 传入意图对象和-1表示不指定请求码,如果需要则启动活动 from.startActivityIfNeeded(intent, -1); } + // 用于处理列表项的点击事件 private class OnListItemClickListener implements OnItemClickListener { + // 重写onItemClick方法,传入父视图,点击的视图,点击的位置和点击的id public void onItemClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { + // 获取该视图对应的笔记数据项 NoteItemData item = ((NotesListItem) view).getItemData(); + // 如果笔记列表适配器处于选择模式 if (mNotesListAdapter.isInChoiceMode()) { + // 如果笔记数据项的类型是普通笔记 if (item.getType() == Notes.TYPE_NOTE) { + // 计算点击的位置,减去列表视图的头部视图数 position = position - mNotesListView.getHeaderViewsCount(); + // 调用模式回调的onItemCheckedStateChanged方法, + // 传入null表示不指定操作模式,传入位置和id,以及取反后的选择状态, + // 用于改变列表项的选择状态 mModeCallBack.onItemCheckedStateChanged(null, position, id, !mNotesListAdapter.isSelectedItem(position)); } return; } + // 根据当前状态进行分支判断 switch (mState) { + // 如果当前状态是笔记列表 case NOTE_LIST: + // 如果笔记数据项的类型是文件夹或系统文件夹 if (item.getType() == Notes.TYPE_FOLDER || item.getType() == Notes.TYPE_SYSTEM) { + // 调用打开文件夹的方法,传入笔记数据项 openFolder(item); } else if (item.getType() == Notes.TYPE_NOTE) { + // 如果笔记数据项的类型是普通笔记 + // 调用打开笔记的方法,传入笔记数据项 openNode(item); } else { + // 如果笔记数据项的类型不属于以上任何一种 + // 打印错误日志,显示错误的笔记类型 Log.e(TAG, "Wrong note type in NOTE_LIST"); } break; + // 如果当前状态是子文件夹 case SUB_FOLDER: + // 或者当前状态是通话记录文件夹 case CALL_RECORD_FOLDER: if (item.getType() == Notes.TYPE_NOTE) { + // 如果笔记数据项的类型是普通笔记 + // 调用打开笔记的方法,传入笔记数据项 openNode(item); } else { + // 如果笔记数据项的类型不是普通笔记 + // 打印错误日志,显示错误的笔记类型 Log.e(TAG, "Wrong note type in SUB_FOLDER"); } break; @@ -917,11 +1253,18 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } + // 用于启动查询目标文件夹的操作 private void startQueryDestinationFolders() { + // 用于存储查询条件,表示笔记的类型是文件夹,且父id不是回收站文件夹,且id不是当前文件夹 String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + + // 如果当前状态是笔记列表,不改变查询条件, + // 否则在查询条件的基础上增加一个或条件,表示id等于根文件夹 selection = (mState == ListEditState.NOTE_LIST) ? selection: "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + // 调用后台查询处理器的startQuery方法,传入文件夹列表查询标记, + // null表示不指定cookie对象,笔记内容的uri,文件夹列表适配器的投影数组,查询条件和参数数组,以及按修改日期降序排序 mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, @@ -935,20 +1278,33 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt NoteColumns.MODIFIED_DATE + " DESC"); } + // 用于处理列表项的长按事件,传入父视图,长按的视图,长按的位置和长按的id public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + // 如果长按的视图是一个NotesListItem if (view instanceof NotesListItem) { + // 获取该视图对应的笔记数据项,并赋值给mFocusNoteDataItem变量 mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + // 如果笔记数据项的类型是普通笔记,并且笔记列表适配器不处于选择模式 if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + // 如果调用列表视图的startActionMode方法,并传入模式回调对象,返回不为空 if (mNotesListView.startActionMode(mModeCallBack) != null) { + // 传入null表示不指定操作模式,传入位置和id,以及true表示选中该列表项 mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + // 传入长按常量,表示触发触觉反馈 mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } else { + // 如果调用列表视图的startActionMode方法失败 + // 打印错误日志,显示启动操作模式失败 Log.e(TAG, "startActionMode fails"); } } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 如果笔记数据项的类型是文件夹 + // 调用列表视图的setOnCreateContextMenuListener方法, + // 并传入文件夹创建上下文菜单监听器对象,表示设置该监听器为创建上下文菜单的监听器 mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); } } + // 返回false表示不消费长按事件 return false; } }