2017年4月20日 星期四

Android: 製作Heart Beat視圖

前言

做了一個心跳的效果動畫,名為HeartBeatView,不靠Animation或Animator即可達到的客製化視圖,當中還包括可以直接在xml設定其速度和跳動幅度,效果如下:


這邊我歸納幾個實現的步驟:
  1. 定義HeartBeatView的屬性(declare-styleaqle)
  2. 實作HeartBeatView
  3. 初始化資源檔內容與Paint物件
  4. 繪製愛心
  5. 繪製跳動與幅度

定義HeartBeatView的屬性(declare-styleaqle) 

<resources>
    <declare-styleable name="HeartBeatImageView">
        <attr name="animMoveFactor" format="float" />
        <attr name="animCounter" format="integer" />
        <attr name="heartColor" format="color" />
    </declare-styleable>
</resources>

實作HeartBeatView

public class HeartBeatImageView extends View {
}

初始化資源檔內容與Paint物件

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

    public HeartBeatImageView(Context context) {
        this(context, null);
    }

    public HeartBeatImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HeartBeatImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.initParams(context, attrs);
        this.initPaint();
    }

    private void initParams(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HeartBeatImageView);
        this.animMoveFactor = typedArray.getFloat(R.styleable.HeartBeatImageView_animMoveFactor, DEFAUT_ANIM_MOVE_FACTOR);
        this.animCounter = typedArray.getInt(R.styleable.HeartBeatImageView_animCounter, DEFAULT_ANIM_COUNTER);
        this.heartColor = typedArray.getColor(R.styleable.HeartBeatImageView_heartColor, DEFAULT_COLOR);
        typedArray.recycle();
    }

    private void initPaint() {
        this.redCirclePaint = new Paint();
        this.redCirclePaint.setAntiAlias(true);
        this.redCirclePaint.setColor(this.heartColor);
    }

繪製愛心

先繪製一個正方形,再加上兩個圓形,並將之轉成45度即可,程式碼如下。

        int centerX = canvas.getWidth() / 2;
        int centerY = canvas.getHeight() / 2;
        float radius = this.getCircleRadius(canvas.getWidth() / 6, this.animCounter, this.animMoveFactor);

        canvas.rotate(45, centerX, centerY);

        canvas.drawRect(centerX - radius, centerY - radius, centerX + radius, centerY + radius, this.redCirclePaint);
        canvas.drawCircle(centerX - radius, centerY, radius, this.redCirclePaint);
        canvas.drawCircle(centerX, centerY - radius, radius, this.redCirclePaint);

繪製跳動與幅度

這邊是透過animCounter與step這兩個參數來控制增減,並期重新更新radius數值,程式碼如下。


    private void drawHeart(Canvas canvas) {
        this.animCounter = this.animCounter + this.step;

        int centerX = canvas.getWidth() / 2;
        int centerY = canvas.getHeight() / 2;
        float radius = this.getCircleRadius(canvas.getWidth() / 6, this.animCounter, this.animMoveFactor);

        canvas.rotate(45, centerX, centerY);

        canvas.drawRect(centerX - radius, centerY - radius, centerX + radius, centerY + radius, this.redCirclePaint);
        canvas.drawCircle(centerX - radius, centerY, radius, this.redCirclePaint);
        canvas.drawCircle(centerX, centerY - radius, radius, this.redCirclePaint);

        if (this.animCounter >= ANIM_COUNTER_MAX) {
            this.step = -1;
        } else if (this.animCounter <= 0) {
            this.step = 1;
        }
    }

    private float getCircleRadius(int radius, int animCounter, double animFactor) {
        return (float) (radius + animCounter * animFactor);
    }

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