Add a checkable QuickContactBadge.

To be used from multi picker UI.
FlipDrawable and CheckableFlipDrawable were kanged from UnifiedEmail.

Change-Id: Ic910071da9314526c3d75ab354b62645399824ce
diff --git a/res/drawable-hdpi/ic_check_wht_24dp.png b/res/drawable-hdpi/ic_check_wht_24dp.png
new file mode 100644
index 0000000..12ce8e0
--- /dev/null
+++ b/res/drawable-hdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_check_wht_24dp.png b/res/drawable-mdpi/ic_check_wht_24dp.png
new file mode 100644
index 0000000..c7de705
--- /dev/null
+++ b/res/drawable-mdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_check_wht_24dp.png b/res/drawable-xhdpi/ic_check_wht_24dp.png
new file mode 100644
index 0000000..e34b73e
--- /dev/null
+++ b/res/drawable-xhdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_check_wht_24dp.png b/res/drawable-xxhdpi/ic_check_wht_24dp.png
new file mode 100644
index 0000000..4c6a653
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_check_wht_24dp.png
Binary files differ
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 64397ca..c0b4f8c 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -17,6 +17,7 @@
 <resources>
     <declare-styleable name="Theme">
         <attr name="android:textColorSecondary" />
+        <attr name="android:colorPrimary" />
     </declare-styleable>
 
     <declare-styleable name="ContactsDataKind">
diff --git a/src/com/android/contacts/common/list/ContactListItemView.java b/src/com/android/contacts/common/list/ContactListItemView.java
index 27df89b..24b6ff4 100644
--- a/src/com/android/contacts/common/list/ContactListItemView.java
+++ b/src/com/android/contacts/common/list/ContactListItemView.java
@@ -51,6 +51,8 @@
 import com.android.contacts.common.format.TextHighlighter;
 import com.android.contacts.common.util.SearchUtil;
 import com.android.contacts.common.util.ViewUtil;
+import com.android.contacts.common.widget.CheckableImageView;
+import com.android.contacts.common.widget.CheckableQuickContactBadge;
 
 import com.google.common.collect.Lists;
 
@@ -150,8 +152,8 @@
 
     // The views inside the contact view
     private boolean mQuickContactEnabled = true;
-    private QuickContactBadge mQuickContact;
-    private ImageView mPhotoView;
+    private CheckableQuickContactBadge mQuickContact;
+    private CheckableImageView mPhotoView;
     private TextView mNameTextView;
     private TextView mPhoneticNameTextView;
     private TextView mLabelView;
@@ -755,7 +757,7 @@
             throw new IllegalStateException("QuickContact is disabled for this view");
         }
         if (mQuickContact == null) {
-            mQuickContact = new QuickContactBadge(getContext());
+            mQuickContact = new CheckableQuickContactBadge(getContext());
             mQuickContact.setOverlay(null);
             mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
             if (mNameTextView != null) {
@@ -769,12 +771,21 @@
         return mQuickContact;
     }
 
+    public void setChecked(boolean checked, boolean animate) {
+        if (mQuickContact != null) {
+            mQuickContact.setChecked(checked, animate);
+        }
+        if (mPhotoView != null) {
+            mPhotoView.setChecked(checked, animate);
+        }
+    }
+
     /**
      * Returns the photo view, creating it if necessary.
      */
     public ImageView getPhotoView() {
         if (mPhotoView == null) {
-            mPhotoView = new ImageView(getContext());
+            mPhotoView = new CheckableImageView(getContext());
             mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
             // Quick contact style used above will set a background - remove it
             mPhotoView.setBackground(null);
diff --git a/src/com/android/contacts/common/widget/CheckableFlipDrawable.java b/src/com/android/contacts/common/widget/CheckableFlipDrawable.java
new file mode 100644
index 0000000..cca8288
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableFlipDrawable.java
@@ -0,0 +1,220 @@
+package com.android.contacts.common.widget;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.QuickContactBadge;
+
+import com.android.contacts.common.R;
+
+public class CheckableFlipDrawable extends FlipDrawable implements
+        ValueAnimator.AnimatorUpdateListener {
+
+    private final CheckmarkDrawable mCheckmarkDrawable;
+
+    private final ValueAnimator mCheckmarkScaleAnimator;
+    private final ValueAnimator mCheckmarkAlphaAnimator;
+
+    private static final int POST_FLIP_DURATION_MS = 150;
+
+    private static final float CHECKMARK_SCALE_BEGIN_VALUE = 0.2f;
+    private static final float CHECKMARK_ALPHA_BEGIN_VALUE = 0f;
+
+    /** Must be <= 1f since the animation value is used as a percentage. */
+    private static final float END_VALUE = 1f;
+
+    public CheckableFlipDrawable(Drawable front, final Resources res,
+            final int checkBackgroundColor, final int flipDurationMs) {
+        super(front, new CheckmarkDrawable(res, checkBackgroundColor),
+                flipDurationMs, 0 /* preFlipDurationMs */, POST_FLIP_DURATION_MS);
+
+        mCheckmarkDrawable = (CheckmarkDrawable) mBack;
+
+        // We will create checkmark animations that are synchronized with the
+        // flipping animation. The entire delay + duration of the checkmark animation
+        // needs to equal the entire duration of the flip animation (where delay is 0).
+
+        // The checkmark animation is in effect only when the back drawable is being shown.
+        // For the flip animation duration    <pre>[_][]|[][_]<post>
+        // The checkmark animation will be    |--delay--|-duration-|
+
+        // Need delay to skip the first half of the flip duration.
+        final long animationDelay = mPreFlipDurationMs + mFlipDurationMs / 2;
+        // Actual duration is the second half of the flip duration.
+        final long animationDuration = mFlipDurationMs / 2 + mPostFlipDurationMs;
+
+        mCheckmarkScaleAnimator = ValueAnimator.ofFloat(CHECKMARK_SCALE_BEGIN_VALUE, END_VALUE)
+                .setDuration(animationDuration);
+        mCheckmarkScaleAnimator.setStartDelay(animationDelay);
+        mCheckmarkScaleAnimator.addUpdateListener(this);
+
+        mCheckmarkAlphaAnimator = ValueAnimator.ofFloat(CHECKMARK_ALPHA_BEGIN_VALUE, END_VALUE)
+                .setDuration(animationDuration);
+        mCheckmarkAlphaAnimator.setStartDelay(animationDelay);
+        mCheckmarkAlphaAnimator.addUpdateListener(this);
+    }
+
+    public void setFront(Drawable front) {
+        mFront.setCallback(null);
+
+        mFront = front;
+
+        mFront.setCallback(this);
+        mFront.setBounds(getBounds());
+        mFront.setAlpha(getAlpha());
+        mFront.setColorFilter(getColorFilter());
+        mFront.setLevel(getLevel());
+
+        reset();
+        invalidateSelf();
+    }
+
+    public void setCheckMarkBackgroundColor(int color) {
+        mCheckmarkDrawable.setBackgroundColor(color);
+        invalidateSelf();
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        if (mCheckmarkScaleAnimator == null) {
+            // Call from super's constructor. Not yet initialized.
+            return;
+        }
+        mCheckmarkScaleAnimator.cancel();
+        mCheckmarkAlphaAnimator.cancel();
+        boolean side = getSideFlippingTowards();
+        mCheckmarkDrawable.setScaleAnimatorValue(side ? CHECKMARK_SCALE_BEGIN_VALUE : END_VALUE);
+        mCheckmarkDrawable.setAlphaAnimatorValue(side ? CHECKMARK_ALPHA_BEGIN_VALUE : END_VALUE);
+    }
+
+    @Override
+    public void flip() {
+        super.flip();
+        // Keep the checkmark animators in sync with the flip animator.
+        if (mCheckmarkScaleAnimator.isStarted()) {
+            mCheckmarkScaleAnimator.reverse();
+            mCheckmarkAlphaAnimator.reverse();
+        } else {
+            if (!getSideFlippingTowards() /* front to back */) {
+                mCheckmarkScaleAnimator.start();
+                mCheckmarkAlphaAnimator.start();
+            } else /* back to front */ {
+                mCheckmarkScaleAnimator.reverse();
+                mCheckmarkAlphaAnimator.reverse();
+            }
+        }
+    }
+
+    @Override
+    public void onAnimationUpdate(final ValueAnimator animation) {
+        //noinspection ConstantConditions
+        final float value = (Float) animation.getAnimatedValue();
+
+        if (animation == mCheckmarkScaleAnimator) {
+            mCheckmarkDrawable.setScaleAnimatorValue(value);
+        } else if (animation == mCheckmarkAlphaAnimator) {
+            mCheckmarkDrawable.setAlphaAnimatorValue(value);
+        }
+    }
+
+    private static class CheckmarkDrawable extends Drawable {
+        private static Bitmap sCheckMark;
+
+        private final Paint mPaint;
+
+        private float mScaleFraction;
+        private float mAlphaFraction;
+
+        private static final Matrix sMatrix = new Matrix();
+
+        public CheckmarkDrawable(final Resources res, int backgroundColor) {
+            if (sCheckMark == null) {
+                sCheckMark = BitmapFactory.decodeResource(res, R.drawable.ic_check_wht_24dp);
+            }
+            mPaint = new Paint();
+            mPaint.setAntiAlias(true);
+            mPaint.setFilterBitmap(true);
+            mPaint.setColor(backgroundColor);
+        }
+
+        public void setBackgroundColor(int color) {
+            mPaint.setColor(color);
+        }
+
+        @Override
+        public void draw(final Canvas canvas) {
+            final Rect bounds = getBounds();
+            if (!isVisible() || bounds.isEmpty()) {
+                return;
+            }
+
+            canvas.drawCircle(bounds.centerX(), bounds.centerY(), bounds.width() / 2, mPaint);
+
+            // Scale the checkmark.
+            sMatrix.reset();
+            sMatrix.setScale(mScaleFraction, mScaleFraction, sCheckMark.getWidth() / 2,
+                    sCheckMark.getHeight() / 2);
+            sMatrix.postTranslate(bounds.centerX() - sCheckMark.getWidth() / 2,
+                    bounds.centerY() - sCheckMark.getHeight() / 2);
+
+            // Fade the checkmark.
+            final int oldAlpha = mPaint.getAlpha();
+            // Interpolate the alpha.
+            mPaint.setAlpha((int) (oldAlpha * mAlphaFraction));
+            canvas.drawBitmap(sCheckMark, sMatrix, mPaint);
+            // Restore the alpha.
+            mPaint.setAlpha(oldAlpha);
+        }
+
+        @Override
+        public void setAlpha(final int alpha) {
+            mPaint.setAlpha(alpha);
+        }
+
+        @Override
+        public void setColorFilter(final ColorFilter cf) {
+            mPaint.setColorFilter(cf);
+        }
+
+        @Override
+        public int getOpacity() {
+            // Always a gray background.
+            return PixelFormat.OPAQUE;
+        }
+
+        /**
+         * Set value as a fraction from 0f to 1f.
+         */
+        public void setScaleAnimatorValue(final float value) {
+            final float old = mScaleFraction;
+            mScaleFraction = value;
+            if (old != mScaleFraction) {
+                invalidateSelf();
+            }
+        }
+
+        /**
+         * Set value as a fraction from 0f to 1f.
+         */
+        public void setAlphaAnimatorValue(final float value) {
+            final float old = mAlphaFraction;
+            mAlphaFraction = value;
+            if (old != mAlphaFraction) {
+                invalidateSelf();
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/widget/CheckableImageView.java b/src/com/android/contacts/common/widget/CheckableImageView.java
new file mode 100644
index 0000000..914d4ea
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableImageView.java
@@ -0,0 +1,103 @@
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.ImageView;
+
+import com.android.contacts.common.R;
+
+public class CheckableImageView extends ImageView implements Checkable {
+    private boolean mChecked = false;
+    private int mCheckMarkBackgroundColor;
+    private CheckableFlipDrawable mDrawable;
+
+    public CheckableImageView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public CheckableImageView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public CheckableImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    public CheckableImageView(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context);
+    }
+
+    private void init(Context context) {
+        TypedArray a = context.obtainStyledAttributes(android.R.styleable.Theme);
+        setCheckMarkBackgroundColor(a.getColor(android.R.styleable.Theme_colorPrimary,
+                context.getResources().getColor(R.color.people_app_theme_color)));
+        a.recycle();
+    }
+
+    public void setCheckMarkBackgroundColor(int color) {
+        mCheckMarkBackgroundColor = color;
+        if (mDrawable != null) {
+            mDrawable.setCheckMarkBackgroundColor(color);
+        }
+    }
+
+    public void toggle() {
+        setChecked(!mChecked);
+    }
+
+    @Override
+    public boolean isChecked() {
+        return mChecked;
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        setChecked(checked, true);
+    }
+
+    public void setChecked(boolean checked, boolean animate) {
+        if (mChecked == checked) {
+            return;
+        }
+
+        mChecked = checked;
+
+        Drawable d = getDrawable();
+        if (d instanceof CheckableFlipDrawable) {
+            CheckableFlipDrawable cfd = (CheckableFlipDrawable) d;
+            cfd.flipTo(!mChecked);
+            if (!animate) {
+                cfd.reset();
+            }
+        }
+    }
+
+    @Override
+    public void setImageDrawable(Drawable d) {
+        if (d != null) {
+            if (mDrawable == null) {
+                mDrawable = new CheckableFlipDrawable(d, getResources(),
+                        mCheckMarkBackgroundColor, 150);
+            } else {
+                int oldWidth = mDrawable.getIntrinsicWidth();
+                int oldHeight = mDrawable.getIntrinsicHeight();
+                mDrawable.setFront(d);
+                if (oldWidth != mDrawable.getIntrinsicWidth()
+                        || oldHeight != mDrawable.getIntrinsicHeight()) {
+                    // enforce drawable size update + layout
+                    super.setImageDrawable(null);
+                }
+            }
+            d = mDrawable;
+        }
+        super.setImageDrawable(d);
+    }
+}
diff --git a/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java b/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java
new file mode 100644
index 0000000..8516056
--- /dev/null
+++ b/src/com/android/contacts/common/widget/CheckableQuickContactBadge.java
@@ -0,0 +1,103 @@
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.QuickContactBadge;
+
+import com.android.contacts.common.R;
+
+public class CheckableQuickContactBadge extends QuickContactBadge implements Checkable {
+    private boolean mChecked = false;
+    private int mCheckMarkBackgroundColor;
+    private CheckableFlipDrawable mDrawable;
+
+    public CheckableQuickContactBadge(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public CheckableQuickContactBadge(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public CheckableQuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context);
+    }
+
+    public CheckableQuickContactBadge(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context);
+    }
+
+    private void init(Context context) {
+        TypedArray a = context.obtainStyledAttributes(android.R.styleable.Theme);
+        setCheckMarkBackgroundColor(a.getColor(android.R.styleable.Theme_colorPrimary,
+                context.getResources().getColor(R.color.people_app_theme_color)));
+        a.recycle();
+    }
+
+    public void setCheckMarkBackgroundColor(int color) {
+        mCheckMarkBackgroundColor = color;
+        if (mDrawable != null) {
+            mDrawable.setCheckMarkBackgroundColor(color);
+        }
+    }
+
+    public void toggle() {
+        setChecked(!mChecked);
+    }
+
+    @Override
+    public boolean isChecked() {
+        return mChecked;
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        setChecked(checked, true);
+    }
+
+    public void setChecked(boolean checked, boolean animate) {
+        if (mChecked == checked) {
+            return;
+        }
+
+        mChecked = checked;
+
+        Drawable d = getDrawable();
+        if (d instanceof CheckableFlipDrawable) {
+            CheckableFlipDrawable cfd = (CheckableFlipDrawable) d;
+            cfd.flipTo(!mChecked);
+            if (!animate) {
+                cfd.reset();
+            }
+        }
+    }
+
+    @Override
+    public void setImageDrawable(Drawable d) {
+        if (d != null) {
+            if (mDrawable == null) {
+                mDrawable = new CheckableFlipDrawable(d, getResources(),
+                        mCheckMarkBackgroundColor, 150);
+            } else {
+                int oldWidth = mDrawable.getIntrinsicWidth();
+                int oldHeight = mDrawable.getIntrinsicHeight();
+                mDrawable.setFront(d);
+                if (oldWidth != mDrawable.getIntrinsicWidth()
+                        || oldHeight != mDrawable.getIntrinsicHeight()) {
+                    // enforce drawable size update + layout
+                    super.setImageDrawable(null);
+                }
+            }
+            d = mDrawable;
+        }
+        super.setImageDrawable(d);
+    }
+}
diff --git a/src/com/android/contacts/common/widget/FlipDrawable.java b/src/com/android/contacts/common/widget/FlipDrawable.java
new file mode 100644
index 0000000..ff14b50
--- /dev/null
+++ b/src/com/android/contacts/common/widget/FlipDrawable.java
@@ -0,0 +1,276 @@
+/*
+ * 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.android.contacts.common.widget;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A drawable that wraps two other drawables and allows flipping between them. The flipping
+ * animation is a 2D rotation around the y axis.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+ *   |       |       |
+ *   V       V       V
+ * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
+ * </pre>
+ */
+public class FlipDrawable extends Drawable implements Drawable.Callback {
+
+    /**
+     * The inner drawables.
+     */
+    protected Drawable mFront;
+    protected final Drawable mBack;
+
+    protected final int mFlipDurationMs;
+    protected final int mPreFlipDurationMs;
+    protected final int mPostFlipDurationMs;
+    private final ValueAnimator mFlipAnimator;
+
+    private static final float END_VALUE = 2f;
+
+    /**
+     * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means
+     * mFront is fully shown, while END_VALUE means mBack is fully shown.
+     */
+    private float mFlipFraction = 0f;
+
+    /**
+     * True if flipping towards front, false if flipping towards back.
+     */
+    private boolean mFlipToSide = true;
+
+    /**
+     * Create a new FlipDrawable. The front is fully shown by default.
+     *
+     * <p/>
+     * The 3 durations are: (best viewed in documentation form)
+     * <pre>
+     * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+     *   |       |       |
+     *   V       V       V
+     * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
+     * </pre>
+     *
+     * @param front              The front drawable.
+     * @param back               The back drawable.
+     * @param flipDurationMs     The duration of the actual flip. This duration includes both
+     *                           animating away one side and showing the other.
+     * @param preFlipDurationMs  The duration before the actual flip begins. Subclasses can use this
+     *                           to add flourish.
+     * @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
+     *                           to add flourish.
+     */
+    public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
+            final int preFlipDurationMs, final int postFlipDurationMs) {
+        if (front == null || back == null) {
+            throw new IllegalArgumentException("Front and back drawables must not be null.");
+        }
+        mFront = front;
+        mBack = back;
+
+        mFront.setCallback(this);
+        mBack.setCallback(this);
+
+        mFlipDurationMs = flipDurationMs;
+        mPreFlipDurationMs = preFlipDurationMs;
+        mPostFlipDurationMs = postFlipDurationMs;
+
+        mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
+                .setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
+        mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(final ValueAnimator animation) {
+                final float old = mFlipFraction;
+                //noinspection ConstantConditions
+                mFlipFraction = (Float) animation.getAnimatedValue();
+                if (old != mFlipFraction) {
+                    invalidateSelf();
+                }
+            }
+        });
+
+        reset();
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mFront.getIntrinsicWidth();
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mFront.getIntrinsicHeight();
+    }
+
+    @Override
+    protected void onBoundsChange(final Rect bounds) {
+        super.onBoundsChange(bounds);
+        if (bounds.isEmpty()) {
+            mFront.setBounds(0, 0, 0, 0);
+            mBack.setBounds(0, 0, 0, 0);
+        } else {
+            mFront.setBounds(bounds);
+            mBack.setBounds(bounds);
+        }
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final Rect bounds = getBounds();
+        if (!isVisible() || bounds.isEmpty()) {
+            return;
+        }
+
+        final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;
+
+        final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+
+        final float scaleX;
+        if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
+            // During pre-flip.
+            scaleX = 1;
+        } else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
+            // During post-flip.
+            scaleX = 1;
+        } else {
+            // During flip.
+            final float flipFraction = mFlipFraction / 2;
+            final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
+                    + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+            final float distFraction = Math.abs(flipFraction - flipMiddle);
+            final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
+            scaleX = distFraction * multiplier;
+        }
+
+        canvas.save();
+        // The flip is a simple 1 dimensional scale.
+        canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
+        inner.draw(canvas);
+        canvas.restore();
+    }
+
+    @Override
+    public void setAlpha(final int alpha) {
+        mFront.setAlpha(alpha);
+        mBack.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(final ColorFilter cf) {
+        mFront.setColorFilter(cf);
+        mBack.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
+    }
+
+    @Override
+    protected boolean onLevelChange(final int level) {
+        return mFront.setLevel(level) || mBack.setLevel(level);
+    }
+
+    @Override
+    public void invalidateDrawable(final Drawable who) {
+        invalidateSelf();
+    }
+
+    @Override
+    public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
+        scheduleSelf(what, when);
+    }
+
+    @Override
+    public void unscheduleDrawable(final Drawable who, final Runnable what) {
+        unscheduleSelf(what);
+    }
+
+    /**
+     * Stop animating the flip and reset to one side.
+     * @param side Pass true if reset to front, false if reset to back.
+     */
+    public void reset() {
+        final float old = mFlipFraction;
+        mFlipAnimator.cancel();
+        mFlipFraction = mFlipToSide ? 0f : 2f;
+        if (mFlipFraction != old) {
+            invalidateSelf();
+        }
+    }
+
+    /**
+     * Returns true if the front is shown. Returns false if the back is shown.
+     */
+    public boolean getSideShown() {
+        final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+        final float middleFraction = (mPreFlipDurationMs / totalDurationMs
+                + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+        return mFlipFraction / 2 < middleFraction;
+    }
+
+    /**
+     * Returns true if the front is being flipped towards. Returns false if the back is being
+     * flipped towards.
+     */
+    public boolean getSideFlippingTowards() {
+        return mFlipToSide;
+    }
+
+    /**
+     * Starts an animated flip to the other side. If a flip animation is currently started,
+     * it will be reversed.
+     */
+    public void flip() {
+        mFlipToSide = !mFlipToSide;
+        if (mFlipAnimator.isStarted()) {
+            mFlipAnimator.reverse();
+        } else {
+            if (!mFlipToSide /* front to back */) {
+                mFlipAnimator.start();
+            } else /* back to front */ {
+                mFlipAnimator.reverse();
+            }
+        }
+    }
+
+    /**
+     * Start an animated flip to a side. This works regardless of whether a flip animation is
+     * currently started.
+     * @param side Pass true if flip to front, false if flip to back.
+     */
+    public void flipTo(final boolean side) {
+        if (mFlipToSide != side) {
+            flip();
+        }
+    }
+
+    /**
+     * Returns whether flipping is in progress.
+     */
+    public boolean isFlipping() {
+        return mFlipAnimator.isStarted();
+    }
+}