package com.tjerkw.slideexpandable.library; import android.os.Parcel; import android.os.Parcelable; import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.widget.LinearLayout; import android.widget.ListAdapter; import java.util.BitSet; /** * Wraps a ListAdapter to give it expandable list view functionality. * The main thing it does is add a listener to the getToggleButton * which expands the getExpandableView for each list item. * * @author tjerk * @date 6/9/12 4:41 PM */ public abstract class AbstractSlideExpandableListAdapter extends WrapperListAdapterImpl { /** * Reference to the last expanded list item. * Since lists are recycled this might be null if * though there is an expanded list item */ private View lastOpen = null; /** * The position of the last expanded list item. * If -1 there is no list item expanded. * Otherwise it points to the position of the last expanded list item */ private int lastOpenPosition = -1; /** * A list of positions of all list items that are expanded. * Normally only one is expanded. But a mode to expand * multiple will be added soon. * * If an item onj position x is open, its bit is set */ private BitSet openItems = new BitSet(); /** * We remember, for each collapsable view its height. * So we dont need to recalculate. * The height is calculated just before the view is drawn. */ private final SparseIntArray viewHeights = new SparseIntArray(10); public AbstractSlideExpandableListAdapter(ListAdapter wrapped) { super(wrapped); } @Override public View getView(int position, View view, ViewGroup viewGroup) { view = wrapped.getView(position, view, viewGroup); enableFor(view, position); return view; } /** * This method is used to get the Button view that should * expand or collapse the Expandable View. * <br/> * Normally it will be implemented as: * <pre> * return parent.findViewById(R.id.expand_toggle_button) * </pre> * * A listener will be attached to the button which will * either expand or collapse the expandable view * * @see #getExpandableView(View) * @param parent the list view item * @ensure return!=null * @return a child of parent which is a button */ public abstract View getExpandToggleButton(View parent); /** * This method is used to get the view that will be hidden * initially and expands or collapse when the ExpandToggleButton * is pressed @see getExpandToggleButton * <br/> * Normally it will be implemented as: * <pre> * return parent.findViewById(R.id.expandable) * </pre> * * @see #getExpandToggleButton(View) * @param parent the list view item * @ensure return!=null * @return a child of parent which is a view (or often ViewGroup) * that can be collapsed and expanded */ public abstract View getExpandableView(View parent); /** * Gets the duration of the collapse animation in ms. * Default is 330ms. Override this method to change the default. * * @return the duration of the anim in ms */ protected int getAnimationDuration() { return 330; } public void enableFor(View parent, int position) { View more = getExpandToggleButton(parent); View itemToolbar = getExpandableView(parent); itemToolbar.measure(parent.getWidth(), parent.getHeight()); enableFor(more, itemToolbar, position); } private void enableFor(final View button, final View target, final int position) { if(target == lastOpen && position!=lastOpenPosition) { // lastOpen is recycled, so its reference is false lastOpen = null; } if(position == lastOpenPosition) { // re reference to the last view // so when can animate it when collapsed lastOpen = target; } int height = viewHeights.get(position, -1); if(height == -1) { viewHeights.put(position, target.getMeasuredHeight()); updateExpandable(target,position); } else { updateExpandable(target, position); } button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View view) { Animation a = target.getAnimation(); if (a != null && a.hasStarted() && !a.hasEnded()) { a.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { view.performClick(); } @Override public void onAnimationRepeat(Animation animation) { } }); } else { target.setAnimation(null); int type = target.getVisibility() == View.VISIBLE ? ExpandCollapseAnimation.COLLAPSE : ExpandCollapseAnimation.EXPAND; // remember the state if (type == ExpandCollapseAnimation.EXPAND) { openItems.set(position, true); } else { openItems.set(position, false); } // check if we need to collapse a different view if (type == ExpandCollapseAnimation.EXPAND) { if (lastOpenPosition != -1 && lastOpenPosition != position) { if (lastOpen != null) { animateView(lastOpen, ExpandCollapseAnimation.COLLAPSE); } openItems.set(lastOpenPosition, false); } lastOpen = target; lastOpenPosition = position; } else if (lastOpenPosition == position) { lastOpenPosition = -1; } animateView(target, type); } } }); } private void updateExpandable(View target, int position) { final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)target.getLayoutParams(); if(openItems.get(position)) { target.setVisibility(View.VISIBLE); params.bottomMargin = 0; } else { target.setVisibility(View.GONE); params.bottomMargin = 0-viewHeights.get(position); } } /** * Performs either COLLAPSE or EXPAND animation on the target view * @param target the view to animate * @param type the animation type, either ExpandCollapseAnimation.COLLAPSE * or ExpandCollapseAnimation.EXPAND */ private void animateView(final View target, final int type) { Animation anim = new ExpandCollapseAnimation( target, type ); anim.setDuration(getAnimationDuration()); target.startAnimation(anim); } /** * Closes the current open item. * If it is current visible it will be closed with an animation. * * @return true if an item was closed, false otherwise */ public boolean collapseLastOpen() { if(lastOpenPosition != -1) { // if visible animate it out if(lastOpen != null) { animateView(lastOpen, ExpandCollapseAnimation.COLLAPSE); } openItems.set(lastOpenPosition, false); lastOpenPosition = -1; return true; } return false; } public Parcelable onSaveInstanceState(Parcelable parcelable) { SavedState ss = new SavedState(parcelable); ss.lastOpenPosition = this.lastOpenPosition; ss.openItems = this.openItems; return ss; } public void onRestoreInstanceState(SavedState state) { this.lastOpenPosition = state.lastOpenPosition; this.openItems = state.openItems; } /** * Utility methods to read and write a bitset from and to a Parcel */ private static BitSet readBitSet(Parcel src) { int cardinality = src.readInt(); BitSet set = new BitSet(); for (int i = 0; i < cardinality; i++) { set.set(src.readInt()); } return set; } private static void writeBitSet(Parcel dest, BitSet set) { int nextSetBit = -1; dest.writeInt(set.cardinality()); while ((nextSetBit = set.nextSetBit(nextSetBit + 1)) != -1) { dest.writeInt(nextSetBit); } } /** * The actual state class */ static class SavedState extends View.BaseSavedState { public BitSet openItems = null; public int lastOpenPosition = -1; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); in.writeInt(lastOpenPosition); writeBitSet(in, openItems); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); lastOpenPosition = out.readInt(); openItems = readBitSet(out); } //required field that makes Parcelables from a Parcel public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }