2017年4月17日 星期一

Android: 下雪撒圖的效果製作

前言

由於想研究關於UI方面的議題,所以最近試著寫一個客製化的FrameLayout,可以顯示撒圖的效果,這篇文章主要就是介紹概念與一些作法。這裡我製作了一個名為SnowEffectFrameLayout的客製化PercentFrameLayout,這個Layout主要是用來控制圖片的動畫與生成,當中還包括一些參數的設定。我們先來看一下實際效果,效果如下:


這邊我歸納幾個實現的步驟:
  1. 定義SnowEffectFrameLayout的屬性(declare-styleaqle)
  2. 實作SnowEffectFrameLayout
  3. 初始化資源檔內容
  4. 初始化圖片物件池
  5. 設定每張圖片的TranslateAnimation

定義SnowEffectFrameLayout的屬性(declare-styleaqle)

<resources>
    <declare-styleable name="SnowEffectFrameLayout">
        <attr name="snowBasicCount" format="integer" />
        <attr name="dropAverageDuration" format="integer" />
        <attr name="isRotation" format="boolean" />
    </declare-styleable>
</resources>

實作SnowEffectFrameLayout

首先讓SnowEffectFrameLayout繼承PercentFrameLayout,主要是讓每個圖片能夠利用PercentFrameLayout的百分比特性,均勻地分布在螢幕的X軸上,至於什麼是PercentFrameLayout,其實就是透過百分比來控制子視圖的大小,程式碼如下。

# Gradle: dependencies

compile 'com.android.support:percent:25.3.1'

# Java

public class SnowEffectFrameLayout extends PercentFrameLayout {
}

初始化資源檔內容

這邊則是在呼叫SnowEffectFrameLayout的建構子時,順便將我們在xml裡設定的屬性給帶進來(要是你有使用到的話!),可以透過AttributeSet這個參數獲取其在xml設定的內容,程式碼如下。

private void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SnowEffectFrameLayout);
        this.snowBasicCount = typedArray.getInteger(R.styleable.SnowEffectFrameLayout_snowBasicCount, DEFAULT_SNOW_BASIC_COUNT);
        this.dropAverageDuration = typedArray.getInteger(R.styleable.SnowEffectFrameLayout_dropAverageDuration, DEFAULT_DROP_AVERAGE_DURATION);
        this.isRotation = typedArray.getBoolean(R.styleable.SnowEffectFrameLayout_isRotation, DEFAULT_IS_ROTATION);
        this.dropFrequency = DEFAULT_DROP_FREQUENCY;

        this.snowList = new ArrayList<>();
        if ((this.snowList == null || this.snowList.size() == 0)) {
            this.snowList.add(ContextCompat.getDrawable(this.getContext(), R.drawable.snow));
        }
}

初始化圖片物件池

為了要能重複利用圖片,這邊初始了一個物件池,用來存放這些大量的視圖,並且不用每次重新產生,造成資源的浪費,程式碼如下。

private void initSnowPool() {
        final int snowCount = this.snowList.size();
        if (snowCount == 0) {
            throw new IllegalStateException("There are no drawables.");
        }

        this.cleanSnowPool();

        final int expectedMaxSnowCountOnScreen = (int) ((1 + RELATIVE_DROP_DURATION_OFFSET) * snowBasicCount * dropAverageDuration / ((float) dropFrequency));
        this.snowPool = new Pools.SynchronizedPool<>(expectedMaxSnowCountOnScreen);
        for (int i = 0; i < expectedMaxSnowCountOnScreen; i++) {
            final ImageView snow = this.generateSnowImage(this.snowList.get(i % snowCount));
            this.addView(snow, 0);
            this.snowPool.release(snow);
        }

        RandomTool.setSeed(10);
}

設定每張圖片的TranslateAnimation

最後就是透過TranslateAnimation讓每張圖片呈現撒落的感覺,再結合RotateAnimation在撒落同時能有旋轉的效果,程式碼如下。

private void startDropAnimationForSingleSnow(final ImageView snow) {
        final int currentDuration = (int) (this.dropAverageDuration * RandomTool.floatInRange(1, RELATIVE_DROP_DURATION_OFFSET));

        final AnimationSet animationSet = new AnimationSet(false);
        final TranslateAnimation translateAnimation = new TranslateAnimation(
                Animation.RELATIVE_TO_SELF, 0,
                Animation.RELATIVE_TO_SELF, RandomTool.floatInRange(0, 5),
                Animation.RELATIVE_TO_PARENT, 0,
                Animation.ABSOLUTE, this.windowHeight);

        if (this.isRotation) {
            final RotateAnimation rotateAnimation = new RotateAnimation(
                    0,
                    RandomTool.floatInRange(0, 360),
                    Animation.RELATIVE_TO_SELF,
                    0.5f,
                    Animation.RELATIVE_TO_SELF,
                    0.5f);
            animationSet.addAnimation(rotateAnimation);
        }

        animationSet.addAnimation(translateAnimation);
        animationSet.setDuration(currentDuration);
        animationSet.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                snowPool.release(snow);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });

        snow.startAnimation(animationSet);
    }

以上還有些小細節沒有交代,就是透過亂數的方式製造位置和動畫速度不一的落差,但這邊不多做贅言,實際的程式碼則在GitHub上,有興趣的人可以參考看看。

Source Code

GitHub: https://github.com/xavier0507/SnowEffect.git


沒有留言:

張貼留言