“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:

  1. If the current number of circles does not exceed the upper limit, circles with different radii are randomly generated.
  2. Set the initial position of these circles.
  3. Randomly set vertical upward translation speed.
  4. Set the horizontal translation speed randomly.
  5. Constantly refresh the position of the circle and then draw.
  6. Remove circles that are outside the display area.
  7. 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:

  1. Remove bubbles that are outside the display area.
  2. 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]

Other articles in this series