Android实现八大行星绕太阳3D旋转效果
效果圖:
本文目的:
鞏固/練習(xí) 自定義View
分析解決問(wèn)題的思路
需要解決的問(wèn)題:
1、行星的整體布局,3D的視覺(jué)效果
2、行星轉(zhuǎn)到太陽(yáng)后面時(shí),會(huì)被太陽(yáng)擋住,轉(zhuǎn)到太陽(yáng)前面時(shí),會(huì)擋住太陽(yáng)
3、行星自動(dòng)旋轉(zhuǎn),并且可以根據(jù)手勢(shì)滑動(dòng),滑動(dòng)完之后繼續(xù)自動(dòng)旋轉(zhuǎn)
4、中間的太陽(yáng)有照射的旋轉(zhuǎn)動(dòng)畫(huà)
分析問(wèn)題:
1、行星的整體布局,3D的視覺(jué)效果
如果我們draw()的之前通過(guò)Camera將Canvas繞x軸旋轉(zhuǎn)60°是不是就可以搞定?這種方式實(shí)則是不可行的。因?yàn)閐raw()之前Canvas的變化會(huì)作用于子View,從效果圖可以看出,子View并沒(méi)有rotateX的變換,只有縮放變換。所以我們通過(guò)子View layout時(shí)變化其位置,即計(jì)算子View的left、top、right、bottom四個(gè)值
行星繞太陽(yáng)旋轉(zhuǎn)其軌跡實(shí)際上就是圓形,如下圖:
我們看手機(jī),其實(shí)是沿著z軸方向。想象一下,如果讓坐標(biāo)系沿著x軸旋轉(zhuǎn)60°,不就能達(dá)到我們想要的效果了嘛。
旋轉(zhuǎn)60°,我們?cè)傺刂鴛軸方向看,如下圖:
圖中藍(lán)色是旋轉(zhuǎn)前的軌跡,紫色是旋轉(zhuǎn)之后的軌跡。假設(shè)P點(diǎn)是地球,P旋轉(zhuǎn)前的y坐標(biāo)是y0,則旋轉(zhuǎn)之后地球的y坐標(biāo)是:
y0 * 旋轉(zhuǎn)角度的余切值,即:
y1 = y0* cos(60°)現(xiàn)在的結(jié)論是,只需要把圖1的所有行星的y 坐標(biāo) * cos60°,就能達(dá)到效果了。
而圖1中,計(jì)算各個(gè)行星旋轉(zhuǎn)之前的x 、y坐標(biāo)比較簡(jiǎn)單。
x0 = Radius * cos60°y0 = Radius * sin60°
2、行星轉(zhuǎn)到太陽(yáng)后面時(shí),會(huì)被太陽(yáng)擋住,轉(zhuǎn)到太陽(yáng)前面時(shí),會(huì)擋住太陽(yáng)
剛看到這個(gè)效果,覺(jué)得這個(gè)問(wèn)題是個(gè)比較難的點(diǎn),如果所有行星的父容器和太陽(yáng)是平級(jí)關(guān)系,結(jié)果就是要么所有的行星都會(huì)擋住太陽(yáng),要么就是太陽(yáng)都會(huì)擋住行星。不能達(dá)到行星轉(zhuǎn)到太陽(yáng)后面時(shí),會(huì)被太陽(yáng)擋住,轉(zhuǎn)到太陽(yáng)前面時(shí),會(huì)擋住太陽(yáng) * 的這種效果。
但是如果所有的行星和太陽(yáng)是平級(jí)關(guān)系,即他們是同一個(gè)父容器下的子View,那么我們就可以達(dá)到這個(gè)效果,方法有三種:
1、重寫(xiě)父容器dispatchDraw()方法,改變子View的繪制順序(圖3中先draw土星,再draw太陽(yáng),再draw地球);
2、在子View draw之前依次調(diào)用bringToFront()方法(圖3中先調(diào)用土星的bringToFront()方法,再調(diào)用太陽(yáng)的bringToFront()方法,最后調(diào)用地球的bringToFront()方法);
3、通過(guò)改變所有子View的z值(高度)以改變View的繪制順序。
這三種方法理論是都可以實(shí)現(xiàn),但是方法1 成本太高、風(fēng)險(xiǎn)也高,重新dispatchDraw()可能會(huì)發(fā)生未知問(wèn)題,至于方法2,細(xì)心的朋友可能發(fā)現(xiàn),每次調(diào)用bringToFront()方法,都會(huì)出發(fā)requestLayout(),降低了測(cè)量布局繪制效率,更重要的原因是在layout(問(wèn)題1的解決需要重新layout方法)之后再調(diào)用requestLayout()方法,會(huì)導(dǎo)致循環(huán)layout-draw-layout-draw-layout-draw....
綜上,我們選擇方法3,簡(jiǎn)單,風(fēng)險(xiǎn)小。
3、行星自動(dòng)旋轉(zhuǎn),并且可以根據(jù)手勢(shì)滑動(dòng),滑動(dòng)完之后繼續(xù)自動(dòng)旋轉(zhuǎn)
自動(dòng)滑動(dòng):在父容器中設(shè)置一個(gè)成員變量:角度偏移量sweepAngle,計(jì)算子View的位置時(shí)將偏移量也考慮進(jìn)去。然后定時(shí)不斷增加或者減小sweepAngle(增加或減小 將決定子View是順時(shí)針or逆時(shí)針旋轉(zhuǎn))
手勢(shì):用的比較多,從后面的代碼中體現(xiàn)。
4、中間的太陽(yáng)有照射的旋轉(zhuǎn)動(dòng)畫(huà)
效果圖中的太陽(yáng)由兩張圖片組成,一張是前景,一張是背景帶亮光,讓背景圖繞著z軸無(wú)限旋轉(zhuǎn)即可。
代碼實(shí)現(xiàn):
核心就是行星的父容器
/*** 行星和太陽(yáng)的父容器*/ public class StarGroupView extends FrameLayout {// 從這個(gè)角度開(kāi)始畫(huà)View ,可以調(diào)整private static final float START_ANGLE = 270f; // 270°// 父容器的邊界 單位dpprivate static final int PADDING = 80;// 繞x軸旋轉(zhuǎn)的角度 70°對(duì)應(yīng)的弧度private static final double ROTATE_X = Math.PI * 7 / 18;// 以上幾個(gè)值都可以根據(jù)最終效果調(diào)整/*** 角度偏差值*/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);// 邊距轉(zhuǎn)換為pxmPadding = (int) (context.getResources().getDisplayMetrics().density * PADDING);}@Overrideprotected 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;// 行星之間的角度f(wàn)loat averageAngle = 360f / childCount;for (int index = 0; index < childCount; index++) {View child = getChildAt(index);int childWidth = child.getMeasuredWidth();int childHeight = child.getMeasuredHeight();// 第index 個(gè)子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坐標(biāo)轉(zhuǎn)換為旋轉(zhuǎn)之后的y坐標(biāo)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));// 假設(shè)view的最小縮放是原來(lái)的0.3倍,則縮放比例和角度的關(guān)系是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.StarGroupViewandroid:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tv1"android:layout_width="100dp"android:layout_height="100dp"android:background="@color/colorAccent"android:gravity="center"android:text="1" /><TextViewandroid:id="@+id/tv2"android:layout_width="100dp"android:layout_height="100dp"android:background="@android:color/darker_gray"android:gravity="center"android:text="2" /><TextViewandroid:id="@+id/tv3"android:layout_width="100dp"android:layout_height="100dp"android:background="@android:color/holo_green_dark"android:gravity="center"android:text="3" /><TextViewandroid:id="@+id/tv4"android:layout_width="100dp"android:layout_height="100dp"android:background="@android:color/holo_blue_dark"android:gravity="center"android:text="4" /><TextViewandroid:id="@+id/tv5"android:layout_width="100dp"android:layout_height="100dp"android:background="@android:color/holo_green_light"android:gravity="center"android:text="5" /><TextViewandroid:id="@+id/tv6"android:layout_width="100dp"android:layout_height="100dp"android:background="@android:color/holo_orange_light"android:gravity="center"android:text="6" /><TextViewandroid:id="@+id/tv7"android:layout_width="100dp"android:layout_height="100dp"android:background="#ff3311"android:gravity="center"android:text="7" /><TextViewandroid:id="@+id/tv8"android:layout_width="100dp"android:layout_height="100dp"android:background="#11aa44"android:gravity="center"android:text="8" /><TextViewandroid: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>運(yùn)行,效果如下:
上述代碼正如前面分析的,計(jì)算所有子View的left 、top 、right 、bottom,注釋寫(xiě)的也詳細(xì)。說(shuō)明兩點(diǎn):
1、其中,64行
double angle = (START_ANGLE - averageAngle * index + sweepAngle) * Math.PI / 180;公式中- averageAngle * index代表逆時(shí)針添加,如果是+ averageAngle * index則是順時(shí)針添加。
2、78到80行,計(jì)算子View的scale,這里說(shuō)明下角度和scale的計(jì)算公司
float scale = (float) ((1 - 0.3f) / 2 * (1 - Math.sin(angle)) + 0.3f);假如View的最小scale是0.3f,最大scale是1。按照效果View在270°時(shí)scale最大,在90°時(shí)scale最小,并且從270°到90°scale越來(lái)越小。正玄曲線(xiàn)如下:
正玄曲線(xiàn)中,270°最小,90°時(shí)最大,我們把正玄值取反然后再加1,那么[90°,270°]對(duì)應(yīng)的值就是[0,1]即,設(shè)z = -sin(angle) + 1 當(dāng)angle在90°到270°變化時(shí) ,z將在0到1之間變化z在0~1之間變化時(shí),scale 要在0.3~1之間變化,如下圖:
顯然,
scale = (1 - 0.3) * z + 0.3 = (1-0.3)*(-sin(angle) + 1)+0.3接下來(lái),再把中間的太陽(yáng)加進(jìn)去
太陽(yáng)也是StarGroupView的子View,但是和其他子View 不同的是,太陽(yáng)在最中間,不參與類(lèi)似行星的位置計(jì)算
簡(jiǎn)單期間我們使用tag=“center"來(lái)標(biāo)識(shí)子View是中間的太陽(yáng)。
修改xml文件:
<com.glong.demo.view.StarGroupViewandroid:layout_width="match_parent"android:layout_height="match_parent"><!-- 增加太陽(yáng)View --><ImageViewandroid: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 不參與計(jì)算角度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 個(gè)子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坐標(biāo)轉(zhuǎn)換為旋轉(zhuǎn)之后的y坐標(biāo)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));// 假設(shè)view的最小縮放是原來(lái)的0.3倍,則縮放比例和角度的關(guān)系是float scale = (float) ((1 - 0.3f) / 2 * (1 - Math.sin(angle)) + 0.3f);child.setScaleX(scale);child.setScaleY(scale);number++;}}}/*** 獲取centerView** @return 太陽(yáng)*/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;} }代碼注釋寫(xiě)的很全面,不做過(guò)多解釋了,這個(gè)時(shí)候我們把PADDING改大一點(diǎn),改成160,運(yùn)行如下:
問(wèn)題很明顯,3應(yīng)該在4的上面, 2 應(yīng)該在3的上面,中間的View應(yīng)該在5,6的上面。
這是因?yàn)橄到y(tǒng)默認(rèn)按照View的添加順序畫(huà)View的,即我們xml文件里面的順序。xml里面我們centerView在第一個(gè),所以就先畫(huà)centerView,導(dǎo)致centerView被其他View覆蓋。按照上面的分析,動(dòng)態(tài)改變View的z值以改變View的draw順序。
修改StarGroupView.java代碼
public class StarGroupView extends FrameLayout {private void layoutChildren() {// ...省略之前代碼changeZ();}/*** 改變子View的z值以改變子View的繪制優(yōu)先級(jí),z越大優(yōu)先級(jí)越低(最后繪制)*/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>() {@Overridepublic 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根據(jù)他的scaleY排序,由于centerView的scaleY 在layoutChildren()時(shí)并沒(méi)有改變,我們把centerView的scaleY設(shè)置為0.5f,最后再還原回去。現(xiàn)在運(yùn)行,效果如下:
到這里基本已經(jīng)達(dá)到了我們想要的效果啦,接下來(lái)讓其自動(dòng)旋轉(zhuǎn)和響應(yīng)手勢(shì),肯定就難不倒我們啦。
加入自動(dòng)旋轉(zhuǎn)
子StarGroupView中循環(huán)postDelayed(runnable,16)即可,這里為什么是16ms,大家都懂
修改StarGroupView.java
public class StarGroupView extends FrameLayout {// ...省略已有代碼//自動(dòng)旋轉(zhuǎn)角度,16ms(一幀)旋轉(zhuǎn)的角度,值越大轉(zhuǎn)的越快private static final float AUTO_SWEEP_ANGLE = 0.1f;private Runnable autoScrollRunnable = new Runnable() {@Overridepublic void run() {sweepAngle += AUTO_SWEEP_ANGLE;// 取個(gè)模 防止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);} }這樣就開(kāi)始自動(dòng)旋轉(zhuǎn)了,調(diào)節(jié)AUTO_SWEEP_ANGLE的值 改變旋轉(zhuǎn)速度
加入手勢(shì)
老寫(xiě)法,先上代碼
在StarGroupView.java中增加
public class StarGroupView extends FrameLayout {//px轉(zhuǎn)化為angle的比例 ps:一定要給設(shè)置一個(gè)轉(zhuǎn)換,不然旋轉(zhuǎn)的太歡了private static final float SCALE_PX_ANGLE = 0.2f;/*** 手勢(shì)處理*/private float downX = 0f;/*** 手指按下時(shí)的角度*/private float downAngle = sweepAngle;/*** 速度追蹤器*/private VelocityTracker velocity = VelocityTracker.obtain();/*** 滑動(dòng)結(jié)束后的動(dòng)畫(huà)*/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() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (float) animation.getAnimatedValue();// 乘以SCALE_PX_ANGLE是因?yàn)槿绻怀?轉(zhuǎn)得太歡了sweepAngle += (value * SCALE_PX_ANGLE);layoutChildren();}});}@Overridepublic boolean onTouchEvent(MotionEvent event) {float x = event.getX();velocity.addMovement(event);switch (event.getAction()) {case MotionEvent.ACTION_DOWN:downX = x;downAngle = sweepAngle;// 取消動(dòng)畫(huà)和自動(dòng)旋轉(zhuǎn)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);// 速度為負(fù)值代表順時(shí)針scrollByVelocity(velocity.getXVelocity());postDelayed(autoScrollRunnable, 16);}return super.onTouchEvent(event);}private void scrollByVelocity(float velocity) {float end;if (velocity < 0)end = -AUTO_SWEEP_ANGLE;elseend = 0f;velocityAnim.setFloatValues(-velocity, end);velocityAnim.start();} }手勢(shì)處理的代碼比較簡(jiǎn)單,這里就不再贅述了,需要注意的是:
1、ACTION_DOWN需返回true,不然收不到后續(xù)的ACTION_MOVE事件;
2、ACTION_DOWN時(shí)需要暫停動(dòng)畫(huà)和自動(dòng)旋轉(zhuǎn)
3、這里根據(jù)手指離開(kāi)屏幕時(shí)的速度做Animator動(dòng)畫(huà),當(dāng)然你也可以用scroller實(shí)現(xiàn)。
4、第59行,我們給dx * SCALE_PX_ANGLE代表一個(gè)像素可以轉(zhuǎn)換成SCALE_PX_ANGLE角度
最后,加上中間太陽(yáng)旋轉(zhuǎn)的動(dòng)畫(huà)
在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"><rotateandroid:duration="8000"android:fromDegrees="0"android:pivotX="50%"android:pivotY="50%"android:repeatCount="-1"android:toDegrees="360" /> </set>在Activity中:
public class LandActivity extends AppCompatActivity {@Overrideprotected 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方法用來(lái)暫停和開(kāi)始動(dòng)畫(huà)
public class StarGroupView extends FrameLayout {// 省略...public void pause() {velocityAnim.cancel();removeCallbacks(autoScrollRunnable);}public void start() {postDelayed(autoScrollRunnable, 16);} }最終效果:
源碼地址:
https://github.com/glongdev/Demos總結(jié)
以上是生活随笔為你收集整理的Android实现八大行星绕太阳3D旋转效果的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Computer Science Fun
- 下一篇: 计算机网络形考作业三,《计算机应用基础》