am 55b53b8c: First commit for CardFlip.

* commit '55b53b8c8d209a5f28adc0438b93249abc5e5487':
  First commit for CardFlip.
diff --git a/samples/devbytes/animation/CardFlip/AndroidManifest.xml b/samples/devbytes/animation/CardFlip/AndroidManifest.xml
new file mode 100644
index 0000000..d915f3e
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.cardflip"
+          android:versionCode="1"
+          android:versionName="1.0">
+    <uses-sdk android:minSdkVersion="14"
+              android:targetSdkVersion="17"/>
+    <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+        <activity android:name=".CardFlip"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
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 0000000..2f7b786
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg
Binary files 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 0000000..96a442e
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/ic_launcher.png
Binary files 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 0000000..f433603
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg
Binary files 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 0000000..9923872
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-ldpi/ic_launcher.png
Binary files 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 0000000..359047d
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-mdpi/ic_launcher.png
Binary files 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 0000000..71c6d76
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/drawable-xhdpi/ic_launcher.png
Binary files 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 0000000..ef23d69
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/layout/main.xml
@@ -0,0 +1,19 @@
+<!-- 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/main_relative_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+</RelativeLayout>
\ 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 0000000..2eb363c
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/values/integer.xml
@@ -0,0 +1,20 @@
+<!-- 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.
+-->
+<resources>
+
+    <integer name="vertical_card_magin">30</integer>
+    <integer name="horizontal_card_magin">30</integer>
+
+</resources>
\ 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 0000000..bd248d1
--- /dev/null
+++ b/samples/devbytes/animation/CardFlip/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!-- 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.
+-->
+<resources>
+
+    <string name="app_name">CardFlip</string>
+
+</resources>
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 0000000..746afec
--- /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<ArrayList<CardView>> mStackCards;
+
+    GestureDetector gDetector;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+
+        mStackCards = new ArrayList<ArrayList<CardView>>();
+        mStackCards.add(new ArrayList<CardView>());
+        mStackCards.add(new ArrayList<CardView>());
+
+        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<CardView> 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];
+
+        ArrayList<CardView>leftStack = mStackCards.get(LEFT_STACK);
+        ArrayList<CardView>rightStack = 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<Animator> animations = new ArrayList<Animator>();
+
+        ArrayList <CardView> 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<Animator> animations = new ArrayList<Animator>();
+
+        ArrayList <CardView> 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 0000000..0af6941
--- /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 0000000..9a3ab71
--- /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;
+    }
+
+}