diff --git a/samples/devbytes/animation/CardFlip/AndroidManifest.xml b/samples/devbytes/animation/CardFlip/AndroidManifest.xml new file mode 100644 index 000000000..d915f3eff --- /dev/null +++ b/samples/devbytes/animation/CardFlip/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg new file mode 100644 index 000000000..2f7b7868f Binary files /dev/null and b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg differ diff --git a/samples/devbytes/animation/CardFlip/res/drawable-hdpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..96a442e5b Binary files /dev/null and b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/ic_launcher.png differ diff --git a/samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg new file mode 100644 index 000000000..f433603c4 Binary files /dev/null and b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg differ diff --git a/samples/devbytes/animation/CardFlip/res/drawable-ldpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 000000000..99238729d Binary files /dev/null and b/samples/devbytes/animation/CardFlip/res/drawable-ldpi/ic_launcher.png differ diff --git a/samples/devbytes/animation/CardFlip/res/drawable-mdpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..359047dfa Binary files /dev/null and b/samples/devbytes/animation/CardFlip/res/drawable-mdpi/ic_launcher.png differ diff --git a/samples/devbytes/animation/CardFlip/res/drawable-xhdpi/ic_launcher.png b/samples/devbytes/animation/CardFlip/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..71c6d760f Binary files /dev/null and b/samples/devbytes/animation/CardFlip/res/drawable-xhdpi/ic_launcher.png differ diff --git a/samples/devbytes/animation/CardFlip/res/layout/main.xml b/samples/devbytes/animation/CardFlip/res/layout/main.xml new file mode 100644 index 000000000..ef23d69ee --- /dev/null +++ b/samples/devbytes/animation/CardFlip/res/layout/main.xml @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/samples/devbytes/animation/CardFlip/res/values/integer.xml b/samples/devbytes/animation/CardFlip/res/values/integer.xml new file mode 100644 index 000000000..2eb363c43 --- /dev/null +++ b/samples/devbytes/animation/CardFlip/res/values/integer.xml @@ -0,0 +1,20 @@ + + + + 30 + 30 + + \ No newline at end of file diff --git a/samples/devbytes/animation/CardFlip/res/values/strings.xml b/samples/devbytes/animation/CardFlip/res/values/strings.xml new file mode 100644 index 000000000..bd248d1c5 --- /dev/null +++ b/samples/devbytes/animation/CardFlip/res/values/strings.xml @@ -0,0 +1,19 @@ + + + + CardFlip + + diff --git a/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlip.java b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlip.java new file mode 100644 index 000000000..746afec3d --- /dev/null +++ b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlip.java @@ -0,0 +1,297 @@ +/* + * 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(); + } +} \ No newline at end of file diff --git a/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlipListener.java b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlipListener.java new file mode 100644 index 000000000..0af6941bf --- /dev/null +++ b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardFlipListener.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * This interface is used to prevent flipping multiple cards at the same time. + * These callback methods are used to disable and re-enable touches when a card + * flip animation begins and ends respectively. + * */ +public interface CardFlipListener { + public void onCardFlipEnd(); + public void onCardFlipStart(); +} diff --git a/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardView.java b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardView.java new file mode 100644 index 000000000..9a3ab7126 --- /dev/null +++ b/samples/devbytes/animation/CardFlip/src/com/example/android/cardflip/CardView.java @@ -0,0 +1,329 @@ +/* + * 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.animation.Keyframe; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +/** + * This CardView object is a view which can flip horizontally about its edges, + * as well as rotate clockwise or counter-clockwise about any of its corners. In + * the middle of a flip animation, this view darkens to imitate a shadow-like effect. + * + * The key behind the design of this view is the fact that the layout parameters and + * the animation properties of this view are updated and reset respectively after + * every single animation. Therefore, every consecutive animation that this + * view experiences is completely independent of what its prior state was. + */ +public class CardView extends ImageView { + + enum Corner { + TOP_LEFT, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_RIGHT + } + + private final int CAMERA_DISTANCE = 8000; + private final int MIN_FLIP_DURATION = 300; + private final int VELOCITY_TO_DURATION_CONSTANT = 15; + private final int MAX_FLIP_DURATION = 700; + private final int ROTATION_PER_CARD = 2; + private final int ROTATION_DELAY_PER_CARD = 50; + private final int ROTATION_DURATION = 2000; + private final int ANTIALIAS_BORDER = 1; + + private BitmapDrawable mFrontBitmapDrawable, mBackBitmapDrawable, mCurrentBitmapDrawable; + + private boolean mIsFrontShowing = true; + private boolean mIsHorizontallyFlipped = false; + + private Matrix mHorizontalFlipMatrix; + + private CardFlipListener mCardFlipListener; + + public CardView(Context context) { + super(context); + init(context); + } + + public CardView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** Loads the bitmap drawables used for the front and back for this card.*/ + public void init(Context context) { + mHorizontalFlipMatrix = new Matrix(); + + setCameraDistance(CAMERA_DISTANCE); + + mFrontBitmapDrawable = bitmapWithBorder((BitmapDrawable)getResources() + .getDrawable(R.drawable.red)); + mBackBitmapDrawable = bitmapWithBorder((BitmapDrawable) getResources() + .getDrawable(R.drawable.blue)); + + updateDrawableBitmap(); + } + + /** + * Adding a 1 pixel transparent border around the bitmap can be used to + * anti-alias the image as it rotates. + */ + private BitmapDrawable bitmapWithBorder(BitmapDrawable bitmapDrawable) { + Bitmap bitmapWithBorder = Bitmap.createBitmap(bitmapDrawable.getIntrinsicWidth() + + ANTIALIAS_BORDER * 2, bitmapDrawable.getIntrinsicHeight() + ANTIALIAS_BORDER * 2, + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmapWithBorder); + canvas.drawBitmap(bitmapDrawable.getBitmap(), ANTIALIAS_BORDER, ANTIALIAS_BORDER, null); + return new BitmapDrawable(getResources(), bitmapWithBorder); + } + + /** Initiates a horizontal flip from right to left. */ + public void flipRightToLeft(int numberInPile, int velocity) { + setPivotX(0); + flipHorizontally(numberInPile, false, velocity); + } + + /** Initiates a horizontal flip from left to right. */ + public void flipLeftToRight(int numberInPile, int velocity) { + setPivotX(getWidth()); + flipHorizontally(numberInPile, true, velocity); + } + + /** + * Animates a horizontal (about the y-axis) flip of this card. + * @param numberInPile Specifies how many cards are underneath this card in the new + * pile so as to properly adjust its position offset in the stack. + * @param clockwise Specifies whether the horizontal animation is 180 degrees + * clockwise or 180 degrees counter clockwise. + */ + public void flipHorizontally (int numberInPile, boolean clockwise, int velocity) { + toggleFrontShowing(); + + PropertyValuesHolder rotation = PropertyValuesHolder.ofFloat(View.ROTATION_Y, + clockwise ? 180 : -180); + + PropertyValuesHolder xOffset = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, + numberInPile * CardFlip.CARD_PILE_OFFSET); + PropertyValuesHolder yOffset = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, + numberInPile * CardFlip.CARD_PILE_OFFSET); + + ObjectAnimator cardAnimator = ObjectAnimator.ofPropertyValuesHolder(this, rotation, + xOffset, yOffset); + cardAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + if (valueAnimator.getAnimatedFraction() >= 0.5) { + updateDrawableBitmap(); + } + } + }); + + Keyframe shadowKeyFrameStart = Keyframe.ofFloat(0, 0); + Keyframe shadowKeyFrameMid = Keyframe.ofFloat(0.5f, 1); + Keyframe shadowKeyFrameEnd = Keyframe.ofFloat(1, 0); + PropertyValuesHolder shadowPropertyValuesHolder = PropertyValuesHolder.ofKeyframe + ("shadow", shadowKeyFrameStart, shadowKeyFrameMid, shadowKeyFrameEnd); + ObjectAnimator colorizer = ObjectAnimator.ofPropertyValuesHolder(this, + shadowPropertyValuesHolder); + + mCardFlipListener.onCardFlipStart(); + AnimatorSet set = new AnimatorSet(); + int duration = MAX_FLIP_DURATION - Math.abs(velocity) / VELOCITY_TO_DURATION_CONSTANT; + duration = duration < MIN_FLIP_DURATION ? MIN_FLIP_DURATION : duration; + set.setDuration(duration); + set.playTogether(cardAnimator, colorizer); + set.setInterpolator(new AccelerateDecelerateInterpolator()); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + toggleIsHorizontallyFlipped(); + updateDrawableBitmap(); + updateLayoutParams(); + mCardFlipListener.onCardFlipEnd(); + } + }); + set.start(); + } + + /** Darkens this ImageView's image by applying a shadow color filter over it. */ + public void setShadow(float value) { + int colorValue = (int)(255 - 200 * value); + setColorFilter(Color.rgb(colorValue, colorValue, colorValue), + android.graphics.PorterDuff.Mode.MULTIPLY); + } + + public void toggleFrontShowing() { + mIsFrontShowing = !mIsFrontShowing; + } + + public void toggleIsHorizontallyFlipped() { + mIsHorizontallyFlipped = !mIsHorizontallyFlipped; + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mHorizontalFlipMatrix.setScale(-1, 1, w / 2, h / 2); + } + + /** + * Scale the canvas horizontally about its midpoint in the case that the card + * is in a horizontally flipped state. + */ + @Override + protected void onDraw(Canvas canvas) { + if (mIsHorizontallyFlipped) { + canvas.concat(mHorizontalFlipMatrix); + } + super.onDraw(canvas); + } + + /** + * Updates the layout parameters of this view so as to reset the rotationX and + * rotationY parameters, and remain independent of its previous position, while + * also maintaining its current position in the layout. + */ + public void updateLayoutParams () { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams(); + + params.leftMargin = (int)(params.leftMargin + ((Math.abs(getRotationY()) % 360) / 180) * + (2 * getPivotX () - getWidth())); + + setRotationX(0); + setRotationY(0); + + setLayoutParams(params); + } + + /** + * Toggles the visible bitmap of this view between its front and back drawables + * respectively. + */ + public void updateDrawableBitmap () { + mCurrentBitmapDrawable = mIsFrontShowing ? mFrontBitmapDrawable : mBackBitmapDrawable; + setImageDrawable(mCurrentBitmapDrawable); + } + + /** + * Sets the appropriate translation of this card depending on how many cards + * are in the pile underneath it. + */ + public void updateTranslation (int numInPile) { + setTranslationX(CardFlip.CARD_PILE_OFFSET * numInPile); + setTranslationY(CardFlip.CARD_PILE_OFFSET * numInPile); + } + + /** + * Returns a rotation animation which rotates this card by some degree about + * one of its corners either in the clockwise or counter-clockwise direction. + * Depending on how many cards lie below this one in the stack, this card will + * be rotated by a different amount so all the cards are visible when rotated out. + */ + public ObjectAnimator getRotationAnimator (int cardFromTop, Corner corner, + boolean isRotatingOut, boolean isClockwise) { + rotateCardAroundCorner(corner); + int rotation = cardFromTop * ROTATION_PER_CARD; + + if (!isClockwise) { + rotation = -rotation; + } + + if (!isRotatingOut) { + rotation = 0; + } + + return ObjectAnimator.ofFloat(this, View.ROTATION, rotation); + } + + /** + * Returns a full rotation animator which rotates this card by 360 degrees + * about one of its corners either in the clockwise or counter-clockwise direction. + * Depending on how many cards lie below this one in the stack, a different start + * delay is applied to the animation so the cards don't all animate at once. + */ + public ObjectAnimator getFullRotationAnimator (int cardFromTop, Corner corner, + boolean isClockwise) { + final int currentRotation = (int)getRotation(); + + rotateCardAroundCorner(corner); + int rotation = 360 - currentRotation; + rotation = isClockwise ? rotation : -rotation; + + ObjectAnimator animator = ObjectAnimator.ofFloat(this, View.ROTATION, rotation); + + animator.setStartDelay(ROTATION_DELAY_PER_CARD * cardFromTop); + animator.setDuration(ROTATION_DURATION); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setRotation(currentRotation); + } + }); + + return animator; + } + + /** + * Sets the appropriate pivot of this card so that it can be rotated about + * any one of its four corners. + */ + public void rotateCardAroundCorner(Corner corner) { + switch(corner) { + case TOP_LEFT: + setPivotX(0); + setPivotY(0); + break; + case TOP_RIGHT: + setPivotX(getWidth()); + setPivotY(0); + break; + case BOTTOM_LEFT: + setPivotX(0); + setPivotY(getHeight()); + break; + case BOTTOM_RIGHT: + setPivotX(getWidth()); + setPivotY(getHeight()); + break; + } + } + + public void setCardFlipListener(CardFlipListener cardFlipListener) { + mCardFlipListener = cardFlipListener; + } + +}