Home | History | Annotate | Download | only in cardflip
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.cardflip;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.AnimatorSet;
     22 import android.app.Activity;
     23 import android.os.Build;
     24 import android.os.Bundle;
     25 import android.view.GestureDetector;
     26 import android.view.MotionEvent;
     27 import android.view.ViewTreeObserver;
     28 import android.widget.RelativeLayout;
     29 
     30 import java.util.ArrayList;
     31 import java.util.List;
     32 
     33 /**
     34  * This application creates 2 stacks of playing cards. Using fling events,
     35  * these cards can be flipped from one stack to another where each flip comes with
     36  * an associated animation. The cards can be flipped horizontally from left to right
     37  * or right to left depending on which stack the animating card currently belongs to.
     38  *
     39  * This application demonstrates an animation where a stack of cards can either be
     40  * be rotated out or back in about their bottom left corner in a counter-clockwise direction.
     41  * Rotate out: Down fling on stack of cards
     42  * Rotate in: Up fling on stack of cards
     43  * Full rotation: Tap on stack of cards
     44  *
     45  * Note that in this demo touch events are disabled in the middle of any animation so
     46  * only one card can be flipped at a time. When the cards are in a rotated-out
     47  * state, no new cards can be rotated to or from that stack. These changes were made to
     48  * simplify the code for this demo.
     49  */
     50 
     51 public class CardFlip extends Activity implements CardFlipListener {
     52 
     53     final static int CARD_PILE_OFFSET = 3;
     54     final static int STARTING_NUMBER_CARDS = 15;
     55     final static int RIGHT_STACK = 0;
     56     final static int LEFT_STACK = 1;
     57 
     58     int mCardWidth = 0;
     59     int mCardHeight = 0;
     60 
     61     int mVerticalPadding;
     62     int mHorizontalPadding;
     63 
     64     boolean mTouchEventsEnabled = true;
     65     boolean[] mIsStackEnabled;
     66 
     67     RelativeLayout mLayout;
     68 
     69     List<ArrayList<CardView>> mStackCards;
     70 
     71     GestureDetector gDetector;
     72 
     73     @Override
     74     public void onCreate(Bundle savedInstanceState) {
     75         super.onCreate(savedInstanceState);
     76         setContentView(R.layout.main);
     77 
     78         mStackCards = new ArrayList<ArrayList<CardView>>();
     79         mStackCards.add(new ArrayList<CardView>());
     80         mStackCards.add(new ArrayList<CardView>());
     81 
     82         mIsStackEnabled = new boolean[2];
     83         mIsStackEnabled[0] = true;
     84         mIsStackEnabled[1] = true;
     85 
     86         mVerticalPadding = getResources().getInteger(R.integer.vertical_card_magin);
     87         mHorizontalPadding = getResources().getInteger(R.integer.horizontal_card_magin);
     88 
     89         gDetector = new GestureDetector(this, mGestureListener);
     90 
     91         mLayout = (RelativeLayout)findViewById(R.id.main_relative_layout);
     92         ViewTreeObserver observer = mLayout.getViewTreeObserver();
     93         observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
     94             @Override
     95             public void onGlobalLayout() {
     96                 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
     97                     mLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
     98                 } else {
     99                     mLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    100                 }
    101 
    102                 mCardHeight = mLayout.getHeight();
    103                 mCardWidth = mLayout.getWidth() / 2;
    104 
    105                 for (int x = 0; x < STARTING_NUMBER_CARDS; x++) {
    106                     addNewCard(RIGHT_STACK);
    107                 }
    108             }
    109         });
    110     }
    111 
    112     /**
    113      * Adds a new card to the specified stack. Also performs all the necessary layout setup
    114      * to place the card in the correct position.
    115      */
    116     public void addNewCard(int stack) {
    117         CardView view = new CardView(this);
    118         view.updateTranslation(mStackCards.get(stack).size());
    119         view.setCardFlipListener(this);
    120         view.setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding);
    121 
    122         RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(mCardWidth,
    123                 mCardHeight);
    124         params.topMargin = 0;
    125         params.leftMargin = (stack == RIGHT_STACK ? mCardWidth : 0);
    126 
    127         mStackCards.get(stack).add(view);
    128         mLayout.addView(view, params);
    129     }
    130 
    131     /**
    132      * Gesture Detector listens for fling events in order to potentially initiate
    133      * a card flip event when a fling event occurs. Also listens for tap events in
    134      * order to potentially initiate a full rotation animation.
    135      */
    136     private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector
    137             .SimpleOnGestureListener() {
    138         @Override
    139         public boolean onSingleTapUp(MotionEvent motionEvent) {
    140             int stack = getStack(motionEvent);
    141             rotateCardsFullRotation(stack, CardView.Corner.BOTTOM_LEFT);
    142             return true;
    143         }
    144 
    145         @Override
    146         public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent2, float v,
    147                                float v2) {
    148             int stack = getStack(motionEvent);
    149             ArrayList<CardView> cardStack = mStackCards.get(stack);
    150             int size = cardStack.size();
    151             if (size > 0) {
    152                 rotateCardView(cardStack.get(size - 1), stack, v, v2);
    153             }
    154             return true;
    155         }
    156     };
    157 
    158     /** Returns the appropriate stack corresponding to the MotionEvent. */
    159     public int getStack(MotionEvent ev) {
    160         boolean isLeft = ev.getX() <= mCardWidth;
    161         return isLeft ? LEFT_STACK : RIGHT_STACK;
    162     }
    163 
    164     /**
    165      * Uses the stack parameter, along with the velocity values of the fling event
    166      * to determine in what direction the card must be flipped. By the same logic, the
    167      * new stack that the card belongs to after the animation is also determined
    168      * and updated.
    169      */
    170     public void rotateCardView(final CardView cardView, int stack, float velocityX,
    171                                float velocityY) {
    172 
    173         boolean xGreaterThanY = Math.abs(velocityX) > Math.abs(velocityY);
    174 
    175         boolean bothStacksEnabled = mIsStackEnabled[RIGHT_STACK] && mIsStackEnabled[LEFT_STACK];
    176 
    177         ArrayList<CardView>leftStack = mStackCards.get(LEFT_STACK);
    178         ArrayList<CardView>rightStack = mStackCards.get(RIGHT_STACK);
    179 
    180         switch (stack) {
    181             case RIGHT_STACK:
    182                 if (velocityX < 0 &&  xGreaterThanY) {
    183                     if (!bothStacksEnabled) {
    184                         break;
    185                     }
    186                     mLayout.bringChildToFront(cardView);
    187                     mLayout.requestLayout();
    188                     rightStack.remove(rightStack.size() - 1);
    189                     leftStack.add(cardView);
    190                     cardView.flipRightToLeft(leftStack.size() - 1, (int)velocityX);
    191                     break;
    192                 } else if (!xGreaterThanY) {
    193                     boolean rotateCardsOut = velocityY > 0;
    194                     rotateCards(RIGHT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut);
    195                 }
    196                 break;
    197             case LEFT_STACK:
    198                 if (velocityX > 0 && xGreaterThanY) {
    199                     if (!bothStacksEnabled) {
    200                         break;
    201                     }
    202                     mLayout.bringChildToFront(cardView);
    203                     mLayout.requestLayout();
    204                     leftStack.remove(leftStack.size() - 1);
    205                     rightStack.add(cardView);
    206                     cardView.flipLeftToRight(rightStack.size() - 1, (int)velocityX);
    207                     break;
    208                 } else if (!xGreaterThanY) {
    209                     boolean rotateCardsOut = velocityY > 0;
    210                     rotateCards(LEFT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut);
    211                 }
    212                 break;
    213             default:
    214                 break;
    215         }
    216     }
    217 
    218     @Override
    219     public void onCardFlipEnd() {
    220         mTouchEventsEnabled = true;
    221     }
    222 
    223     @Override
    224     public void onCardFlipStart() {
    225         mTouchEventsEnabled = false;
    226     }
    227 
    228     @Override
    229     public boolean onTouchEvent(MotionEvent me) {
    230         if (mTouchEventsEnabled) {
    231             return gDetector.onTouchEvent(me);
    232         } else {
    233             return super.onTouchEvent(me);
    234         }
    235     }
    236 
    237     /**
    238      * Retrieves an animator object for each card in the specified stack that either
    239      * rotates it in or out depending on its current state. All of these animations
    240      * are then played together.
    241      */
    242     public void rotateCards (final int stack, CardView.Corner corner,
    243                              final boolean isRotatingOut) {
    244         List<Animator> animations = new ArrayList<Animator>();
    245 
    246         ArrayList <CardView> cards = mStackCards.get(stack);
    247 
    248         for (int i = 0; i < cards.size(); i++) {
    249             CardView cardView = cards.get(i);
    250             animations.add(cardView.getRotationAnimator(i, corner, isRotatingOut, false));
    251             mLayout.bringChildToFront(cardView);
    252         }
    253         /** All the cards are being brought to the front in order to guarantee that
    254          * the cards being rotated in the current stack will overlay the cards in the
    255          * other stack. After the z-ordering of all the cards is updated, a layout must
    256          * be requested in order to apply the changes made.*/
    257         mLayout.requestLayout();
    258 
    259         AnimatorSet set = new AnimatorSet();
    260         set.playTogether(animations);
    261         set.addListener(new AnimatorListenerAdapter() {
    262             @Override
    263             public void onAnimationEnd(Animator animation) {
    264                 mIsStackEnabled[stack] = !isRotatingOut;
    265             }
    266         });
    267         set.start();
    268     }
    269 
    270     /**
    271      * Retrieves an animator object for each card in the specified stack to complete a
    272      * full revolution around one of its corners, and plays all of them together.
    273      */
    274     public void rotateCardsFullRotation (int stack, CardView.Corner corner) {
    275         List<Animator> animations = new ArrayList<Animator>();
    276 
    277         ArrayList <CardView> cards = mStackCards.get(stack);
    278         for (int i = 0; i < cards.size(); i++) {
    279             CardView cardView = cards.get(i);
    280             animations.add(cardView.getFullRotationAnimator(i, corner, false));
    281             mLayout.bringChildToFront(cardView);
    282         }
    283         /** Same reasoning for bringing cards to front as in rotateCards().*/
    284         mLayout.requestLayout();
    285 
    286         mTouchEventsEnabled = false;
    287         AnimatorSet set = new AnimatorSet();
    288         set.playTogether(animations);
    289         set.addListener(new AnimatorListenerAdapter() {
    290             @Override
    291             public void onAnimationEnd(Animator animation) {
    292                 mTouchEventsEnabled = true;
    293             }
    294         });
    295         set.start();
    296     }
    297 }