content: package io.virtualapp.widgets;
  
  import java.util.ArrayList;
  import java.util.List;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.AnimatorSet;
  import android.animation.ObjectAnimator;
  import android.content.Context;
  import android.content.res.Resources;
  import android.util.DisplayMetrics;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.animation.DecelerateInterpolator;
  import android.widget.FrameLayout;
  
  import io.virtualapp.R;
  
  /**
   * This class acts as an adapter for the {@link CardStackLayout} view. This
   * adapter is intentionally made an abstract class with following abstract
   * methods -
   * <p>
   * <p>
   * {@link #getCount()} - Decides the number of views present in the view
   * <p>
   * {@link #createView(int, ViewGroup)} - Creates the view for all positions in
   * range [0, {@link #getCount()})
   * <p>
   * Contains the logic for touch events in {@link #onTouch(View, MotionEvent)}
   */
  public abstract class CardStackAdapter implements View.OnTouchListener, View.OnClickListener {
  
  	public static final int ANIM_DURATION = 600;
  	public static final int DECELERATION_FACTOR = 2;
  
  	public static final int INVALID_CARD_POSITION = -1;
  	private final int mScreenHeight;
  	private final int dp30;
  	// Settings for the adapter from layout
  	private float mCardGapBottom;
  	private float mCardGap;
  	private int mParallaxScale;
  	private boolean mParallaxEnabled;
  	private boolean mShowInitAnimation;
  	private int fullCardHeight;
  	private View[] mCardViews;
  	private float dp8;
  	private CardStackLayout mParent;
  
  	private boolean mScreenTouchable = false;
  	private float mTouchFirstY = -1;
  	private float mTouchPrevY = -1;
  	private float mTouchDistance = 0;
  	private int mSelectedCardPosition = INVALID_CARD_POSITION;
  	private float scaleFactorForElasticEffect;
  	private int mParentPaddingTop = 0;
  	private int mCardPaddingInternal = 0;
  
  	public CardStackAdapter(Context context) {
  		Resources resources = context.getResources();
  
  		DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
  		mScreenHeight = dm.heightPixels;
  		dp30 = (int) resources.getDimension(R.dimen.dp30);
  		scaleFactorForElasticEffect = (int) resources.getDimension(R.dimen.dp8);
  		dp8 = (int) resources.getDimension(R.dimen.dp8);
  	}
  
  	protected float getCardGapBottom() {
  		return mCardGapBottom;
  	}
  
  	/**
  	 * Defines and initializes the view to be shown in the
  	 * {@link CardStackLayout} Provides two parameters to the sub-class namely -
  	 *
  	 * @param position
  	 * @param container
  	 * @return View corresponding to the position and parent container
  	 */
  	public abstract View createView(int position, ViewGroup container);
  
  	/**
  	 * Defines the number of cards that are present in the
  	 * {@link CardStackLayout}
  	 *
  	 * @return cardCount - Number of views in the related
  	 *         {@link CardStackLayout}
  	 */
  	public abstract int getCount();
  
  	/**
  	 * Returns true if no animation is in progress currently. Can be used to
  	 * disable any events if they are not allowed during an animation. Returns
  	 * false if an animation is in progress.
  	 *
  	 * @return - true if animation in progress, false otherwise
  	 */
  	public boolean isScreenTouchable() {
  		return mScreenTouchable;
  	}
  
  	private void setScreenTouchable(boolean screenTouchable) {
  		this.mScreenTouchable = screenTouchable;
  	}
  
  	void addView(final int position) {
  		View root = createView(position, mParent);
  		root.setOnTouchListener(this);
  		root.setTag(R.id.cardstack_internal_position_tag, position);
  		root.setLayerType(View.LAYER_TYPE_HARDWARE, null);
  
  		mCardPaddingInternal = root.getPaddingTop();
  
  		FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, fullCardHeight);
  		root.setLayoutParams(lp);
  		if (mShowInitAnimation) {
  			root.setY(getCardFinalY(position));
  			setScreenTouchable(false);
  		} else {
  			root.setY(getCardOriginalY(position) - mParentPaddingTop);
  			setScreenTouchable(true);
  		}
  
  		mCardViews[position] = root;
  
  		mParent.addView(root);
  	}
  
  	protected float getCardFinalY(int position) {
  		return mScreenHeight - dp30 - ((getCount() - position) * mCardGapBottom) - mCardPaddingInternal;
  	}
  
  	protected float getCardOriginalY(int position) {
  		return mParentPaddingTop + mCardGap * position;
  	}
  
  	/**
  	 * Resets all cards in {@link CardStackLayout} to their initial positions
  	 *
  	 * @param r
  	 *            Execute r.run() once the reset animation is done
  	 */
  	public void resetCards(Runnable r) {
  		List<Animator> animations = new ArrayList<>(getCount());
  		for (int i = 0; i < getCount(); i++) {
  			final View child = mCardViews[i];
  			animations.add(ObjectAnimator.ofFloat(child, View.Y, (int) child.getY(), getCardOriginalY(i)));
  		}
  		startAnimations(animations, r, true);
  	}
  
  	/**
  	 * Plays together all animations passed in as parameter. Once animation is
  	 * completed, r.run() is executed. If parameter isReset is set to true,
  	 * {@link #mSelectedCardPosition} is set to {@link #INVALID_CARD_POSITION}
  	 *
  	 * @param animations
  	 * @param r
  	 * @param isReset
  	 */
  	private void startAnimations(List<Animator> animations, final Runnable r, final boolean isReset) {
  		AnimatorSet animatorSet = new AnimatorSet();
  		animatorSet.playTogether(animations);
  		animatorSet.setDuration(ANIM_DURATION);
  		animatorSet.setInterpolator(new DecelerateInterpolator(DECELERATION_FACTOR));
  		animatorSet.addListener(new AnimatorListenerAdapter() {
  			@Override
  			public void onAnimationEnd(Animator animation) {
  				if (r != null)
  					r.run();
  				setScreenTouchable(true);
  				if (isReset)
  					mSelectedCardPosition = INVALID_CARD_POSITION;
  			}
  		});
  		animatorSet.start();
  	}
  
  	@Override
  	public boolean onTouch(View v, MotionEvent event) {
  		if (!isScreenTouchable()) {
  			return false;
  		}
  
  		float y = event.getRawY();
  		int positionOfCardToMove = (int) v.getTag(R.id.cardstack_internal_position_tag);
  
  		switch (event.getAction()) {
  			case MotionEvent.ACTION_DOWN :
  				if (mTouchFirstY != -1) {
  					return false;
  				}
  				mTouchPrevY = mTouchFirstY = y;
  				mTouchDistance = 0;
  				break;
  			case MotionEvent.ACTION_MOVE :
  				if (mSelectedCardPosition == INVALID_CARD_POSITION)
  					moveCards(positionOfCardToMove, y - mTouchFirstY);
  				mTouchDistance += Math.abs(y - mTouchPrevY);
  				break;
  			case MotionEvent.ACTION_CANCEL :
  			case MotionEvent.ACTION_UP :
  				if (mTouchDistance < dp8 && Math.abs(y - mTouchFirstY) < dp8
  						&& mSelectedCardPosition == INVALID_CARD_POSITION) {
  					onClick(v);
  				} else {
  					resetCards();
  				}
  				mTouchPrevY = mTouchFirstY = -1;
  				mTouchDistance = 0;
  				return false;
  		}
  		return true;
  	}
  
  	@Override
  	public void onClick(final View v) {
  
  		if (!isScreenTouchable()) {
  			return;
  		}
  		setScreenTouchable(false);
  		if (mSelectedCardPosition == INVALID_CARD_POSITION) {
  			mSelectedCardPosition = (int) v.getTag(R.id.cardstack_internal_position_tag);
  
  			List<Animator> animations = new ArrayList<>(getCount());
  			for (int i = 0; i < getCount(); i++) {
  				View child = mCardViews[i];
  				animations.add(getAnimatorForView(child, i, mSelectedCardPosition));
  			}
  			startAnimations(animations, () -> {
  				setScreenTouchable(true);
  				if (mParent.getOnCardSelectedListener() != null) {
  					mParent.getOnCardSelectedListener().onCardSelected(v, mSelectedCardPosition);
  				}
  			}, false);
  
  		}
  	}
  
  	/**
  	 * This method can be overridden to have different animations for each card
  	 * when a click event happens on any card view. This method will be called
  	 * for every
  	 *
  	 * @param view
  	 *            The view for which this method needs to return an animator
  	 * @param selectedCardPosition
  	 *            Position of the card that was clicked
  	 * @param currentCardPosition
  	 *            Position of the current card
  	 * @return animator which has to be applied on the current card
  	 */
  	protected Animator getAnimatorForView(View view, int currentCardPosition, int selectedCardPosition) {
  		if (currentCardPosition != selectedCardPosition) {
  			return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(), getCardFinalY(currentCardPosition));
  		} else {
  			return ObjectAnimator.ofFloat(view, View.Y, (int) view.getY(),
  					getCardOriginalY(0) + (currentCardPosition * mCardGapBottom));
  		}
  	}
  
  	private void moveCards(int positionOfCardToMove, float diff) {
  		if (diff < 0 || positionOfCardToMove < 0 || positionOfCardToMove >= getCount())
  			return;
  		for (int i = positionOfCardToMove; i < getCount(); i++) {
  			final View child = mCardViews[i];
  			float diffCard = diff / scaleFactorForElasticEffect;
  			if (mParallaxEnabled) {
  				if (mParallaxScale > 0) {
  					diffCard = diffCard * (mParallaxScale / 3) * (getCount() + 1 - i);
  				} else {
  					int scale = mParallaxScale * -1;
  					diffCard = diffCard * (i * (scale / 3) + 1);
  				}
  			} else
  				diffCard = diffCard * (getCount() * 2 + 1);
  			child.setY(getCardOriginalY(i) + diffCard);
  		}
  	}
  
  	/**
  	 * Provides an API to {@link CardStackLayout} to set the parameters provided
  	 * to it in its XML
  	 *
  	 * @param cardStackLayout
  	 *            Parent of all cards
  	 */
  	void setAdapterParams(CardStackLayout cardStackLayout) {
  		mParent = cardStackLayout;
  		mCardViews = new View[getCount()];
  		mCardGapBottom = cardStackLayout.getCardGapBottom();
  		mCardGap = cardStackLayout.getCardGap();
  		mParallaxScale = cardStackLayout.getParallaxScale();
  		mParallaxEnabled = cardStackLayout.isParallaxEnabled();
  		if (mParallaxEnabled && mParallaxScale == 0)
  			mParallaxEnabled = false;
  		mShowInitAnimation = cardStackLayout.isShowInitAnimation();
  		mParentPaddingTop = cardStackLayout.getPaddingTop();
  		fullCardHeight = (int) (mScreenHeight - dp30 - dp8 - getCount() * mCardGapBottom);
  	}
  
  	/**
  	 * Resets all cards in {@link CardStackLayout} to their initial positions
  	 */
  	public void resetCards() {
  		resetCards(null);
  	}
  
  	/**
  	 * Returns false if all the cards are in their initial position i.e. no card
  	 * is selected
  	 * <p>
  	 * Returns true if the {@link CardStackLayout} has a card selected and all
  	 * other cards are at the bottom of the screen.
  	 *
  	 * @return true if any card is selected, false otherwise
  	 */
  	public boolean isCardSelected() {
  		return mSelectedCardPosition != INVALID_CARD_POSITION;
  	}
  
  	/**
  	 * Returns the position of selected card. If no card is selected, returns
  	 * {@link #INVALID_CARD_POSITION}
  	 */
  	public int getSelectedCardPosition() {
  		return mSelectedCardPosition;
  	}
  
  	/**
  	 * Since there is no view recycling in {@link CardStackLayout}, we maintain
  	 * an instance of every view that is set for every position. This method
  	 * returns a view at the requested position.
  	 *
  	 * @param position
  	 *            Position of card in {@link CardStackLayout}
  	 * @return View at requested position
  	 */
  	public View getCardView(int position) {
  		if (mCardViews == null)
  			return null;
  
  		return mCardViews[position];
  	}
  }