Home | History | Annotate | Download | only in cardflip
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.cardflip;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.AnimatorSet;
     22 import android.animation.Keyframe;
     23 import android.animation.ObjectAnimator;
     24 import android.animation.PropertyValuesHolder;
     25 import android.animation.ValueAnimator;
     26 import android.content.Context;
     27 import android.graphics.Bitmap;
     28 import android.graphics.Canvas;
     29 import android.graphics.Color;
     30 import android.graphics.Matrix;
     31 import android.graphics.drawable.BitmapDrawable;
     32 import android.util.AttributeSet;
     33 import android.view.View;
     34 import android.view.animation.AccelerateDecelerateInterpolator;
     35 import android.widget.ImageView;
     36 import android.widget.RelativeLayout;
     37 
     38 /**
     39  * This CardView object is a view which can flip horizontally about its edges,
     40  * as well as rotate clockwise or counter-clockwise about any of its corners. In
     41  * the middle of a flip animation, this view darkens to imitate a shadow-like effect.
     42  *
     43  * The key behind the design of this view is the fact that the layout parameters and
     44  * the animation properties of this view are updated and reset respectively after
     45  * every single animation. Therefore, every consecutive animation that this
     46  * view experiences is completely independent of what its prior state was.
     47  */
     48 public class CardView extends ImageView {
     49 
     50     enum Corner {
     51         TOP_LEFT,
     52         TOP_RIGHT,
     53         BOTTOM_LEFT,
     54         BOTTOM_RIGHT
     55     }
     56 
     57     private final int CAMERA_DISTANCE = 8000;
     58     private final int MIN_FLIP_DURATION = 300;
     59     private final int VELOCITY_TO_DURATION_CONSTANT = 15;
     60     private final int MAX_FLIP_DURATION = 700;
     61     private final int ROTATION_PER_CARD = 2;
     62     private final int ROTATION_DELAY_PER_CARD = 50;
     63     private final int ROTATION_DURATION = 2000;
     64     private final int ANTIALIAS_BORDER = 1;
     65 
     66     private BitmapDrawable mFrontBitmapDrawable, mBackBitmapDrawable, mCurrentBitmapDrawable;
     67 
     68     private boolean mIsFrontShowing = true;
     69     private boolean mIsHorizontallyFlipped = false;
     70 
     71     private Matrix mHorizontalFlipMatrix;
     72 
     73     private CardFlipListener mCardFlipListener;
     74 
     75     public CardView(Context context) {
     76         super(context);
     77         init(context);
     78     }
     79 
     80     public CardView(Context context, AttributeSet attrs) {
     81         super(context, attrs);
     82         init(context);
     83     }
     84 
     85     /** Loads the bitmap drawables used for the front and back for this card.*/
     86     public void init(Context context) {
     87         mHorizontalFlipMatrix = new Matrix();
     88 
     89         setCameraDistance(CAMERA_DISTANCE);
     90 
     91         mFrontBitmapDrawable = bitmapWithBorder((BitmapDrawable)getResources()
     92                 .getDrawable(R.drawable.red));
     93         mBackBitmapDrawable = bitmapWithBorder((BitmapDrawable) getResources()
     94                 .getDrawable(R.drawable.blue));
     95 
     96         updateDrawableBitmap();
     97     }
     98 
     99     /**
    100      *  Adding a 1 pixel transparent border around the bitmap can be used to
    101      *  anti-alias the image as it rotates.
    102      */
    103     private BitmapDrawable bitmapWithBorder(BitmapDrawable bitmapDrawable) {
    104         Bitmap bitmapWithBorder = Bitmap.createBitmap(bitmapDrawable.getIntrinsicWidth() +
    105                 ANTIALIAS_BORDER * 2, bitmapDrawable.getIntrinsicHeight() + ANTIALIAS_BORDER * 2,
    106                 Bitmap.Config.ARGB_8888);
    107         Canvas canvas = new Canvas(bitmapWithBorder);
    108         canvas.drawBitmap(bitmapDrawable.getBitmap(), ANTIALIAS_BORDER, ANTIALIAS_BORDER, null);
    109         return new BitmapDrawable(getResources(), bitmapWithBorder);
    110     }
    111 
    112     /** Initiates a horizontal flip from right to left. */
    113     public void flipRightToLeft(int numberInPile, int velocity) {
    114         setPivotX(0);
    115         flipHorizontally(numberInPile, false, velocity);
    116     }
    117 
    118     /** Initiates a horizontal flip from left to right. */
    119     public void flipLeftToRight(int numberInPile, int velocity) {
    120         setPivotX(getWidth());
    121         flipHorizontally(numberInPile, true, velocity);
    122     }
    123 
    124     /**
    125      * Animates a horizontal (about the y-axis) flip of this card.
    126      * @param numberInPile Specifies how many cards are underneath this card in the new
    127      *                     pile so as to properly adjust its position offset in the stack.
    128      * @param clockwise Specifies whether the horizontal animation is 180 degrees
    129      *                  clockwise or 180 degrees counter clockwise.
    130      */
    131     public void flipHorizontally (int numberInPile, boolean clockwise, int velocity) {
    132         toggleFrontShowing();
    133 
    134         PropertyValuesHolder rotation = PropertyValuesHolder.ofFloat(View.ROTATION_Y,
    135                 clockwise ? 180 : -180);
    136 
    137         PropertyValuesHolder xOffset = PropertyValuesHolder.ofFloat(View.TRANSLATION_X,
    138                 numberInPile * CardFlip.CARD_PILE_OFFSET);
    139         PropertyValuesHolder yOffset = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
    140                 numberInPile * CardFlip.CARD_PILE_OFFSET);
    141 
    142         ObjectAnimator cardAnimator = ObjectAnimator.ofPropertyValuesHolder(this, rotation,
    143                 xOffset, yOffset);
    144         cardAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    145             @Override
    146             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    147                 if (valueAnimator.getAnimatedFraction() >= 0.5) {
    148                     updateDrawableBitmap();
    149                 }
    150             }
    151         });
    152 
    153         Keyframe shadowKeyFrameStart = Keyframe.ofFloat(0, 0);
    154         Keyframe shadowKeyFrameMid = Keyframe.ofFloat(0.5f, 1);
    155         Keyframe shadowKeyFrameEnd = Keyframe.ofFloat(1, 0);
    156         PropertyValuesHolder shadowPropertyValuesHolder = PropertyValuesHolder.ofKeyframe
    157                 ("shadow", shadowKeyFrameStart, shadowKeyFrameMid, shadowKeyFrameEnd);
    158         ObjectAnimator colorizer = ObjectAnimator.ofPropertyValuesHolder(this,
    159                 shadowPropertyValuesHolder);
    160 
    161         mCardFlipListener.onCardFlipStart();
    162         AnimatorSet set = new AnimatorSet();
    163         int duration = MAX_FLIP_DURATION - Math.abs(velocity) / VELOCITY_TO_DURATION_CONSTANT;
    164         duration = duration < MIN_FLIP_DURATION ? MIN_FLIP_DURATION : duration;
    165         set.setDuration(duration);
    166         set.playTogether(cardAnimator, colorizer);
    167         set.setInterpolator(new AccelerateDecelerateInterpolator());
    168         set.addListener(new AnimatorListenerAdapter() {
    169             @Override
    170             public void onAnimationEnd(Animator animation) {
    171                 toggleIsHorizontallyFlipped();
    172                 updateDrawableBitmap();
    173                 updateLayoutParams();
    174                 mCardFlipListener.onCardFlipEnd();
    175             }
    176         });
    177         set.start();
    178     }
    179 
    180     /** Darkens this ImageView's image by applying a shadow color filter over it. */
    181     public void setShadow(float value) {
    182         int colorValue = (int)(255 - 200 * value);
    183         setColorFilter(Color.rgb(colorValue, colorValue, colorValue),
    184                 android.graphics.PorterDuff.Mode.MULTIPLY);
    185     }
    186 
    187     public void toggleFrontShowing() {
    188         mIsFrontShowing = !mIsFrontShowing;
    189     }
    190 
    191     public void toggleIsHorizontallyFlipped() {
    192         mIsHorizontallyFlipped = !mIsHorizontallyFlipped;
    193         invalidate();
    194     }
    195 
    196     @Override
    197     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    198         super.onSizeChanged(w, h, oldw, oldh);
    199         mHorizontalFlipMatrix.setScale(-1, 1, w / 2, h / 2);
    200     }
    201 
    202     /**
    203      *  Scale the canvas horizontally about its midpoint in the case that the card
    204      *  is in a horizontally flipped state.
    205      */
    206     @Override
    207     protected void onDraw(Canvas canvas) {
    208         if (mIsHorizontallyFlipped) {
    209             canvas.concat(mHorizontalFlipMatrix);
    210         }
    211         super.onDraw(canvas);
    212     }
    213 
    214     /**
    215      *  Updates the layout parameters of this view so as to reset the rotationX and
    216      *  rotationY parameters, and remain independent of its previous position, while
    217      *  also maintaining its current position in the layout.
    218      */
    219     public void updateLayoutParams () {
    220         RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
    221 
    222         params.leftMargin = (int)(params.leftMargin + ((Math.abs(getRotationY()) % 360) / 180) *
    223                 (2 * getPivotX () - getWidth()));
    224 
    225         setRotationX(0);
    226         setRotationY(0);
    227 
    228         setLayoutParams(params);
    229     }
    230 
    231     /**
    232      * Toggles the visible bitmap of this view between its front and back drawables
    233      * respectively.
    234      */
    235     public void updateDrawableBitmap () {
    236         mCurrentBitmapDrawable = mIsFrontShowing ? mFrontBitmapDrawable : mBackBitmapDrawable;
    237         setImageDrawable(mCurrentBitmapDrawable);
    238     }
    239 
    240     /**
    241      * Sets the appropriate translation of this card depending on how many cards
    242      * are in the pile underneath it.
    243      */
    244     public void updateTranslation (int numInPile) {
    245         setTranslationX(CardFlip.CARD_PILE_OFFSET * numInPile);
    246         setTranslationY(CardFlip.CARD_PILE_OFFSET * numInPile);
    247     }
    248 
    249     /**
    250      * Returns a rotation animation which rotates this card by some degree about
    251      * one of its corners either in the clockwise or counter-clockwise direction.
    252      * Depending on how many cards lie below this one in the stack, this card will
    253      * be rotated by a different amount so all the cards are visible when rotated out.
    254      */
    255     public ObjectAnimator getRotationAnimator (int cardFromTop, Corner corner,
    256                                                boolean isRotatingOut, boolean isClockwise) {
    257         rotateCardAroundCorner(corner);
    258         int rotation = cardFromTop * ROTATION_PER_CARD;
    259 
    260         if (!isClockwise) {
    261             rotation = -rotation;
    262         }
    263 
    264         if (!isRotatingOut) {
    265             rotation = 0;
    266         }
    267 
    268         return ObjectAnimator.ofFloat(this, View.ROTATION, rotation);
    269     }
    270 
    271     /**
    272      * Returns a full rotation animator which rotates this card by 360 degrees
    273      * about one of its corners either in the clockwise or counter-clockwise direction.
    274      * Depending on how many cards lie below this one in the stack, a different start
    275      * delay is applied to the animation so the cards don't all animate at once.
    276      */
    277     public ObjectAnimator getFullRotationAnimator (int cardFromTop, Corner corner,
    278                                                    boolean isClockwise) {
    279         final int currentRotation = (int)getRotation();
    280 
    281         rotateCardAroundCorner(corner);
    282         int rotation = 360 - currentRotation;
    283         rotation =  isClockwise ? rotation : -rotation;
    284 
    285         ObjectAnimator animator = ObjectAnimator.ofFloat(this, View.ROTATION, rotation);
    286 
    287         animator.setStartDelay(ROTATION_DELAY_PER_CARD * cardFromTop);
    288         animator.setDuration(ROTATION_DURATION);
    289 
    290         animator.addListener(new AnimatorListenerAdapter() {
    291             @Override
    292             public void onAnimationEnd(Animator animation) {
    293                 setRotation(currentRotation);
    294             }
    295         });
    296 
    297         return animator;
    298     }
    299 
    300     /**
    301      * Sets the appropriate pivot of this card so that it can be rotated about
    302      * any one of its four corners.
    303      */
    304     public void rotateCardAroundCorner(Corner corner) {
    305         switch(corner) {
    306             case TOP_LEFT:
    307                 setPivotX(0);
    308                 setPivotY(0);
    309                 break;
    310             case TOP_RIGHT:
    311                 setPivotX(getWidth());
    312                 setPivotY(0);
    313                 break;
    314             case BOTTOM_LEFT:
    315                 setPivotX(0);
    316                 setPivotY(getHeight());
    317                 break;
    318             case BOTTOM_RIGHT:
    319                 setPivotX(getWidth());
    320                 setPivotY(getHeight());
    321                 break;
    322         }
    323     }
    324 
    325     public void setCardFlipListener(CardFlipListener cardFlipListener) {
    326         mCardFlipListener = cardFlipListener;
    327     }
    328 
    329 }
    330