“Diao Worm Xiao Skill Catalogue”
[Example project: BubbleSample]
Useful to the effect of underwater bubble rising recently, so check the information on the Internet, the result is found, it is this article instances [Android] underwater bubble rising interface effect, but the sample code included with this article is that some of the problems, such as the View after removing the thread was not closed correctly, lock screen and then open the screen, Bubbles will squeeze into a group and other problems, so I made some adjustments and modifications on the basis of its principle, to solve these problems, it can achieve the following effect:
0. Fundamentals
The basic principle of bubble effect is very simple, in fact, the so-called bubble is a translucent circle, its basic logic is as follows:
- If the current number of circles does not exceed the upper limit, circles with different radii are randomly generated.
- Set the initial position of these circles.
- Randomly set vertical upward translation speed.
- Set the horizontal translation speed randomly.
- Constantly refresh the position of the circle and then draw.
- Remove circles that are outside the display area.
- Repeat.
The principle is pretty simple, but there are a few things to watch out for, especially threads.
In the original demo, the thread creation and calculation logic was put directly into onDraw without closing the thread, which naturally caused a lot of problems. Not closing the thread will cause memory leaks in the View, and putting calculation logic in onDraw will add to the drawing burden, slow down the refresh rate, and lead to noticeable lag in weak cases. The best way to solve these problems is to stay focused and put the right content in the right place. Let’s look at the code implementation.
1. Code implementation
1.1 Defining bubbles
Bubble effect we don’t care about a lot of attributes, mainly the following: radius, coordinates, rise speed, horizontal translation speed. Since we only use it inside the View, we create an inner class directly, and then define these properties in the inner class.
private class Bubble {
int radius; // Bubble radius
float speedY; // Speed of ascent
float speedX; // Translation speed
float x; // Bubble x coordinates
float y; // Bubble y coordinates
}
Copy the code
1.2 Life cycle processing
Since we need threads to compute and control the refresh, we need to start and close threads, which is consistent with the View’s life cycle, so I start a thread to generate bubbles and refresh the bubble position when the View is added to the interface, and then close the thread when the View is removed from the interface.
@Override
protected void onAttachedToWindow(a) {
super.onAttachedToWindow();
startBubbleSync();
}
@Override
protected void onDetachedFromWindow(a) {
super.onDetachedFromWindow();
stopBubbleSync();
}
Copy the code
1.3 Enabling a Thread
Starting a thread is very simple. Simply create a thread, add a while loop to it, and then hibernate, create bubbles, refresh bubble positions, request UI updates, etc.
Instead of using variables to control the loop, it listens for interrupt events and uses break to break the loop when InterruptedException is intercepted, thus ending the thread.
// Start the bubble thread
private void startBubbleSync(a) {
stopBubbleSync();
mBubbleThread = new Thread() {
public void run(a) {
while (true) {
try {
Thread.sleep(mBubbleRefreshTime);
tryCreateBubble();
refreshBubbles();
postInvalidate();
} catch (InterruptedException e) {
System.out.println("Bubble thread ends");
break; }}}}; mBubbleThread.start(); }Copy the code
1.4 Closing a Thread
Since the thread is listening for interrupts at runtime, you can simply use the interrupt to notify the thread of an interrupt.
// Stop the bubble thread
private void stopBubbleSync(a) {
if (null == mBubbleThread) return;
mBubbleThread.interrupt();
mBubbleThread = null;
}
Copy the code
1.5 Creating a Bubble
In order to prevent too many bubbles from occupying too much performance, it is necessary to determine how many bubbles there are before creating bubbles. If there are enough bubbles, no new bubbles will be created.
At the same time, in order to make the bubble generation process look more reasonable, bubbles will be randomly created before the number of bubbles reaches the upper limit to prevent bubbles from appearing in a cluster. Therefore, a random item is set to generate bubbles when the generated random number is greater than 0.95, making the bubble generation process slower.
The process of creating a bubble is simple, just randomly generating properties in a set range and putting them into a List.
PS: Some hard coding and magic numbers are used, which is not a good habit. However, due to fixed application scenarios, the probability of adjusting these parameters is relatively small and the impact is not significant.
// Try to create a bubble
private void tryCreateBubble(a) {
if (null == mContentRectF) return;
if (mBubbles.size() >= mBubbleMaxSize) {
return;
}
if (random.nextFloat() < 0.95) {
return;
}
Bubble bubble = new Bubble();
int radius = random.nextInt(mBubbleMaxRadius - mBubbleMinRadius);
radius += mBubbleMinRadius;
float speedY = random.nextFloat() * mBubbleMaxSpeedY;
while (speedY < 1) {
speedY = random.nextFloat() * mBubbleMaxSpeedY;
}
bubble.radius = radius;
bubble.speedY = speedY;
bubble.x = mWaterRectF.centerX();
bubble.y = mWaterRectF.bottom - radius - mBottleBorder / 2;
float speedX = random.nextFloat() - 0.5 f;
while (speedX == 0) {
speedX = random.nextFloat() - 0.5 f;
}
bubble.speedX = speedX * 2;
mBubbles.add(bubble);
}
Copy the code
1.6 Refresh the bubble position
There are two main tasks:
- Remove bubbles that are outside the display area.
- Calculates the new bubble display position.
Can see there is no direct use of the original List, instead of copying a traverse the List and exist mainly in order to avoid abnormal ConcurrentModificationException, (if and when I was in the Vector, the ArrayList iteration to modify will be thrown Java. Util. ConcurrentModificationException exception).
The copied List is iterated over and the Bubble that is outside the display area is removed and the Bubble that is not outside the display area is refreshed. As you can see, the logic here is quite complicated, with various addition and subtraction calculations, in order to solve the problem of bubbles floating to the edge, to prevent bubbles floating out of the water range.
// Refresh the bubble position and remove the bubbles that are out of the area
private void refreshBubbles(a) {
List<Bubble> list = new ArrayList<>(mBubbles);
for (Bubble bubble : list) {
if (bubble.y - bubble.speedY <= mWaterRectF.top + bubble.radius) {
mBubbles.remove(bubble);
} else {
int i = mBubbles.indexOf(bubble);
if (bubble.x + bubble.speedX <= mWaterRectF.left + bubble.radius + mBottleBorder / 2) {
bubble.x = mWaterRectF.left + bubble.radius + mBottleBorder / 2;
} else if (bubble.x + bubble.speedX >= mWaterRectF.right - bubble.radius - mBottleBorder / 2) {
bubble.x = mWaterRectF.right - bubble.radius - mBottleBorder / 2;
} else{ bubble.x = bubble.x + bubble.speedX; } bubble.y = bubble.y - bubble.speedY; mBubbles.set(i, bubble); }}}Copy the code
1.7 Drawing Bubbles
Drawing bubbles is just as easy, just iterate through the List and draw the circle.
Here again, a new List is copied to operate on, but for a different reason, to prevent multithreading problems. Since our computational thread may update the original List during the drawing process, exceptions may occur. To avoid this problem, a List is copied out for traversal drawing.
// Draw bubbles
private void drawBubble(Canvas canvas) {
List<Bubble> list = new ArrayList<>(mBubbles);
for (Bubble bubble : list) {
if (null == bubble) continue; canvas.drawCircle(bubble.x, bubble.y, bubble.radius, mBubblePaint); }}Copy the code
2. Complete code
The complete sample code is simple enough to post directly in the body, but you can also download the complete project code from the bottom of the article.
public class BubbleView extends View {
private int mBubbleMaxRadius = 30; // The maximum bubble radius is px
private int mBubbleMinRadius = 5; // Minimum bubble radius px
private int mBubbleMaxSize = 30; // Number of bubbles
private int mBubbleRefreshTime = 20; // Refresh interval
private int mBubbleMaxSpeedY = 5; // Bubble speed
private int mBubbleAlpha = 128; // Bubble brush
private float mBottleWidth; // Bottle width
private float mBottleHeight; // Bottle height
private float mBottleRadius; // The bottom corner radius of the bottle
private float mBottleBorder; // Bottle edge width
private float mBottleCapRadius; // Bottle top corner radius
private float mWaterHeight; // The height of water
private RectF mContentRectF; // The actual available content area
private RectF mWaterRectF; // Water occupied area
private Path mBottlePath; // External bottle
private Path mWaterPath; / / water
private Paint mBottlePaint; // Bottle brush
private Paint mWaterPaint; / / water brushes
private Paint mBubblePaint; // Bubble brush
public BubbleView(Context context) {
this(context, null);
}
public BubbleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mWaterRectF = new RectF();
mBottleWidth = dp2px(130);
mBottleHeight = dp2px(260);
mBottleBorder = dp2px(8);
mBottleRadius = dp2px(15);
mBottleCapRadius = dp2px(5);
mWaterHeight = dp2px(240);
mBottlePath = new Path();
mWaterPath = new Path();
mBottlePaint = new Paint();
mBottlePaint.setAntiAlias(true);
mBottlePaint.setStyle(Paint.Style.STROKE);
mBottlePaint.setStrokeCap(Paint.Cap.ROUND);
mBottlePaint.setColor(Color.WHITE);
mBottlePaint.setStrokeWidth(mBottleBorder);
mWaterPaint = new Paint();
mWaterPaint.setAntiAlias(true);
initBubble();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mContentRectF = new RectF(getPaddingLeft(), getPaddingTop(), w - getPaddingRight(), h - getPaddingBottom());
float bl = mContentRectF.centerX() - mBottleWidth / 2;
float bt = mContentRectF.centerY() - mBottleHeight / 2;
float br = mContentRectF.centerX() + mBottleWidth / 2;
float bb = mContentRectF.centerY() + mBottleHeight / 2;
mBottlePath.reset();
mBottlePath.moveTo(bl - mBottleCapRadius, bt - mBottleCapRadius);
mBottlePath.quadTo(bl, bt - mBottleCapRadius, bl, bt);
mBottlePath.lineTo(bl, bb - mBottleRadius);
mBottlePath.quadTo(bl, bb, bl + mBottleRadius, bb);
mBottlePath.lineTo(br - mBottleRadius, bb);
mBottlePath.quadTo(br, bb, br, bb - mBottleRadius);
mBottlePath.lineTo(br, bt);
mBottlePath.quadTo(br, bt - mBottleCapRadius, br + mBottleCapRadius, bt - mBottleCapRadius);
mWaterPath.reset();
mWaterPath.moveTo(bl, bb - mWaterHeight);
mWaterPath.lineTo(bl, bb - mBottleRadius);
mWaterPath.quadTo(bl, bb, bl + mBottleRadius, bb);
mWaterPath.lineTo(br - mBottleRadius, bb);
mWaterPath.quadTo(br, bb, br, bb - mBottleRadius);
mWaterPath.lineTo(br, bb - mWaterHeight);
mWaterPath.close();
mWaterRectF.set(bl, bb - mWaterHeight, br, bb);
LinearGradient gradient = new LinearGradient(mWaterRectF.centerX(), mWaterRectF.top,
mWaterRectF.centerX(), mWaterRectF.bottom, 0xFF4286f4.0xFF373B44, Shader.TileMode.CLAMP);
mWaterPaint.setShader(gradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mWaterPath, mWaterPaint);
canvas.drawPath(mBottlePath, mBottlePaint);
drawBubble(canvas);
}
/ / bubbles effect -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
@Override
protected void onAttachedToWindow(a) {
super.onAttachedToWindow();
startBubbleSync();
}
@Override
protected void onDetachedFromWindow(a) {
super.onDetachedFromWindow();
stopBubbleSync();
}
private class Bubble {
int radius; // Bubble radius
float speedY; // Speed of ascent
float speedX; // Translation speed
float x; // Bubble x coordinates
float y; // Bubble y coordinates
}
private ArrayList<Bubble> mBubbles = new ArrayList<>();
private Random random = new Random();
private Thread mBubbleThread;
// Initialize the bubble
private void initBubble(a) {
mBubblePaint = new Paint();
mBubblePaint.setColor(Color.WHITE);
mBubblePaint.setAlpha(mBubbleAlpha);
}
// Start the bubble thread
private void startBubbleSync(a) {
stopBubbleSync();
mBubbleThread = new Thread() {
public void run(a) {
while (true) {
try {
Thread.sleep(mBubbleRefreshTime);
tryCreateBubble();
refreshBubbles();
postInvalidate();
} catch (InterruptedException e) {
System.out.println("Bubble thread ends");
break; }}}}; mBubbleThread.start(); }// Stop the bubble thread
private void stopBubbleSync(a) {
if (null == mBubbleThread) return;
mBubbleThread.interrupt();
mBubbleThread = null;
}
// Draw bubbles
private void drawBubble(Canvas canvas) {
List<Bubble> list = new ArrayList<>(mBubbles);
for (Bubble bubble : list) {
if (null == bubble) continue; canvas.drawCircle(bubble.x, bubble.y, bubble.radius, mBubblePaint); }}// Try to create a bubble
private void tryCreateBubble(a) {
if (null == mContentRectF) return;
if (mBubbles.size() >= mBubbleMaxSize) {
return;
}
if (random.nextFloat() < 0.95) {
return;
}
Bubble bubble = new Bubble();
int radius = random.nextInt(mBubbleMaxRadius - mBubbleMinRadius);
radius += mBubbleMinRadius;
float speedY = random.nextFloat() * mBubbleMaxSpeedY;
while (speedY < 1) {
speedY = random.nextFloat() * mBubbleMaxSpeedY;
}
bubble.radius = radius;
bubble.speedY = speedY;
bubble.x = mWaterRectF.centerX();
bubble.y = mWaterRectF.bottom - radius - mBottleBorder / 2;
float speedX = random.nextFloat() - 0.5 f;
while (speedX == 0) {
speedX = random.nextFloat() - 0.5 f;
}
bubble.speedX = speedX * 2;
mBubbles.add(bubble);
}
// Refresh the bubble position and remove the bubbles that are out of the area
private void refreshBubbles(a) {
List<Bubble> list = new ArrayList<>(mBubbles);
for (Bubble bubble : list) {
if (bubble.y - bubble.speedY <= mWaterRectF.top + bubble.radius) {
mBubbles.remove(bubble);
} else {
int i = mBubbles.indexOf(bubble);
if (bubble.x + bubble.speedX <= mWaterRectF.left + bubble.radius + mBottleBorder / 2) {
bubble.x = mWaterRectF.left + bubble.radius + mBottleBorder / 2;
} else if (bubble.x + bubble.speedX >= mWaterRectF.right - bubble.radius - mBottleBorder / 2) {
bubble.x = mWaterRectF.right - bubble.radius - mBottleBorder / 2;
} else{ bubble.x = bubble.x + bubble.speedX; } bubble.y = bubble.y - bubble.speedY; mBubbles.set(i, bubble); }}}private float dp2px(float dpValue) {
returnTypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics()); }}Copy the code
3. The conclusion
Because this project is an example of the nature of the project, so the design is relatively simple, the structure is simple and rough, and has not been carefully crafted, there are some omissions may also be, if you think there are problems in logic or any doubts, welcome to the following (public number, small column) comment area.
Public account can view the article by clicking on [read the text] to download the required sample code, non-public account can be read from the end of the article or download the sample project.
[Example project: BubbleSample]