Professional Documents
Culture Documents
ICSpad Codelab Steps Australia/New Zealand 2012 Step 1. Target ICS Compatibly Step 2. Check out the ActionBarCompat project Step 3. Add ActionBarCompat (aka ABC) code to ICSpad project Step 4. Enable the Action Bar in ICSpad Step 5. Add Sharing support to ICSpad for pre-ICS Step 6. Add Sharing support to ICSpad using ShareActionProvider Step 7. Create a new View Pager UI for ICSpad Step 8. Complete Edit button implementation Step 9. Complete Add action item implementation Step 10. Complete Delete action item implementation Bonus
Import the projects into Eclipse or prepare Android makefiles for the command line. We will start with the ICSpad_Start project. * = bonus optional step
is not defined. Fix this by compiling the project with Android 4.0.3.
Next, look at ActionBarHelperBase.java, it has many errors. Seems like we are missing some resources like layouts, drawables, attrs, colors, dimens, ids and styles. For layouts, lets copy the actionbar_compat.xml from ABC/res/layout to ICSpad/res/layout. Now the file itself complains @id/actionbar_compat is missing. We can fix this by copying ids.xml from ABC to ICSpad. If you compare the two projects layout structure, you will find layout-v11 is missing in ICSpad, so lets create that and copy actionbar_indeterminate_progress.xml to it. For drawables, lets copy all the file from ABCs drawable, drawable-hdpi, drawable-mdpi and drawable-xhdpi to ICSpad. For attrs, copy ABC/res/values/attrs.xml to ICSPad/res/values For colors, we already have colors.xml so just copy the missing values For dimens, we also already have dimens.xml so just copy the missing values For ids, we took care of that earlier For styles, we have to update the styles.xml in values, values-v11 and values-v13 (new) For strings, we have to update the strings.xml in values
Snippet:
Update notepad_menu.xml to add a few more action items <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/add_note" android:title="@string/menu_add" android:icon="@drawable/ic_menu_add" android:orderInCategory="0" android:showAsAction="always" /> <item android:id="@+id/menu_refresh" android:title="@string/menu_refresh" android:icon="@drawable/ic_action_refresh" android:orderInCategory="1" android:showAsAction="always" /> <item android:id="@+id/menu_search" android:title="@string/menu_search" android:icon="@drawable/ic_action_search" android:orderInCategory="2" android:showAsAction="never" /> <item android:id="@+id/menu_share" android:title="@string/menu_share" android:icon="@drawable/ic_menu_share" android:orderInCategory="3" android:showAsAction="never" /> </menu> Attach logic to the action items @Override public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { // Home icon has special ID from the framework case android.R.id.home: Toast.makeText(this, "Tapped home", Toast.LENGTH_SHORT).show(); break; case R.id.menu_refresh: Toast.makeText(this, "Fake refreshing...", Toast.LENGTH_SHORT).show(); getActionBarHelper().setRefreshActionItemState(true); getWindow().getDecorView().postDelayed( new Runnable() { @Override public void run() { getActionBarHelper().setRefreshActionItemState(false); } }, 1000); break; case R.id.menu_search: Toast.makeText(this, "Tapped search", Toast.LENGTH_SHORT).show(); break; } return super.onOptionsItemSelected(item); }
** Checkpoint: ICSpad_checkpoint_1
Snippet:
Add share logic : case R.id.menu_share: Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND); shareIntent.setType("text/plain"); String shareBody = "Body text"; shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Subject Here"); shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody); startActivity(Intent.createChooser(shareIntent, "Share via")); break;
Snippet:
Update the Share item in the note_menu.xml <item android:id="@+id/menu_share" android:title="@string/menu_share" android:icon="@drawable/ic_menu_share"
android:orderInCategory="3" android:showAsAction="ifRoom" android:actionProviderClass="android.widget.ShareActionProvider" /> Override the onCreateOptionMenu method in ActionBarHelperICS.java @Override public boolean onCreateOptionsMenu(Menu menu) { // Set file with share history to the provider and set the share intent. MenuItem actionItem = menu.findItem(R.id.menu_share); if (actionItem != null) { ShareActionProvider actionProvider = (ShareActionProvider) actionItem.getActionProvider(); // Note that you can set/change the intent any time, // say when the user has selected an image. actionProvider.setShareIntent(((ActionBarFragmentActivity) mActivity).createShareIntent()); } return super.onCreateOptionsMenu(menu); } Add a createShareIntent method in ActionBarFragmentActivity.java protected Intent createShareIntent() { Intent shareIntent = new Intent(Intent.ACTION_SEND); //TODO: Get note String shareBody = "Note Body"; shareIntent.setType("text/plain"); shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Note Title"); shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody); return shareIntent; }
** Checkpoint: ICSpad_checkpoint_2
Fix the scope of method populateFields to default Add a static method to create new NoteViewFragment instance which takes a note ID as an argument Obtain the noteId argument in the onCreate method to set the mCurrentNote string
We need a new layout for NoteViewFragment. Lets call it note_view.xml. Lets create an AsyncTask to query all the note IDs. Ok, next we need an FragmentPagerAdapter. Lets call it MyAdapter and put it within NotepadViewPagerActivity. Lets put everything together in the onCreate method. We need to add a new constant in the NotepadActivity to indicate whether a call is from the new View Pager UI.
Snippet:
Create a new launcher Activity called NotepadViewPagerActivity.java public class NotepadViewPagerActivity extends ActionBarFragmentActivity { // Query projection for loading notes private static final String[] DEFAULT_PROJECTION = new String[] { NotesProvider.KEY_ID }; // Need a view pager and an adapter private FragmentPagerAdapter mAdapter; private ViewPager mPager; // Need an "Edit" button and a "Delete" button private Button editButton; private Button deleteButton; // Need a list to keep track of all the note IDs private List<Long> noteIds = new ArrayList<Long>(); // Need to keep track of the current Note ID and position private long currentNoteId = -1; private int currentNotePosition = 0; } Update AndroidManifest.xml to use NotepadViewPagerActivity as the launcher <activity android:name=".NotepadActivity" android:label="@string/app_name"> <intent-filter> <!-- <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> --> </intent-filter> </activity> <activity android:name=".NotepadViewPagerActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> Lets push the onCreateOptionsMenu method from NotepadActivity up to ActionBarFragmentActivity @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.notepad_menu, menu); return super.onCreateOptionsMenu(menu); } Create a layout for the NotepadViewPagerActivity called fragment_pager.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:padding="4dip" android:gravity="center_horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.view.ViewPager android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="0px"
android:layout_weight="1"> </android.support.v4.view.ViewPager> <LinearLayout android:orientation="horizontal" android:gravity="center" android:measureWithLargestChild="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="0"> <Button android:id="@+id/edit_note" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/edit_note"> </Button> <Button android:id="@+id/delete_note" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/delete_note"> </Button> </LinearLayout> </LinearLayout> Add a static method to create new NoteViewFragment instance public static NoteViewFragment newInstance(long noteId) { NoteViewFragment f = new NoteViewFragment(); // Supply noteId as an argument. Bundle args = new Bundle(); args.putLong("noteId", noteId); f.setArguments(args); return f; } Obtain the noteId argument in the onCreate method to get the current note @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); long noteId = getArguments().getLong("noteId", -1); if (noteId != -1) { mCurrentNote = ContentUris.withAppendedId(NotesProvider.CONTENT_URI, noteId); } } Layout note_view.xml for NoteViewFragment.java <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/margin" android:padding="@dimen/padding" android:background="@drawable/border"> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title" android:layout_marginRight="@dimen/margin" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textAppearance="?android:attr/textAppearanceMedium" android:inputType="textCapWords" android:contentDescription="@string/title_text" /> </LinearLayout> <TextView android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="@string/body" android:textAppearance="?android:attr/textAppearanceMedium" /> <TextView android:id="@+id/body" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:scrollbars="vertical" android:inputType="textCapSentences|textMultiLine" android:textAppearance="?android:attr/textAppearanceMedium" android:gravity="top" android:contentDescription="@string/body_text" /> </LinearLayout> Create an AsyncTask to query all the note IDs. private static class MyAsyncTask extends AsyncTask<Void, Void, Void> { private List<Long> noteIds; private NotepadViewPagerActivity activity; public MyAsyncTask(NotepadViewPagerActivity activity, List<Long> noteIds) { this.noteIds = noteIds; this.activity = activity; } @Override protected Void doInBackground(Void... params) { Cursor cursor = activity.getContentResolver().query(NotesProvider.CONTENT_URI, DEFAULT_PROJECTION, null, null, null); // Update the noteIds list if (cursor.moveToFirst()) { noteIds.clear(); do { noteIds.add(cursor.getLong(0)); } while (cursor.moveToNext()); } // Close the cursor cursor.close(); return null; } @Override protected void onPostExecute(Void result) { super.onPostExecute(result); //activity.updateNoteIds(); } }
Create an adapter called MyAdapter and put it within NotepadViewPagerActivity public static class MyAdapter extends FragmentPagerAdapter { private List<Long> noteIds; public MyAdapter(FragmentManager fm, List<Long> noteIds) { super(fm); this.noteIds = noteIds; } @Override public int getCount() { return noteIds.size(); } @Override public Object instantiateItem(View container, int position) { NoteViewFragment noteViewFrag = (NoteViewFragment) super.instantiateItem(container, position); return noteViewFrag;
} @Override public Fragment getItem(int position) { return new NoteViewFragment(noteIds.get(position)); } @Override public int getItemPosition(Object object) { // This essentially clears the adapter return POSITION_NONE; } } Create the onCreate method for NotepadViewPagerActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.fragment_pager); mAdapter = new MyAdapter(getSupportFragmentManager(), noteIds); mPager = (ViewPager)findViewById(R.id.pager); mPager.setAdapter(mAdapter); // Add a new note editButton = (Button)findViewById(R.id.edit_note); editButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { Intent viewNoteIntent = new Intent(getBaseContext(), NotepadActivity.class); viewNoteIntent.setAction(NotepadActivity.ACTION_VIEW_NOTE); viewNoteIntent.putExtra(NotepadActivity.EXTRA_NOTE_ID, currentNoteId); viewNoteIntent.putExtra(NotepadActivity.EXTRA_FROM_VIEW_PAGER, true); startActivityForResult(viewNoteIntent, 0); } }); // Delete a note by removing the row in the DB deleteButton = (Button)findViewById(R.id.delete_note); deleteButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { getContentResolver().delete(ContentUris.withAppendedId(NotesProvider.CONTENT_URI, currentNoteId), null, null); new MyAsyncTask(NotepadViewPagerActivity.this, noteIds).execute(); } }); new MyAsyncTask(NotepadViewPagerActivity.this, noteIds).execute(); } Add an Extra constant to indicate whether a call to the NotepadActivity is from the View Pager UI public static final String EXTRA_FROM_VIEW_PAGER = "fromViewPager";
** Checkpoint: ICSpad_checkpoint_3
new flag private boolean mFromViewPager in the class and update the viewNote method.
Snippet
Add a SimpleOnPageChangeListener // Keep track of the current note for the Edit and Delete note buttons mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { currentNoteId = noteIds.get(position); currentNotePosition = position; } }); Update the showNote methods else clause with the following code // add the NoteEditFragment to the container FragmentTransaction ft = fm.beginTransaction(); if (edit != null) { ft.remove(edit); // Remove old edit fragment } edit = new NoteEditFragment(); if (mUseMultiplePanes) { // Add a note fragment in the note detail container ft.add(R.id.note_detail_container, edit, NOTE_EDIT_TAG); } else { // Single pane layout // Replace the list fragment with the edit fragment ft.replace(R.id.list, edit, NOTE_EDIT_TAG); // We have two ways to get here. One is from the view pager. // Another one is from the list fragment. For the latter, we // need to pop the list fragment back so added a flag to // indicate this. Bundle b = new Bundle(); if (mFromViewPager) { b.putBoolean(NoteEditFragment.ARGUMENT_POP_ON_SAVE, false); } else { b.putBoolean(NoteEditFragment.ARGUMENT_POP_ON_SAVE, true); ft.addToBackStack(null); } edit.setArguments(b); } // Commit and load the note ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); ft.commit(); edit.loadNote(noteUri); Add a new flag called mFromViewPager in NotepadActivity private boolean mIsAfterGB = Build.VERSION.SDK_INT >= 11; private boolean mFromViewPager; Update the viewNote method with the following line final long noteId = launchIntent.getLongExtra(EXTRA_NOTE_ID, -1); mFromViewPager = launchIntent.getBooleanExtra(EXTRA_FROM_VIEW_PAGER, false); showNote(ContentUris.withAppendedId(NotesProvider.CONTENT_URI, noteId)); Update the saveNote method in two places #1 Activity activity = getActivity();
// Update if (updating) { activity.getContentResolver().update(mCurrentNote, values, null, null); activity.setResult(RESULT_CODE_EDIT); // Insert } else { Uri newNote = activity.getContentResolver().insert( NotesProvider.CONTENT_URI, values); if (newNote != null) { mCurrentNote = newNote; activity.setResult(RESULT_CODE_ADD); } } #2 Bundle b = getArguments(); // Check to see whether this fragment is in a single-pane layout if ( null != b) { boolean popOnSave = b.getBoolean(ARGUMENT_POP_ON_SAVE); if (popOnSave) { getFragmentManager().popBackStack(); } else { getActivity().finish(); } } else { // multi-pane layout // Do nothing, stay in the current activity }
} // // // if
Make sure onSaveInstanceState is not called before updating fragments. A fragment transaction can only be created/committed prior to an activity saving its state. (!onSaveInstanceStateCalled) { mAdapter.notifyDataSetChanged(); if (isNewNoteAdded) { currentNotePosition = mAdapter.getCount() - 1; isNewNoteAdded = false; } mPager.setCurrentItem(currentNotePosition);
} } Added two missing flags - isNewNoteAdded and onSaveInstanceStateCalled // Flag to indicate a new note is added private boolean isNewNoteAdded; // Flag to keep track of whether onSaveInstanceState is called private boolean onSaveInstanceStateCalled = false; Update flag onSaveInstanceStateCalled in onRestart and onSaveInstanceState @Override protected void onRestart() { super.onRestart(); onSaveInstanceStateCalled = false; new MyAsyncTask(NotepadViewPagerActivity.this, noteIds).execute(); } @Override protected void onSaveInstanceState(Bundle outState) { // Keep track of whether this method is called. Will reset in onRestart. onSaveInstanceStateCalled = true; // Remember the current note position outState.putInt(CURRENT_NOTE_POSITION, currentNotePosition); super.onSaveInstanceState(outState); } Call updateNoteIds in the onPostExecute method of the AsyncTask @Override protected void onPostExecute(Void result) { super.onPostExecute(result); activity.updateNoteIds(); } Override the onMenuItemSelected method in the NotepadViewPagerActivity @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch (item.getItemId()) { case R.id.add_note: Intent viewNoteIntent = new Intent(getBaseContext(), NotepadActivity.class); viewNoteIntent.setAction(NotepadActivity.ACTION_VIEW_NOTE); viewNoteIntent.putExtra(NotepadActivity.EXTRA_FROM_VIEW_PAGER, true); startActivityForResult(viewNoteIntent, 0); return true; } return super.onMenuItemSelected(featureId, item); } Update viewNote method even more to show a blank entry if (noteId == -1) { showNote(null); // Show a blank entry for adding new note
} else { showNote(ContentUris.withAppendedId(NotesProvider.CONTENT_URI, noteId)); } Override the onActivityResult method in the NotepadViewPagerActivity to set the isNewNoteAdded flag @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 0) { if (resultCode == NoteEditFragment.RESULT_CODE_ADD) { isNewNoteAdded = true; } } }
** Checkpoint: ICSpad_checkpoint_4
Bonus
Create a multi-pane layout for landscape in GB Do the same for 3.2+ device with width larger or equal to 580dp Find a way to point to the same layout without duplication
** Checkpoint: ICSpad_complete