/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.cardflip; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.app.Activity; import android.os.Build; import android.os.Bundle; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ViewTreeObserver; import android.widget.RelativeLayout; import java.util.ArrayList; import java.util.List; /** * This application creates 2 stacks of playing cards. Using fling events, * these cards can be flipped from one stack to another where each flip comes with * an associated animation. The cards can be flipped horizontally from left to right * or right to left depending on which stack the animating card currently belongs to. * * This application demonstrates an animation where a stack of cards can either be * be rotated out or back in about their bottom left corner in a counter-clockwise direction. * Rotate out: Down fling on stack of cards * Rotate in: Up fling on stack of cards * Full rotation: Tap on stack of cards * * Note that in this demo touch events are disabled in the middle of any animation so * only one card can be flipped at a time. When the cards are in a rotated-out * state, no new cards can be rotated to or from that stack. These changes were made to * simplify the code for this demo. */ public class CardFlip extends Activity implements CardFlipListener { final static int CARD_PILE_OFFSET = 3; final static int STARTING_NUMBER_CARDS = 15; final static int RIGHT_STACK = 0; final static int LEFT_STACK = 1; int mCardWidth = 0; int mCardHeight = 0; int mVerticalPadding; int mHorizontalPadding; boolean mTouchEventsEnabled = true; boolean[] mIsStackEnabled; RelativeLayout mLayout; List> mStackCards; GestureDetector gDetector; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mStackCards = new ArrayList>(); mStackCards.add(new ArrayList()); mStackCards.add(new ArrayList()); mIsStackEnabled = new boolean[2]; mIsStackEnabled[0] = true; mIsStackEnabled[1] = true; mVerticalPadding = getResources().getInteger(R.integer.vertical_card_magin); mHorizontalPadding = getResources().getInteger(R.integer.horizontal_card_magin); gDetector = new GestureDetector(this, mGestureListener); mLayout = (RelativeLayout)findViewById(R.id.main_relative_layout); ViewTreeObserver observer = mLayout.getViewTreeObserver(); observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); } else { mLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this); } mCardHeight = mLayout.getHeight(); mCardWidth = mLayout.getWidth() / 2; for (int x = 0; x < STARTING_NUMBER_CARDS; x++) { addNewCard(RIGHT_STACK); } } }); } /** * Adds a new card to the specified stack. Also performs all the necessary layout setup * to place the card in the correct position. */ public void addNewCard(int stack) { CardView view = new CardView(this); view.updateTranslation(mStackCards.get(stack).size()); view.setCardFlipListener(this); view.setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(mCardWidth, mCardHeight); params.topMargin = 0; params.leftMargin = (stack == RIGHT_STACK ? mCardWidth : 0); mStackCards.get(stack).add(view); mLayout.addView(view, params); } /** * Gesture Detector listens for fling events in order to potentially initiate * a card flip event when a fling event occurs. Also listens for tap events in * order to potentially initiate a full rotation animation. */ private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector .SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent motionEvent) { int stack = getStack(motionEvent); rotateCardsFullRotation(stack, CardView.Corner.BOTTOM_LEFT); return true; } @Override public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent2, float v, float v2) { int stack = getStack(motionEvent); ArrayList cardStack = mStackCards.get(stack); int size = cardStack.size(); if (size > 0) { rotateCardView(cardStack.get(size - 1), stack, v, v2); } return true; } }; /** Returns the appropriate stack corresponding to the MotionEvent. */ public int getStack(MotionEvent ev) { boolean isLeft = ev.getX() <= mCardWidth; return isLeft ? LEFT_STACK : RIGHT_STACK; } /** * Uses the stack parameter, along with the velocity values of the fling event * to determine in what direction the card must be flipped. By the same logic, the * new stack that the card belongs to after the animation is also determined * and updated. */ public void rotateCardView(final CardView cardView, int stack, float velocityX, float velocityY) { boolean xGreaterThanY = Math.abs(velocityX) > Math.abs(velocityY); boolean bothStacksEnabled = mIsStackEnabled[RIGHT_STACK] && mIsStackEnabled[LEFT_STACK]; ArrayListleftStack = mStackCards.get(LEFT_STACK); ArrayListrightStack = mStackCards.get(RIGHT_STACK); switch (stack) { case RIGHT_STACK: if (velocityX < 0 && xGreaterThanY) { if (!bothStacksEnabled) { break; } mLayout.bringChildToFront(cardView); mLayout.requestLayout(); rightStack.remove(rightStack.size() - 1); leftStack.add(cardView); cardView.flipRightToLeft(leftStack.size() - 1, (int)velocityX); break; } else if (!xGreaterThanY) { boolean rotateCardsOut = velocityY > 0; rotateCards(RIGHT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut); } break; case LEFT_STACK: if (velocityX > 0 && xGreaterThanY) { if (!bothStacksEnabled) { break; } mLayout.bringChildToFront(cardView); mLayout.requestLayout(); leftStack.remove(leftStack.size() - 1); rightStack.add(cardView); cardView.flipLeftToRight(rightStack.size() - 1, (int)velocityX); break; } else if (!xGreaterThanY) { boolean rotateCardsOut = velocityY > 0; rotateCards(LEFT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut); } break; default: break; } } @Override public void onCardFlipEnd() { mTouchEventsEnabled = true; } @Override public void onCardFlipStart() { mTouchEventsEnabled = false; } @Override public boolean onTouchEvent(MotionEvent me) { if (mTouchEventsEnabled) { return gDetector.onTouchEvent(me); } else { return super.onTouchEvent(me); } } /** * Retrieves an animator object for each card in the specified stack that either * rotates it in or out depending on its current state. All of these animations * are then played together. */ public void rotateCards (final int stack, CardView.Corner corner, final boolean isRotatingOut) { List animations = new ArrayList(); ArrayList cards = mStackCards.get(stack); for (int i = 0; i < cards.size(); i++) { CardView cardView = cards.get(i); animations.add(cardView.getRotationAnimator(i, corner, isRotatingOut, false)); mLayout.bringChildToFront(cardView); } /** All the cards are being brought to the front in order to guarantee that * the cards being rotated in the current stack will overlay the cards in the * other stack. After the z-ordering of all the cards is updated, a layout must * be requested in order to apply the changes made.*/ mLayout.requestLayout(); AnimatorSet set = new AnimatorSet(); set.playTogether(animations); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mIsStackEnabled[stack] = !isRotatingOut; } }); set.start(); } /** * Retrieves an animator object for each card in the specified stack to complete a * full revolution around one of its corners, and plays all of them together. */ public void rotateCardsFullRotation (int stack, CardView.Corner corner) { List animations = new ArrayList(); ArrayList cards = mStackCards.get(stack); for (int i = 0; i < cards.size(); i++) { CardView cardView = cards.get(i); animations.add(cardView.getFullRotationAnimator(i, corner, false)); mLayout.bringChildToFront(cardView); } /** Same reasoning for bringing cards to front as in rotateCards().*/ mLayout.requestLayout(); mTouchEventsEnabled = false; AnimatorSet set = new AnimatorSet(); set.playTogether(animations); set.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mTouchEventsEnabled = true; } }); set.start(); } }