先来看下效果图:
这个动画的最终效果是支持修改的,比如外围旋转动画在中间停留的时间,外围动画每一次旋转的时间,是否需要中间的太阳,太阳旋转一圈所需要的时间都是可以进行设置的,同时支持点击事件,点击事件分为三种,一是点击中间的太阳,二是点击周边的行星,三是只能点击最前边的行星,如果有手指按在上面,外围的行星动画是会暂停的,同时动画会随着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需要开启动画,上面说了一些动画需要注意的点,接下来就来看看动画是如何布局的,先来看个平面的布局:
这里以横向是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<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值的变化进行设置,还有触摸的点击事件,比较简单的处理,这里就不做介绍了,可以根据实际需求去处理。