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