package de.geeksfactory.opacclient.ui; import android.content.Context; import android.os.Handler; import android.support.v7.widget.CardView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.LinearLayout; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.AnimatorListenerAdapter; import com.nineoldandroids.animation.AnimatorSet; import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.view.ViewHelper; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import de.geeksfactory.opacclient.R; /** * Mananges a list of cards that can be expanded and collapsed with an animation */ public abstract class ExpandingCardListManager { private static final int ANIMATION_DURATION = 500; private Context context; private LayoutInflater inflater; private LinearLayout layout; private int expandedPosition = -1; private CardView mainCard; private LinearLayout llMain; private CardView upperCard; private LinearLayout llUpper; private CardView lowerCard; private LinearLayout llLower; private CardView expandedCard; private List<View> views = new ArrayList<>(); private AnimationInterceptor interceptor; private int unexpandedHeight; private float expandedTranslationY; private float lowerTranslationY; private int heightDifference; /** * An interface to influence the animation created by ExpandingCardListManager by adding additional animations. */ public interface AnimationInterceptor { void beforeExpand(View unexpandedView); Collection<Animator> getExpandAnimations(int heightDifference, View expandedView); Collection<Animator> getCollapseAnimations(int heightDifference, View expandedView); void onCollapseAnimationEnd(); } public ExpandingCardListManager (Context context, LinearLayout layout) { this.context = context; this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.layout = layout; initViews(); addViews(); } private void initViews() { layout.removeAllViews(); LinearLayout.LayoutParams expandedCardParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); int sideMargin2 = context.getResources().getDimensionPixelSize(R.dimen.card_side_margin_selected); int topbottomMargin2 = context.getResources().getDimensionPixelSize(R.dimen.card_topbottom_margin_selected); expandedCardParams.setMargins(sideMargin2, topbottomMargin2, sideMargin2, topbottomMargin2); float maxElevation = context.getResources().getDimension(R.dimen.card_elevation_default); float maxElevationSelected = context.getResources().getDimension(R.dimen.card_elevation_selected); CardView.LayoutParams listParams = new CardView.LayoutParams(CardView.LayoutParams.MATCH_PARENT, CardView.LayoutParams.WRAP_CONTENT); // Upper card upperCard = new CardView(context); upperCard.setVisibility(View.GONE); upperCard.setMaxCardElevation(maxElevation); upperCard.setUseCompatPadding(true); llUpper = new LinearLayout(context); llUpper.setLayoutParams(listParams); llUpper.setOrientation(LinearLayout.VERTICAL); upperCard.addView(llUpper); // Expanded card expandedCard = new CardView(context); expandedCard.setVisibility(View.GONE); expandedCard.setMaxCardElevation(maxElevationSelected); expandedCard.setUseCompatPadding(true); LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); int sideMargin = context.getResources().getDimensionPixelSize(R.dimen.card_side_margin_default) + expandedCard.getPaddingLeft() - upperCard.getPaddingLeft(); int topbottomMargin = context.getResources().getDimensionPixelSize(R.dimen.card_topbottom_margin_default); cardParams.setMargins(sideMargin, topbottomMargin, sideMargin, topbottomMargin); upperCard.setLayoutParams(cardParams); expandedCard.setLayoutParams(expandedCardParams); layout.addView(upperCard); layout.addView(expandedCard); // Lower card lowerCard = new CardView(context); lowerCard.setVisibility(View.GONE); lowerCard.setMaxCardElevation(maxElevation); lowerCard.setUseCompatPadding(true); lowerCard.setLayoutParams(cardParams); llLower = new LinearLayout(context); llLower.setLayoutParams(listParams); llLower.setOrientation(LinearLayout.VERTICAL); lowerCard.addView(llLower); layout.addView(lowerCard); // Main card mainCard = new CardView(context); mainCard.setMaxCardElevation(maxElevation); mainCard.setUseCompatPadding(true); mainCard.setLayoutParams(cardParams); llMain = new LinearLayout(context); llMain.setLayoutParams(listParams); llMain.setOrientation(LinearLayout.VERTICAL); mainCard.addView(llMain); layout.addView(mainCard); } private void addViews() { for (int i = 0; i < getCount(); i++) { View view = getView(i, llMain); views.add(view); llMain.addView(view); if (i < getCount() - 1) addSeparator(llMain); } } private void addSeparator(ViewGroup parent) { View separator = inflater.inflate(R.layout.card_list_separator, parent, false); parent.addView(separator); } public void expand(final int position) { if (isExpanded()) { if (expandedPosition != position) { collapse(new CompleteListener() { @Override public void onComplete() { expand(position); } }); } return; } resetViews(); for (int i = 0; i < position; i++) { llUpper.addView(getView(i, llUpper)); if (i < position - 1) addSeparator(llUpper); } final View expandedView = getView(position, expandedCard); expandView(position, expandedView); expandedCard.addView(expandedView); for (int i = position + 1; i < getCount(); i++) { llLower.addView(getView(i, llLower)); if (i < getCount() - 1) addSeparator(llLower); } final float lowerPos; if (position + 1 < getCount()) { lowerPos = ViewHelper.getY(views.get(position + 1)) + context.getResources() .getDimensionPixelSize(R.dimen.card_topbottom_margin_default); } else lowerPos = -1; final float mainPos = ViewHelper.getY(views.get(position)) - mainCard.getPaddingTop(); unexpandedHeight = views.get(position).getHeight(); if (interceptor != null) interceptor.beforeExpand(views.get(position)); // Wait a little so that touch feedback is visible before hiding buttons new Handler().postDelayed(new Runnable() { @Override public void run() { if (position != 0) upperCard.setVisibility(View.VISIBLE); if (lowerPos > 0) lowerCard.setVisibility(View.VISIBLE); expandedCard.setVisibility(View.VISIBLE); mainCard.setVisibility(View.GONE); final int previousHeight = layout.getHeight(); layout.getViewTreeObserver() .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { int newHeight = layout.getHeight(); heightDifference = newHeight - previousHeight; layout.getViewTreeObserver().removeOnPreDrawListener(this); if (lowerPos > 0) ViewHelper.setY(lowerCard, lowerPos); ViewHelper.setY(expandedCard, mainPos); lowerTranslationY = ViewHelper.getTranslationY(lowerCard); expandedTranslationY = ViewHelper.getTranslationY(expandedCard); AnimatorSet set = new AnimatorSet(); int defaultMargin = context.getResources().getDimensionPixelSize( R.dimen.card_side_margin_default); int expandedMargin = context.getResources().getDimensionPixelSize( R.dimen.card_side_margin_selected); int marginDifference = expandedMargin - defaultMargin; List<Animator> animators = new ArrayList<>(); addAll(animators, ObjectAnimator .ofFloat(lowerCard, "translationY", ViewHelper.getTranslationY(lowerCard), 0), ObjectAnimator.ofFloat(expandedCard, "translationY", ViewHelper.getTranslationY(expandedCard), 0), ObjectAnimator.ofFloat(expandedCard, "cardElevation", context.getResources() .getDimension(R.dimen.card_elevation_default), context.getResources() .getDimension( R.dimen.card_elevation_selected)), ObjectAnimator.ofInt(expandedCard, "bottom", expandedCard.getBottom() + unexpandedHeight - expandedView.getHeight(), expandedCard.getBottom()), ObjectAnimator.ofInt(expandedCard, "left", expandedCard.getLeft() - marginDifference, expandedCard.getLeft()), ObjectAnimator.ofInt(expandedCard, "right", expandedCard.getRight() + marginDifference, expandedCard.getRight()) ); if (interceptor != null) { animators .addAll(interceptor .getExpandAnimations(heightDifference, expandedView)); } set.playTogether(animators); set.setDuration(ANIMATION_DURATION).start(); return false; } }); expandedPosition = position; } }, 100); } public void collapse() { collapse(null); } private void collapse(final CompleteListener listener) { AnimatorSet set = new AnimatorSet(); View expandedView = expandedCard.getChildAt(0); if (expandedView == null) { return; } int defaultMargin = context.getResources().getDimensionPixelSize( R.dimen.card_side_margin_default); int expandedMargin = context.getResources().getDimensionPixelSize( R.dimen.card_side_margin_selected); int marginDifference = expandedMargin - defaultMargin; List<Animator> animators = new ArrayList<>(); addAll(animators, ObjectAnimator .ofFloat(lowerCard, "translationY", 0, lowerTranslationY), ObjectAnimator.ofFloat(expandedCard, "translationY", 0, expandedTranslationY), ObjectAnimator.ofFloat(expandedCard, "cardElevation", context.getResources() .getDimension(R.dimen.card_elevation_selected), context.getResources().getDimension(R.dimen.card_elevation_default)), ObjectAnimator.ofInt(expandedCard, "bottom", expandedCard.getBottom(), expandedCard.getBottom() + unexpandedHeight - expandedView.getHeight()), ObjectAnimator.ofInt(expandedCard, "left", expandedCard.getLeft(), expandedCard.getLeft() - marginDifference), ObjectAnimator.ofInt(expandedCard, "right", expandedCard.getRight(), expandedCard.getRight() + marginDifference) ); if (interceptor != null) animators.addAll(interceptor.getCollapseAnimations( -heightDifference, expandedView)); set.playTogether(animators); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); resetViews(); if (listener != null) listener.onComplete(); } }); set.setDuration(ANIMATION_DURATION).start(); } public void resetViews() { if (interceptor != null) interceptor.onCollapseAnimationEnd(); mainCard.setVisibility(View.VISIBLE); upperCard.setVisibility(View.GONE); lowerCard.setVisibility(View.GONE); expandedCard.clearAnimation(); expandedCard.setVisibility(View.GONE); llUpper.removeAllViews(); llLower.removeAllViews(); expandedCard.removeAllViews(); expandedPosition = -1; unexpandedHeight = 0; expandedTranslationY = 0; lowerTranslationY = 0; heightDifference = 0; } public void notifyDataSetChanged() { llMain.removeAllViews(); llUpper.removeAllViews(); llLower.removeAllViews(); expandedCard.removeAllViews(); addViews(); } public boolean isExpanded() { return expandedPosition != -1; } public void setAnimationInterceptor(AnimationInterceptor interceptor) { this.interceptor = interceptor; } public abstract View getView(int position, ViewGroup container); public abstract void expandView(int position, View view); public abstract void collapseView(int position, View view); public abstract int getCount(); private void addAll(Collection collection, Object... items) { collection.addAll(Arrays.asList(items)); } private interface CompleteListener { public void onComplete(); } public int getExpandedPosition() { return expandedPosition; } }