日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

android fragment中引入自定义view_厉害了,用Android自定义View实现八大行星绕太阳3D旋转效果...

發布時間:2024/9/27 Android 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 android fragment中引入自定义view_厉害了,用Android自定义View实现八大行星绕太阳3D旋转效果... 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

作者:史蒂芬諾夫斯基
鏈接:https://www.jianshu.com/p/2954f2ef8ea5

好久沒寫View了,最近恰巧遇到一個八大行星繞太陽旋轉的假3D效果,寫完之后感覺效果還不錯。能玩十分鐘的那種。本篇將一步步帶您實現這樣的一個效果,ps:我是用kotlin實現的,介于您可能還不太熟悉kotlin或者不像熟悉java那樣,所以本篇使用java語言(寫的過程中老是忘記寫new和分號報錯)。

先上最終效果圖(錄制的比較渣)

本文目的

  • 鞏固/練習 自定義View
  • 分析解決問題的思路

需要解決的問題

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.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>

運行,效果如下:

上述代碼正如前面分析的,計算所有子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.StarGroupViewandroid:layout_width="match_parent"android:layout_height="match_parent">


<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 不參與計算角度
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 children = new ArrayList<>();for (int i = 0; i < getChildCount(); i++) {
children.add(getChildAt(i));
}// 按照scaleY排序
Collections.sort(children, new Comparator() {@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根據他的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">
<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 {

@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/glongdev/Demos

以上就是本文全部內容,喜歡??的話就轉發一下、點個在看支持一下吧!

---END---

推薦閱讀:Android 9.0 Toast源碼改變引發的問題Android自定義ViewGroup實現標題欄的懸浮吸頂漸變效果牛逼!Android Jetpack Compose UI組件庫最新進展,寫法完全類似Flutter漫畫設計模式:策略模式30個極大提高開發效率的Visual Studio Code插件每一個“在看”,我都當成真的喜歡

總結

以上是生活随笔為你收集整理的android fragment中引入自定义view_厉害了,用Android自定义View实现八大行星绕太阳3D旋转效果...的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。