Skip to content

Instantly share code, notes, and snippets.

@MobileSam
Created November 29, 2013 03:29
Show Gist options
  • Save MobileSam/7701218 to your computer and use it in GitHub Desktop.
Save MobileSam/7701218 to your computer and use it in GitHub Desktop.
CardsLib working with StickyListHeaders Note that I'm using the PR/7 merged with master as I need 2.3 support. https://github.com/gabrielemariotti/cardslib https://github.com/emilsjolander/StickyListHeaders
package mobi.dinoz.droid.adapter;
import it.gmariotti.cardslib.library.R;
import it.gmariotti.cardslib.library.internal.Card;
import it.gmariotti.cardslib.library.internal.base.BaseCardArrayAdapter;
import it.gmariotti.cardslib.library.view.CardListView;
import it.gmariotti.cardslib.library.view.CardView;
import it.gmariotti.cardslib.library.view.listener.SwipeDismissListViewTouchListener;
import java.util.List;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import mobi.dinoz.droid.view.HeaderCardListView;
public abstract class HeaderCardAdapter extends BaseCardArrayAdapter implements StickyListHeadersAdapter {
/**
* {@link CardListView}
*/
protected HeaderCardListView mCardListView;
/**
* Listener invoked when a card is swiped
*/
protected SwipeDismissListViewTouchListener mOnTouchListener;
// -------------------------------------------------------------
// Constructors
// -------------------------------------------------------------
/**
* Constructor
*
* @param context
* The current context.
* @param cards
* The cards to represent in the ListView.
*/
public HeaderCardAdapter(Context context, List<Card> cards) {
super(context, cards);
}
// -------------------------------------------------------------
// Views
// -------------------------------------------------------------
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
CardView mCardView;
Card mCard;
LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Retrieve card from items
mCard = (Card) getItem(position);
if (mCard != null) {
int layout = mRowLayoutId;
boolean recycle = false;
// Inflate layout
if (view == null) {
recycle = false;
view = mInflater.inflate(layout, parent, false);
} else {
recycle = true;
}
// Setup card
mCardView = (CardView) view.findViewById(R.id.list_cardId);
if (mCardView != null) {
// It is important to set recycle value for performance issue
mCardView.setRecycle(recycle);
// Save original swipeable to prevent cardSwipeListener
// (listView requires another cardSwipeListener)
boolean origianlSwipeable = mCard.isSwipeable();
mCard.setSwipeable(false);
mCardView.setCard(mCard);
// Set originalValue
mCard.setSwipeable(origianlSwipeable);
// If card has an expandable button override animation
if (mCard.getCardHeader() != null && mCard.getCardHeader().isButtonExpandVisible()) {
setupExpandCollapseListAnimation(mCardView);
}
// Setup swipeable animation
setupSwipeableAnimation(mCard, mCardView);
}
}
return view;
}
/**
* Sets SwipeAnimation on List
*
* @param card
* {@link Card}
* @param cardView
* {@link CardView}
*/
protected void setupSwipeableAnimation(final Card card, CardView cardView) {
if (card.isSwipeable()) {
if (mOnTouchListener == null) {
throw new IllegalStateException("Not working");
// mOnTouchListener = new
// SwipeDismissListViewTouchListener(mCardListView, mCallback);
// Setting this scroll listener is required to ensure that
// during
// ListView scrolling, we don't look for swipes.
// mCardListView.setOnScrollListener(mOnTouchListener.makeScrollListener());
}
cardView.setOnTouchListener(mOnTouchListener);
} else {
// prevent issue with recycle view
cardView.setOnTouchListener(null);
}
}
/**
* Overrides the default collapse/expand animation in a List
*
* @param cardView
* {@link CardView}
*/
protected void setupExpandCollapseListAnimation(CardView cardView) {
if (cardView == null)
return;
cardView.setOnExpandListAnimatorListener(mCardListView);
}
// -------------------------------------------------------------
// SwipeListener
// -------------------------------------------------------------
/**
* Listener invoked when a card is swiped
*/
SwipeDismissListViewTouchListener.DismissCallbacks mCallback = new SwipeDismissListViewTouchListener.DismissCallbacks() {
@Override
public boolean canDismiss(int position, Card card) {
return card.isSwipeable();
}
@Override
public void onDismiss(AbsListView listView, int[] reverseSortedPositions) {
for (int position : reverseSortedPositions) {
Card card = getItem(position);
if (card.getOnSwipeListener() != null)
card.getOnSwipeListener().onSwipe(card);
remove(card);
}
notifyDataSetChanged();
}
};
// -------------------------------------------------------------
// Getters and Setters
// -------------------------------------------------------------
/**
* @return {@link CardListView}
*/
public HeaderCardListView getCardListView() {
return mCardListView;
}
/**
* Sets the {@link CardListView}
*
* @param cardListView
* cardListView
*/
public void setCardListView(HeaderCardListView cardListView) {
this.mCardListView = cardListView;
}
}
package mobi.dinoz.droid.view;
import it.gmariotti.cardslib.library.R;
import it.gmariotti.cardslib.library.internal.Card;
import it.gmariotti.cardslib.library.view.CardView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter;
import se.emilsjolander.stickylistheaders.StickyListHeadersListView;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import mobi.dinoz.droid.adapter.HeaderCardAdapter;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.animation.PropertyValuesHolder;
import com.nineoldandroids.util.FloatProperty;
import com.nineoldandroids.util.Property;
public class HeaderCardListView extends StickyListHeadersListView implements CardView.OnExpandListAnimatorListener {
protected HeaderCardAdapter mAdapter;
private boolean mShouldRemoveObserver = false;
private List<View> mViewsToDraw = new ArrayList<View>();
private int[] mTranslate;
protected int list_card_layout_resourceID = R.layout.list_card_layout;
public HeaderCardListView(Context context) {
super(context);
init(null, 0);
}
public HeaderCardListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public HeaderCardListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
protected void init(AttributeSet attrs, int defStyle) {
initAttrs(attrs, defStyle);
setDividerHeight(0);
}
protected void initAttrs(AttributeSet attrs, int defStyle) {
list_card_layout_resourceID = R.layout.list_card_layout;
TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.card_options, defStyle,
defStyle);
try {
list_card_layout_resourceID = a.getResourceId(R.styleable.card_options_list_card_layout_resourceID,
this.list_card_layout_resourceID);
} finally {
a.recycle();
}
}
@Override
public void setAdapter(StickyListHeadersAdapter adapter) {
if (adapter instanceof HeaderCardAdapter) {
setAdapter((HeaderCardAdapter) adapter);
} else {
Log.e("HeaderCardListView", "The CardListView only accepts HeaderCardAdapter");
super.setAdapter(null);
}
}
public void setAdapter(HeaderCardAdapter adapter) {
super.setAdapter(adapter);
// Set Layout used by items
adapter.setRowLayoutId(list_card_layout_resourceID);
adapter.setCardListView(this);
mAdapter = adapter;
}
@Override
public void onExpandStart(CardView viewCard, View expandingLayout) {
prepareExpandView(viewCard, expandingLayout);
}
@Override
public void onCollapseStart(CardView viewCard, View expandingLayout) {
prepareCollapseView(viewCard, expandingLayout);
}
public static final Property<View, Float> ALPHA = new FloatProperty<View>("alpha") {
@Override
public void setValue(View object, float value) {
object.setAlpha(value);
}
@Override
public Float get(View object) {
return object.getAlpha();
}
};
private void prepareExpandView(final CardView view, final View expandingLayout) {
final Card card = (Card) getItemAtPosition(getPositionForView(view));
/* Store the original top and bottom bounds of all the cells. */
final int oldTop = view.getTop();
final int oldBottom = view.getBottom();
final HashMap<View, int[]> oldCoordinates = new HashMap<View, int[]>();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
if (Build.VERSION.SDK_INT >= 16) {
v.setHasTransientState(true);
}
oldCoordinates.put(v, new int[] { v.getTop(), v.getBottom() });
}
/* Update the layout so the extra content becomes visible. */
if (expandingLayout != null)
expandingLayout.setVisibility(View.VISIBLE);
/*
* Add an onPreDraw Listener to the listview. onPreDraw will get invoked
* after onLayout and onMeasure have run but before anything has been
* drawn. This means that the final post layout properties for all the
* items have already been determined, but still have not been rendered
* onto the screen.
*/
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
/* Determine if this is the first or second pass. */
if (!mShouldRemoveObserver) {
mShouldRemoveObserver = true;
/*
* Calculate what the parameters should be for
* setSelectionFromTop. The ListView must be offset in a
* way, such that after the animation takes place, all the
* cells that remain visible are rendered completely by the
* ListView.
*/
int newTop = view.getTop();
int newBottom = view.getBottom();
int newHeight = newBottom - newTop;
int oldHeight = oldBottom - oldTop;
int delta = newHeight - oldHeight;
mTranslate = getTopAndBottomTranslations(oldTop, oldBottom, delta, true);
int currentTop = view.getTop();
int futureTop = oldTop - mTranslate[0];
int firstChildStartTop = getChildAt(0).getTop();
int firstVisiblePosition = getFirstVisiblePosition();
int deltaTop = currentTop - futureTop;
int i;
int childCount = getChildCount();
for (i = 0; i < childCount; i++) {
View v = getChildAt(i);
int height = v.getBottom() - Math.max(0, v.getTop());
if (deltaTop - height > 0) {
firstVisiblePosition++;
deltaTop -= height;
} else {
break;
}
}
if (i > 0) {
firstChildStartTop = 0;
}
setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop);
/*
* Request another layout to update the layout parameters of
* the cells.
*/
requestLayout();
/*
* Return false such that the ListView does not redraw its
* contents on this layout but only updates all the
* parameters associated with its children.
*/
return false;
}
/*
* Remove the predraw listener so this method does not keep
* getting called.
*/
mShouldRemoveObserver = false;
observer.removeOnPreDrawListener(this);
int yTranslateTop = mTranslate[0];
int yTranslateBottom = mTranslate[1];
ArrayList<Animator> animations = new ArrayList<Animator>();
int index = indexOfChild(view);
/*
* Loop through all the views that were on the screen before the
* cell was expanded. Some cells will still be children of the
* ListView while others will not. The cells that remain
* children of the ListView simply have their bounds animated
* appropriately. The cells that are no longer children of the
* ListView also have their bounds animated, but must also be
* added to a list of views which will be drawn in dispatchDraw.
*/
for (View v : oldCoordinates.keySet()) {
int[] old = oldCoordinates.get(v);
v.setTop(old[0]);
v.setBottom(old[1]);
if (v.getParent() == null) {
mViewsToDraw.add(v);
int delta = old[0] < oldTop ? -yTranslateTop : yTranslateBottom;
animations.add(getAnimation(v, delta, delta));
} else {
int i = indexOfChild(v);
if (v != view) {
int delta = i > index ? yTranslateBottom : -yTranslateTop;
animations.add(getAnimation(v, delta, delta));
}
v.setHasTransientState(false);
}
}
/* Adds animation for expanding the cell that was clicked. */
animations.add(getAnimation(view, -yTranslateTop, yTranslateBottom));
/* Adds an animation for fading in the extra content. */
animations.add(ObjectAnimator.ofFloat(expandingLayout, ALPHA, 0, 1));
/* Disabled the ListView for the duration of the animation. */
setEnabled(false);
setClickable(false);
/*
* Play all the animations created above together at the same
* time.
*/
AnimatorSet s = new AnimatorSet();
s.playTogether(animations);
s.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setExpanded(true);// card.setExpanded(true);
setEnabled(true);
setClickable(true);
if (mViewsToDraw.size() > 0) {
for (View v : mViewsToDraw) {
if (Build.VERSION.SDK_INT >= 16) {
v.setHasTransientState(false);
}
}
}
mViewsToDraw.clear();
if (card.getOnExpandAnimatorEndListener() != null)
card.getOnExpandAnimatorEndListener().onExpandEnd(card);
}
});
s.start();
return true;
}
});
}
private void prepareCollapseView(final CardView view, final View expandingLayout) {
final Card card = (Card) getItemAtPosition(getPositionForView(view));
/* Store the original top and bottom bounds of all the cells. */
final int oldTop = view.getTop();
final int oldBottom = view.getBottom();
final HashMap<View, int[]> oldCoordinates = new HashMap<View, int[]>();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
if (Build.VERSION.SDK_INT >= 16) {
v.setHasTransientState(true);
}
oldCoordinates.put(v, new int[] { v.getTop(), v.getBottom() });
}
/* Update the layout so the extra content becomes invisible. */
view.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, view
.getCollapsedHeight()));
/* Add an onPreDraw listener. */
final ViewTreeObserver observer = getViewTreeObserver();
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (!mShouldRemoveObserver) {
/*
* Same as for expandingView, the parameters for
* setSelectionFromTop must be determined such that the
* necessary cells of the ListView are rendered and added to
* it.
*/
mShouldRemoveObserver = true;
int newTop = view.getTop();
int newBottom = view.getBottom();
int newHeight = newBottom - newTop;
int oldHeight = oldBottom - oldTop;
int deltaHeight = oldHeight - newHeight;
mTranslate = getTopAndBottomTranslations(oldTop, oldBottom, deltaHeight, false);
int currentTop = view.getTop();
int futureTop = oldTop + mTranslate[0];
int firstChildStartTop = getChildAt(0).getTop();
int firstVisiblePosition = getFirstVisiblePosition();
int deltaTop = currentTop - futureTop;
int i;
int childCount = getChildCount();
for (i = 0; i < childCount; i++) {
View v = getChildAt(i);
int height = v.getBottom() - Math.max(0, v.getTop());
if (deltaTop - height > 0) {
firstVisiblePosition++;
deltaTop -= height;
} else {
break;
}
}
if (i > 0) {
firstChildStartTop = 0;
}
setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop);
requestLayout();
return false;
}
mShouldRemoveObserver = false;
observer.removeOnPreDrawListener(this);
int yTranslateTop = mTranslate[0];
int yTranslateBottom = mTranslate[1];
int index = indexOfChild(view);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
int[] old = oldCoordinates.get(v);
if (old != null) {
/*
* If the cell was present in the ListView before the
* collapse and after the collapse then the bounds are
* reset to their old values.
*/
v.setTop(old[0]);
v.setBottom(old[1]);
if (Build.VERSION.SDK_INT >= 16) {
v.setHasTransientState(false);
}
} else {
/*
* If the cell is present in the ListView after the
* collapse but not before the collapse then the bounds
* are calculated using the bottom and top translation
* of the collapsing cell.
*/
int delta = i > index ? yTranslateBottom : -yTranslateTop;
v.setTop(v.getTop() + delta);
v.setBottom(v.getBottom() + delta);
}
}
/*
* Animates all the cells present on the screen after the
* collapse.
*/
ArrayList<Animator> animations = new ArrayList<Animator>();
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
if (v != view) {
float diff = i > index ? -yTranslateBottom : yTranslateTop;
animations.add(getAnimation(v, diff, diff));
}
}
/*
* ValueAnimator animator = ValueAnimator.ofInt(
* yTranslateTop,-yTranslateBottom);
* animator.addUpdateListener(new
* ValueAnimator.AnimatorUpdateListener() {
*
* @Override public void onAnimationUpdate(ValueAnimator
* valueAnimator) { int value = (Integer)
* valueAnimator.getAnimatedValue();
*
* ViewGroup.LayoutParams layoutParams =
* expandingLayout.getLayoutParams(); layoutParams.height =
* value; expandingLayout.setLayoutParams(layoutParams); } });
* animations.add(animator);
*/
/* Adds animation for collapsing the cell that was clicked. */
animations.add(getAnimation(view, yTranslateTop, -yTranslateBottom));
/* Adds an animation for fading out the extra content. */
animations.add(ObjectAnimator.ofFloat(expandingLayout, ALPHA, 1, 0));
/* Disabled the ListView for the duration of the animation. */
setEnabled(false);
setClickable(false);
/*
* Play all the animations created above together at the same
* time.
*/
AnimatorSet s = new AnimatorSet();
s.playTogether(animations);
s.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
expandingLayout.setVisibility(View.GONE);
view.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT,
AbsListView.LayoutParams.WRAP_CONTENT));
view.setExpanded(false);
setEnabled(true);
setClickable(true);
/*
* Note that alpha must be set back to 1 in case this
* view is reused by a cell that was expanded, but not
* yet collapsed, so its state should persist in an
* expanded state with the extra content visible.
*/
expandingLayout.setAlpha(1);
if (card.getOnCollapseAnimatorEndListener() != null)
card.getOnCollapseAnimatorEndListener().onCollapseEnd(card);
}
});
s.start();
return true;
}
});
}
private int[] getTopAndBottomTranslations(int top, int bottom, int yDelta, boolean isExpanding) {
int yTranslateTop = 0;
int yTranslateBottom = yDelta;
int height = bottom - top;
if (isExpanding) {
boolean isOverTop = top < 0;
boolean isBelowBottom = (top + height + yDelta) > getHeight();
if (isOverTop) {
yTranslateTop = top;
yTranslateBottom = yDelta - yTranslateTop;
} else if (isBelowBottom) {
int deltaBelow = top + height + yDelta - getHeight();
yTranslateTop = top - deltaBelow < 0 ? top : deltaBelow;
yTranslateBottom = yDelta - yTranslateTop;
}
} else {
int offset = computeVerticalScrollOffset();
int range = computeVerticalScrollRange();
int extent = computeVerticalScrollExtent();
int leftoverExtent = range - offset - extent;
boolean isCollapsingBelowBottom = (yTranslateBottom > leftoverExtent);
boolean isCellCompletelyDisappearing = bottom - yTranslateBottom < 0;
if (isCollapsingBelowBottom) {
yTranslateTop = yTranslateBottom - leftoverExtent;
yTranslateBottom = yDelta - yTranslateTop;
} else if (isCellCompletelyDisappearing) {
yTranslateBottom = bottom;
yTranslateTop = yDelta - yTranslateBottom;
}
}
return new int[] { yTranslateTop, yTranslateBottom };
}
private Animator getAnimation(final View view, float translateTop, float translateBottom) {
int top = view.getTop();
int bottom = view.getBottom();
int endTop = (int) (top + translateTop);
int endBottom = (int) (bottom + translateBottom);
PropertyValuesHolder translationTop = PropertyValuesHolder.ofInt("top", top, endTop);
PropertyValuesHolder translationBottom = PropertyValuesHolder.ofInt("bottom", bottom, endBottom);
return ObjectAnimator.ofPropertyValuesHolder(view, translationTop, translationBottom);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mViewsToDraw.size() == 0) {
return;
}
for (View v : mViewsToDraw) {
canvas.translate(0, v.getTop());
v.draw(canvas);
canvas.translate(0, -v.getTop());
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment