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 }