package com.marshalchen.common.uimodule.listviewanimations.itemmanipulation; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.AbsListView; import android.widget.FrameLayout; import android.widget.LinearLayout; import com.marshalchen.common.uimodule.listviewanimations.ArrayAdapter; import com.marshalchen.common.uimodule.listviewanimations.ListViewSetter; import com.marshalchen.common.uimodule.listviewanimations.util.AdapterViewUtil; import com.marshalchen.common.uimodule.nineoldandroids.animation.Animator; import com.marshalchen.common.uimodule.nineoldandroids.animation.AnimatorListenerAdapter; import com.marshalchen.common.uimodule.nineoldandroids.animation.ValueAnimator; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * An {@link ArrayAdapter} which allows items to be expanded using an animation. */ @SuppressWarnings("UnusedDeclaration") public abstract class ExpandableListItemAdapter<T> extends ArrayAdapter<T> implements ListViewSetter { private static final int DEFAULTTITLEPARENTRESID = 10000; private static final int DEFAULTCONTENTPARENTRESID = 10001; private final Context mContext; private int mViewLayoutResId; private final int mTitleParentResId; private final int mContentParentResId; private int mActionViewResId; private final List<Long> mExpandedIds; private int mLimit; private AbsListView mAbsListView; private ExpandCollapseListener mExpandCollapseListener; /** * Creates a new ExpandableListItemAdapter with an empty list. */ public ExpandableListItemAdapter(final Context context) { this(context, null); } /** * Creates a new {@link com.marshalchen.common.uimodule.listviewanimations.itemmanipulation.ExpandableListItemAdapter} with the specified list, * or an empty list if items == null. */ public ExpandableListItemAdapter(final Context context, final List<T> items) { super(items); mContext = context; mTitleParentResId = DEFAULTTITLEPARENTRESID; mContentParentResId = DEFAULTCONTENTPARENTRESID; mExpandedIds = new ArrayList<Long>(); } /** * Creates a new ExpandableListItemAdapter with an empty list. Uses given * layout resource for the view; titleParentResId and contentParentResId * should be identifiers for ViewGroups within that layout. */ public ExpandableListItemAdapter(final Context context, final int layoutResId, final int titleParentResId, final int contentParentResId) { this(context, layoutResId, titleParentResId, contentParentResId, null); } /** * Creates a new ExpandableListItemAdapter with the specified list, or an * empty list if items == null. Uses given layout resource for the view; * titleParentResId and contentParentResId should be identifiers for * ViewGroups within that layout. */ public ExpandableListItemAdapter(final Context context, final int layoutResId, final int titleParentResId, final int contentParentResId, final List<T> items) { super(items); mContext = context; mViewLayoutResId = layoutResId; mTitleParentResId = titleParentResId; mContentParentResId = contentParentResId; mExpandedIds = new ArrayList<Long>(); } @Override public void setAbsListView(final AbsListView listView) { mAbsListView = listView; } /** * Set the resource id of the child {@link android.view.View} contained in the View * returned by {@link #getTitleView(int, android.view.View, android.view.ViewGroup)} that will be the * actuator of the expand / collapse animations.<br> * If there is no View in the title View with given resId, a * {@link NullPointerException} is thrown.</p> Default behavior: the whole * title View acts as the actuator. * * @param resId the resource id. */ public void setActionViewResId(final int resId) { mActionViewResId = resId; } /** * Set the maximum number of items allowed to be expanded. When the * (limit+1)th item is expanded, the first expanded item will collapse. * * @param limit the maximum number of items allowed to be expanded. Use <= 0 * for no limit. */ public void setLimit(final int limit) { mLimit = limit; mExpandedIds.clear(); notifyDataSetChanged(); } /** * Set the {@link com.marshalchen.common.uimodule.listviewanimations.itemmanipulation.ExpandCollapseListener} that should be notified of expand / collapse events. */ public void setExpandCollapseListener(final ExpandCollapseListener expandCollapseListener) { mExpandCollapseListener = expandCollapseListener; } @Override public View getView(final int position, final View convertView, final ViewGroup parent) { ViewGroup view = (ViewGroup) convertView; ViewHolder viewHolder; if (view == null) { view = createView(parent); viewHolder = new ViewHolder(); viewHolder.titleParent = (ViewGroup) view.findViewById(mTitleParentResId); viewHolder.contentParent = (ViewGroup) view.findViewById(mContentParentResId); view.setTag(viewHolder); } else { viewHolder = (ViewHolder) view.getTag(); } View titleView = getTitleView(position, viewHolder.titleView, viewHolder.titleParent); if (titleView != viewHolder.titleView) { viewHolder.titleParent.removeAllViews(); viewHolder.titleParent.addView(titleView); if (mActionViewResId == 0) { view.setOnClickListener(new TitleViewOnClickListener(viewHolder.contentParent)); } else { view.findViewById(mActionViewResId).setOnClickListener(new TitleViewOnClickListener(viewHolder.contentParent)); } } viewHolder.titleView = titleView; View contentView = getContentView(position, viewHolder.contentView, viewHolder.contentParent); if (contentView != viewHolder.contentView) { viewHolder.contentParent.removeAllViews(); viewHolder.contentParent.addView(contentView); } viewHolder.contentView = contentView; viewHolder.contentParent.setVisibility(mExpandedIds.contains(getItemId(position)) ? View.VISIBLE : View.GONE); viewHolder.contentParent.setTag(getItemId(position)); LayoutParams layoutParams = viewHolder.contentParent.getLayoutParams(); layoutParams.height = LayoutParams.WRAP_CONTENT; viewHolder.contentParent.setLayoutParams(layoutParams); return view; } private ViewGroup createView(final ViewGroup parent) { ViewGroup view; if (mViewLayoutResId == 0) { view = new RootView(mContext); } else { view = (ViewGroup) LayoutInflater.from(mContext).inflate(mViewLayoutResId, parent, false); } return view; } /** * Get a View that displays the <b>title of the data</b> at the specified * position in the data set. You can either create a View manually or * inflate it from an XML layout file. When the View is inflated, the parent * View (GridView, ListView...) will apply default layout parameters unless * you use * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)} * to specify a root view and to prevent attachment to the root. * * @param position The position of the item within the adapter's data set of the * item whose view we want. * @param convertView The old view to reuse, if possible. Note: You should check * that this view is non-null and of an appropriate type before * using. If it is not possible to convert this view to display * the correct data, this method can create a new view. * @param parent The parent that this view will eventually be attached to * @return A View corresponding to the title of the data at the specified * position. */ public abstract View getTitleView(int position, View convertView, ViewGroup parent); /** * Get a View that displays the <b>content of the data</b> at the specified * position in the data set. You can either create a View manually or * inflate it from an XML layout file. When the View is inflated, the parent * View (GridView, ListView...) will apply default layout parameters unless * you use * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)} * to specify a root view and to prevent attachment to the root. * * @param position The position of the item within the adapter's data set of the * item whose view we want. * @param convertView The old view to reuse, if possible. Note: You should check * that this view is non-null and of an appropriate type before * using. If it is not possible to convert this view to display * the correct data, this method can create a new view. * @param parent The parent that this view will eventually be attached to * @return A View corresponding to the content of the data at the specified * position. */ public abstract View getContentView(int position, View convertView, ViewGroup parent); /** * Indicates if the item at the specified position is expanded. * * @param position Index of the view whose state we want. * @return true if the view is expanded, false otherwise. */ public boolean isExpanded(final int position) { long itemId = getItemId(position); return mExpandedIds.contains(itemId); } /** * Return the title view at the specified position. * * @param position Index of the view we want. * @return the view if it exist, null otherwise. */ public View getTitleView(final int position) { View titleView = null; View parentView = findViewForPosition(position); Object tag = parentView.getTag(); if (tag instanceof ViewHolder) { titleView = ((ViewHolder) tag).titleView; } return titleView; } /** * Return the content view at the specified position. * * @param position Index of the view we want. * @return the view if it exist, null otherwise. */ public View getContentView(final int position) { View contentView = null; View parentView = findViewForPosition(position); if (parentView != null) { Object tag = parentView.getTag(); if (tag instanceof ViewHolder) { contentView = ((ViewHolder) tag).contentView; } } return contentView; } @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); Set<Long> removedIds = new HashSet<Long>(mExpandedIds); for (int i = 0; i < getCount(); ++i) { long id = getItemId(i); removedIds.remove(id); } mExpandedIds.removeAll(removedIds); } /** * Return the content parent at the specified position. * * @param position Index of the view we want. * @return the view if it exist, null otherwise. */ private View getContentParent(final int position) { View contentParent = null; View parentView = findViewForPosition(position); if (parentView != null) { Object tag = parentView.getTag(); if (tag instanceof ViewHolder) { contentParent = ((ViewHolder) tag).contentParent; } } return contentParent; } /** * Expand the view at given position. Will do nothing if the view is already expanded. * * @param position the position to expand. */ public void expand(final int position) { long itemId = getItemId(position); if (mExpandedIds.contains(itemId)) { return; } toggle(position); } /** * Collapse the view at given position. Will do nothing if the view is already collapsed. * * @param position the position to collapse. */ public void collapse(final int position) { long itemId = getItemId(position); if (!mExpandedIds.contains(itemId)) { return; } toggle(position); } private View findViewForPosition(final int position) { View result = null; for (int i = 0; i < mAbsListView.getChildCount() && result == null; i++) { View childView = mAbsListView.getChildAt(i); if (AdapterViewUtil.getPositionForView(mAbsListView, childView) == position) { result = childView; } } return result; } private int findPositionForId(final long id) { for (int i = 0; i < getCount(); i++) { if (getItemId(i) == id) { return i; } } return -1; } /** * Toggle the {@link android.view.View} at given position, ignores header or footer Views. * * @param position the position of the view to toggle. */ public void toggle(final int position) { long itemId = getItemId(position); boolean isExpanded = mExpandedIds.contains(itemId); View contentParent = getContentParent(position); if (contentParent != null) { toggle(contentParent); } if (contentParent == null && isExpanded) { mExpandedIds.remove(itemId); } else if (contentParent == null && !isExpanded) { mExpandedIds.add(itemId); } } private void toggle(final View contentParent) { boolean isVisible = contentParent.getVisibility() == View.VISIBLE; boolean shouldCollapseOther = !isVisible && mLimit > 0 && mExpandedIds.size() >= mLimit; if (shouldCollapseOther) { Long firstId = mExpandedIds.get(0); int firstPosition = findPositionForId(firstId); View firstEV = getContentParent(firstPosition); if (firstEV != null) { ExpandCollapseHelper.animateCollapsing(firstEV); } mExpandedIds.remove(firstId); if (mExpandCollapseListener != null) { mExpandCollapseListener.onItemCollapsed(firstPosition); } } Long id = (Long) contentParent.getTag(); int position = findPositionForId(id); if (isVisible) { ExpandCollapseHelper.animateCollapsing(contentParent); mExpandedIds.remove(id); if (mExpandCollapseListener != null) { mExpandCollapseListener.onItemCollapsed(position); } } else { ExpandCollapseHelper.animateExpanding(contentParent, mAbsListView); mExpandedIds.add(id); if (mExpandCollapseListener != null) { mExpandCollapseListener.onItemExpanded(position); } } } private class TitleViewOnClickListener implements View.OnClickListener { private final View mContentParent; private TitleViewOnClickListener(final View contentParent) { mContentParent = contentParent; } @Override public void onClick(final View view) { toggle(mContentParent); } } private static class RootView extends LinearLayout { private ViewGroup mTitleViewGroup; private ViewGroup mContentViewGroup; public RootView(final Context context) { super(context); init(); } private void init() { setOrientation(VERTICAL); mTitleViewGroup = new FrameLayout(getContext()); mTitleViewGroup.setId(DEFAULTTITLEPARENTRESID); addView(mTitleViewGroup); mContentViewGroup = new FrameLayout(getContext()); mContentViewGroup.setId(DEFAULTCONTENTPARENTRESID); addView(mContentViewGroup); } } private static class ViewHolder { ViewGroup titleParent; ViewGroup contentParent; View titleView; View contentView; } private static class ExpandCollapseHelper { public static void animateCollapsing(final View view) { int origHeight = view.getHeight(); ValueAnimator animator = createHeightAnimator(view, origHeight, 0); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(final Animator animator) { view.setVisibility(View.GONE); } }); animator.start(); } public static void animateExpanding(final View view, final AbsListView listView) { view.setVisibility(View.VISIBLE); View parent = (View) view.getParent(); final int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getMeasuredWidth() - parent.getPaddingLeft() - parent.getPaddingRight(), View.MeasureSpec.AT_MOST); final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); view.measure(widthSpec, heightSpec); ValueAnimator animator = createHeightAnimator(view, 0, view.getMeasuredHeight()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { final int listViewHeight = listView.getHeight(); final int listViewBottomPadding = listView.getPaddingBottom(); final View v = findDirectChild(view, listView); @Override public void onAnimationUpdate(final ValueAnimator valueAnimator) { final int bottom = v.getBottom(); if (bottom > listViewHeight) { final int top = v.getTop(); if (top > 0) { listView.smoothScrollBy(Math.min(bottom - listViewHeight + listViewBottomPadding, top), 0); } } } }); animator.start(); } private static View findDirectChild(final View view, final AbsListView listView) { View result = view; View parent = (View) result.getParent(); while (parent != listView) { result = parent; parent = (View) result.getParent(); } return result; } public static ValueAnimator createHeightAnimator(final View view, final int start, final int end) { ValueAnimator animator = ValueAnimator.ofInt(start, end); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(final ValueAnimator valueAnimator) { int value = (Integer) valueAnimator.getAnimatedValue(); LayoutParams layoutParams = view.getLayoutParams(); layoutParams.height = value; view.setLayoutParams(layoutParams); } }); return animator; } } }