经过前面近半个月的学习,虽然很多控件与相关的事件都会单独使用与操作,但是至今还没有真正做个比较完成的例子,因为毕竟刚开始学习不久而且都是学习的一些控件的基础操作与数据库操作。网上很多例子比较复杂,感觉自己很多需要研究才能做,比较花费时间,后来下载了一些SDK提供的samples,发现NotePad这个例子刚好能够总结一下我们之前一些控件的使用,Activity的生命周期,相关事件的触发与控制,所以这里决定就好好分析一下这个例子。
这里贴一下部分代码,项目源码最后一起附上吧。
【更新】源码免费下载地址:android SDK 下NotePad例子详解源码
整个例子的功能大致分为:
1. 便签(Note)列表的显示;
2. 便签内容的编辑与查看,删除;
3. 便签标题的编辑;
4. 便签程序的实时文件夹(桌面快捷方式的建立)
我按照功能一个个实现,并在源码中,增加相关的注意点。
一、便签(Note)列表的显示
该功能主要是一个数据的显示,效果图如下:
左图为项目起始运行图;右图为点击menu菜单时效果图;
分析:通过效果图可以看出,起始初始界面就是显示数据的列表,然后支持菜单按钮事件的监听即可。
1. 当然要是显示数据列表,肯定是要先建立数据库以及相应的表,
这里就使用了NotePad类来存储一些常量,NotePadProvider实现ContentProvider提供对数据库与表的操作。
public class NotePadProvider extends ContentProvider { private static final String TAG = "NotePadProvider"; private static final String DATABASE_NAME = "note_pad.db"; private static final int DATABASE_VERSION = 2; private static final String NOTES_TABLE_NAME = "notes"; private static HashMap<String, String> sNotesProjectionMap; private static HashMap<String, String> sLiveFolderProjectionMap; private static final int NOTES = 1; private static final int NOTE_ID = 2; private static final int LIVE_FOLDER_NOTES = 3; private static final UriMatcher sUriMatcher; static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES); sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID); sUriMatcher.addURI(NotePad.AUTHORITY, "live_folders/notes", LIVE_FOLDER_NOTES); sNotesProjectionMap = new HashMap<String, String>(); sNotesProjectionMap.put(Notes._ID, Notes._ID); sNotesProjectionMap.put(Notes.TITLE, Notes.TITLE); sNotesProjectionMap.put(Notes.NOTE, Notes.NOTE); sNotesProjectionMap.put(Notes.CREATED_DATE, Notes.CREATED_DATE); sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE); // Support for Live Folders. sLiveFolderProjectionMap = new HashMap<String, String>(); sLiveFolderProjectionMap.put(LiveFolders._ID, Notes._ID + " AS " + LiveFolders._ID); sLiveFolderProjectionMap.put(LiveFolders.NAME, Notes.TITLE + " AS " + LiveFolders.NAME); // Add more columns here for more robust Live Folders. } /** * This class helps open, create, and upgrade the database file. */ private static class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + NOTES_TABLE_NAME + " (" + Notes._ID + " INTEGER PRIMARY KEY," + Notes.TITLE + " TEXT," + Notes.NOTE + " TEXT," + Notes.CREATED_DATE + " INTEGER," + Notes.MODIFIED_DATE + " INTEGER" + ");"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS notes"); onCreate(db); } } private DatabaseHelper mOpenHelper; @Override public boolean onCreate() { mOpenHelper = new DatabaseHelper(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(NOTES_TABLE_NAME); switch (sUriMatcher.match(uri)) { case NOTES: qb.setProjectionMap(sNotesProjectionMap); break; case NOTE_ID: qb.setProjectionMap(sNotesProjectionMap); qb.appendWhere(Notes._ID + "=" + uri.getPathSegments().get(1)); break; case LIVE_FOLDER_NOTES: qb.setProjectionMap(sLiveFolderProjectionMap); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } // If no sort order is specified use the default String orderBy; if (TextUtils.isEmpty(sortOrder)) { orderBy = NotePad.Notes.DEFAULT_SORT_ORDER; } else { orderBy = sortOrder; } // Get the database and run the query SQLiteDatabase db = mOpenHelper.getReadableDatabase(); Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy); // Tell the cursor what uri to watch, so it knows when its source data changes c.setNotificationUri(getContext().getContentResolver(), uri); return c; }
从上面部分代码就可以看出来,该类在装载时就会调用static静态块,来初始化数据库以及创建对应的表。关于其他操作这里就不再讲解,刚刚写的SQLite数据库操作的博客里这些方法都已经分析过了。
2 下面就是关键NoteList.java与noteslist_item.xml的实现
public class NotesList extends ListActivity { private static final String TAG = "NotesList"; public static final int MENU_ITEM_DELETE = Menu.FIRST; //其实就是设置为1 public static final int MENU_ITEM_INSERT = Menu.FIRST+1; private static final String[] PROJECTION = new String[]{//数据库的Notes表中需要用到的两列 Notes._ID, // 0 Notes.TITLE, // 1 }; private static final int COLUMN_INDEX_TITLE = 1; //title列的索引 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.i(TAG, "Enter in NotesList的onCreate方法"); /** * 用来设置一个Activity的默认的按键模式,注意默认键动作完全不支持中文 * 也就是指这种情况,当Activity中发生了一些按键事件,但是这些事件没有被任何控件的监听器截获时,系统应该如何处理这些按键事件。 * mode一共有五种 * DEFAULT_KEYS_DISABLE:直接丢弃,系统部处理 * DEFAULT_KEYS_DIALER:将键盘事件传入拨号器进行处理 * DEFAULT_KEYS_SHORTCUT:将键盘输入作为当前窗体上注册的快捷键,进行快捷键处理。如果当前菜单项注册了快捷键,则可以在不呼出菜单的情况下,将键盘输入作为菜单快捷键处理。 * 详细参考:DEFAULT_KEYS_SHORTCUT 功能的验证 及其 源码实现分析 http://blog.csdn.net/silenceburn/article/details/6069988 * DEFAULT_KEYS_SEARCH_LOCAL:将键盘输入作为搜索内容,进行本地搜索,如果本地没有实现自定义搜索,则使用全局搜索 * DEFAULT_KEYS_SEARCH_GLOBAL:将键盘输入作为搜索内容,进行全局搜索 */ this.setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); Intent intent = this.getIntent(); if (intent.getData() == null) { //如果为空,则使用我们自己默认的URI(URI标示一个Content provider) intent.setData(Notes.CONTENT_URI); } //Register a callback to be invoked when the context menu for this view is being built this.getListView().setOnCreateContextMenuListener(this); Cursor cursor = managedQuery(getIntent().getData(), PROJECTION, null, null, Notes.DEFAULT_SORT_ORDER);//可以理解为查询Notes表中_ID与TITLE两列数据 //Adapter是数据适配器 SimpleCursorAdapter simpleCursorAdapter = new SimpleCursorAdapter(this, R.layout.noteslist_item, cursor, new String[]{Notes.TITLE}, new int[]{android.R.id.text1}); this.setListAdapter(simpleCursorAdapter); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { Log.i(TAG, "Enter in NotesList的onListItemClick方法"); Uri uri = ContentUris.withAppendedId(getIntent().getData(), id); String action = getIntent().getAction(); if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) { // The caller is waiting for us to return a note selected by // the user. The have clicked on one, so return it now. setResult(RESULT_OK, new Intent().setData(uri)); } else { // Launch activity to view/edit the currently selected item startActivity(new Intent(Intent.ACTION_EDIT, uri)); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // TODO Auto-generated method stub super.onCreateOptionsMenu(menu); Log.i(TAG, "Enter in NotesList的onCreateOptionsMenu方法"); menu.add(0, MENU_ITEM_INSERT, 0, R.string.menu_insert) .setShortcut('3', 'a') .setIcon(android.R.drawable.ic_menu_add); Intent intent = new Intent(null, getIntent().getData()); intent.addCategory(Intent.CATEGORY_ALTERNATIVE); menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, new ComponentName(this, NotesList.class), null, intent, 0, null); return true; } @Override //控制menu,可以参考http://blog.163.com/gobby_1110/blog/static/292817152010101973515369/;http://www.2cto.com/kf/201107/95317.html public boolean onPrepareOptionsMenu(Menu menu) { // TODO Auto-generated method stub super.onPrepareOptionsMenu(menu); Log.i(TAG, "Enter in NotesList的onPrepareOptionsMenu方法"); final boolean haveItems = getListAdapter().getCount() > 0; // If there are any notes in the list (which implies that one of // them is selected), then we need to generate the actions that // can be performed on the current selection. This will be a combination // of our own specific actions along with any extensions that can be // found. if(haveItems){ // This is the selected item. Uri uri = ContentUris.withAppendedId(getIntent().getData(), getSelectedItemId()); Intent[] specifics = new Intent[1]; specifics[0] = new Intent(Intent.ACTION_EDIT, uri); MenuItem[] items = new MenuItem[1]; Intent intent = new Intent(null, uri); intent.addCategory(Intent.CATEGORY_ALTERNATIVE); menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, null, specifics, intent, 0, items); if (items[0] != null) { items[0].setShortcut('1', 'e'); } }else { menu.removeGroup(Menu.CATEGORY_ALTERNATIVE); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // TODO Auto-generated method stub Log.i(TAG, "Enter in NotesList的onOptionsItemSelected方法"); switch (item.getItemId()) { case MENU_ITEM_INSERT: /* * 该方法在之前Intent的讲解中也详细分析了,系统会按照 * String android.content.Intent.ACTION_INSERT = "android.intent.action.INSERT" * 到AndroidManifest.xml里找符合条件的activity */ startActivity(new Intent(Intent.ACTION_INSERT,getIntent().getData())); return true; } return super.onOptionsItemSelected(item); } @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { Log.i(TAG, "Enter in NotesList的onCreateContextMenu方法"); AdapterView.AdapterContextMenuInfo info; try { info = (AdapterView.AdapterContextMenuInfo) menuInfo; } catch (ClassCastException e) { Log.e(TAG, "bad menuInfo", e); return; } Cursor cursor = (Cursor) getListAdapter().getItem(info.position); if (cursor == null) { return; } menu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE)); menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_delete); } @Override public boolean onContextItemSelected(MenuItem item) { Log.i(TAG, "Enter in NotesList的onContextItemSelected方法"); AdapterView.AdapterContextMenuInfo info; try { info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); } catch (ClassCastException e) { Log.e(TAG, "bad menuInfo", e); return false; } switch (item.getItemId()) { case MENU_ITEM_DELETE: { // Delete the note that the context menu is for Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id); getContentResolver().delete(noteUri, null, null); return true; } } return false; } }
【需要注意】:
(1)、在运行程序并点击menu弹出菜单项,这个过程一共会触发以下方法:
日志截取如下:
11-11 06:44:13.574: I/NotesList(19438): Enter in NotesList的onCreate方法 11-11 06:44:35.254: I/ActivityManager(59): Displayed activity com.jercy.android.SDKNotePad/.NotesList: 70858 ms (total 70858 ms) 11-11 06:44:57.254: I/NotesList(19438): Enter in NotesList的onCreateOptionsMenu方法 11-11 06:45:32.175: I/NotesList(19438): Enter in NotesList的onPrepareOptionsMenu方法
两个方法之间的区别
onPrepareOptionsMenu(Menu menu),onCreateOptionsMenu。
前者是每次点击menu键都会重新调用,所以,如果菜单需要更新的话,就用此方法。而后者只是在activity创建的时候执行一次。 所以如果onPrepareOptionsMenu方法中调用了menu.add()方法的话,那么菜单中的项目就会越来越多,所以,一般情况下是要调用一下menu.clear()的。
3. 把该 activity注册到配置文件中
<provider android:name="NotePadProvider" android:authorities="com.google.provider.NotePad"/> <activity android:label="@string/app_name" android:name=".NotesList" > <intent-filter > <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.EDIT" /> <action android:name="android.intent.action.PICK" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.item/vnd.google.note" /> </intent-filter> </activity>
这里第一步就完成了。
二、便签内容的编辑与查看,删除
分析,其实这一步就是通过上一步onOptionsItemSelected方法中的
/* * 该方法在之前Intent的讲解中也详细分析了,系统会按照 * String android.content.Intent.ACTION_INSERT = "android.intent.action.INSERT" * 到AndroidManifest.xml里找符合条件的activity */ startActivity(new Intent(Intent.ACTION_INSERT,getIntent().getData()));
这里会找到NoteEditor.java这个activity。
然后便进入新增便签界面中。效果图:
监控显示该界面,所记录的日志:
其中主要调用了NoteEditor类中onCreate和onResume方法
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.i(TAG, "Enter in NoteEditor的onCreate方法"); final Intent intent = getIntent(); // Do some setup based on the action being performed. final String action = intent.getAction(); if (Intent.ACTION_EDIT.equals(action)) { // Requested to edit: set that state, and the data being edited. mState = STATE_EDIT; mUri = intent.getData(); } else if (Intent.ACTION_INSERT.equals(action)) { // Requested to insert: set that state, and create a new entry // in the container. mState = STATE_INSERT; Log.i(TAG, "getContentResolver().insert(intent.getData(), null),其中intent.getData().toString()为:"+intent.getData().toString()); mUri = getContentResolver().insert(intent.getData(), null); // If we were unable to create a new note, then just finish // this activity. A RESULT_CANCELED will be sent back to the // original activity if they requested a result. if (mUri == null) { Log.e(TAG, "Failed to insert new note into " + getIntent().getData()); finish(); return; } // The new entry was created, so assume all will end well and // set the result to be returned. Log.i(TAG, "返回RESULT_OK,setAction(mUri.toString(),其中mUri.toString()为:"+mUri.toString()); setResult(RESULT_OK, (new Intent()).setAction(mUri.toString())); } else { // Whoops, unknown action! Bail. Log.e(TAG, "Unknown action, exiting"); finish(); return; } // Set the layout for this activity. You can find it in res/layout/note_editor.xml setContentView(R.layout.note_editor); // The text view for our note, identified by its ID in the XML file. mText = (EditText) findViewById(R.id.note); // Get the note! mCursor = managedQuery(mUri, PROJECTION, null, null, null); // If an instance of this activity had previously stopped, we can // get the original text it started with. if (savedInstanceState != null) { mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT); } } @Override protected void onResume() { super.onResume(); Log.i(TAG, "Enter in NoteEditor的onResume方法"); // If we didn't have any trouble retrieving the data, it is now time to get at the stuff. if (mCursor != null) { // Make sure we are at the one and only row in the cursor. mCursor.moveToFirst(); // Modify our overall title depending on the mode we are running in. if (mState == STATE_EDIT) { setTitle(getText(R.string.title_edit)); } else if (mState == STATE_INSERT) { setTitle(getText(R.string.title_create)); } // This is a little tricky: we may be resumed after previously being // paused/stopped. We want to put the new text in the text view, // but leave the user where they were (retain the cursor position // etc). This version of setText does that for us. String note = mCursor.getString(COLUMN_INDEX_NOTE);//显示title的数据列的信息 mText.setTextKeepState(note); // If we hadn't previously retrieved the original text, do so // now. This allows the user to revert their changes. if (mOriginalContent == null) { mOriginalContent = note; } Log.i(TAG, "Enter in NoteEditor的onResume方法后,当前mOriginalContent的值为:"+mOriginalContent); } else { setTitle(getText(R.string.error_title)); mText.setText(getText(R.string.error_message)); } }
然后输入相应的数据后需要保存,或者需要在离开当前程序时保存当前编写的数据。
上图,我输入数据时候点击返回按钮,后台日志便会跟踪到:
调用了NoteEditor类的onPause方法:
@Override protected void onPause() { super.onPause(); //界面失去控制权时保存数据 Log.i(TAG, "Enter in NoteEditor的onPause方法"); // The user is going somewhere else, so make sure their current // changes are safely saved away in the provider. We don't need // to do this if only editing. if (mCursor != null) { String text = mText.getText().toString(); int length = text.length(); // If this activity is finished, and there is no text, then we // do something a little special: simply delete the note entry. // Note that we do this both for editing and inserting... it // would be reasonable to only do it when inserting. if (isFinishing() && (length == 0) && !mNoteOnly) { setResult(RESULT_CANCELED); deleteNote(); // Get out updates into the provider. } else { ContentValues values = new ContentValues(); // This stuff is only done when working with a full-fledged note. if (!mNoteOnly) { // Bump the modification time to now. values.put(Notes.MODIFIED_DATE, System.currentTimeMillis()); // If we are creating a new note, then we want to also create // an initial title for it. if (mState == STATE_INSERT) { String title = text.substring(0, Math.min(30, length)); if (length > 30) { int lastSpace = title.lastIndexOf(' '); if (lastSpace > 0) { title = title.substring(0, lastSpace); } } values.put(Notes.TITLE, title); } } // Write our text back into the provider. values.put(Notes.NOTE, text); // Commit all of our changes to persistent storage. When the update completes // the content provider will notify the cursor of the change, which will // cause the UI to be updated. getContentResolver().update(mUri, values, null, null); } } }
当然还有一种情况,我录入数据之后,直接点击home键回到系统主界面,这时你看一下日志会发现:先后调用了调用了NoteEditor类的onSaveInstanceState方法和onPause方法:
protected void onSaveInstanceState(Bundle outState) { // Save away the original text, so we still have it if the activity // needs to be killed while paused. //界面销毁之前保存数据 Log.i(TAG, "Enter in NoteEditor的onSaveInstanceState方法后,当前mOriginalContent的值为:"+mOriginalContent); outState.putString(ORIGINAL_CONTENT, mOriginalContent); }
【需要注意】:
运用onPause()和onSaveInstanceState保存数据 ,对这两个方法的使用进行讲解
参考:http://dev.10086.cn/cmdn/wiki/index.php?edition-view-6259-1.html
本例在测试中调用该方法的情景为:在编辑Note时,直接按home键回到首页,就先执行onSaveInstanceState方法,然后执行onPause方法。
再次打开程序时会直接进入刚刚编辑note的NoteEditor界面,因为在离开程序时,底层Activity.class会执行该方法,保持当前程序退出的状态:
final void performSaveInstanceState(Bundle outState) {
onSaveInstanceState(outState);
saveManagedDialogs(outState);
}
以上两个方法保证了,在界面退出之前保存当前记录。
如下图是数据库中表中得数据:
继续往下走,保存好之后,回到刚刚便签列表里
实现了基本的保存功能之后,我们就要加一些辅助的功能,比如:
编辑的数据项回复到之前数据状态;或者删除本条记录。
效果图,增加两个菜单按钮,分别处理上面两个操作:
对应该activity的NoteEditor类对应的创建菜单方法:
public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); Log.i(TAG, "Enter in NoteEditor的onCreateOptionsMenu方法"); // Build the menus that are shown when editing. if (mState == STATE_EDIT) { menu.add(0, REVERT_ID, 0, R.string.menu_revert) .setShortcut('0', 'r') .setIcon(android.R.drawable.ic_menu_revert); if (!mNoteOnly) { menu.add(0, DELETE_ID, 0, R.string.menu_delete) .setShortcut('1', 'd') .setIcon(android.R.drawable.ic_menu_delete); } // Build the menus that are shown when inserting. } else { menu.add(0, DISCARD_ID, 0, R.string.menu_discard) .setShortcut('0', 'd') .setIcon(android.R.drawable.ic_menu_delete); } return true; }
而对应的按钮的处理事件:
public boolean onOptionsItemSelected(MenuItem item) { Log.i(TAG, "Enter in NoteEditor的onOptionsItemSelected方法"); // Handle all of the possible menu actions. switch (item.getItemId()) { case DELETE_ID: deleteNote(); finish(); break; case DISCARD_ID: cancelNote(); break; case REVERT_ID: cancelNote(); break; } return super.onOptionsItemSelected(item); }
以上处理完之后需要把该Activity注册到配置文件中:
<activity android:name="NoteEditor" android:theme="@android:style/Theme.Light" android:label="@string/title_note" android:screenOrientation="sensor" android:configChanges="keyboardHidden|orientation"> <!-- 这个filter说明我们可以对一条note的数据进行查看或编辑 --> <intent-filter android:label="@string/resolve_edit"> <action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.EDIT" /> <action android:name="com.android.notepad.action.EDIT_NOTE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.item/vnd.google.note" /> </intent-filter> <!-- 这个filter说明我可以新增一条note --> <intent-filter> <action android:name="android.intent.action.INSERT" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" /> </intent-filter> </activity>