First commit for CardFlip.
Change-Id: Ic0e68fdcb23ea70208adfda69f9e500955fa1254
This commit is contained in:
30
samples/devbytes/animation/CardFlip/AndroidManifest.xml
Normal file
30
samples/devbytes/animation/CardFlip/AndroidManifest.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.example.android.cardflip"
|
||||||
|
android:versionCode="1"
|
||||||
|
android:versionName="1.0">
|
||||||
|
<uses-sdk android:minSdkVersion="14"
|
||||||
|
android:targetSdkVersion="17"/>
|
||||||
|
<application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
|
||||||
|
<activity android:name=".CardFlip"
|
||||||
|
android:label="@string/app_name">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
BIN
samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg
Normal file
BIN
samples/devbytes/animation/CardFlip/res/drawable-hdpi/blue.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
BIN
samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg
Normal file
BIN
samples/devbytes/animation/CardFlip/res/drawable-hdpi/red.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
19
samples/devbytes/animation/CardFlip/res/layout/main.xml
Normal file
19
samples/devbytes/animation/CardFlip/res/layout/main.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/main_relative_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
</RelativeLayout>
|
||||||
20
samples/devbytes/animation/CardFlip/res/values/integer.xml
Normal file
20
samples/devbytes/animation/CardFlip/res/values/integer.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<integer name="vertical_card_magin">30</integer>
|
||||||
|
<integer name="horizontal_card_magin">30</integer>
|
||||||
|
|
||||||
|
</resources>
|
||||||
19
samples/devbytes/animation/CardFlip/res/values/strings.xml
Normal file
19
samples/devbytes/animation/CardFlip/res/values/strings.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<string name="app_name">CardFlip</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
/*
|
||||||
|
* 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.app.Activity;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This application creates 2 stacks of playing cards. Using fling events,
|
||||||
|
* these cards can be flipped from one stack to another where each flip comes with
|
||||||
|
* an associated animation. The cards can be flipped horizontally from left to right
|
||||||
|
* or right to left depending on which stack the animating card currently belongs to.
|
||||||
|
*
|
||||||
|
* This application demonstrates an animation where a stack of cards can either be
|
||||||
|
* be rotated out or back in about their bottom left corner in a counter-clockwise direction.
|
||||||
|
* Rotate out: Down fling on stack of cards
|
||||||
|
* Rotate in: Up fling on stack of cards
|
||||||
|
* Full rotation: Tap on stack of cards
|
||||||
|
*
|
||||||
|
* Note that in this demo touch events are disabled in the middle of any animation so
|
||||||
|
* only one card can be flipped at a time. When the cards are in a rotated-out
|
||||||
|
* state, no new cards can be rotated to or from that stack. These changes were made to
|
||||||
|
* simplify the code for this demo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class CardFlip extends Activity implements CardFlipListener {
|
||||||
|
|
||||||
|
final static int CARD_PILE_OFFSET = 3;
|
||||||
|
final static int STARTING_NUMBER_CARDS = 15;
|
||||||
|
final static int RIGHT_STACK = 0;
|
||||||
|
final static int LEFT_STACK = 1;
|
||||||
|
|
||||||
|
int mCardWidth = 0;
|
||||||
|
int mCardHeight = 0;
|
||||||
|
|
||||||
|
int mVerticalPadding;
|
||||||
|
int mHorizontalPadding;
|
||||||
|
|
||||||
|
boolean mTouchEventsEnabled = true;
|
||||||
|
boolean[] mIsStackEnabled;
|
||||||
|
|
||||||
|
RelativeLayout mLayout;
|
||||||
|
|
||||||
|
List<ArrayList<CardView>> mStackCards;
|
||||||
|
|
||||||
|
GestureDetector gDetector;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.main);
|
||||||
|
|
||||||
|
mStackCards = new ArrayList<ArrayList<CardView>>();
|
||||||
|
mStackCards.add(new ArrayList<CardView>());
|
||||||
|
mStackCards.add(new ArrayList<CardView>());
|
||||||
|
|
||||||
|
mIsStackEnabled = new boolean[2];
|
||||||
|
mIsStackEnabled[0] = true;
|
||||||
|
mIsStackEnabled[1] = true;
|
||||||
|
|
||||||
|
mVerticalPadding = getResources().getInteger(R.integer.vertical_card_magin);
|
||||||
|
mHorizontalPadding = getResources().getInteger(R.integer.horizontal_card_magin);
|
||||||
|
|
||||||
|
gDetector = new GestureDetector(this, mGestureListener);
|
||||||
|
|
||||||
|
mLayout = (RelativeLayout)findViewById(R.id.main_relative_layout);
|
||||||
|
ViewTreeObserver observer = mLayout.getViewTreeObserver();
|
||||||
|
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
mLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||||
|
} else {
|
||||||
|
mLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mCardHeight = mLayout.getHeight();
|
||||||
|
mCardWidth = mLayout.getWidth() / 2;
|
||||||
|
|
||||||
|
for (int x = 0; x < STARTING_NUMBER_CARDS; x++) {
|
||||||
|
addNewCard(RIGHT_STACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new card to the specified stack. Also performs all the necessary layout setup
|
||||||
|
* to place the card in the correct position.
|
||||||
|
*/
|
||||||
|
public void addNewCard(int stack) {
|
||||||
|
CardView view = new CardView(this);
|
||||||
|
view.updateTranslation(mStackCards.get(stack).size());
|
||||||
|
view.setCardFlipListener(this);
|
||||||
|
view.setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding);
|
||||||
|
|
||||||
|
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(mCardWidth,
|
||||||
|
mCardHeight);
|
||||||
|
params.topMargin = 0;
|
||||||
|
params.leftMargin = (stack == RIGHT_STACK ? mCardWidth : 0);
|
||||||
|
|
||||||
|
mStackCards.get(stack).add(view);
|
||||||
|
mLayout.addView(view, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gesture Detector listens for fling events in order to potentially initiate
|
||||||
|
* a card flip event when a fling event occurs. Also listens for tap events in
|
||||||
|
* order to potentially initiate a full rotation animation.
|
||||||
|
*/
|
||||||
|
private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector
|
||||||
|
.SimpleOnGestureListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onSingleTapUp(MotionEvent motionEvent) {
|
||||||
|
int stack = getStack(motionEvent);
|
||||||
|
rotateCardsFullRotation(stack, CardView.Corner.BOTTOM_LEFT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent2, float v,
|
||||||
|
float v2) {
|
||||||
|
int stack = getStack(motionEvent);
|
||||||
|
ArrayList<CardView> cardStack = mStackCards.get(stack);
|
||||||
|
int size = cardStack.size();
|
||||||
|
if (size > 0) {
|
||||||
|
rotateCardView(cardStack.get(size - 1), stack, v, v2);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns the appropriate stack corresponding to the MotionEvent. */
|
||||||
|
public int getStack(MotionEvent ev) {
|
||||||
|
boolean isLeft = ev.getX() <= mCardWidth;
|
||||||
|
return isLeft ? LEFT_STACK : RIGHT_STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the stack parameter, along with the velocity values of the fling event
|
||||||
|
* to determine in what direction the card must be flipped. By the same logic, the
|
||||||
|
* new stack that the card belongs to after the animation is also determined
|
||||||
|
* and updated.
|
||||||
|
*/
|
||||||
|
public void rotateCardView(final CardView cardView, int stack, float velocityX,
|
||||||
|
float velocityY) {
|
||||||
|
|
||||||
|
boolean xGreaterThanY = Math.abs(velocityX) > Math.abs(velocityY);
|
||||||
|
|
||||||
|
boolean bothStacksEnabled = mIsStackEnabled[RIGHT_STACK] && mIsStackEnabled[LEFT_STACK];
|
||||||
|
|
||||||
|
ArrayList<CardView>leftStack = mStackCards.get(LEFT_STACK);
|
||||||
|
ArrayList<CardView>rightStack = mStackCards.get(RIGHT_STACK);
|
||||||
|
|
||||||
|
switch (stack) {
|
||||||
|
case RIGHT_STACK:
|
||||||
|
if (velocityX < 0 && xGreaterThanY) {
|
||||||
|
if (!bothStacksEnabled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mLayout.bringChildToFront(cardView);
|
||||||
|
mLayout.requestLayout();
|
||||||
|
rightStack.remove(rightStack.size() - 1);
|
||||||
|
leftStack.add(cardView);
|
||||||
|
cardView.flipRightToLeft(leftStack.size() - 1, (int)velocityX);
|
||||||
|
break;
|
||||||
|
} else if (!xGreaterThanY) {
|
||||||
|
boolean rotateCardsOut = velocityY > 0;
|
||||||
|
rotateCards(RIGHT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LEFT_STACK:
|
||||||
|
if (velocityX > 0 && xGreaterThanY) {
|
||||||
|
if (!bothStacksEnabled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mLayout.bringChildToFront(cardView);
|
||||||
|
mLayout.requestLayout();
|
||||||
|
leftStack.remove(leftStack.size() - 1);
|
||||||
|
rightStack.add(cardView);
|
||||||
|
cardView.flipLeftToRight(rightStack.size() - 1, (int)velocityX);
|
||||||
|
break;
|
||||||
|
} else if (!xGreaterThanY) {
|
||||||
|
boolean rotateCardsOut = velocityY > 0;
|
||||||
|
rotateCards(LEFT_STACK, CardView.Corner.BOTTOM_LEFT, rotateCardsOut);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCardFlipEnd() {
|
||||||
|
mTouchEventsEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCardFlipStart() {
|
||||||
|
mTouchEventsEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent me) {
|
||||||
|
if (mTouchEventsEnabled) {
|
||||||
|
return gDetector.onTouchEvent(me);
|
||||||
|
} else {
|
||||||
|
return super.onTouchEvent(me);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an animator object for each card in the specified stack that either
|
||||||
|
* rotates it in or out depending on its current state. All of these animations
|
||||||
|
* are then played together.
|
||||||
|
*/
|
||||||
|
public void rotateCards (final int stack, CardView.Corner corner,
|
||||||
|
final boolean isRotatingOut) {
|
||||||
|
List<Animator> animations = new ArrayList<Animator>();
|
||||||
|
|
||||||
|
ArrayList <CardView> cards = mStackCards.get(stack);
|
||||||
|
|
||||||
|
for (int i = 0; i < cards.size(); i++) {
|
||||||
|
CardView cardView = cards.get(i);
|
||||||
|
animations.add(cardView.getRotationAnimator(i, corner, isRotatingOut, false));
|
||||||
|
mLayout.bringChildToFront(cardView);
|
||||||
|
}
|
||||||
|
/** All the cards are being brought to the front in order to guarantee that
|
||||||
|
* the cards being rotated in the current stack will overlay the cards in the
|
||||||
|
* other stack. After the z-ordering of all the cards is updated, a layout must
|
||||||
|
* be requested in order to apply the changes made.*/
|
||||||
|
mLayout.requestLayout();
|
||||||
|
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(animations);
|
||||||
|
set.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
mIsStackEnabled[stack] = !isRotatingOut;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
set.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an animator object for each card in the specified stack to complete a
|
||||||
|
* full revolution around one of its corners, and plays all of them together.
|
||||||
|
*/
|
||||||
|
public void rotateCardsFullRotation (int stack, CardView.Corner corner) {
|
||||||
|
List<Animator> animations = new ArrayList<Animator>();
|
||||||
|
|
||||||
|
ArrayList <CardView> cards = mStackCards.get(stack);
|
||||||
|
for (int i = 0; i < cards.size(); i++) {
|
||||||
|
CardView cardView = cards.get(i);
|
||||||
|
animations.add(cardView.getFullRotationAnimator(i, corner, false));
|
||||||
|
mLayout.bringChildToFront(cardView);
|
||||||
|
}
|
||||||
|
/** Same reasoning for bringing cards to front as in rotateCards().*/
|
||||||
|
mLayout.requestLayout();
|
||||||
|
|
||||||
|
mTouchEventsEnabled = false;
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(animations);
|
||||||
|
set.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
mTouchEventsEnabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
set.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface is used to prevent flipping multiple cards at the same time.
|
||||||
|
* These callback methods are used to disable and re-enable touches when a card
|
||||||
|
* flip animation begins and ends respectively.
|
||||||
|
* */
|
||||||
|
public interface CardFlipListener {
|
||||||
|
public void onCardFlipEnd();
|
||||||
|
public void onCardFlipStart();
|
||||||
|
}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user