Animation / Custom / View

Snowfall

Ho, ho, ho. ‘Tis the season to be jolly, and all that malarkey. As the publication date for this post is Christmas Day 2015, it seems only fitting that we should cover something festive on Styling Android. For those reading who do not celebrate Christmas, and those who are reading this post in June – please accept my apologies.

So, the big question is: What can we do that encapsulates the spirit and true meaning of Christmas? The obvious answer to that question is: A picture of your truly wearing a Christmas hat:

tree

As you can see I’m exuding Christmas cheer!

While I feel that picture alone is worthy of a mic drop ending to this post, I’m feeling generous so let’s actually make it snow as well.

We can do this by adding a custom View to a layout containing the image:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="com.stylingandroid.snowfall.MainActivity">

  <ImageView
    android:id="@+id/image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerInParent="true"
    android:contentDescription="@null"
    android:scaleType="fitCenter"
    android:src="@drawable/tree" />

  <com.stylingandroid.snowfall.SnowView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_alignBottom="@id/image"
    android:layout_alignEnd="@id/image"
    android:layout_alignLeft="@id/image"
    android:layout_alignRight="@id/image"
    android:layout_alignStart="@id/image"
    android:layout_alignTop="@id/image" />
</RelativeLayout>

Although there is a temptation to do this in a single custom view which extends ImageView, I have elected to keep them separate so that we can invalidate SnowView for each animation frame and not re-render the image each time.

So let’s take a look at our custom View:

public class SnowView extends View {
    private static final int NUM_SNOWFLAKES = 150;
    private static final int DELAY = 5;

    private SnowFlake[] snowflakes;

    public SnowView(Context context) {
        super(context);
    }

    public SnowView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SnowView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    protected void resize(int width, int height) {
        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.WHITE);
        paint.setStyle(Paint.Style.FILL);
        snowflakes = new SnowFlake[NUM_SNOWFLAKES];
        for (int i = 0; i < NUM_SNOWFLAKES; i++) {
            snowflakes[i] = SnowFlake.create(width, height, paint);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w != oldw || h != oldh) {
            resize(w, h);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (SnowFlake snowFlake : snowflakes) {
            snowFlake.draw(canvas);
        }
        getHandler().postDelayed(runnable, DELAY);
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            invalidate();
        }
    };
}

This is fairly straightforward. When the View is resized we initialise it with 150 SnowFlake objects which will be randomly positioned. The onDraw() method draws all of the SnowFlake objects and then schedules an invalidate() which will be called after a short pause so that we don’t completely hog the UI thread.

The SnowFlake code is very loosely based upon the snowfall algorithm by Samuel Arbesman:

class SnowFlake {
    private static final float ANGE_RANGE = 0.1f;
    private static final float HALF_ANGLE_RANGE = ANGE_RANGE / 2f;
    private static final float HALF_PI = (float) Math.PI / 2f;
    private static final float ANGLE_SEED = 25f;
    private static final float ANGLE_DIVISOR = 10000f;
    private static final float INCREMENT_LOWER = 2f;
    private static final float INCREMENT_UPPER = 4f;
    private static final float FLAKE_SIZE_LOWER = 7f;
    private static final float FLAKE_SIZE_UPPER = 20f;

    private final Random random;
    private final Point position;
    private float angle;
    private final float increment;
    private final float flakeSize;
    private final Paint paint;

    public static SnowFlake create(int width, int height, Paint paint) {
        Random random = new Random();
        int x = random.getRandom(width);
        int y = random.getRandom(height);
        Point position = new Point(x, y);
        float angle = random.getRandom(ANGLE_SEED) / ANGLE_SEED * ANGE_RANGE + HALF_PI - HALF_ANGLE_RANGE;
        float increment = random.getRandom(INCREMENT_LOWER, INCREMENT_UPPER);
        float flakeSize = random.getRandom(FLAKE_SIZE_LOWER, FLAKE_SIZE_UPPER);
        return new SnowFlake(random, position, angle, increment, flakeSize, paint);
    }

    SnowFlake(Random random, Point position, float angle, float increment, float flakeSize, Paint paint) {
        this.random = random;
        this.position = position;
        this.angle = angle;
        this.increment = increment;
        this.flakeSize = flakeSize;
        this.paint = paint;
    }

    private void move(int width, int height) {
        double x = position.x + (increment * Math.cos(angle));
        double y = position.y + (increment * Math.sin(angle));

        angle += random.getRandom(-ANGLE_SEED, ANGLE_SEED) / ANGLE_DIVISOR;

        position.set((int) x, (int) y);

        if (!isInside(width, height)) {
            reset(width);
        }
    }

    private boolean isInside(int width, int height) {
        int x = position.x;
        int y = position.y;
        return x >= -flakeSize - 1 && x + flakeSize <= width && y >= -flakeSize - 1 && y - flakeSize < height;
    }

    private void reset(int width) {
        position.x = random.getRandom(width);
        position.y = (int) (-flakeSize - 1);
        angle = random.getRandom(ANGLE_SEED) / ANGLE_SEED * ANGE_RANGE + HALF_PI - HALF_ANGLE_RANGE;
    }

    public void draw(Canvas canvas) {
        int width = canvas.getWidth();
        int height = canvas.getHeight();
        move(width, height);
        canvas.drawCircle(position.x, position.y, flakeSize, paint);
    }
}

When each SnowFlake is initialised it is placed in a random position on the Canvas. This is to ensure that when the snow is first drawn it appears that the snowfall is already in progress rather than starting if we were to start all of the flakes from the top. When a flake goes out of the Canvas bounds, it is re-positioned to a random horizontal location at the top – so we re-cycle our flakes to avoid unnecessary object creation.

When each frame is drawn, we first move the SnowFlake by adding some random elements to simulate small gusts of wind which could cause individual flakes to change direction slightly. We then perform our in bounds check (and move it back to the top, if necessary) before actually drawing the SnowFlake on the Canvas.

All of the constant values were tweaked until I found a snowfall simulation that I was happy with.

So when we run this we get the following:

Of course drawing to a Canvas is not the most performant way of rendering stuff like this (such as rendering using OpenGL) but, quite frankly, I have presents to open and turkey to eat so it can wait until another time. See ya!

The source code for this article is available here.

Part of this code is based upon “Snowfall” by Sam Arbesman, licensed under Creative Commons Attribution-Share Alike 3.0 and GNU GPL license.
Work: http://openprocessing.org/visuals/?visualID= 84771
License:
http://creativecommons.org/licenses/by-sa/3.0/
http://creativecommons.org/licenses/GPL/2.0/

© 2015, Mark Allison. All rights reserved.

Copyright © 2015 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.