/* * 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; } }