绕太阳三维旋转动效

先来看下效果图:

这个动画的最终效果是支持修改的,比如外围旋转动画在中间停留的时间,外围动画每一次旋转的时间,是否需要中间的太阳,太阳旋转一圈所需要的时间都是可以进行设置的,同时支持点击事件,点击事件分为三种,一是点击中间的太阳,二是点击周边的行星,三是只能点击最前边的行星,如果有手指按在上面,外围的行星动画是会暂停的,同时动画会随着activity生命周期(onStop()和onStart())进行暂停和开始,说了这么多,这里先把所有的代码贴出来以及如何使用:

/**
 * @auther tangedegushi
 * @creat 2019/11/21
 * @Decribe
 */
public class StarGroupView extends FrameLayout {
    private static final String TAG = "StarGroupView";

    //圆半径
    private float mRadius;
    private final float ROTATE_ANGLE_X = 60;
    //起始角度
    private float START_ANGLE = 90;
    private ValueAnimator rotateAnimator;
    //每个view的均分角度
    private float avgAngle;
    //旋转动画每帧移动的角度
    private float moveAngle;
    //下一个view移动到前面时所经历的时间
    private final long MOVE_TIME = 2_000;
    //下一个view移动到前面时所停留的时间
    private final long STAY_TIME = 3_000;
    //中间view旋转一圈所需时间
    private final long ROTATE_TIME = 14_000;
    //当前界面执行了onStop()之后在执行onStart()动画开始执行时间
    private final long REAGAIN_TIME = 1_000;
    //view的最小缩放比例
    private final float minScale = 0.3f;
    private final String CENTER_TAG = "center";
    //中间的view
    private View centerChild;
    private ValueAnimator centerAnimator;
    private boolean isActionDown = false;
    private OnClickListener currentOnClicklistener;

    public StarGroupView(Context context) {
        this(context, null);
    }

    public StarGroupView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StarGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initLifeCycle();
    }

    //处理界面可见时动画才会执行,界面不可见时停止动画
    private void initLifeCycle() {
        Context context = getContext();
        if (context instanceof AppCompatActivity) {
            ((AppCompatActivity) context).getLifecycle().addObserver(new LifecycleObserver() {
                @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
                public void onStart() {
                    if (rotateAnimator != null && rotateAnimator.isPaused()) {
                        postDelayed(()->rotateAnimator.resume(),REAGAIN_TIME);
                    }
                    if (centerAnimator != null && centerAnimator.isPaused()) {
                        postDelayed(()->centerAnimator.resume(),REAGAIN_TIME);
                    }
                }

                @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
                public void onStop() {
                    rotateAnimator.pause();
                    centerAnimator.pause();
                }

            });
        }
    }

    //在这里执行启动动画
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        int childCount = getChildCount();
        if (childCount == 0) {
            try {
                throw new NoChildrenException();
            } catch (NoChildrenException e) {
                e.printStackTrace();
            }
        }
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (isCenterView(child)) {
                centerChild = child;
                break;
            }
        }
        avgAngle = 360f / (childCount - 1);
        initAnimator();
        startDelayAnimator();
        startCenterViewAnimator();
    }

    //释放动画资源
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        removeCallbacks(delayAnimator);
        centerAnimator = null;
        rotateAnimator = null;
    }

    //开启中间view的动画
    private void startCenterViewAnimator() {
        if (centerChild == null) return;
        if (centerAnimator != null) {
            centerAnimator.start();
            return;
        }
        centerAnimator = new ValueAnimator();
        centerAnimator.setRepeatCount(ValueAnimator.INFINITE);
        centerAnimator.setFloatValues(360);
        centerAnimator.setDuration(ROTATE_TIME);
        centerAnimator.setInterpolator(new LinearInterpolator());
        centerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float angle = (float) animation.getAnimatedValue();
                centerChild.setRotation(angle);
            }
        });
        centerAnimator.start();
    }

    //开启四周旋转延时动画
    private void startDelayAnimator() {
        postDelayed(delayAnimator, STAY_TIME);
    }

    private Runnable delayAnimator = new Runnable() {
        @Override
        public void run() {
            if (!isActionDown) {
                rotateAnimator.start();
            } else {
                startDelayAnimator();
            }
        }
    };

    private void initAnimator() {
        if (rotateAnimator != null) {
            rotateAnimator.start();
            return;
        }
        rotateAnimator = new ValueAnimator();
        rotateAnimator.setFloatValues(avgAngle);
        rotateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                moveAngle = (float) animation.getAnimatedValue();
                layoutChildren();
                invalidate();
            }
        });
        rotateAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                startDelayAnimator();
                START_ANGLE += avgAngle;
            }
        });
        rotateAnimator.setDuration(MOVE_TIME);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childW = getChildAt(0).getMeasuredWidth();
        float mHRadius = (float) (((getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) / cos(ROTATE_ANGLE_X)) / 2);
        float mWRadius = (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) >> 1;
        mRadius = mHRadius > mWRadius ? (mWRadius - childW) : (mHRadius - childW);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutChildren();
    }

    private void layoutChildren() {
        int childCount = getChildCount();
        int centerX = getMeasuredWidth() >> 1;
        int centerY = getMeasuredHeight() >> 1;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth() >> 1;
            int childHeight = childView.getMeasuredHeight() >> 1;
            if (isCenterView(childView)) {
                childView.layout(centerX - childWidth, centerY - childHeight, centerX + childWidth, centerY + childHeight);
                continue;
            }
            //每个图片的位置均分
            float childAngle = i * avgAngle + START_ANGLE + moveAngle;
            int x = (int) (centerX + mRadius * cos(childAngle));
            int y = (int) (centerY + mRadius * sin(childAngle) * cos(ROTATE_ANGLE_X));
            childView.layout(x - childWidth, y - childHeight, x + childWidth, y + childHeight);
            float scale = (float) ((1 + sin(childAngle)) / 2 * (1 - minScale) + minScale);
            childView.setScaleX(scale);
            childView.setScaleY(scale);
        }
        changeChildrenZ();
    }

    //改变view的z轴以便上面的view覆盖下面的view
    private void changeChildrenZ() {
        int childCount = getChildCount();
        if (childCount <= 0) return;
        ArrayList<View> list = new ArrayList<>();
        for (int i = 0; i < childCount; i++) {
            list.add(getChildAt(i));
        }
        Collections.sort(list, (o1, o2) -> ((o1.getY() - o2.getY()) >= 0) ? -1 : 1);
        float z = 0;
        for (int i = 0; i < list.size(); i++) {
            z -= 0.1;
            View view = list.get(i);
            if (!isCenterView(view)) {
                if (i == 0) view.setOnClickListener(currentOnClicklistener);
                else view.setOnClickListener(null);
            }
            view.setZ(z);
        }
    }

    private double sin(double angle) {
        return Math.sin(angle / 180 * Math.PI);
    }

    private double cos(double angle) {
        return Math.cos(angle / 180 * Math.PI);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        super.dispatchTouchEvent(ev);
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                isActionDown = true;
                if (rotateAnimator.isRunning()){
                    rotateAnimator.pause();
                }
                break;
            case MotionEvent.ACTION_UP:
                isActionDown = false;
                if (rotateAnimator.isPaused()) {
                    rotateAnimator.resume();
                }
                break;
            case MotionEvent.ACTION_MOVE:


                break;
            default:
                break;
        }
        return true;
    }

    //对处在最前面的view进行设置点击监听
    public void setOnCurrentChildClickListener(OnClickListener listener){
        currentOnClicklistener = listener;
    }

    //对外围的的View进行设置点击监听
    public void setOnChildClickListener(OnClickListener listener){
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (isCenterView(child)) continue;
            child.setOnClickListener(listener);
        }
    }

    //对中间view进行设置点击监听
    public void setOnCenterViewListener(OnClickListener listener){
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (isCenterView(child)) {
                child.setOnClickListener(listener);
            }
        }
    }

    private boolean isCenterView(View view){
        String tag = (String) view.getTag();
        return tag != null && tag.equals(CENTER_TAG);
    }

    public class NoChildrenException extends Exception{
        public NoChildrenException() {
            super("you must add child to the StarViewGroup");
        }
    }

}

这是一个ViewGroup,使用的话只需在xml中进行添加子项,如下:

<com.example.ubt.myapplication.view.StarGroupView
        android:id="@+id/sgv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <ImageView
            android:layout_width="@dimen/img_size"
            android:layout_height="@dimen/img_size"
            android:background="@drawable/planet_1"/>
        <ImageView
            android:layout_width="@dimen/img_size"
            android:layout_height="@dimen/img_size"
            android:background="@drawable/planet_2"/>
        <ImageView
            android:layout_width="@dimen/img_size"
            android:layout_height="@dimen/img_size"
            android:background="@drawable/planet_3"/>
        <ImageView
            android:layout_width="@dimen/img_size"
            android:layout_height="@dimen/img_size"
            android:background="@drawable/planet_8"/>
        <ImageView
            android:layout_width="@dimen/img_size"
            android:layout_height="@dimen/img_size"
            android:background="@drawable/planet_5"/>
        <ImageView
            android:layout_width="@dimen/img_size"
            android:layout_height="@dimen/img_size"
            android:background="@drawable/planet_6"/>
        <ImageView
            android:layout_width="@dimen/img_size"
            android:layout_height="@dimen/img_size"
            android:background="@drawable/planet_7"/>
        <ImageView
            android:layout_width="160dp"
            android:layout_height="160dp"
            android:tag="center"
            android:background="@drawable/sun"/>
    com.example.ubt.myapplication.view.StarGroupView>

这里有一个需要注意的地方,当要设置中间view的时候,需要给这个view设置tag,它的值是center,
这样动画就可以运行起来了。
接下来就对代码中的一些点进行讲解,首先就是动画了,代码中使用了两个动画,使用的都是valueAnimator进行设置,这两个动画分别是太阳旋转动画centerAnimator和外围动画rotateAnimator,先说下这两个动画是如何与activity的生命周期进行同步的,上面的代码中有initLifeCycle()这样一个方法:

    private void initLifeCycle() {
        Context context = getContext();
        if (context instanceof AppCompatActivity) {
            ((AppCompatActivity) context).getLifecycle().addObserver(new LifecycleObserver() {
                @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
                public void onStart() {
                    if (rotateAnimator != null && rotateAnimator.isPaused()) {
                        postDelayed(()->rotateAnimator.resume(),REAGAIN_TIME);
                    }
                    if (centerAnimator != null && centerAnimator.isPaused()) {
                        postDelayed(()->centerAnimator.resume(),REAGAIN_TIME);
                    }
                }

                @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
                public void onStop() {
                    rotateAnimator.pause();
                    centerAnimator.pause();
                }

            });
        }
    }

可以看出,当界面执行onStop()方法时,动画都将会暂停,当执行onStart()方法是,动画都将会恢复,注意这里动画恢复是有个延时的,这个延时主要是为了解决动画恢复时会有一个闪跳,这个也好理解,动画恢复时,界面还是不可见的,当可见时,动画自然会有一个闪跳的过程,这个延时可根据需要进行调整。
动画的启动是放在onAttachedToWindow()方法中,为什么要选在这个方法中呢?动画的启动需要知道子view的情况,比如外围旋转动画每一次旋转的角度,是否中间有view需要开启动画,上面说了一些动画需要注意的点,接下来就来看看动画是如何布局的,先来看个平面的布局:
绕太阳三维旋转动效_第1张图片
这里以横向是X坐标,纵向是Y坐标,以太阳中心为坐标原点,如何才能让上面的效果看起来像三维的呢,那就是让图片绕X旋转一定的角度,然后让里面的球缩小一点,这样看起来就有三维的效果了:
绕太阳三维旋转动效_第2张图片
这样看起来是不是就有点三维的感觉了,这个绕X旋转的角度可以根据具体的需求效果去调整,上面那个gif旋转的角度是60度,上面这照片旋转的角度是80度,这个可以对比下,说明了原理,现在要做的就是去布局了,直接来看代码:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        layoutChildren();
    }

    private void layoutChildren() {
        int childCount = getChildCount();
        int centerX = getMeasuredWidth() >> 1;
        int centerY = getMeasuredHeight() >> 1;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth() >> 1;
            int childHeight = childView.getMeasuredHeight() >> 1;
            if (isCenterView(childView)) {
                childView.layout(centerX - childWidth, centerY - childHeight, centerX + childWidth, centerY + childHeight);
                continue;
            }
            //每个图片的位置均分
            float childAngle = i * avgAngle + START_ANGLE + moveAngle;
            int x = (int) (centerX + mRadius * cos(childAngle));
            int y = (int) (centerY + mRadius * sin(childAngle) * cos(ROTATE_ANGLE_X));
            childView.layout(x - childWidth, y - childHeight, x + childWidth, y + childHeight);
            float scale = (float) ((1 + sin(childAngle)) / 2 * (1 - minScale) + minScale);
            childView.setScaleX(scale);
            childView.setScaleY(scale);
        }
        changeChildrenZ();
    }

    //改变view的z轴以便上面的view覆盖下面的view
    private void changeChildrenZ() {
        int childCount = getChildCount();
        if (childCount <= 0) return;
        ArrayList<View> list = new ArrayList<>();
        for (int i = 0; i < childCount; i++) {
            list.add(getChildAt(i));
        }
        Collections.sort(list, (o1, o2) -> ((o1.getY() - o2.getY()) >= 0) ? -1 : 1);
        float z = 0;
        for (int i = 0; i < list.size(); i++) {
            z -= 0.1;
            View view = list.get(i);
            if (!isCenterView(view)) {
                if (i == 0) view.setOnClickListener(currentOnClicklistener);
                else view.setOnClickListener(null);
            }
            view.setZ(z);
        }
    }

外围球的位置是根据角度进行均分的,根据ValueAnimator进行角度的改变,算出位置然后进行布局就可以了,上面还有一个changeChildrenZ()方法,这个方法主要是改变view绘制的先后顺序,z值大的会覆盖小的,这也就解决了view遮挡的问题,这里还处理了最大z值view的监听问题,如果设置了就会根据z值的变化进行设置,还有触摸的点击事件,比较简单的处理,这里就不做介绍了,可以根据实际需求去处理。

你可能感兴趣的:(三维旋转动画)