Refreshing ExpandableListView
Refreshing visible ExpandableListView items has some extra steps and concerns. A quick glance at ExpandableListView’s source shows that relatively few inherited methods are overridden. Instead, ExpandableListConnector does the heavy lifting for ExpandableListView’s extra functionality.
Internally, this connector translates the flat list position that the ListAdapter expects to/from group and child positions that the ExpandableListAdapter expects.
If you browse the whole file, you’ll find some TODOs and get the feeling that the Android team probably didn’t get to spend as much time as they may have wanted on this widget. Thankfully, they’ve provided us some convenience methods to translate positions in a mostly straightforward manner which gives us access to some of ListView’s functionality directly.
Do you mean position, position or position?
As the heading hints at, ExpandableListView and ListView have different terminology to differentiate which type of position is being used. Inside the ExpandableListConnector source, some helpful implementation notes were left for us defining three types of positions. Along with the published ExpandableListView docs, we get a decent picture of the four position types we’ll be using. I imagine you’re already familiar with group and child positions, but I’ve listed them for completeness anyway.
- Flat list position:
- The position used by ListView and its adapters
- Packed position:
- A long that "packs" (bit twiddling) what type of position (group, child, or null for headers/footers) as well as the position value(s) (group/child values)
- Group position:
- The position of a group among all the groups used by ExpandableListView and its adapters.
- Child position:
- The position of a child among all the children in a group used by ExpandableListView and its adapters
Note: We’re going to be dealing with 5 different kinds of positions for our color refresh example. The other type if you recall from the previous ListView color refresh example, is the ViewGroup position we used with getChildAt(int position)
.
Two methods allow us to convert flat list positions to packed positions and vice versa:
Public Methods | |
---|---|
long | getExpandableListPosition(int flatListPosition) Converts a flat list position (the raw position of an item (child or group) in the list) to a group and/or child position (represented in a packed position). This is useful in situations where the caller needs to use the underlying ListView's methods. |
int | getFlatListPosition(long packedPosition) Reverses the conversion from a packed position to a flat list position. |
Finally, five static methods allow us to pack group/child positions and unpack packed positions. These first three take a packed position and return which type of ExpandableListView (group and child positions) position it is and the position value(s). Note that for a PACKED_POSITION_TYPE_CHILD
both group and child position values are packed up.
Public Methods | |
---|---|
static int | getPackedPositionGroup(long packedPosition) Gets the group position from a packed position. |
static int | getPackedPositionChild(long packedPosition) Gets the child position from a packed position that is of `PACKED_POSITION_TYPE_CHILD` type. |
static int | getPackedPositionType(long packedPosition) Gets the type of a packed position. |
static long | getPackedPositionForChild(int groupPosition, int childPosition) Returns the packed position representation of a child's position. |
static long | getPackedPositionForGroup(int groupPosition) Returns the packed position representation of a group's position. |
Possible packed position types are:
Constants | ||
---|---|---|
int | PACKED_POSITION_TYPE_CHILD | The packed position represents a child. |
int | PACKED_POSITION_TYPE_GROUP | The packed position represents a group. |
int | PACKED_POSITION_TYPE_NULL | The packed position represents a neither/null/no preference. |
Here’s a quick code snippet to illustrate the conversion flow:
/* FlatList position -> Packed position -> ExpandableList Group/Child positions
* ^ ^ ^ ^
* integer long integers
*/
// 1. Flat list position -> Packed position
long packedPosition = getExpandableListPosition(flatListPosition);
// 2. Unpack packed position type
int positionType = getPackedPositionType(packedPosition);
// 3. Unpack position values based on positionType
// if not PACKED_POSITION_TYPE_NULL there will at least be a groupPosition
if( positionType != PACKED_POSITION_TYPE_NULL ){
groupPosition = getPackedPositionGroup(packedPosition);
if(positionType == PACKED_POSITION_TYPE_CHILD){
childPosition = getPackedPositionChild(packedPosition);
}
}else{
Log.d("FooLabel", "positionType was NULL - header/footer?");
}
The reverse is a breeze.
/* To Convert from an ExpandableList position to a packed position there's
* only two steps.
* 1. Pack up our ExpandableList position. Two methods are available to pack
* up the two kinds of ExpandableList positions; group and child.
* 2. Convert from a packed position to a flat list position
*/
long packedPosition =
getPackedPositionForGroup(groupPosition); //For a group position
getPackedPositionForChild(groupPosition, childPosition); //For a child position
// Can now use this to access ListView functionality directly
int flatListPosition = getFlatListPosition(packedPosition);
Now Can We Refresh Visible Items?
Yes! Now that the various positions values make sense, let’s implement our color refresh example.
// Declare the initial list item colors
private int groupTextColor = Color.BLACK;
private int childTextColor = Color.BLACK;
private int childBgColor = Color.WHITE;
private int groupBgColor = Color.WHITE;
/* Grabs the user color inputs and converts them proper integer color values.
* Since we're working on an ExpandableListView, we'll need four inputs for
* group and child text colors as well as group and child background colors.
*
* Definitely check out Color.parseColor(String). It's very handy!
*/
private void retrieveColors() {
if (mGroupTextColorInput.getText().length() > 0)
groupTextColor = Color.parseColor(mGroupTextColorInput.getText()
.toString()
.toLowerCase());
if (mChildTextColorInput.getText().length() > 0)
childTextColor = Color.parseColor(mChildTextColorInput.getText()
.toString()
.toLowerCase());
if (mChildBackgroundColorInput.getText().length() > 0)
childBgColor = Color.parseColor(mChildBackgroundColorInput.getText()
.toString()
.toLowerCase());
if (mGroupBackgroundColorInput.getText().length() > 0)
groupBgColor = Color.parseColor(mGroupBackgroundColorInput.getText()
.toString()
.toLowerCase());
}
/* Applies the user supplied color values to the visible list items. This
* does not touch the recycled off screen views.*/
private void applyColors(){
View listItem;
ViewHolder mHolder;
retrieveColors();
int packedPositionType, groupPosition, childPosition;
// These are both implemented in AdapterView which means
// they're flat list positions
int firstVis = mList.getFirstVisiblePosition();
int lastVis = mList.getLastVisiblePosition();
/* This is the "conversion" from flat list positions to ViewGroup child positions */
int count = lastVis - firstVis;
long packedPosition;
while (count >= 0){ // looping through visible list items
/* getChildAt(pos) is implemented in ViewGroup and has a different meaning for
* its position values. ViewGroup tracks visible items as children and is 0 indexed.
* This means you'll have 0 - X positions where X is however many items it takes
* to fill the visible area of your screen; usually less than 10. */
listItem = mList.getChildAt(count);
if (listItem != null){
mHolder = (ViewHolder) listItem.getTag();
if (mHolder == null){ // This shouldn't happen, but we'll make
// sure in case some strange concurrency
// bugs appear
mHolder = new ViewHolder();
mHolder.mText = (TextView) listItem.findViewById(android.R.id.text1);
listItem.setTag(mHolder);
}
/* Adding count, which is our current ViewGroup visible item position to
* AdapterView's firstVisiblePosition which is a flat list position gives us
* our current flat list position. From that we'll follow the flat list to
* expandable list position (group/child values) flow:
*
* FlatListPos > packed position > group/child position(s)
*/
packedPosition = mList.getExpandableListPosition(count + firstVis);
// Using our static helpers to unpack what type of position this item represents.
packedPositionType = ExpandableListView.getPackedPositionType(packedPosition);
if (packedPositionType != ExpandableListView.PACKED_POSITION_TYPE_NULL){
/* If the packedPosition type isn't NULL then we know there has to be at least
* a group position value. If it's a child item, we'll additionally have a
* child position value */
groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition);
if (packedPositionType == ExpandableListView.PACKED_POSITION_TYPE_CHILD){
childPosition = ExpandableListView.getPackedPositionChild(packedPosition);
listItem.setBackgroundColor(childBgColor); // Setting our child bgColor
mHolder.mText.setTextColor(childTextColor);// Setting our child textColor
}else{
listItem.setBackgroundColor(groupBgColor); // Setting our group bgColor
mHolder.mText.setTextColor(groupTextColor); // Setting our child textColor
}
}else
Log.d( TAG,
"Packed position type was null.");
}else
Log.d( TAG,
"getChildAt didn't retrieve a non-null view");
count--;
}
}
Applying Changes to All Items
Once again, we find that although our adapter requires a bit more work compared to its ListView siblings, the strategy is the same. I’m using one ViewHolder for both since I’m using the same layout. You could make separate Holders for group and child items if they have some big differences. It’s up to you.
@Override
public View getGroupView(int groupPosition,
boolean isExpanded,
View convertView,
ViewGroup parent){
ViewHolder mHolder;
if (convertView == null){
convertView = getLayoutInflater().inflate(
android.R.layout.simple_expandable_list_item_1,
parent,
false);
mHolder = new ViewHolder();
mHolder.mText = (TextView) convertView.findViewById(android.R.id.text1);
convertView.setTag(mHolder);
}else
mHolder = (ViewHolder) convertView.getTag();
/* Since all visible items have had their appropriate user selected
* colors applied, we need to make sure any of the recycler's
* scrapped views (which wouldn't be touched by our visible item
* changes) also reflect our changes. */
convertView.setBackgroundColor(groupBgColor);
mHolder.mText.setTextColor(groupTextColor);
mHolder.mText.setText(getGroup(groupPosition).toString().toUpperCase());
return convertView;
}
@Override
public View getChildView(int groupPosition,
int childPosition,
boolean isLastChild,
View convertView,
ViewGroup parent){
ViewHolder mHolder;
if (convertView == null){
convertView = getLayoutInflater().inflate(
android.R.layout.simple_expandable_list_item_1,
parent,
false);
mHolder = new ViewHolder();
mHolder.mText = (TextView) convertView.findViewById(android.R.id.text1);
convertView.setTag(mHolder);
}else
mHolder = (ViewHolder) convertView.getTag();
/* Since all visible items have had their appropriate user selected
* colors applied, we need to make sure any of the recycler's
* scrapped views (which wouldn't be touched by our visible item
* changes) that may get returned also reflect our changes. */
convertView.setBackgroundColor(childBgColor);
mHolder.mText.setTextColor(childTextColor);
mHolder.mText.setText(getChild(groupPosition, childPosition).toString());
return convertView;
}