博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
绕太阳三维旋转动效
阅读量:4181 次
发布时间:2019-05-26

本文共 14043 字,大约阅读时间需要 46 分钟。

先来看下效果图:

在这里插入图片描述
这个动画的最终效果是支持修改的,比如外围旋转动画在中间停留的时间,外围动画每一次旋转的时间,是否需要中间的太阳,太阳旋转一圈所需要的时间都是可以进行设置的,同时支持点击事件,点击事件分为三种,一是点击中间的太阳,二是点击周边的行星,三是只能点击最前边的行星,如果有手指按在上面,外围的行星动画是会暂停的,同时动画会随着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
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中进行添加子项,如下:

这里有一个需要注意的地方,当要设置中间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需要开启动画,上面说了一些动画需要注意的点,接下来就来看看动画是如何布局的,先来看个平面的布局:
在这里插入图片描述
这里以横向是X坐标,纵向是Y坐标,以太阳中心为坐标原点,如何才能让上面的效果看起来像三维的呢,那就是让图片绕X旋转一定的角度,然后让里面的球缩小一点,这样看起来就有三维的效果了:
在这里插入图片描述
这样看起来是不是就有点三维的感觉了,这个绕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
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值的变化进行设置,还有触摸的点击事件,比较简单的处理,这里就不做介绍了,可以根据实际需求去处理。

转载地址:http://ewhai.baihongyu.com/

你可能感兴趣的文章
源码编译安装LNMP环境之PHP篇
查看>>
Linux中rpm工具使用教程
查看>>
Linux中yum工具使用教程
查看>>
C++字符串函数
查看>>
mknod详解
查看>>
linux中的run-level何解?
查看>>
Linux内核编译详解(转自linuxSir)
查看>>
实模式,保护模式与V86模式
查看>>
628. Maximum Product of Three Numbers(排序)
查看>>
Linux内核-------同步机制(二)
查看>>
面试题31-------连续子数组的最大和(数组)
查看>>
epoll 实现Chat
查看>>
21. Merge Two Sorted Lists(链表)
查看>>
2. Add Two Numbers(链表)
查看>>
637. Average of Levels in Binary Tree(Tree)
查看>>
226. Invert Binary Tree(Tree)
查看>>
328. Odd Even Linked List(链表)
查看>>
199. Binary Tree Right Side View(Tree)
查看>>
230. Kth Smallest Element in a BST(Tree)
查看>>
求字符串的最长回文串-----Manacher's Algorithm 马拉车算法
查看>>