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
onCreate(Bundle savedInstanceState)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      */
addNewCard(int stack)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. */
getStack(MotionEvent ev)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      */
rotateCardView(final CardView cardView, int stack, float velocityX, float velocityY)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
onCardFlipEnd()219     public void onCardFlipEnd() {
220         mTouchEventsEnabled = true;
221     }
222 
223     @Override
onCardFlipStart()224     public void onCardFlipStart() {
225         mTouchEventsEnabled = false;
226     }
227 
228     @Override
onTouchEvent(MotionEvent me)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      */
rotateCards(final int stack, CardView.Corner corner, final boolean isRotatingOut)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      */
rotateCardsFullRotation(int stack, CardView.Corner corner)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 }