package com.haarman.listviewanimations.itemmanipulation; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.FrameLayout; import android.widget.LinearLayout; import com.haarman.listviewanimations.ArrayAdapter; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorListenerAdapter; import com.nineoldandroids.animation.ValueAnimator; /** * An {@link ArrayAdapter} which allows items to be expanded using an animation. */ public abstract class ExpandableListItemAdapter<T> extends ArrayAdapter<T> { private static final int DEFAULTTITLEPARENTRESID = 10000; private static final int DEFAULTCONTENTPARENTRESID = 10001; private Context mContext; private int mViewLayoutResId; private int mTitleParentResId; private int mContentParentResId; private int mActionViewResId; private List<Long> mVisibleIds; private int mLimit; private Map<Long, View> mExpandedViews; /** * Creates a new ExpandableListItemAdapter with an empty list. */ protected ExpandableListItemAdapter(Context context) { this(context, null); } /** * Creates a new {@link ExpandableListItemAdapter} with the specified list, * or an empty list if items == null. */ protected ExpandableListItemAdapter(Context context, List<T> items) { super(items); mContext = context; mTitleParentResId = DEFAULTTITLEPARENTRESID; mContentParentResId = DEFAULTCONTENTPARENTRESID; mVisibleIds = 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. */ protected ExpandableListItemAdapter(Context context, int layoutResId, int titleParentResId, 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. */ protected ExpandableListItemAdapter(Context context, int layoutResId, int titleParentResId, int contentParentResId, List<T> items) { super(items); mContext = context; mViewLayoutResId = layoutResId; mTitleParentResId = titleParentResId; mContentParentResId = contentParentResId; mVisibleIds = new ArrayList<Long>(); mExpandedViews = new HashMap<Long, View>(); } /** * Set the resource id of the child {@link View} contained in the View returned by * {@link #getTitleView(int, 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(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(int limit) { mLimit = limit; mVisibleIds.clear(); mExpandedViews.clear(); notifyDataSetChanged(); } @Override public View getView(int position, View convertView, 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(); } if (mLimit > 0) { if (mVisibleIds.contains(getItemId(position))) { mExpandedViews.put(getItemId(position), view); } else if (mExpandedViews.containsValue(view) && !mVisibleIds.contains(getItemId(position))) { mExpandedViews.remove(getItemId(position)); } } 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(mVisibleIds.contains(getItemId(position)) ? View.VISIBLE : View.GONE); viewHolder.contentParent.setTag(getItemId(position)); ViewGroup.LayoutParams layoutParams = viewHolder.contentParent.getLayoutParams(); layoutParams.height = LayoutParams.WRAP_CONTENT; viewHolder.contentParent.setLayoutParams(layoutParams); return view; } private ViewGroup createView(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); private static class ViewHolder { ViewGroup titleParent; ViewGroup contentParent; View titleView; View contentView; } private static class RootView extends LinearLayout { private ViewGroup mTitleViewGroup; private ViewGroup mContentViewGroup; public RootView(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 class TitleViewOnClickListener implements View.OnClickListener { private View mContentParent; private TitleViewOnClickListener(View contentParent) { this.mContentParent = contentParent; } @Override public void onClick(View view) { boolean isVisible = mContentParent.getVisibility() == View.VISIBLE; if (!isVisible && mLimit > 0 && mVisibleIds.size() >= mLimit) { Long firstId = mVisibleIds.get(0); View firstEV = mExpandedViews.get(firstId); if (firstEV != null) { ViewHolder firstVH = ((ViewHolder) firstEV.getTag()); ViewGroup contentParent = firstVH.contentParent; ExpandCollapseHelper.animateCollapsing(contentParent); mExpandedViews.remove(mVisibleIds.get(0)); } mVisibleIds.remove(mVisibleIds.get(0)); } if (isVisible) { ExpandCollapseHelper.animateCollapsing(mContentParent); mVisibleIds.remove(mContentParent.getTag()); mExpandedViews.remove(mContentParent.getTag()); } else { ExpandCollapseHelper.animateExpanding(mContentParent); mVisibleIds.add((Long) mContentParent.getTag()); if (mLimit > 0) { View parent = (View) mContentParent.getParent(); mExpandedViews.put((Long) mContentParent.getTag(), parent); } } } } 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(Animator animator) { view.setVisibility(View.GONE); } }); animator.start(); } public static void animateExpanding(final View view) { view.setVisibility(View.VISIBLE); final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); view.measure(widthSpec, heightSpec); ValueAnimator animator = createHeightAnimator(view, 0, view.getMeasuredHeight()); animator.start(); } public static ValueAnimator createHeightAnimator(final View view, int start, int end) { ValueAnimator animator = ValueAnimator.ofInt(start, end); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int value = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); layoutParams.height = value; view.setLayoutParams(layoutParams); } }); return animator; } } }