android實(shí)現(xiàn)八大行星繞太陽(yáng)3D旋轉(zhuǎn)效果

好久沒(méi)寫View了,最近恰巧遇到一個(gè)八大行星繞太陽(yáng)旋轉(zhuǎn)的假3D效果,寫完之后感覺(jué)效果還不錯(cuò)蜒什。能玩十分鐘的那種。
本篇將一步步帶您實(shí)現(xiàn)這樣的一個(gè)效果疤估,ps:我是用kotlin實(shí)現(xiàn)的灾常,介于您可能還不太熟悉kotlin或者不像熟悉java那樣,所以本篇使用java語(yǔ)言(寫的過(guò)程中老是忘記寫new和分號(hào)報(bào)錯(cuò))铃拇。
先上最終效果圖(錄制的比較渣)

太陽(yáng)系.gif

本文目的

  • 鞏固/練習(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)畫


分析問(wèn)題

1.行星的整體布局,3D的視覺(jué)效果

如果我們draw()的之前通過(guò)Camera將Canvas繞x軸旋轉(zhuǎn)60°是不是就可以搞定晋修?這種方式實(shí)則是不可行的吧碾。因?yàn)?code>draw()之前Canvas的變化會(huì)作用于子View,從效果圖可以看出墓卦,子View并沒(méi)有rotateX的變換倦春,只有縮放變換。所以我們通過(guò)子View layout時(shí)變化其位置,即計(jì)算子View的left睁本、top尿庐、right、bottom四個(gè)值

行星繞太陽(yáng)旋轉(zhuǎn)其軌跡實(shí)際上就是圓形呢堰,如下圖:

平面.png

我們看手機(jī)抄瑟,其實(shí)是沿著z軸方向。想象一下枉疼,如果讓坐標(biāo)系沿著x軸旋轉(zhuǎn)60°皮假,不就能達(dá)到我們想要的效果了嘛。

旋轉(zhuǎn)60°骂维,我們?cè)傺刂鴛軸方向看惹资,如下圖:

round_x.png

圖中藍(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)

3.png

剛看到這個(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芭届、重寫父容器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)畫

效果圖中的太陽(yáng)由兩張圖片組成厚掷,一張是前景,一張是背景帶亮光级解,讓背景圖繞著z軸無(wú)限旋轉(zhuǎn)即可冒黑。


開始編碼

開始愉快的編碼

核心就是行星的父容器

/**
 * 行星和太陽(yáng)的父容器
 *
 * @author guolong
 * @since 2019/8/20
 */
public class StarGroupView extends FrameLayout {

    // 從這個(gè)角度開始畫View ,可以調(diào)整
    private static final float START_ANGLE = 270f; // 270°
    // 父容器的邊界 單位dp
    private 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)換為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 個(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.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>

運(yùn)行,效果如下:

上述代碼正如前面分析的俺陋,計(jì)算所有子View的left 豁延、top 、right 腊状、bottom诱咏,注釋寫的也詳細(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)越小。正玄曲線如下:

正玄曲線中贬循,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之間變化坟比,如下圖:

5.png

顯然,

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)在最中間间影,不參與類似行星的位置計(jì)算

簡(jiǎn)單期間我們使用tag=“center"來(lái)標(biāo)識(shí)子View是中間的太陽(yáng)注竿。

修改xml文件

    <com.glong.demo.view.StarGroupView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!-- 增加太陽(yáng)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 不參與計(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;
    }
}

代碼注釋寫的很全面魂贬,不做過(guò)多解釋了巩割,這個(gè)時(shí)候我們把PADDING改大一點(diǎn),改成160付燥,運(yùn)行如下:

20190820184234.jpg

問(wèn)題很明顯宣谈,3應(yīng)該在4的上面, 2 應(yīng)該在3的上面键科,中間的View應(yīng)該在5闻丑,6的上面漩怎。

這是因?yàn)橄到y(tǒng)默認(rèn)按照View的添加順序畫View的,即我們xml文件里面的順序嗦嗡。xml里面我們centerView在第一個(gè)勋锤,所以就先畫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>() {
            @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根據(jù)他的scaleY排序谈宛,由于centerViewscaleYlayoutChildren()時(shí)并沒(méi)有改變,我們把centerViewscaleY設(shè)置為0.5f胎署,最后再還原回去∵郝迹現(xiàn)在運(yùn)行,效果如下:

微信截圖_20190820231253.png

到這里基本已經(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() {
        @Override
        public 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);
    }
}

這樣就開始自動(dòng)旋轉(zhuǎn)了抱究,調(diào)節(jié)AUTO_SWEEP_ANGLE的值 改變旋轉(zhuǎn)速度

加入手勢(shì)

老寫法恢氯,先上代碼

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)畫
     */
    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是因?yàn)槿绻怀?轉(zhuǎn)得太歡了
                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;

                // 取消動(dòng)畫和自動(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;
        else
            end = 0f;
        velocityAnim.setFloatValues(-velocity, end);
        velocityAnim.start();
    }
}

手勢(shì)處理的代碼比較簡(jiǎn)單鼓寺,這里就不再贅述了勋拟,需要注意的是

1.ACTION_DOWN需返回true,不然收不到后續(xù)的ACTION_MOVE事件妈候;

2.ACTION_DOWN時(shí)需要暫停動(dòng)畫和自動(dòng)旋轉(zhuǎn)

3.這里根據(jù)手指離開屏幕時(shí)的速度做Animator動(dòng)畫敢靡,當(dāng)然你也可以用scroller實(shí)現(xiàn)。

4.第59行苦银,我們給dx * SCALE_PX_ANGLE代表一個(gè)像素可以轉(zhuǎn)換成SCALE_PX_ANGLE角度

最后啸胧,加上中間太陽(yáng)旋轉(zhuǎn)的動(dòng)畫

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方法用來(lái)暫停和開始動(dòng)畫

public class StarGroupView extends FrameLayout {

    // 省略...
    public void pause() {
        velocityAnim.cancel();
        removeCallbacks(autoScrollRunnable);
    }

    public void start() {
        postDelayed(autoScrollRunnable, 16);
    }
}

最終不到算上注釋260代碼搞定幔虏!

最終效果

demo.gif

我把完整的Demo代碼和星球效果代碼放在github上了

點(diǎn)我下載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纺念,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子想括,更是在濱河造成了極大的恐慌陷谱,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑟蜈,死亡現(xiàn)場(chǎng)離奇詭異烟逊,居然都是意外死亡渣窜,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門焙格,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)图毕,“玉大人夷都,你說(shuō)我怎么就攤上這事眷唉。” “怎么了囤官?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵冬阳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我党饮,道長(zhǎng)肝陪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任刑顺,我火速辦了婚禮氯窍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹲堂。我一直安慰自己狼讨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布柒竞。 她就那樣靜靜地躺著政供,像睡著了一般。 火紅的嫁衣襯著肌膚如雪朽基。 梳的紋絲不亂的頭發(fā)上布隔,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音稼虎,去河邊找鬼衅檀。 笑死,一個(gè)胖子當(dāng)著我的面吹牛霎俩,可吹牛的內(nèi)容都是我干的哀军。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼茸苇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼排苍!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起学密,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤淘衙,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后腻暮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體彤守,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡毯侦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了具垫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侈离。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖筝蚕,靈堂內(nèi)的尸體忽然破棺而出卦碾,到底是詐尸還是另有隱情,我是刑警寧澤起宽,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布洲胖,位于F島的核電站,受9級(jí)特大地震影響坯沪,放射性物質(zhì)發(fā)生泄漏绿映。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一腐晾、第九天 我趴在偏房一處隱蔽的房頂上張望叉弦。 院中可真熱鬧,春花似錦藻糖、人聲如沸淹冰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)榄棵。三九已至,卻和暖如春潘拱,著一層夾襖步出監(jiān)牢的瞬間疹鳄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工芦岂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瘪弓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓禽最,卻偏偏與公主長(zhǎng)得像腺怯,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子川无,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容