About some weeks ago I got the chance to fiddle with Android (http://www.android.com) that is used in mobile service discovery research in Mobile Communication System Engineering Lab (http://ds.informatik.rwth-aachen.de/teaching/ws0910/mcse/). The assignment was to create a simple ToDo list (http://github.com/eus/android_todo). But, it turned out that programming Android's ListActivity to highlight a ToDo item was not that simple.
The complete problem is described in "Android >= 1.6: Invalid View object passed to onListItemClick()?" (http://groups.google.com/group/android-beginners/browse_thread/thread/77...) whose solution is given in "Android >= 1.6: Invalid View object passed to onListItemClick()?" (http://groups.google.com/group/android-developers/browse_thread/thread/2...).
But, I don't really get what Romain Guy means by "Views are reused by ListView" and 'Because v is always what you expect. The "problem" is what happens to v after the click.' Beside that, I also wonder what View is passed as the second argument to onListItemClick(). Again, the benefit of being Free Software, the source code of Android can be inspected to know what really is happening there without bothering Romain Guy with the details (the author doesn't have more load and the developer is well-educated resulting in more productive authors and developers). So, I inspect the code through the gitweb.
A ListActivity maintains two primary objects: a ListAdapter and a ListView. But, ListActivity has nothing to do with the details of the machinery because ListView is the one that takes care of it once ListActivity connects the ListAdapter to the ListView (http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=c...). So, we will look into ListActivity to know how the things are set up for ListView (http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=c...):
Line 243:
public void onContentChanged() {
...
mList.setOnItemClickListener(mOnClickListener);
...
}
Line 309:
private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView parent, View v, int position, long id)
{
onListItemClick((ListView)parent, v, position, id);
}
}
Line 209:
protected void onListItemClick(ListView l, View v, int position, long id) {
}
As I have said before, I am interested in knowing what v is. The comment for it in android.widget.AdapterView (http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=c...) says: The view within the AdapterView that was clicked (this will be a view provided by the adapter). Okay, since I am unable to find the real code that invokes mOnClickListener.onListItemClick(), I will just believe the comment and be sure that v is the View for the row in the ListView that I define through the resource layout. Logically the chain of code should be like:
I click on a ListView's item
|
+-> device event
|
+-> driver
|
+-> Dalvik VM
|
+-> ListView's mOnClickListener
|
+-> onItemClick()
So, okay, now I understand why Romain Guy said: 'Because v is always what you expect. The "problem" is what happens to v after the click.'
Now it's time to see what happens to v after the click by using the hint that Romain Guy gives: "this type of change must be done from the Adapter's getView() method." So, to ArrayAdapter we go (http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=c...):
Line 322:
public View getView(int position, View convertView, ViewGroup parent) {
return createViewFromResource(position, convertView, parent, mResource);
}
Line 326:
private View createViewFromResource(int position, View convertView, ViewGroup parent,
int resource) {
...
if (convertView == null) {
view = mInflater.inflate(resource, parent, false);
} else {
view = convertView;
}
...
}
The use of mInflater means the creation of a new view. So considering that Romain Guy said "Views are reused by ListView", convertView should be a reused view. To see the real code, I need to find an invocation to getView(). ListActivity should be the one that does it but the code is not in ListActivity along with its parents. So, since the machinery is in ListView like what I mentioned above, to ListView we go (http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=c...). But, it is not there, so to its parent AbsListView we go (http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=c...):
Line 1244:
View obtainView(int position) {
View scrapView;
scrapView = mRecycler.getScrapView(position);
View child;
if (scrapView != null) {
...
child = mAdapter.getView(position, scrapView, this);
...
if (child != scrapView) {
mRecycler.addScrapView(scrapView);
...
}
} else {
child = mAdapter.getView(position, null, this);
...
}
return child;
}
In turn obtainView() is called by makeAndAddView() in ListView that in turn will be called by various drawing methods in ListView (e.g., layoutChildren(), fillUp() and the like).
Aha, now I can truly understand what Romain Guy means by "Views are reused by ListView".
In short, when coding up a ListView, you should remember the position of a user selection and the corresponding row ID in the method onListItemClick() of your ListActivity class because in touch mode the selection returned by getSelectedItemPosition() and the selected row ID returned by getSelectedItemId() are invalidated upon leaving onListItemClick() (this does not happen in trackball mode though but you want to handle all types of input, don't you?). After that, you have to override the method getView() of the ArrayAdapter to set the reused view's background color properly by comparing the supplied position to getView() and the remembered position from onListItemClick().
The Real Code that Invokes mOnClickListener.onListItemClick()
The best place to know is of course the developer mailing list of Android: http://groups.google.com/group/android-developers/browse_thread/thread/1...
So, it turned out that it didn't happen in the kernel like what I illustrated above but inside the core library as described in the following stack trace when the touch mode is in use:
The interesting code to know what v actually is is in android.widget.AbsListView$PerformClick.run(AbsListView.java:1635):
private class PerformClick extends WindowRunnnable implements Runnable { View mChild; ... public void run() { ... performItemClick(mChild, mClickMotionPosition, getAdapter().getItemId( mClickMotionPosition)); ... } }And, where does mChild get set? In onTouchEvent() in the same class:
public boolean onTouchEvent(MotionEvent ev) { ... final View child = getChildAt(motionPosition - mFirstPosition); ... final AbsListView.PerformClick performClick = mPerformClick; performClick.mChild = child; performClick.mClickMotionPosition = motionPosition; ... }Yes, the getChildAt() from android.view.ViewGroup tells me the exact origin of v. That's when in touch mode.
If the navigation key is used instead, the following stack trace shows where v is supplied at:
The interesting code to know what v actually is should be in android.widget.AbsListView.onKeyUp(AbsListView.java:1742):
public boolean onKeyUp(int keyCode, KeyEvent event) { ... final View view = getChildAt(mSelectedPosition - mFirstPosition); performItemClick(view, mSelectedPosition, mSelectedRowId); ... }Once again the getChildAt() from android.view.ViewGroup tells me the exact origin of v.
To conclude, it is proven from the code that v is the View for the row in the ListView that I define through the resource layout.