Android卡牌翻转动画效果实现

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>

素材

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

资源链接

Rotatable-master项目代码


版权声明:本文为wood12943907原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。