Copyright notice: This article is the blogger’s original article, shall not be reproduced without the permission of the blogger

Android Development from Scratch series

Source: AnliaLee/FallingView, welcome star

If you see any mistakes or have any good suggestions, please leave a comment

prefaceIt is the second half of November, and the weather is turning cold. I wonder if it has begun to snow in the north. In this tutorial, we will follow the theme of the seasonsnowThe effect of. The effect of the idea of reference from foreign godsAndroid snowflakes flying effectOn this basis, further encapsulation and function extension are realized

This article only focuses on the ideas and implementation steps, some knowledge principles used in it will not be very detailed. If there are unclear APIS or methods, you can search the corresponding information on the Internet, there must be a god to explain very clearly, I will not present the ugly. In the spirit of serious and responsible, I will post the relevant knowledge of the blog links (in fact, is lazy do not want to write so much ha ha), you can send their own. For the benefit of those who are reading this series of blogs for the first time, this post contains some content that has been covered in the previous series of blogs

International convention, go up effect drawing first


Draw a circular falling “snowball”

Let’s start from the simplest part, custom View there are many ways to achieve cycle Animation directly, of course, is to use the simplest Animation classes to implement, but considering whether it is snow, snow or rain what of, each individual has their own starting point, speed and direction, etc., the process of its whereabouts can appear many random factors, Animation class is not very suitable to implement such irregular Animation, so we will use thread communication to implement a simple timer, to achieve the effect of periodically drawing the View. Here we will simply draw a “snowball” to see how the timer works and create a new FallingView

public class FallingView extends View {

    private Context mContext;
    private AttributeSet mAttrs;

    private int viewWidth;
    private int viewHeight;

    private static final int defaultWidth = 600;// Default width
    private static final int defaultHeight = 1000;// Default height
    private static final int intervalTime = 5;// Redraw interval time

    private Paint testPaint;
    private int snowY;

    public FallingView(Context context) {
        super(context);
        mContext = context;
        init();
    }

    public FallingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        mAttrs = attrs;
        init();
    }

    private void init(a){
        testPaint = new Paint();
        testPaint.setColor(Color.WHITE);
        testPaint.setStyle(Paint.Style.FILL);
        snowY = 0;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = measureSize(defaultHeight, heightMeasureSpec);
        int width = measureSize(defaultWidth, widthMeasureSpec);
        setMeasuredDimension(width, height);

        viewWidth = width;
        viewHeight = height;
    }

    private int measureSize(int defaultSize,int measureSpec) {
        int result = defaultSize;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);

        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == View.MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(100,snowY,25,testPaint);
        getHandler().postDelayed(runnable, intervalTime);// Redraw at an interval
    }

    // redraw the thread
    private Runnable runnable = new Runnable() {
        @Override
        public void run(a) {
            snowY += 15;
            if(snowY>viewHeight){// Resets the snowball position when the screen is off
                snowY = 0; } invalidate(); }}; }Copy the code

The effect is shown in figure

In the above code, the basic framework of the View has been set up. The idea is actually very simple, we just need to update the position of the falling object before each redraw

Encapsulates the falling object object

Related blog links

The Ubiquitous design pattern in Android development — the Builder pattern

[Android] Gets the View width and height

We wanted to be able to customize snow, not just snow, but rain, coins, etc., so we had to encapsulate falling objects. In order to make the object class external method code readable in the future, we use the Builder design mode to build the object object class and create FallObject

public class FallObject {
    private int initX;
    private int initY;
    private Random random;
    private int parentWidth;// Parent container width
    private int parentHeight;// Parent container height
    private float objectWidth;// Drop object width
    private float objectHeight;// Drop object height

    public int initSpeed;// Initial descent speed

    public float presentX;// Current position X coordinates
    public float presentY;// The current position is the Y coordinate
    public float presentSpeed;// Current speed of descent

    private Bitmap bitmap;
    public Builder builder;

    private static final int defaultSpeed = 10;// Default drop speed

    public FallObject(Builder builder, int parentWidth, int parentHeight){
        random = new Random();
        this.parentWidth = parentWidth;
        this.parentHeight = parentHeight;
        initX = random.nextInt(parentWidth);// The X coordinate of the random object
        initY = random.nextInt(parentHeight)- parentHeight;// Random object's Y coordinate, and let the object fall from the top of the screen at first
        presentX = initX;
        presentY = initY;

        initSpeed = builder.initSpeed;

        presentSpeed = initSpeed;
        bitmap = builder.bitmap;
        objectWidth = bitmap.getWidth();
        objectHeight = bitmap.getHeight();
    }

    private FallObject(Builder builder) {
        this.builder = builder;
        initSpeed = builder.initSpeed;
        bitmap = builder.bitmap;
    }

    public static final class Builder {
        private int initSpeed;
        private Bitmap bitmap;

        public Builder(Bitmap bitmap) {
            this.initSpeed = defaultSpeed;
            this.bitmap = bitmap;
        }

        /** * Sets the initial falling speed of the object *@param speed
         * @return* /
        public Builder setSpeed(int speed) {
            this.initSpeed = speed;
            return this;
        }

        public FallObject build(a) {
            return new FallObject(this); }}/** * Draw object object *@param canvas
     */
    public void drawObject(Canvas canvas){
        moveObject();
        canvas.drawBitmap(bitmap,presentX,presentY,null);
    }

    /** * Move object object */
    private void moveObject(a){
        moveY();
        if(presentY>parentHeight){ reset(); }}/** * move logic on the Y axis */
    private void moveY(a){
        presentY += presentSpeed;
    }

    /** * resets the object position */
    private void reset(a){ presentY = -objectHeight; presentSpeed = initSpeed; }}Copy the code

The method for adding objects is set in FallingView accordingly

public class FallingView extends View {
	// omit some code...
    private List<FallObject> fallObjects;

    private void init(a){
        fallObjects = new ArrayList<>();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(fallObjects.size()>0) {for (int i=0; i<fallObjects.size(); i++) {// Then draw
                fallObjects.get(i).drawObject(canvas);
            }
            // Redraw once in a while for animation effectgetHandler().postDelayed(runnable, intervalTime); }}// redraw the thread
    private Runnable runnable = new Runnable() {
        @Override
        public void run(a) { invalidate(); }};/** * Add drop object * to View@paramFallObject Drops the object object *@param num
     */
    public void addFallObject(final FallObject fallObject, final int num) {
        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw(a) {
                getViewTreeObserver().removeOnPreDrawListener(this);
                for (int i = 0; i < num; i++) {
                    FallObject newFallObject = new FallObject(fallObject.builder,viewWidth,viewHeight);
                    fallObjects.add(newFallObject);
                }
                invalidate();
                return true; }}); }}Copy the code

Add some objects to the FallingView in the Activity to see what it looks like

// Draw a snowball bitmap
snowPaint = new Paint();
snowPaint.setColor(Color.WHITE);
snowPaint.setStyle(Paint.Style.FILL);
bitmap = Bitmap.createBitmap(50.50, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
bitmapCanvas.drawCircle(25.25.25,snowPaint);

// Initialize a snowball-style fallObject
FallObject.Builder builder = new FallObject.Builder(bitmap);
FallObject fallObject = builder
		.setSpeed(10)
		.build();

fallingView = (FallingView) findViewById(R.id.fallingView);
fallingView.addFallObject(fallObject,50);// Add 50 snowball objects
Copy the code

The effect is shown in figure

At this point we have completed a basic falling object class, and now we are ready to extend the functionality and effects


Extension 1: Added constructors for importing Drawable resources and interfaces for setting object sizes

In our FallObject class, the Builder only supports importing bitmaps. Many times we get our image styles from the Drawable resource folder. Converting a Drawable into a bitmap every time is cumbersome. So we need to modify FallObject by encapsulating the drawable resource import constructor in the FallObject class

public static final class Builder {
	// omit some code...
	public Builder(Bitmap bitmap) {
		this.initSpeed = defaultSpeed;
		this.bitmap = bitmap;
	}

	public Builder(Drawable drawable) {
		this.initSpeed = defaultSpeed;
		this.bitmap = drawableToBitmap(drawable); }}/** * drawable * /** * drawable *@param drawable
 * @return* /
public static Bitmap drawableToBitmap(Drawable drawable) { Bitmap bitmap = Bitmap.createBitmap( drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), drawable.getOpacity() ! = PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565); Canvas canvas =new Canvas(bitmap);
	drawable.setBounds(0.0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
	drawable.draw(canvas);
	return bitmap;
}
Copy the code

With the drawable resource import constructor, you definitely need an interface to change the size of FallObject’s image style, again extending the interface in FallObject’s Builder

public static final class Builder {
	// omit some code...
	public Builder setSize(int w, int h){
		this.bitmap = changeBitmapSize(this.bitmap,w,h);
		return this; }}/** * Change the size of bitmap *@paramBitmap Target bitmap *@paramNewW Target width *@paramNewH Target height *@return* /
public static Bitmap changeBitmapSize(Bitmap bitmap, int newW, int newH) {
	int oldW = bitmap.getWidth();
	int oldH = bitmap.getHeight();
	// Calculate the scale
	float scaleWidth = ((float) newW) / oldW;
	float scaleHeight = ((float) newH) / oldH;
	// Get the matrix argument you want to scale
	Matrix matrix = new Matrix();
	matrix.postScale(scaleWidth, scaleHeight);
	// Get the new image
	bitmap = Bitmap.createBitmap(bitmap, 0.0, oldW, oldH, matrix, true);
	return bitmap;
}
Copy the code

When initializing the drop object style in the Activity, we can import the Drawable resource and set the object size.

FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
		.setSpeed(10)
		.setSize(50.50)
		.build();
Copy the code

So let’s see what happens


Extension 2: Achieve the effect of “different size” and “different speed of snowflakes”

Before, we imported the drawable resource to make the screen “fall snow”, but the snowflakes were all the same size and falling at the same speed, which was very monotonous and looked nothing like a real snow scene. So we need to modify the FallObject using random numbers to make the snowflakes different in size and speed

public class FallObject {
	// omit some code...
    private boolean isSpeedRandom;// Is the initial velocity ratio of the object random
    private boolean isSizeRandom;// The initial size ratio of the object is random

    public FallObject(Builder builder, int parentWidth, int parentHeight){
		// omit some code...
        this.builder = builder;
        isSpeedRandom = builder.isSpeedRandom;
        isSizeRandom = builder.isSizeRandom;

        initSpeed = builder.initSpeed;
        randomSpeed();
        randomSize();
    }

    private FallObject(Builder builder) {
		// omit some code...
        isSpeedRandom = builder.isSpeedRandom;
        isSizeRandom = builder.isSizeRandom;
    }

    public static final class Builder {
		// omit some code...
        private boolean isSpeedRandom;
        private boolean isSizeRandom;

        public Builder(Bitmap bitmap) {
			// omit some code...
            this.isSpeedRandom = false;
            this.isSizeRandom = false;
        }

        public Builder(Drawable drawable) {
			// omit some code...
            this.isSpeedRandom = false;
            this.isSizeRandom = false;
        }

        /** * Sets the initial falling speed of the object *@param speed
         * @return* /
        public Builder setSpeed(int speed) {
            this.initSpeed = speed;
            return this;
        }

        /** * Sets the initial falling speed of the object *@param speed
         * @paramIsRandomSpeed Whether the initial falling speed of the object is proportional to random *@return* /
        public Builder setSpeed(int speed,boolean isRandomSpeed) {
            this.initSpeed = speed;
            this.isSpeedRandom = isRandomSpeed;
            return this;
        }

        /** * Sets the object size *@param w
         * @param h
         * @return* /
        public Builder setSize(int w, int h){
            this.bitmap = changeBitmapSize(this.bitmap,w,h);
            return this;
        }

        /** * Sets the object size *@param w
         * @param h
         * @paramIsRandomSize Whether the initial size ratio of the object is random *@return* /
        public Builder setSize(int w, int h, boolean isRandomSize){
            this.bitmap = changeBitmapSize(this.bitmap,w,h);
            this.isSizeRandom = isRandomSize;
            return this; }}/** * resets the object position */
    private void reset(a){
        presentY = -objectHeight;
        randomSpeed();// Remember to reset the speed as well, it will be much better
    }

    /** * Initial falling speed of random object */
    private void randomSpeed(a){
        if(isSpeedRandom){
            presentSpeed = (float)((random.nextInt(3) +1) *0.1+1)* initSpeed;// These random numbers can be adjusted according to your own needs
        }else{ presentSpeed = initSpeed; }}/** * Random object initial size ratio */
    private void randomSize(a){
        if(isSizeRandom){
            float r = (random.nextInt(10) +1) *0.1 f;
            float rW = r * builder.bitmap.getWidth();
            float rH = r * builder.bitmap.getHeight();
            bitmap = changeBitmapSize(builder.bitmap,(int)rW,(int)rH);
        }else{ bitmap = builder.bitmap; } objectWidth = bitmap.getWidth(); objectHeight = bitmap.getHeight(); }}Copy the code

Set the parameters in the Activity

FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
		.setSpeed(10.true)
		.setSize(50.50.true)
		.build();
Copy the code

The effect is seen here, and looks a lot better from the appearance 乛 one item 乛 jun


Extension 3: Introduce the concept of “wind”

“Wind” is actually a metaphor, but actually what we want to do is to make snowflakes not only fall, but also move horizontally, which means we want to simulate the effect of snowflakes dancing in the wind. In order to make the displacement of the snowflake on the X axis not appear to be spooky, we use the sine function to obtain the displacement distance on the X axis, as shown in the figure

The sine curve is shown below

When we take the curve from -π to π, we can see that the sine of an Angle is maximum when it is PI /2 and minimum when it is – PI /2, so we also need to consider its limit value when calculating the Angle. Also, because we added the horizontal movement, remember to determine the leftmost and rightmost boundaries when determining boundaries, and modify the FallObject

public class FallObject {
	// omit some code...
    public int initSpeed;// Initial descent speed
    public int initWindLevel;// Initial wind level
	
    private float angle;// Object falling Angle
	
    private boolean isWindRandom;// Is the ratio of the initial wind direction to the size of the force random
    private boolean isWindChange;// Whether the wind direction and force change randomly as the object falls

    private static final int defaultWindLevel = 0;// Default wind level
    private static final int defaultWindSpeed = 10;// Default unit wind speed
    private static final float HALF_PI = (float) Math.PI / 2;/ / PI / 2

    public FallObject(Builder builder, int parentWidth, int parentHeight){
		// omit some code...
        isWindRandom = builder.isWindRandom;
        isWindChange = builder.isWindChange;

        initSpeed = builder.initSpeed;
        randomSpeed();
        randomSize();
        randomWind();
    }

    private FallObject(Builder builder) {
		// omit some code...
        isWindRandom = builder.isWindRandom;
        isWindChange = builder.isWindChange;
    }

    public static final class Builder {
		// omit some code...
        private boolean isWindRandom;
        private boolean isWindChange;

        public Builder(Bitmap bitmap) {
			// omit some code...
            this.isWindRandom = false;
            this.isWindChange = false;
        }

        public Builder(Drawable drawable) {
			// omit some code...
            this.isWindRandom = false;
            this.isWindChange = false;
        }

        /** * Sets the wind level, direction, and random factors *@paramLevel Indicates the wind level (the effect is better when the absolute value is 5). When it is positive, the wind blows from left to right (the object moves in the positive direction of the X axis); when it is negative, it is the opposite *@paramIsWindRandom Whether the ratio of the initial wind direction to the size of the wind is random *@paramIsWindChange Whether there is a random change in the direction and force of the wind during an object's fall *@return* /
        public Builder setWind(int level,boolean isWindRandom,boolean isWindChange){
            this.initWindLevel = level;
            this.isWindRandom = isWindRandom;
            this.isWindChange = isWindChange;
            return this; }}/** * Move object object */
    private void moveObject(a){
        moveX();
        moveY();
        if(presentY>parentHeight || presentX<-bitmap.getWidth() || presentX>parentWidth+bitmap.getWidth()){ reset(); }}/** ** move logic on the X axis */
    private void moveX(a){
        presentX += defaultWindSpeed * Math.sin(angle);
        if(isWindChange){
            angle += (float) (random.nextBoolean()? -1:1) * Math.random() * 0.0025; }}/** * resets the object position */
    private void reset(a){
        presentY = -objectHeight;
        randomSpeed();// Remember to reset the speed as well, it will be much better
        randomWind();// Remember to reset the initial Angle, otherwise the snowflake will fall less and less (because Angle accumulation will make the snowflake fall more and more biased)
    }

    /** * The ratio of wind direction to wind force, i.e. the initial falling Angle of the random object */
    private void randomWind(a){
        if(isWindRandom){
            angle = (float) ((random.nextBoolean()? -1:1) * Math.random() * initWindLevel /50);
        }else {
            angle = (float) initWindLevel /50;
        }

        // Limit the maximum and minimum Angle values
        if(angle>HALF_PI){
            angle = HALF_PI;
        }else if(angle<-HALF_PI){ angle = -HALF_PI; }}}Copy the code

Invoke the newly added interface in the Activity

FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
		.setSpeed(7.true)
		.setSize(50.50.true)
		.setWind(5.true.true)
		.build();
Copy the code

The effect is shown in figure

This is the end of this tutorial. If you enjoy it, please give me a thumbs up. Your support is my biggest motivation