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