Android卡牌翻转动画效果实现
简述
之前项目有个需求,需要实现卡牌翻转效果,自己试着写过,效果不是很好,后来找到了一个Rotatable-master的项目,使用里面提供的类实现了卡牌翻转的效果。默认情况下,翻转动画卡牌在动画进行中的拉伸变形幅度会很大,可以通过setCameraDistance方法来改变控件的应该说是观察点距离吧,让观察点远一点,这样反转动画看起来会舒服一点。
效果图
代码
.java 旋转工具类
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.support.annotation.IntDef;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.DisplayMetrics;
import android.util.Property;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.CycleInterpolator;
import java.util.ArrayList;
/**
* 旋转工具类
* Created by Yahya Bayramoglu on 01/12/15.
*/
public class Rotatable implements View.OnTouchListener {
private static final int NULL_INT = -1;
private final int FIT_ANIM_TIME = 300;
public static final int DEFAULT_ROTATE_ANIM_TIME = 500;
public static final int ROTATE_BOTH = 0;
public static final int ROTATE_X = 1;
public static final int ROTATE_Y = 2;
@IntDef({ROTATE_X, ROTATE_Y, ROTATE_BOTH})
public @interface Direction {
}
public static final int FRONT_VIEW = 3;
public static final int BACK_VIEW = 4;
@IntDef({FRONT_VIEW, BACK_VIEW})
public @interface Side {
}
private RotationListener rotationListener;
private View rootView, frontView, backView;
private boolean touchEnable = true;
private boolean shouldSwapViews = false;
private int rotation;
private int screenWidth = NULL_INT, screenHeight = NULL_INT;
private int currentVisibleView = FRONT_VIEW;
private float rotationCount;
private float rotationDistance;
private float oldX, oldY, currentX, currentY;
private float currentXRotation = 0, currentYRotation = 0;
private float maxDistanceX = NULL_INT, maxDistanceY = NULL_INT;
private float defaultPivotX = NULL_INT, defaultPivotY = NULL_INT;
private Rotatable(Builder builder) {
this.rootView = builder.root;
this.defaultPivotX = rootView.getPivotX();
this.defaultPivotY = rootView.getPivotY();
this.rotationListener = builder.listener;
if (builder.pivotX != NULL_INT) {
this.rootView.setPivotX(builder.pivotX);
}
if (builder.pivotY != NULL_INT) {
this.rootView.setPivotY(builder.pivotY);
}
if (builder.frontId != NULL_INT) {
this.frontView = rootView.findViewById(builder.frontId);
}
if (builder.backId != NULL_INT) {
this.backView = rootView.findViewById(builder.backId);
}
this.rotation = builder.rotation;
this.rotationCount = builder.rotationCount;
this.rotationDistance = builder.rotationDistance;
this.shouldSwapViews = frontView != null && backView != null;
rootView.setOnTouchListener(this);
}
/**
* This method needs to be call, if only you need to reset and
* rebuild a view as rotatable with different configurations
*/
public void drop() {
rootView.setPivotX(defaultPivotX);
rootView.setPivotY(defaultPivotY);
rootView.setOnTouchListener(null);
rootView = null;
frontView = null;
backView = null;
}
/**
* You can specify rotation direction as axis X, Y or BOTH
*/
public void setDirection(@Direction int direction) {
if (!isRotationValid(direction)) {
throw new IllegalArgumentException("Cannot specify given value as rotation direction!");
}
this.rotation = direction;
}
/**
* You may need to enable / disable touch interaction at some point,
* so it is possible to do it so anytime by rotatable object
*/
public void setTouchEnable(boolean enable) {
this.touchEnable = enable;
}
/**
* To determine rotatable object is currently touchable or not
*/
public boolean isTouchEnable() {
return touchEnable;
}
/**
* If your application can be used multi orientated, then you have to declare
* orientation changes to rotatable object, so it can recalculate its maxDistances.
* <p>
* You only need to inform rotatable object about orientation changes, when you specified
* {@link Builder#rotationCount(float)} or {@link Builder#rotationDistance(float)}
*/
public void orientationChanged(int newOrientation) {
if (screenWidth == NULL_INT) {
calculateScreenDimensions();
}
measureScreenUpToOrientation(newOrientation);
// reset maxDistances values to recalculate them
maxDistanceX = NULL_INT;
maxDistanceY = NULL_INT;
}
/**
* Call this method to reveal rotatable view's existence
*/
public void takeAttention() {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(rootView, View.ROTATION_X, 10);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(rootView, View.ROTATION_Y, -10);
AnimatorSet set = new AnimatorSet();
set.setDuration(DEFAULT_ROTATE_ANIM_TIME);
set.setInterpolator(new CycleInterpolator(0.8f));
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
rootView.animate().rotationX(0).rotationY(0).setDuration(FIT_ANIM_TIME)
.setInterpolator(new FastOutSlowInInterpolator()).start();
}
});
set.playTogether(animatorX, animatorY);
set.start();
}
/**
* Animate rotatable object with given direction and degree also possible
* to set duration and a listener with other derivation of this method
*/
public void rotate(int direction, float degree) {
rotate(direction, degree, DEFAULT_ROTATE_ANIM_TIME);
}
public void rotate(int direction, float degree, int duration) {
rotate(direction, degree, duration, null);
}
public void rotate(final int direction, float degree, int duration, Animator.AnimatorListener listener) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(duration);
animatorSet.setInterpolator(new FastOutSlowInInterpolator());
ArrayList<Animator> animators = new ArrayList<>();
if (direction == ROTATE_X || direction == ROTATE_BOTH) {
animators.add(getAnimatorForProperty(View.ROTATION_X, direction, degree));
}
if (direction == ROTATE_Y || direction == ROTATE_BOTH) {
animators.add(getAnimatorForProperty(View.ROTATION_Y, direction, degree));
}
if (listener != null) {
animatorSet.addListener(listener);
}
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
updateRotationValues(true);
}
});
animatorSet.playTogether(animators);
animatorSet.start();
}
/**
* Rotates once around in given direction
*/
public void rotateOnce() {
float toDegree;
if (rotation == ROTATE_X) {
toDegree = rootView.getRotationX();
} else if (rotation == ROTATE_Y) {
toDegree = rootView.getRotationY();
} else {
toDegree = rootView.getRotation();
}
toDegree += 180;
rotate(rotation, toDegree);
}
/**
* Returns true if currently frontView is visible, false otherwise
*/
public boolean isFront() {
return getCurrentVisibleView() == FRONT_VIEW;
}
/**
* Returns currentVisibleView value as {@link Side}
*/
public
@Side
int getCurrentVisibleView() {
return currentVisibleView;
}
public float getCurrentXRotation() {
return currentXRotation;
}
public float getCurrentYRotation() {
return currentYRotation;
}
private Animator getAnimatorForProperty(Property property, final int direction, float degree) {
ObjectAnimator animator = ObjectAnimator.ofFloat(rootView, property, degree);
if (shouldSwapViews) {
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
updateRotationValues(false);
swapViews(direction);
}
});
}
return animator;
}
private void updateRotationValues(boolean notifyListener) {
currentXRotation = rootView.getRotationX();
currentYRotation = rootView.getRotationY();
if (notifyListener) {
notifyListenerRotationChanged();
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (touchEnable) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
restoreOldPositions(event);
break;
}
case MotionEvent.ACTION_MOVE: {
restoreNewPositions(event);
handleRotation();
if (shouldSwapViews) {
swapViews(rotation);
}
notifyListenerRotationChanged();
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
fitRotation();
break;
}
}
return true;
} else {
return false;
}
}
private void restoreOldPositions(MotionEvent event) {
if (shouldRotateX()) {
oldY = getYValue(event.getRawY());
}
if (shouldRotateY()) {
oldX = getXValue(event.getRawX());
}
}
private float getXValue(float rawX) {
if (rotationCount != NULL_INT && maxDistanceX != NULL_INT) {
return rawX * rotationCount * 180 / maxDistanceX;
}
if (rotationDistance != NULL_INT) {
return rawX * 180 / rotationDistance;
}
return rawX;
}
private float getYValue(float rawY) {
if (rotationCount != NULL_INT && maxDistanceY != NULL_INT) {
return rawY * rotationCount * 180 / maxDistanceY;
}
if (rotationDistance != NULL_INT) {
return rawY * 180 / rotationDistance;
}
return rawY;
}
private void restoreNewPositions(MotionEvent event) {
if (shouldRotateX()) {
if (rotationCount != NULL_INT && maxDistanceY == NULL_INT) {
maxDistanceY = (event.getRawY() - oldY) > 0 ? (getScreenHeight() - oldY) : oldY;
oldY = getYValue(oldY);
}
currentY = getYValue(event.getRawY());
}
if (shouldRotateY()) {
if (rotationCount != NULL_INT && maxDistanceX == NULL_INT) {
maxDistanceX = (event.getRawX() - oldX) > 0 ? (getScreenWidth() - oldX) : oldX;
oldX = getXValue(oldX);
}
currentX = getXValue(event.getRawX());
}
}
private int getScreenWidth() {
if (screenWidth == NULL_INT) {
calculateScreenDimensions();
}
return screenWidth;
}
private int getScreenHeight() {
if (screenHeight == NULL_INT) {
calculateScreenDimensions();
}
return screenHeight;
}
private void calculateScreenDimensions() {
Display display = ((WindowManager) rootView.getContext()
.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
screenWidth = metrics.widthPixels;
screenHeight = metrics.heightPixels;
}
private void measureScreenUpToOrientation(int screenOrientation) {
int tempWidth = screenWidth, tempHeight = screenHeight;
if (screenOrientation == Configuration.ORIENTATION_LANDSCAPE) {
/**
* If screenOrientation is landscape, then width will have a larger value than height
*/
screenWidth = Math.max(tempWidth, tempHeight);
screenHeight = Math.min(tempWidth, tempHeight);
} else {
/**
* If screenOrientation:
* is portrait, then width will have a smaller value than height
* is square, then width and height will be same
* is unknown, then unknown to rotatable as well
* so either way...
*/
screenWidth = Math.min(tempWidth, tempHeight);
screenHeight = Math.max(tempWidth, tempHeight);
}
}
private boolean shouldRotateX() {
return rotation == ROTATE_X || rotation == ROTATE_BOTH;
}
private boolean shouldRotateY() {
return rotation == ROTATE_Y || rotation == ROTATE_BOTH;
}
private void handleRotation() {
if (shouldRotateX()) {
float newXRotation = (rootView.getRotationX() + (oldY - currentY)) % 360;
rootView.setRotationX(newXRotation);
currentXRotation = newXRotation;
oldY = currentY;
}
if (shouldRotateY()) {
float newYRotation;
if (isInFrontArea(currentXRotation)) {
newYRotation = (rootView.getRotationY() + (currentX - oldX)) % 360;
} else {
newYRotation = (rootView.getRotationY() - (currentX - oldX)) % 360;
}
rootView.setRotationY(newYRotation);
currentYRotation = newYRotation;
oldX = currentX;
}
}
private boolean isInFrontArea(float value) {
return (-270 >= value && value >= -360)
|| (-90 <= value && value <= 90)
|| (270 <= value && value <= 360);
}
private void swapViews(int rotation) {
boolean isFront = false;
if (rotation == ROTATE_Y) {
isFront = isInFrontArea(currentYRotation);
if (!isInFrontArea(currentXRotation)) {
isFront = !isFront;
}
}
if (rotation == ROTATE_X) {
isFront = isInFrontArea(currentXRotation);
if (!isInFrontArea(currentYRotation)) {
isFront = !isFront;
}
}
if (rotation == ROTATE_BOTH) {
isFront = (currentXRotation > -90 && currentXRotation < 90) && (currentYRotation > -90 && currentYRotation < 90)
|| (currentXRotation > -90 && currentXRotation < 90) && (currentYRotation > -360 && currentYRotation < -270)
|| (currentXRotation > -360 && currentXRotation < -270) && (currentYRotation > -90 && currentYRotation < 90)
|| (currentXRotation > -90 && currentXRotation < 90) && (currentYRotation > 270 && currentYRotation < 360)
|| (currentXRotation > 270 && currentXRotation < 360) && (currentYRotation > -90 && currentYRotation < 90)
|| (currentXRotation > 90 && currentXRotation < 270) && (currentYRotation > -270 && currentYRotation < -90)
|| (currentXRotation > -270 && currentXRotation < -90) && (currentYRotation > 90 && currentYRotation < 270)
|| (currentXRotation > 90 && currentXRotation < 270) && (currentYRotation > 90 && currentYRotation < 270)
|| (currentXRotation > -270 && currentXRotation < -90) && (currentYRotation > -270 && currentYRotation < -90);
}
boolean shouldSwap = (isFront && currentVisibleView == BACK_VIEW) || (!isFront && currentVisibleView == FRONT_VIEW);
if (shouldSwap) {
frontView.setVisibility(isFront ? View.VISIBLE : View.GONE);
backView.setVisibility(isFront ? View.GONE : View.VISIBLE);
currentVisibleView = isFront ? FRONT_VIEW : BACK_VIEW;
}
}
private void notifyListenerRotationChanged() {
if (rotationListener != null) {
rotationListener.onRotationChanged(currentXRotation, currentYRotation);
}
}
private void fitRotation() {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(FIT_ANIM_TIME);
animatorSet.setInterpolator(new FastOutSlowInInterpolator());
ArrayList<Animator> animators = new ArrayList<>();
if (shouldRotateY()) {
animators.add(ObjectAnimator.ofFloat(rootView, View.ROTATION_Y, getRequiredRotation(rootView.getRotationY())));
}
if (shouldRotateX()) {
animators.add(ObjectAnimator.ofFloat(rootView, View.ROTATION_X, getRequiredRotation(rootView.getRotationX())));
}
animatorSet.playTogether(animators);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
updateRotationValues(true);
}
});
animatorSet.start();
// Reset max values to calculate again on touch down
maxDistanceX = NULL_INT;
maxDistanceY = NULL_INT;
}
private float getRequiredRotation(float currentRotation) {
float requiredRotation;
if (currentRotation < -270) {
requiredRotation = -360;
} else if (currentRotation < -90 && currentRotation > -270) {
requiredRotation = -180;
} else if (currentRotation > -90 && currentRotation < 90) {
requiredRotation = 0;
} else if (currentRotation > 90 && currentRotation < 270) {
requiredRotation = 180;
} else {
requiredRotation = 360;
}
return requiredRotation;
}
/**
* Listener to get notified whenever view's rotation is changed
*/
public interface RotationListener {
void onRotationChanged(float newRotationX, float newRotationY);
}
public static class Builder {
private View root;
private RotationListener listener;
private int rotation = NULL_INT;
private int frontId = NULL_INT;
private int backId = NULL_INT;
private int pivotX = NULL_INT;
private int pivotY = NULL_INT;
private float rotationCount = NULL_INT;
private float rotationDistance = NULL_INT;
public Builder(View viewToRotate) {
this.root = viewToRotate;
}
/**
* This listener will receive current rotation values of given view
*/
public Builder listener(RotationListener listener) {
this.listener = listener;
return this;
}
/**
* Declaring sides will provide swapping between them when necessary,
* if not declared, then rootView will be rotating by itself without any other effect
*/
public Builder sides(int frontViewId, int backViewId) {
this.frontId = frontViewId;
this.backId = backViewId;
return this;
}
/**
* Specify an axis or both axises to rotate around
*/
public Builder direction(@Direction int rotation) {
this.rotation = rotation;
return this;
}
/**
* This method provides view to rotate only as given rotation count,
* irrelevant to its position or touch distance
*/
public Builder rotationCount(float count) {
if (rotationDistance != NULL_INT) {
throw new IllegalArgumentException("You cannot specify both distance and count for rotation limitation.");
}
this.rotationCount = count;
return this;
}
/**
* This method provides view to rotate once in given distance,
* note that it won't rotate full if touch distance is not enough
* but it may still fit the rotation. If you want to ensure rotation gets completed
* see {@link #rotationCount(float}
*/
public Builder rotationDistance(float distance) {
if (rotationCount != NULL_INT) {
throw new IllegalArgumentException("You cannot specify both distance and count for rotation limitation.");
}
this.rotationDistance = distance;
return this;
}
/**
* Consider not to change pivot values because view may out of its bounders and get invisible.
*/
public Builder pivot(int pivotX, int pivotY) {
this.pivotX = pivotX;
this.pivotY = pivotY;
return this;
}
/**
* Consider not to change pivot values because view may out of its bounders and get invisible.
*/
public Builder pivotX(int pivotX) {
this.pivotX = pivotX;
return this;
}
/**
* Consider not to change pivot values because view may out of its bounders and get invisible.
*/
public Builder pivotY(int pivotY) {
this.pivotY = pivotY;
return this;
}
public Rotatable build() {
if (rotation == NULL_INT || !isRotationValid(rotation)) {
throw new IllegalArgumentException("You must specify a direction!");
}
return new Rotatable(this);
}
}
private static boolean isRotationValid(int value) {
return value == ROTATE_X || value == ROTATE_Y || value == ROTATE_BOTH;
}
}.java 用到了nineoldandroids辅助做了一些动画
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import com.example.wood.haha.DisplayImageOptionsUtil;
import com.example.wood.haha.R;
import com.example.wood.haha.Rotatable;
import com.nineoldandroids.view.ViewHelper;
import com.nostra13.universalimageloader.core.ImageLoader;
/**
* Created by Wood on 2016/8/12.
*/
public class RotateCardActivity extends Activity implements View.OnClickListener {
private static final String LOG_TAG = "RotateCardActivity";
private RelativeLayout rlCardRoot;
private ImageView imageViewBack;
private ImageView imageViewFront;
private void initView() {
rlCardRoot = (RelativeLayout) findViewById(R.id.rl_card_root);
imageViewBack = (ImageView) findViewById(R.id.imageView_back);
imageViewFront = (ImageView) findViewById(R.id.imageView_front);
imageViewBack.setOnClickListener(this);
imageViewFront.setOnClickListener(this);
setCameraDistance();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rotate_card);
initView();
initData();
}
/**
* 设置数据
*/
public void initData() {
String imageUri = "drawable://" + R.drawable.blue_back;
ImageLoader.getInstance().displayImage(imageUri, imageViewBack, DisplayImageOptionsUtil.getDisplayImageOptionsNoCache(R.drawable.card_bg_gray));
imageUri = "drawable://" + R.drawable.blue_front;
ImageLoader.getInstance().displayImage(imageUri, imageViewFront, DisplayImageOptionsUtil.getDisplayImageOptionsNoCache(R.drawable.card_bg_gray));
imageViewBack.setVisibility(View.VISIBLE);
imageViewFront.setVisibility(View.INVISIBLE);
}
/**
* 翻牌
*/
public void cardTurnover() {
if (View.VISIBLE == imageViewBack.getVisibility()) {
ViewHelper.setRotationY(imageViewFront, 180f);//先翻转180,转回来时就不是反转的了
Rotatable rotatable = new Rotatable.Builder(rlCardRoot)
.sides(R.id.imageView_back, R.id.imageView_front)
.direction(Rotatable.ROTATE_Y)
.rotationCount(1)
.build();
rotatable.setTouchEnable(false);
rotatable.rotate(Rotatable.ROTATE_Y, -180, 1500);
} else if (View.VISIBLE == imageViewFront.getVisibility()) {
Rotatable rotatable = new Rotatable.Builder(rlCardRoot)
.sides(R.id.imageView_back, R.id.imageView_front)
.direction(Rotatable.ROTATE_Y)
.rotationCount(1)
.build();
rotatable.setTouchEnable(false);
rotatable.rotate(Rotatable.ROTATE_Y, 0, 1500);
}
}
/**
* 改变视角距离, 贴近屏幕
*/
private void setCameraDistance() {
int distance = 10000;
float scale = getResources().getDisplayMetrics().density * distance;
rlCardRoot.setCameraDistance(scale);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.imageView_back:
case R.id.imageView_front:
cardTurnover();
break;
}
}
}
.xml 卡牌的正反面,布局需要一个viewgroup包着两个控件,这两个控件就是正反面
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#62ADC6">
<RelativeLayout
android:id="@+id/rl_card_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent">
<ImageView
android:id="@+id/imageView_back"
android:layout_width="258dp"
android:layout_height="350dp"
android:layout_centerInParent="true" />
<ImageView
android:id="@+id/imageView_front"
android:layout_width="258dp"
android:layout_height="350dp"
android:layout_centerInParent="true" />
</RelativeLayout>
</FrameLayout>
素材
资源链接
版权声明:本文为wood12943907原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。