diff --git a/library/src/se/emilsjolander/stickylistheaders/DistinctMultiHashMap.java b/library/src/se/emilsjolander/stickylistheaders/DistinctMultiHashMap.java new file mode 100644 index 00000000..77c1353c --- /dev/null +++ b/library/src/se/emilsjolander/stickylistheaders/DistinctMultiHashMap.java @@ -0,0 +1,147 @@ +package se.emilsjolander.stickylistheaders; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * a hash map can maintain an one-to-many relationship which the value only belongs to one “one” part + * and the map also support getKey by value quickly + * + * @author lsjwzh + */ +public class DistinctMultiHashMap { + private IDMapper mIDMapper; + + public interface IDMapper{ + public Object keyToKeyId(TKey key); + public TKey keyIdToKey(Object keyId); + public Object valueToValueId(TItemValue value); + public TItemValue valueIdToValue(Object valueId); + } + + LinkedHashMap> mKeyToValuesMap = new LinkedHashMap>(); + LinkedHashMap mValueToKeyIndexer = new LinkedHashMap(); + + public DistinctMultiHashMap(){ + this(new IDMapper() { + @Override + public Object keyToKeyId(TKey key) { + return key; + } + + @Override + public TKey keyIdToKey(Object keyId) { + return (TKey) keyId; + } + + @Override + public Object valueToValueId(TItemValue value) { + return value; + } + + @Override + public TItemValue valueIdToValue(Object valueId) { + return (TItemValue) valueId; + } + }); + } + public DistinctMultiHashMap(IDMapper idMapper){ + mIDMapper = idMapper; + } + + public List get(TKey key){ + //todo immutable + return mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)); + } + public TKey getKey(TItemValue value){ + return mValueToKeyIndexer.get(mIDMapper.valueToValueId(value)); + } + + public void add(TKey key,TItemValue value){ + Object keyId = mIDMapper.keyToKeyId(key); + if(mKeyToValuesMap.get(keyId)==null){ + mKeyToValuesMap.put(keyId,new ArrayList()); + } + //remove old relationship + TKey keyForValue = getKey(value); + if(keyForValue !=null){ + mKeyToValuesMap.get(mIDMapper.keyToKeyId(keyForValue)).remove(value); + } + mValueToKeyIndexer.put(mIDMapper.valueToValueId(value), key); + if(!containsValue(mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)),value)) { + mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)).add(value); + } + } + + public void removeKey(TKey key){ + if(mKeyToValuesMap.get(mIDMapper.keyToKeyId(key))!=null){ + for (TItemValue value : mKeyToValuesMap.get(mIDMapper.keyToKeyId(key))){ + mValueToKeyIndexer.remove(mIDMapper.valueToValueId(value)); + } + mKeyToValuesMap.remove(mIDMapper.keyToKeyId(key)); + } + } + public void removeValue(TItemValue value){ + if(getKey(value)!=null){ + List itemValues = mKeyToValuesMap.get(mIDMapper.keyToKeyId(getKey(value))); + if(itemValues!=null){ + itemValues.remove(value); + } + } + mValueToKeyIndexer.remove(mIDMapper.valueToValueId(value)); + } + + public void clear(){ + mValueToKeyIndexer.clear(); + mKeyToValuesMap.clear(); + } + + public void clearValues(){ + for (Map.Entry> entry:entrySet()){ + if(entry.getValue()!=null){ + entry.getValue().clear(); + } + } + mValueToKeyIndexer.clear(); + } + + public Set>> entrySet(){ + return mKeyToValuesMap.entrySet(); + } + + public Set> reverseEntrySet(){ + return mValueToKeyIndexer.entrySet(); + } + + public int size(){ + return mKeyToValuesMap.size(); + } + public int valuesSize(){ + return mValueToKeyIndexer.size(); + } + + protected boolean containsValue(List list,TItemValue value){ + for (TItemValue itemValue :list){ + if(mIDMapper.valueToValueId(itemValue).equals(mIDMapper.valueToValueId(value))){ + return true; + } + } + return false; + } + + /** + * @param position + * @return + */ + public TItemValue getValueByPosition(int position){ + Object[] vauleIdArray = mValueToKeyIndexer.keySet().toArray(); + if(position>vauleIdArray.length){ + throw new IndexOutOfBoundsException(); + } + Object valueId = vauleIdArray[position]; + return mIDMapper.valueIdToValue(valueId); + } +} diff --git a/library/src/se/emilsjolander/stickylistheaders/DualHashMap.java b/library/src/se/emilsjolander/stickylistheaders/DualHashMap.java new file mode 100644 index 00000000..7b3f34e8 --- /dev/null +++ b/library/src/se/emilsjolander/stickylistheaders/DualHashMap.java @@ -0,0 +1,39 @@ +package se.emilsjolander.stickylistheaders; + +import java.util.HashMap; + +/** + * simple two way hashmap + * @author lsjwzh + */ +public class DualHashMap { + HashMap mKeyToValue = new HashMap(); + HashMap mValueToKey = new HashMap(); + + public void put(TKey t1, TValue t2){ + remove(t1); + removeByValue(t2); + mKeyToValue.put(t1, t2); + mValueToKey.put(t2, t1); + } + + public TKey getKey(TValue value){ + return mValueToKey.get(value); + } + public TValue get(TKey key){ + return mKeyToValue.get(key); + } + + public void remove(TKey key){ + if(get(key)!=null){ + mValueToKey.remove(get(key)); + } + mKeyToValue.remove(key); + } + public void removeByValue(TValue value){ + if(getKey(value)!=null){ + mKeyToValue.remove(getKey(value)); + } + mValueToKey.remove(value); + } +} diff --git a/library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersAdapter.java b/library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersAdapter.java new file mode 100644 index 00000000..c34711d2 --- /dev/null +++ b/library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersAdapter.java @@ -0,0 +1,131 @@ +package se.emilsjolander.stickylistheaders; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import java.util.ArrayList; +import java.util.List; + + +/** + * @author lsjwzh + */ +public class ExpandableStickyListHeadersAdapter extends BaseAdapter implements StickyListHeadersAdapter { + + private final StickyListHeadersAdapter mInnerAdapter; + DualHashMap mViewToItemMap = new DualHashMap(); + DistinctMultiHashMap mGroupIdToViewMap = new DistinctMultiHashMap(); + List mCollapseGroupIds = new ArrayList(); + + public ExpandableStickyListHeadersAdapter(StickyListHeadersAdapter innerAdapter){ + this.mInnerAdapter = innerAdapter; + } + + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + return mInnerAdapter.getHeaderView(position,convertView,parent); + } + + @Override + public long getHeaderId(int position) { + return mInnerAdapter.getHeaderId(position); + } + + @Override + public boolean areAllItemsEnabled() { + return mInnerAdapter.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int i) { + return mInnerAdapter.isEnabled(i); + } + + @Override + public void registerDataSetObserver(DataSetObserver dataSetObserver) { + mInnerAdapter.registerDataSetObserver(dataSetObserver); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver dataSetObserver) { + mInnerAdapter.unregisterDataSetObserver(dataSetObserver); + } + + @Override + public int getCount() { + return mInnerAdapter.getCount(); + } + + @Override + public Object getItem(int i) { + return mInnerAdapter.getItem(i); + } + + @Override + public long getItemId(int i) { + return mInnerAdapter.getItemId(i); + } + + @Override + public boolean hasStableIds() { + return mInnerAdapter.hasStableIds(); + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + View convertView = mInnerAdapter.getView(i,view,viewGroup); + mViewToItemMap.put(convertView,getItemId(i)); + mGroupIdToViewMap.add((int) getHeaderId(i),convertView); + if(mCollapseGroupIds.contains(getHeaderId(i))){ + convertView.setVisibility(View.GONE); + }else { + convertView.setVisibility(View.VISIBLE); + } + return convertView; + } + + @Override + public int getItemViewType(int i) { + return mInnerAdapter.getItemViewType(i); + } + + @Override + public int getViewTypeCount() { + return mInnerAdapter.getViewTypeCount(); + } + + @Override + public boolean isEmpty() { + return mInnerAdapter.isEmpty(); + } + + public List getItemViewsByGroup(long groupId){ + return mGroupIdToViewMap.get((int) groupId); + } + + public boolean isGroupCollapsed(long groupId){ + return mCollapseGroupIds.contains(groupId); + } + + public void expand(long groupId) { + if(isGroupCollapsed(groupId)){ + mCollapseGroupIds.remove((Object)groupId); + } + } + + public void collapse(long groupId) { + if(!isGroupCollapsed(groupId)){ + mCollapseGroupIds.add(groupId); + } + } + + public View findViewByItemId(long itemId){ + return mViewToItemMap.getKey(itemId); + } + + public long findItemIdByView(View view){ + return mViewToItemMap.get(view); + } +} diff --git a/library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersListView.java b/library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersListView.java new file mode 100644 index 00000000..60030b5e --- /dev/null +++ b/library/src/se/emilsjolander/stickylistheaders/ExpandableStickyListHeadersListView.java @@ -0,0 +1,128 @@ +package se.emilsjolander.stickylistheaders; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import java.util.List; +import java.util.WeakHashMap; + +/** + * add expand/collapse functions like ExpandableListView + * provide customizable expand/collapse animation + * @author lsjwzh + */ +public class ExpandableStickyListHeadersListView extends StickyListHeadersListView { + public interface IAnimationExecutor{ + public void executeAnim(View target,int animType,int viewHeight); + } + + public final static int ANIMATION_COLLAPSE = 1; + public final static int ANIMATION_EXPAND = 0; + + WeakHashMap mOriginalViewHeight = new WeakHashMap(); + ExpandableStickyListHeadersAdapter mExpandableStickyListHeadersAdapter; + + + + IAnimationExecutor mAnimExecutor = new IAnimationExecutor() { + @Override + public void executeAnim(View target, int animType, int viewHeight) { + if(animType==ANIMATION_EXPAND){ + target.setVisibility(VISIBLE); + }else if(animType==ANIMATION_COLLAPSE){ + target.setVisibility(GONE); + } + } + }; + + + public ExpandableStickyListHeadersListView(Context context) { + super(context); + } + + public ExpandableStickyListHeadersListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ExpandableStickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void setAdapter(StickyListHeadersAdapter adapter) { + mExpandableStickyListHeadersAdapter = new ExpandableStickyListHeadersAdapter(adapter); + super.setAdapter(mExpandableStickyListHeadersAdapter); + } + + public View findViewByItemId(long itemId){ + return mExpandableStickyListHeadersAdapter.findViewByItemId(itemId); + } + + public long findItemIdByView(View view){ + return mExpandableStickyListHeadersAdapter.findItemIdByView(view); + } + + public void expand(long groupId) { + if(!mExpandableStickyListHeadersAdapter.isGroupCollapsed(groupId)){ + return; + } + mExpandableStickyListHeadersAdapter.expand(groupId); + //find and expand views in group + List itemViews = mExpandableStickyListHeadersAdapter.getItemViewsByGroup(groupId); + if(itemViews==null){ + return; + } + for (View view : itemViews) { + animateView(view, ANIMATION_EXPAND); + } + } + + public void collapse(long groupId) { + if(mExpandableStickyListHeadersAdapter.isGroupCollapsed(groupId)){ + return; + } + mExpandableStickyListHeadersAdapter.collapse(groupId); + //find and hide views in group + List itemViews = mExpandableStickyListHeadersAdapter.getItemViewsByGroup(groupId); + if(itemViews==null){ + return; + } + for (View view : itemViews) { + animateView(view, ANIMATION_COLLAPSE); + } + } + + public boolean isGroupCollapsed(long groupId){ + return mExpandableStickyListHeadersAdapter.isGroupCollapsed(groupId); + } + + public void setAnimExecutor(IAnimationExecutor animExecutor) { + this.mAnimExecutor = animExecutor; + } + + /** + * 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) { + if(ANIMATION_EXPAND==type&&target.getVisibility()==VISIBLE){ + return; + } + if(ANIMATION_COLLAPSE==type&&target.getVisibility()!=VISIBLE){ + return; + } + if(mOriginalViewHeight.get(target)==null){ + mOriginalViewHeight.put(target,target.getLayoutParams().height); + } + final int viewHeight = mOriginalViewHeight.get(target); + if(mAnimExecutor!=null){ + mAnimExecutor.executeAnim(target,type,viewHeight); + } + + } + +}