package org.holoeverywhere.widget;
import java.util.ArrayList;
import com.actionbarsherlock.R;
import org.holoeverywhere.widget.ExpandableListConnector.PositionMetadata;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ExpandableListAdapter;
import android.widget.ListAdapter;
@SuppressLint("NewApi")
public class ExpandableListView extends ListView {
public static class ExpandableListContextMenuInfo implements ContextMenu.ContextMenuInfo {
public long id;
public long packedPosition;
public View targetView;
public ExpandableListContextMenuInfo(View targetView, long packedPosition, long id) {
this.targetView = targetView;
this.packedPosition = packedPosition;
this.id = id;
}
}
public interface OnChildClickListener {
boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
int childPosition, long id);
}
public interface OnGroupClickListener {
boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
long id);
}
public interface OnGroupCollapseListener {
void onGroupCollapse(int groupPosition);
}
public interface OnGroupExpandListener {
void onGroupExpand(int groupPosition);
}
static class SavedState extends BaseSavedState {
public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList;
private SavedState(Parcel in) {
super(in.readParcelable(ListView.SavedState.class.getClassLoader()));
expandedGroupMetadataList = new ArrayList<ExpandableListConnector.GroupMetadata>();
in.readList(expandedGroupMetadataList, ExpandableListConnector.class.getClassLoader());
}
SavedState(
Parcelable superState,
ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList) {
super(superState);
this.expandedGroupMetadataList = expandedGroupMetadataList;
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeList(expandedGroupMetadataList);
}
}
public static final int CHILD_INDICATOR_INHERIT = -1;
private static final int[] CHILD_LAST_STATE_SET =
{
android.R.attr.state_last
};
private static final int[] EMPTY_STATE_SET = {};
private static final int[] GROUP_EMPTY_STATE_SET =
{
android.R.attr.state_empty
};
private static final int[] GROUP_EXPANDED_EMPTY_STATE_SET =
{
android.R.attr.state_expanded, android.R.attr.state_empty
};
private static final int[] GROUP_EXPANDED_STATE_SET =
{
android.R.attr.state_expanded
};
private static final int[][] GROUP_STATE_SETS = {
EMPTY_STATE_SET,
GROUP_EXPANDED_STATE_SET,
GROUP_EMPTY_STATE_SET,
GROUP_EXPANDED_EMPTY_STATE_SET
};
private static final long PACKED_POSITION_INT_MASK_CHILD = 0xFFFFFFFF;
private static final long PACKED_POSITION_INT_MASK_GROUP = 0x7FFFFFFF;
private static final long PACKED_POSITION_MASK_CHILD = 0x00000000FFFFFFFFL;
private static final long PACKED_POSITION_MASK_GROUP = 0x7FFFFFFF00000000L;
private static final long PACKED_POSITION_MASK_TYPE = 0x8000000000000000L;
private static final long PACKED_POSITION_SHIFT_GROUP = 32;
private static final long PACKED_POSITION_SHIFT_TYPE = 63;
public static final int PACKED_POSITION_TYPE_CHILD = 1;
public static final int PACKED_POSITION_TYPE_GROUP = 0;
public static final int PACKED_POSITION_TYPE_NULL = 2;
public static final long PACKED_POSITION_VALUE_NULL = 0x00000000FFFFFFFFL;
public static int getPackedPositionChild(long packedPosition) {
if (packedPosition == PACKED_POSITION_VALUE_NULL) {
return -1;
}
if ((packedPosition & PACKED_POSITION_MASK_TYPE) != PACKED_POSITION_MASK_TYPE) {
return -1;
}
return (int) (packedPosition & PACKED_POSITION_MASK_CHILD);
}
public static long getPackedPositionForChild(int groupPosition, int childPosition) {
return (long) PACKED_POSITION_TYPE_CHILD << PACKED_POSITION_SHIFT_TYPE
| (groupPosition & PACKED_POSITION_INT_MASK_GROUP)
<< PACKED_POSITION_SHIFT_GROUP
| childPosition & PACKED_POSITION_INT_MASK_CHILD;
}
public static long getPackedPositionForGroup(int groupPosition) {
return (groupPosition & PACKED_POSITION_INT_MASK_GROUP)
<< PACKED_POSITION_SHIFT_GROUP;
}
public static int getPackedPositionGroup(long packedPosition) {
if (packedPosition == PACKED_POSITION_VALUE_NULL) {
return -1;
}
return (int) ((packedPosition & PACKED_POSITION_MASK_GROUP) >> PACKED_POSITION_SHIFT_GROUP);
}
public static int getPackedPositionType(long packedPosition) {
if (packedPosition == PACKED_POSITION_VALUE_NULL) {
return PACKED_POSITION_TYPE_NULL;
}
return (packedPosition & PACKED_POSITION_MASK_TYPE) == PACKED_POSITION_MASK_TYPE
? PACKED_POSITION_TYPE_CHILD
: PACKED_POSITION_TYPE_GROUP;
}
private ExpandableListAdapter mAdapter;
private Drawable mChildDivider;
private Drawable mChildIndicator;
private int mChildIndicatorLeft;
private int mChildIndicatorRight;
private boolean mClipToPadding = false;
private ExpandableListConnector mConnector;
private Drawable mGroupIndicator;
private int mIndicatorLeft;
private final Rect mIndicatorRect = new Rect();
private int mIndicatorRight;
private OnChildClickListener mOnChildClickListener;
private OnGroupClickListener mOnGroupClickListener;
private OnGroupCollapseListener mOnGroupCollapseListener;
private OnGroupExpandListener mOnGroupExpandListener;
public ExpandableListView(Context context) {
this(context, null);
}
public ExpandableListView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.expandableListViewStyle);
}
public ExpandableListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExpandableListView,
defStyle, R.style.Holo_ExpandableListView);
mGroupIndicator = a
.getDrawable(R.styleable.ExpandableListView_android_groupIndicator);
mChildIndicator = a
.getDrawable(R.styleable.ExpandableListView_android_childIndicator);
mIndicatorLeft = a
.getDimensionPixelSize(
R.styleable.ExpandableListView_android_indicatorLeft, 0);
mIndicatorRight = a
.getDimensionPixelSize(
R.styleable.ExpandableListView_android_indicatorRight, 0);
if (mIndicatorRight == 0 && mGroupIndicator != null) {
mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
}
mChildIndicatorLeft = a.getDimensionPixelSize(
R.styleable.ExpandableListView_android_childIndicatorLeft,
CHILD_INDICATOR_INHERIT);
mChildIndicatorRight = a.getDimensionPixelSize(
R.styleable.ExpandableListView_android_childIndicatorRight,
CHILD_INDICATOR_INHERIT);
mChildDivider = a
.getDrawable(R.styleable.ExpandableListView_android_childDivider);
a.recycle();
}
public boolean collapseGroup(int groupPos) {
boolean retValue = mConnector.collapseGroup(groupPos);
if (mOnGroupCollapseListener != null) {
mOnGroupCollapseListener.onGroupCollapse(groupPos);
}
return retValue;
}
@Override
protected ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) {
if (isHeaderOrFooterPosition(flatListPosition)) {
return super.createContextMenuInfo(view, flatListPosition, id);
}
final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
ExpandableListPosition pos = pm.position;
id = getChildOrGroupId(pos);
long packedPosition = pos.getPackedPosition();
pm.recycle();
return new ExpandableListContextMenuInfo(view, packedPosition, id);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mChildIndicator == null && mGroupIndicator == null) {
return;
}
int saveCount = 0;
final boolean clipToPadding = mClipToPadding;
if (clipToPadding) {
saveCount = canvas.save();
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.clipRect(scrollX + getPaddingLeft(), scrollY + getPaddingTop(),
scrollX + getRight() - getLeft() - getPaddingRight(),
scrollY + getBottom() - getTop() - getPaddingBottom());
}
final int headerViewsCount = getHeaderViewsCount();
final int lastChildFlPos = getCount() - getFooterViewsCount() - headerViewsCount - 1;
final int myB = getBottom();
PositionMetadata pos;
View item;
Drawable indicator;
int t, b;
int lastItemType = ~(ExpandableListPosition.CHILD | ExpandableListPosition.GROUP);
final Rect indicatorRect = mIndicatorRect;
final int childCount = getChildCount();
for (int i = 0, childFlPos = getFirstVisiblePosition() - headerViewsCount; i < childCount; i++, childFlPos++) {
if (childFlPos < 0) {
continue;
} else if (childFlPos > lastChildFlPos) {
break;
}
item = getChildAt(i);
t = item.getTop();
b = item.getBottom();
if (b < 0 || t > myB) {
continue;
}
pos = mConnector.getUnflattenedPos(childFlPos);
if (pos.position.type != lastItemType) {
if (pos.position.type == ExpandableListPosition.CHILD) {
indicatorRect.left = mChildIndicatorLeft == CHILD_INDICATOR_INHERIT ?
mIndicatorLeft : mChildIndicatorLeft;
indicatorRect.right = mChildIndicatorRight == CHILD_INDICATOR_INHERIT ?
mIndicatorRight : mChildIndicatorRight;
} else {
indicatorRect.left = mIndicatorLeft;
indicatorRect.right = mIndicatorRight;
}
indicatorRect.left += getPaddingLeft();
indicatorRect.right += getPaddingLeft();
lastItemType = pos.position.type;
}
if (indicatorRect.left != indicatorRect.right) {
if (isStackFromBottom()) {
indicatorRect.top = t;
indicatorRect.bottom = b;
} else {
indicatorRect.top = t;
indicatorRect.bottom = b;
}
indicator = getIndicator(pos);
if (indicator != null) {
indicator.setBounds(indicatorRect);
indicator.draw(canvas);
}
}
pos.recycle();
}
if (clipToPadding) {
canvas.restoreToCount(saveCount);
}
}
@Override
void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
int flatListPosition = childIndex + getFirstVisiblePosition();
if (flatListPosition >= 0) {
final int adjustedPosition =
getFlatPositionForConnector(flatListPosition);
PositionMetadata pos = mConnector.getUnflattenedPos(adjustedPosition);
if (pos.position.type ==
ExpandableListPosition.CHILD || pos.isExpanded() &&
pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos) {
Drawable divider = mChildDivider;
divider.setBounds(bounds);
divider.draw(canvas);
pos.recycle();
return;
}
pos.recycle();
}
super.drawDivider(canvas, bounds, flatListPosition);
}
public boolean expandGroup(int groupPos) {
return expandGroup(groupPos, false);
}
public boolean expandGroup(int groupPos, boolean animate) {
ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
ExpandableListPosition.GROUP, groupPos, -1, -1);
PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
elGroupPos.recycle();
boolean retValue = mConnector.expandGroup(pm);
if (mOnGroupExpandListener != null) {
mOnGroupExpandListener.onGroupExpand(groupPos);
}
// TODO Make it works on Eclair
if (animate && VERSION.SDK_INT >= VERSION_CODES.FROYO) {
final int groupFlatPos = pm.position.flatListPos;
final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
shiftedGroupPosition);
}
pm.recycle();
return retValue;
}
private int getAbsoluteFlatPosition(int flatListPosition) {
return flatListPosition + getHeaderViewsCount();
}
private long getChildOrGroupId(ExpandableListPosition position) {
if (position.type == ExpandableListPosition.CHILD) {
return mAdapter.getChildId(position.groupPos, position.childPos);
} else {
return mAdapter.getGroupId(position.groupPos);
}
}
public ExpandableListAdapter getExpandableListAdapter() {
return mAdapter;
}
public long getExpandableListPosition(int flatListPosition) {
if (isHeaderOrFooterPosition(flatListPosition)) {
return PACKED_POSITION_VALUE_NULL;
}
final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
long packedPos = pm.position.getPackedPosition();
pm.recycle();
return packedPos;
}
public int getFlatListPosition(long packedPosition) {
ExpandableListPosition elPackedPos = ExpandableListPosition
.obtainPosition(packedPosition);
PositionMetadata pm = mConnector.getFlattenedPos(elPackedPos);
elPackedPos.recycle();
final int flatListPosition = pm.position.flatListPos;
pm.recycle();
return getAbsoluteFlatPosition(flatListPosition);
}
private int getFlatPositionForConnector(int flatListPosition) {
return flatListPosition - getHeaderViewsCount();
}
private Drawable getIndicator(PositionMetadata pos) {
Drawable indicator;
if (pos.position.type == ExpandableListPosition.GROUP) {
indicator = mGroupIndicator;
if (indicator != null && indicator.isStateful()) {
boolean isEmpty = pos.groupMetadata == null ||
pos.groupMetadata.lastChildFlPos == pos.groupMetadata.flPos;
final int stateSetIndex =
(pos.isExpanded() ? 1 : 0) | // Expanded?
(isEmpty ? 2 : 0); // Empty?
indicator.setState(GROUP_STATE_SETS[stateSetIndex]);
}
} else {
indicator = mChildIndicator;
if (indicator != null && indicator.isStateful()) {
final int stateSet[] = pos.position.flatListPos == pos.groupMetadata.lastChildFlPos
? CHILD_LAST_STATE_SET
: EMPTY_STATE_SET;
indicator.setState(stateSet);
}
}
return indicator;
}
public long getSelectedId() {
long packedPos = getSelectedPosition();
if (packedPos == PACKED_POSITION_VALUE_NULL) {
return -1;
}
int groupPos = getPackedPositionGroup(packedPos);
if (getPackedPositionType(packedPos) == PACKED_POSITION_TYPE_GROUP) {
return mAdapter.getGroupId(groupPos);
} else {
return mAdapter.getChildId(groupPos, getPackedPositionChild(packedPos));
}
}
public long getSelectedPosition() {
final int selectedPos = getSelectedItemPosition();
return getExpandableListPosition(selectedPos);
}
boolean handleItemClick(View v, int position, long id) {
final PositionMetadata posMetadata = mConnector.getUnflattenedPos(position);
id = getChildOrGroupId(posMetadata.position);
boolean returnValue;
if (posMetadata.position.type == ExpandableListPosition.GROUP) {
if (mOnGroupClickListener != null) {
if (mOnGroupClickListener.onGroupClick(this, v,
posMetadata.position.groupPos, id)) {
posMetadata.recycle();
return true;
}
}
if (posMetadata.isExpanded()) {
mConnector.collapseGroup(posMetadata);
playSoundEffect(SoundEffectConstants.CLICK);
if (mOnGroupCollapseListener != null) {
mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos);
}
} else {
mConnector.expandGroup(posMetadata);
playSoundEffect(SoundEffectConstants.CLICK);
if (mOnGroupExpandListener != null) {
mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos);
}
// TODO Make it works on Eclair
if (VERSION.SDK_INT >= VERSION_CODES.FROYO) {
final int groupPos = posMetadata.position.groupPos;
final int groupFlatPos = posMetadata.position.flatListPos;
final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
smoothScrollToPosition(
shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
shiftedGroupPosition);
}
}
returnValue = true;
} else {
if (mOnChildClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos,
posMetadata.position.childPos, id);
}
returnValue = false;
}
posMetadata.recycle();
return returnValue;
}
public boolean isGroupExpanded(int groupPosition) {
return mConnector.isGroupExpanded(groupPosition);
}
private boolean isHeaderOrFooterPosition(int position) {
final int footerViewsStart = getCount() - getFooterViewsCount();
return position < getHeaderViewsCount() || position >= footerViewsStart;
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setClassName(ExpandableListView.class.getName());
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(ExpandableListView.class.getName());
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (mConnector != null && ss.expandedGroupMetadataList != null) {
mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList);
}
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
return new SavedState(superState,
mConnector != null ? mConnector.getExpandedGroupMetadataList() : null);
}
@Override
public boolean performItemClick(View v, int position, long id) {
if (isHeaderOrFooterPosition(position)) {
return super.performItemClick(v, position, id);
}
final int adjustedPosition = getFlatPositionForConnector(position);
return handleItemClick(v, adjustedPosition, id);
}
public void setAdapter(ExpandableListAdapter adapter) {
mAdapter = adapter;
if (adapter != null) {
mConnector = new ExpandableListConnector(adapter);
} else {
mConnector = null;
}
super.setAdapter(mConnector);
}
@Override
public void setAdapter(ListAdapter adapter) {
throw new RuntimeException(
"For ExpandableListView, use setAdapter(ExpandableListAdapter) instead of " +
"setAdapter(ListAdapter)");
}
public void setChildDivider(Drawable childDivider) {
mChildDivider = childDivider;
}
public void setChildIndicator(Drawable childIndicator) {
mChildIndicator = childIndicator;
}
public void setChildIndicatorBounds(int left, int right) {
mChildIndicatorLeft = left;
mChildIndicatorRight = right;
}
@Override
public void setClipToPadding(boolean clipToPadding) {
super.setClipToPadding(mClipToPadding = clipToPadding);
}
public void setGroupIndicator(Drawable groupIndicator) {
mGroupIndicator = groupIndicator;
if (mIndicatorRight == 0 && mGroupIndicator != null) {
mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
}
}
public void setIndicatorBounds(int left, int right) {
mIndicatorLeft = left;
mIndicatorRight = right;
}
public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
mOnChildClickListener = onChildClickListener;
}
public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener) {
mOnGroupClickListener = onGroupClickListener;
}
public void setOnGroupCollapseListener(
OnGroupCollapseListener onGroupCollapseListener) {
mOnGroupCollapseListener = onGroupCollapseListener;
}
public void setOnGroupExpandListener(
OnGroupExpandListener onGroupExpandListener) {
mOnGroupExpandListener = onGroupExpandListener;
}
@Override
public void setOnItemClickListener(OnItemClickListener l) {
super.setOnItemClickListener(l);
}
public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
ExpandableListPosition elChildPos = ExpandableListPosition.obtainChildPosition(
groupPosition, childPosition);
PositionMetadata flatChildPos = mConnector.getFlattenedPos(elChildPos);
if (flatChildPos == null) {
if (!shouldExpandGroup) {
return false;
}
expandGroup(groupPosition);
flatChildPos = mConnector.getFlattenedPos(elChildPos);
if (flatChildPos == null) {
throw new IllegalStateException("Could not find child");
}
}
int absoluteFlatPosition = getAbsoluteFlatPosition(flatChildPos.position.flatListPos);
super.setSelection(absoluteFlatPosition);
elChildPos.recycle();
flatChildPos.recycle();
return true;
}
public void setSelectedGroup(int groupPosition) {
ExpandableListPosition elGroupPos = ExpandableListPosition
.obtainGroupPosition(groupPosition);
PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
elGroupPos.recycle();
final int absoluteFlatPosition = getAbsoluteFlatPosition(pm.position.flatListPos);
super.setSelection(absoluteFlatPosition);
pm.recycle();
}
}