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]; } }