实现八大行星绕太阳3D旋转效果,这波操作不来喊个666?
/ 今日科技快讯 /
针对媒体报道的关于法拉第未来进行重组以及贾跃亭或辞去CEO职务的内容,FF发布声明称,已正式进入公司顶层治理架构变革的执行阶段,近期会公布相关细节。对于贾跃亭的债务问题,FF声明称,贾跃亭过去两年来通过多种方式已陆续偿还超过30亿美金的国内债务,还债信托基金的设立就是为了尽快彻底解决债务问题。
/ 作者简介 /
明天又是愉快的周末了,提前祝大家周末愉快!
本篇文章来自史蒂芬诺夫斯基的投稿,分享了自己的自定义View,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
史蒂芬诺夫斯基的博客地址:
https://www.jianshu.com/u/46056ddc3d3d
/ 本文目的 /
巩固/练习 自定义View
分析解决问题的思路
好久没写View了,最近恰巧遇到一个八大行星绕太阳旋转的假3D效果,写完之后感觉效果还不错。能玩十分钟的那种。
本篇将一步步带您实现这样的一个效果,ps:我是用kotlin实现的,介于您可能还不太熟悉kotlin或者不像熟悉java那样,所以本篇使用java语言(写的过程中老是忘记写new和分号报错)。
先上最终效果图(录制的比较渣)
需要解决的问题
1. 行星的整体布局,3D的视觉效果
2. 行星转到太阳后面时,会被太阳挡住,转到太阳前面时,会挡住太阳
3. 行星自动旋转,并且可以根据手势滑动,滑动完之后继续自动旋转
4. 中间的太阳有照射的旋转动画
/ 分析问题 /
1. 行星的整体布局,3D的视觉效果
如果我们draw()的之前通过Camera将Canvas绕x轴旋转60°是不是就可以搞定?这种方式实则是不可行的。因为draw()之前Canvas的变化会作用于子View,从效果图可以看出,子View并没有rotateX的变换,只有缩放变换。所以我们通过子View layout时变化其位置,即计算子View的left、top、right、bottom四个值
行星绕太阳旋转其轨迹实际上就是圆形,如下图:
我们看手机,其实是沿着z轴方向。想象一下,如果让坐标系沿着x轴旋转60°,不就能达到我们想要的效果了嘛。
旋转60°,我们再沿着x轴方向看,如下图:
图中蓝色是旋转前的轨迹,紫色是旋转之后的轨迹。假设P点是地球,P旋转前的y坐标是y0,则旋转之后地球的y坐标是:
y0 * 旋转角度的余切值,即:
y1 = y0* cos(60°)
好了。现在的结论是,只需要把图1的所有行星的y 坐标 * cos60°,就能达到效果了。
而图1中,计算各个行星旋转之前的x 、y坐标比较简单。
x0 = Radius * cos60°
y0 = Radius * sin60°
2. 行星转到太阳后面时,会被太阳挡住,转到太阳前面时,会挡住太阳
刚看到这个效果,觉得这个问题是个比较难的点,如果所有行星的父容器和太阳是平级关系,结果就是要么所有的行星都会挡住太阳,要么就是太阳都会挡住行星。不能达到行星转到太阳后面时,会被太阳挡住,转到太阳前面时,会挡住太阳 * 的这种效果
但是如果所有的行星和太阳是平级关系,即他们是同一个父容器下的子View,那么我们就可以达到这个效果,方法有三种:
1、重写父容器dispatchDraw()方法,改变子View的绘制顺序(图3中先draw土星,再draw太阳,再draw地球);
2、在子View draw之前依次调用bringToFront()方法(图3中先调用土星的bringToFront()方法,再调用太阳的bringToFront()方法,最后调用地球的bringToFront()方法);
3、通过改变所有子View的z值(高度)以改变View的绘制顺序。
这三种方法理论是都可以实现,但是方法1 成本太高、风险也高,重新dispatchDraw()可能会发生未知问题。
至于方法2,细心的朋友可能发现,每次调用bringToFront()方法,都会出发requestLayout(),降低了测量布局绘制效率,更重要的原因是在layout(问题1的解决需要重新layout方法)之后再调用requestLayout()方法,会导致循环layout-draw-layout-draw-layout-draw....
综上,我们选择方法3。简单,风险小。
3. 行星自动旋转,并且可以根据手势滑动,滑动完之后继续自动旋转
自动滑动:在父容器中设置一个成员变量:角度偏移量sweepAngle,计算子View的位置时将偏移量也考虑进去。然后定时不断增加或者减小sweepAngle(增加或减小 将决定子View是顺时针or逆时针旋转)
手势:用的比较多,从后面的代码中体现。
4. 中间的太阳有照射的旋转动画
效果图中的太阳由两张图片组成,一张是前景,一张是背景带亮光,让背景图绕着z轴无限旋转即可。
/ 开始编码 /
核心就是行星的父容器
/**
* 行星和太阳的父容器
*
* @author guolong
* @since 2019/8/20
*/
public class StarGroupView extends FrameLayout {
// 从这个角度开始画View ,可以调整
private static final float START_ANGLE = 270f; // 270°
// 父容器的边界 单位dp
private static final int PADDING = 80;
// 绕x轴旋转的角度 70°对应的弧度
private static final double ROTATE_X = Math.PI * 7 / 18;
// 以上几个值都可以根据最终效果调整
/**
* 角度偏差值
*/
private float sweepAngle = 0f;
/**
* 行星轨迹的半径
*/
private float mRadius;
/**
* 父容器的边界 ,单位px
*/
private int mPadding;
public StarGroupView(@NonNull Context context) {
this(context, null);
}
public StarGroupView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public StarGroupView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 边距转换为px
mPadding = (int) (context.getResources().getDisplayMetrics().density * PADDING);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// super.onLayout(changed, left, top, right, bottom);
mRadius = (getMeasuredWidth() / 2f - mPadding);
layoutChildren();
}
private void layoutChildren() {
int childCount = getChildCount();
if (childCount == 0) return;
// 行星之间的角度
float averageAngle = 360f / childCount;
for (int index = 0; index < childCount; index++) {
View child = getChildAt(index);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 第index 个子View的角度
double angle = (START_ANGLE - averageAngle * index + sweepAngle) * Math.PI / 180;
double sin = Math.sin(angle);
double cos = Math.cos(angle);
double coordinateX = getMeasuredWidth() / 2f - mRadius * cos;
// * Math.cos(ROTATE_X) 代表将y坐标转换为旋转之后的y坐标
double coordinateY = mRadius / 2f - mRadius * sin * Math.cos(ROTATE_X);
child.layout((int) (coordinateX - childWidth / 2),
(int) (coordinateY - childHeight / 2),
(int) (coordinateX + childWidth / 2),
(int) (coordinateY + childHeight / 2));
// 假设view的最小缩放是原来的0.3倍,则缩放比例和角度的关系是
float scale = (float) ((1 - 0.3f) / 2 * (1 - Math.sin(angle)) + 0.3f);
child.setScaleX(scale);
child.setScaleY(scale);
}
}
}
然后再xml中配置View
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LandActivity">
<com.glong.demo.view.StarGroupView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv1"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/colorAccent"
android:gravity="center"
android:text="1" />
<TextView
android:id="@+id/tv2"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/darker_gray"
android:gravity="center"
android:text="2" />
<TextView
android:id="@+id/tv3"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_green_dark"
android:gravity="center"
android:text="3" />
<TextView
android:id="@+id/tv4"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center"
android:text="4" />
<TextView
android:id="@+id/tv5"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:text="5" />
<TextView
android:id="@+id/tv6"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_orange_light"
android:gravity="center"
android:text="6" />
<TextView
android:id="@+id/tv7"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#ff3311"
android:gravity="center"
android:text="7" />
<TextView
android:id="@+id/tv8"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#11aa44"
android:gravity="center"
android:text="8" />
<TextView
android:id="@+id/tv9"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#ff99cc"
android:gravity="center"
android:text="9" />
</com.glong.demo.view.StarGroupView>
</androidx.constraintlayout.widget.ConstraintLayout>
运行,效果如下:
上述代码正如前面分析的,计算所有子View的left 、top 、right 、bottom,注释写的也详细。说明两点:
1、其中,64行
double angle = (START_ANGLE - averageAngle * index + sweepAngle) * Math.PI / 180;
公式中- averageAngle * index代表逆时针添加,如果是+ averageAngle * index则是顺时针添加。
2、78到80行,计算子View的scale,这里说明下角度和scale的计算公司
float scale = (float) ((1 - 0.3f) / 2 * (1 - Math.sin(angle)) + 0.3f);
假如View的最小scale是0.3f,最大scale是1。按照效果View在270°时scale最大,在90°时scale最小,并且从270°到90°scale越来越小。正玄曲线如下:
正玄曲线中,270°最小,90°时最大,我们把正玄值取反然后再加1,那么[90°,270°]对应的值就是[0,1]
即,设 z = -sin(angle) + 1 当angle在90°到270°变化时 ,z将在0到1之间变化
z在0~1之间变化时,scale 要在0.3~1之间变化,如下图:
显然
scale = (1 - 0.3) * z + 0.3 = (1-0.3)*(-sin(angle) + 1)+0.3
接下来,再把中间的太阳加进去
太阳也是StarGroupView的子View,但是和其他子View 不同的是,太阳在最中间,不参与类似行星的位置计算
简单期间我们使用tag=“center"来标识子View是中间的太阳。
修改xml文件
<com.glong.demo.view.StarGroupView
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 增加太阳View -->
<ImageView
android:layout_width="130dp"
android:layout_height="130dp"
android:src="@drawable/ic_launcher_background"
android:tag="center" />
<!--省略行星-->
</com.glong.demo.view.StarGroupView>
修改 StarGroupView.java
public class StarGroupView extends FrameLayout {
// ... 省略部分代码
private void layoutChildren() {
int childCount = getChildCount();
if (childCount == 0) return;
// 行星之间的角度
View centerView = centerView();
float averageAngle;
if (centerView == null) {
averageAngle = 360f / childCount;
} else {
// centerView 不参与计算角度
averageAngle = 360f / (childCount - 1);
}
int number = 0;
for (int index = 0; index < childCount; index++) {
View child = getChildAt(index);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 如果是centerView 直接居中布局
if ("center".equals(child.getTag())) {
child.layout(getMeasuredWidth() / 2 - childWidth / 2, getMeasuredHeight() / 2 - childHeight / 2,
getMeasuredWidth() / 2 + childWidth / 2, getMeasuredHeight() / 2 + childHeight / 2);
} else {
// 第index 个子View的角度
double angle = (START_ANGLE - averageAngle * number + sweepAngle) * Math.PI / 180;
double sin = Math.sin(angle);
double cos = Math.cos(angle);
double coordinateX = getMeasuredWidth() / 2f - mRadius * cos;
// * Math.cos(ROTATE_X) 代表将y坐标转换为旋转之后的y坐标
double coordinateY = mRadius / 2f - mRadius * sin * Math.cos(ROTATE_X);
child.layout((int) (coordinateX - childWidth / 2),
(int) (coordinateY - childHeight / 2),
(int) (coordinateX + childWidth / 2),
(int) (coordinateY + childHeight / 2));
// 假设view的最小缩放是原来的0.3倍,则缩放比例和角度的关系是
float scale = (float) ((1 - 0.3f) / 2 * (1 - Math.sin(angle)) + 0.3f);
child.setScaleX(scale);
child.setScaleY(scale);
number++;
}
}
}
/**
* 获取centerView
*
* @return 太阳
*/
private View centerView() {
View result = null;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if ("center".equals(child.getTag())) {
return child;
}
}
return null;
}
}
代码注释写的很全面,不做过多解释了,这个时候我们把PADDING改大一点,改成160,运行如下:
问题很明显,3应该在4的上面, 2 应该在3的上面,中间的View应该在5,6的上面。
这是因为系统默认按照View的添加顺序画View的,即我们xml文件里面的顺序。xml里面我们centerView在第一个,所以就先画centerView,导致centerView被其他View覆盖。按照上面的分析,动态改变View的z值以改变View的draw顺序。
修改StarGroupView.java代码
public class StarGroupView extends FrameLayout {
private void layoutChildren() {
// ...省略之前代码
changeZ();
}
/**
* 改变子View的z值以改变子View的绘制优先级,z越大优先级越低(最后绘制)
*/
private void changeZ() {
View centerView = centerView();
float centerViewScaleY = 1f;
if (centerView != null) {
centerViewScaleY = centerView.getScaleY();
centerView.setScaleY(0.5f);
}
List<View> children = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
children.add(getChildAt(i));
}
// 按照scaleY排序
Collections.sort(children, new Comparator<View>() {
@Override
public int compare(View o1, View o2) {
return (int) ((o1.getScaleY() - o2.getScaleY())*1000000);
}
});
float z = 0.1f;
for (int i = 0; i < children.size(); i++) {
children.get(i).setZ(z);
z += 0.1f;
}
if (centerView != null) {
centerView.setScaleY(centerViewScaleY);
}
}
}
我们先给所有子View根据他的scaleY排序,由于centerView的scaleY 在layoutChildren()时并没有改变,我们把centerView的scaleY设置为0.5f,最后再还原回去。现在运行,效果如下:
到这里基本已经达到了我们想要的效果啦,接下来让其自动旋转和响应手势,肯定就难不倒我们啦。
加入自动旋转
子StarGroupView中循环postDelayed(runnable,16)即可,这里为什么是16ms,大家都懂
修改StarGroupView.java
public class StarGroupView extends FrameLayout {
// ...省略已有代码
//自动旋转角度,16ms(一帧)旋转的角度,值越大转的越快
private static final float AUTO_SWEEP_ANGLE = 0.1f;
private Runnable autoScrollRunnable = new Runnable() {
@Override
public void run() {
sweepAngle += AUTO_SWEEP_ANGLE;
// 取个模 防止sweepAngle爆表
sweepAngle %= 360;
Log.d("guolong", "auto , sweepAngle == " +sweepAngle);
layoutChildren();
postDelayed(this, 16);
}
};
public StarGroupView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// ...省略已有代码
postDelayed(autoScrollRunnable,100);
}
}
这样就开始自动旋转了,调节AUTO_SWEEP_ANGLE的值 改变旋转速度
加入手势
老写法,先上代码
在StarGroupView.java中增加
public class StarGroupView extends FrameLayout {
//px转化为angle的比例 ps:一定要给设置一个转换,不然旋转的太欢了
private static final float SCALE_PX_ANGLE = 0.2f;
/**
* 手势处理
*/
private float downX = 0f;
/**
* 手指按下时的角度
*/
private float downAngle = sweepAngle;
/**
* 速度追踪器
*/
private VelocityTracker velocity = VelocityTracker.obtain();
/**
* 滑动结束后的动画
*/
private ValueAnimator velocityAnim = new ValueAnimator();
public StarGroupView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
// ...
initAnim();
}
private void initAnim() {
velocityAnim.setDuration(1000);
velocityAnim.setInterpolator(new DecelerateInterpolator());
velocityAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
// 乘以SCALE_PX_ANGLE是因为如果不乘 转得太欢了
sweepAngle += (value * SCALE_PX_ANGLE);
layoutChildren();
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
velocity.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downAngle = sweepAngle;
// 取消动画和自动旋转
velocityAnim.cancel();
removeCallbacks(autoScrollRunnable);
return true;
case MotionEvent.ACTION_MOVE:
float dx = downX - x ;
sweepAngle = (dx * SCALE_PX_ANGLE + downAngle);
layoutChildren();
break;
case MotionEvent.ACTION_UP:
velocity.computeCurrentVelocity(16);
// 速度为负值代表顺时针
scrollByVelocity(velocity.getXVelocity());
postDelayed(autoScrollRunnable, 16);
}
return super.onTouchEvent(event);
}
private void scrollByVelocity(float velocity) {
float end;
if (velocity < 0)
end = -AUTO_SWEEP_ANGLE;
else
end = 0f;
velocityAnim.setFloatValues(-velocity, end);
velocityAnim.start();
}
}
手势处理的代码比较简单,这里就不再赘述了,需要注意的是
1. ACTION_DOWN需返回true,不然收不到后续的ACTION_MOVE事件;
2. ACTION_DOWN时需要暂停动画和自动旋转
3. 这里根据手指离开屏幕时的速度做Animator动画,当然你也可以用scroller实现。
4. 第59行,我们给dx * SCALE_PX_ANGLE代表一个像素可以转换成SCALE_PX_ANGLE角度
最后,加上中间太阳旋转的动画
在res/anim/sun_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="true"
android:interpolator="@android:interpolator/linear">
<rotate
android:duration="8000"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="-1"
android:toDegrees="360" />
</set>
在Activity中
public class LandActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ....省略部分代码
View sunView = findViewById(R.id.sun_view);
sunView.startAnimation((AnimationUtils.loadAnimation(this, R.anim.sun_anim)));
}
}
最后的最后,我们可以给外部提供start和pause方法用来暂停和开始动画
public class StarGroupView extends FrameLayout {
// 省略...
public void pause() {
velocityAnim.cancel();
removeCallbacks(autoScrollRunnable);
}
public void start() {
postDelayed(autoScrollRunnable, 16);
}
}
最终算上注释不到260代码搞定!最终效果:
完整的Demo代码和星球效果代码放在github上了
https://github.com/GarrettLance/Demos
推荐阅读:
腾讯新开源的插件化框架 Shadow,原来是这么玩的
实现八大行星绕太阳3D旋转效果,这波操作不来喊个666?相关推荐
- 仿八大行星绕太阳3D旋转效果
android实现八大行星绕太阳3D旋转效果 仿上面效果,采用kotlin实现,逻辑要简单些,注释在源码中,一看就懂 <com.example.androidxdemo.star.StarGro ...
- android fragment中引入自定义view_厉害了,用Android自定义View实现八大行星绕太阳3D旋转效果...
作者:史蒂芬诺夫斯基 链接:https://www.jianshu.com/p/2954f2ef8ea5 好久没写View了,最近恰巧遇到一个八大行星绕太阳旋转的假3D效果,写完之后感觉效果还不错.能 ...
- android立体3D效果_Android实现八大行星绕太阳3D旋转效果
code小生 一个专注大前端领域的技术平台公众号回复Android加入安卓技术群 作者:史蒂芬诺夫斯基链接:https://www.jianshu.com/p/2954f2ef8ea5声明:本文已获史 ...
- Android实现八大行星绕太阳3D旋转效果
效果图: 本文目的: 巩固/练习 自定义View 分析解决问题的思路 需要解决的问题: 1.行星的整体布局,3D的视觉效果 2.行星转到太阳后面时,会被太阳挡住,转到太阳前面时,会挡住太阳 3.行星自 ...
- 2021高考成绩查询镇远一中,离太阳由近到远的八大行星排序及记忆方法
八大行星按照离太阳的距离从近到远,它们依次为水星.金星.地球.火星.木星.土星.天王星.海王星. 更多高考资讯尽在30高考网https://www.30gk.com/ 八大行星的排序 1.水星:在太阳 ...
- QT实现太阳系系统八大行星
QT实现太阳系系统八大行星 项目简介 项目技术 项目展示 主要源码片段解析 获取完整项目源码传送门 项目简介 演示结合了Qt 3D渲染和Qt Quick 2元素. Planets演示了如何实现将Qt ...
- css3动画实现八大行星
css3动画实现八大行星 话不多说 上代码 看效果 <!DOCTYPE html> <html lang="en"><head><meta ...
- 学完javaee基础,编的一个小游戏—太阳系的八大行星
项目完整代码已放到码云上 自行下载: https://gitee.com/tutu_57893_7590/planets 学完javaee基础,编的一个小游戏-太阳系的八大行星 要使太阳系的八大行星, ...
- 太阳系八大行星碰撞的视频_高中地理——太阳系与地球
知识点 (1)太阳系八大恒星,及其排列顺序. (2)行星公转的特点 (3)为什么,地球上可以存在生命? 考点详解 1.太阳系八大行星的顺序 金木水火土?No!!! 根据距离太阳由近至远的顺序,分别为: ...
最新文章
- 条令考试小程序辅助器_在线考试题库小程序开发有哪些功能?
- Add margining capability to a dc/dc converter
- (POJ-3279)Fliptile (dfs经典---也可以枚举)
- 使用DDMS中的内存监测工具Heap来优化内存
- Virtualbox虚机无法启动因断电
- php作业案例10,5月23日作业——实例演示查询构造器中的10个最常用的方法
- Zstd 压缩性能分析(含比较)
- (Python)零起步数学+神经网络入门
- Python字符串的encode与decode 解决乱码问题
- 手机4g模块坏了怎么办_古交联通突然断网,手机没信号,不能接打电话该怎么办?...
- Visual Assist安装完之后的文件复制路径
- python查找多个关键词,在字符串列表中搜索多个关键字
- HDU1568 Fibonacci【斐波拉契数列】
- 浏览器的NPAPI插件技术不要学了,已经淘汰几年了
- 车载电子电器防水防尘等级介绍
- 大数据----数据仓库架构
- python3 已知两点坐标算角度
- CSS 选择器 CSS3选择器
- The fifth day
- 第二证券|两大板块掀涨停潮,有个股猛拉20cm!这只港股复牌一度暴跌