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 
CardView(Context context)75     public CardView(Context context) {
76         super(context);
77         init(context);
78     }
79 
CardView(Context context, AttributeSet attrs)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.*/
init(Context context)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      */
bitmapWithBorder(BitmapDrawable bitmapDrawable)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. */
flipRightToLeft(int numberInPile, int velocity)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. */
flipLeftToRight(int numberInPile, int velocity)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      */
flipHorizontally(int numberInPile, boolean clockwise, int velocity)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