Android custom View advanced dynamic effects --- meteor shower dynamic effects | meteor shower album cover

Key points of text objectives

Recently, I saw a motion effect of dome meteor from KuWo Music App. After reading it, I decided to try it myself. This article will focus on realizing the meteor shower effect through custom View, and you can see the advanced motion effect of meteor shower around the album picture. By completing this development, you can have a deeper understanding of custom View.

Realization effect

Let's take a look at the effect in the figure above. In the middle is the picture imported from the outside, surrounded by the meteor effect realized by custom View

Core design ideas

1. Crop the ImageView original image to obtain a circular image

The basic idea of this part is to scale the original image to the size appropriate to the target size, and then cut it.

The key step is to set the intersection background. There are many personnel and relevant data in this part. The details are as follows. SRC represents the background image and DST represents the foreground image.

2. Rotate the circular picture

In onDraw, images are processed based on Bitmap type objects, and Java provides Matrix usage to handle Bitmap rotation. Here, although we need the processed image with circular effect, the canvas of the image is not circular, but rectangular, but its background is transparent color. Therefore, during the rotation process, the most appropriate size will be rotated to accommodate the original image, Therefore, the width and height of the processed picture change, as shown in the figure below. Supplementary areas will be generated during rotation.

3. Draw meteors

The basic composition of a static meteor consists of two parts: the meteor body is a circle and the meteor trajectory is an arc

This is only a static meteor. If you want to achieve the effect of dynamic meteor, you need to ensure it through the following three parameters

Serial numberparameterParameter requirements
1Arc end angleThe end angle of the arc = start angle + sweep angle. Its value should change clockwise to show the rotation effect
2Arc sweep angleIn order to realize the two stages of meteor tailing and extinction, the sweep angle should increase or decrease linearly, and be recovered after reducing to 0
3Arc colorThe arc color should have a gradient effect, which can reflect the trailing light attenuation effect

Key code description

1. First, create an internal class to implement the meteor object, which defines several main element information, such as starting point angle / sweep angle / arc radius / abscissa of the center of the meteor head / ordinate of the meteor head

    /**
     * The meteor object is mainly composed of five elements: starting point angle / sweep angle / arc radius / abscissa of the center of the meteor head / ordinate of the meteor head
     */
    private class FallingStar{
        //Meteor regular gradient
        private final int[] SWEEP_COLORS = new int[]{Color.TRANSPARENT, 0xFFE0E0E0};
        private final float[] POSITIONS = new float[]{0.2f, 0.8f};
        private int startAngle;
        private int sweepAngle;
        private int radius;
        //Abscissa and ordinate of meteor head circle
        private int starCenterX;
        private int starCenterY;
        private SweepGradient shader;
        private RectF rect;
        //true indicates that the meteor trajectory is in the growth period; Anyway, it is the attenuation period
        private boolean rise;
        //Meteor rotation speed
        private int velocity;

        public int getStartAngle() {
            return startAngle;
        }

        public FallingStar setStartAngle(int startAngle) {
            this.startAngle = startAngle;
            return this;
        }

        public int getSweepAngle() {
            return sweepAngle;
        }

        public FallingStar setSweepAngle(int sweepAngle) {
            this.sweepAngle = sweepAngle;
            return this;
        }

        public int getRadius() {
            return radius;
        }

        public FallingStar setRadius(int radius) {
            this.radius = radius;
            return this;
        }

        public int getStarCenterX() {
            return starCenterX;
        }

        public int getStarCenterY() {
            return starCenterY;
        }

        public SweepGradient getShader() {
            return shader;
        }

        public RectF getRect() {
            return rect;
        }

        public FallingStar build(){
            //Calculate the center coordinates of the meteor head
            double endAngle = 2 *Math.PI*(getStartAngle()+getSweepAngle()) / 360 ;
            starCenterX = (int)(radius*Math.cos(endAngle)) + width/2;
            starCenterY = (int)(radius*Math.sin(endAngle)) + height/2;
            shader = new SweepGradient(width/2, height/2, SWEEP_COLORS, POSITIONS);
            rect = new RectF(width/2 - getRadius(), height/2 - getRadius(), width/2 + getRadius(), height/2 + getRadius());
            rise = true;
            velocity = mRandomInt.nextInt(2) + 2;
            return this;
        }

        /**
         * Adjust the meteor starting point angle and scanning angle. The algorithm of starting point angle is counterclockwise uniform adjustment. The algorithm of scanning angle is to reduce the uniform speed above the threshold to 0, and increase the uniform speed below the threshold to the threshold
         */
        public void changeAngle(){
            startAngle +=velocity;
            startAngle %= 360;
            if(rise){
                sweepAngle ++;
                if(sweepAngle > MAX_SWEEP_ANGLE){
                    rise = false;
                    sweepAngle = MAX_SWEEP_ANGLE;
                }
            }else{
                sweepAngle --;
                if(sweepAngle <= 0){
                    rise = true;
                    sweepAngle = 0;
                    mFallingStarList.remove(this);
                }
            }
            //Adjust the position of the round head accordingly
            double endAngle = 2 *Math.PI*(getStartAngle()+getSweepAngle()) / 360 ;
            starCenterX = (int)(getRadius()*Math.cos(endAngle)) + width/2;
            starCenterY = (int)(getRadius()*Math.sin(endAngle)) + height/2;
        }
    }

2. Then get familiar with the drawing of a single meteor, mainly drawing a circle and an arc

    /**
    * Drawing a single meteor includes drawing the circle of the star's head and drawing the gradient line of the meteor track
    * @param canvas
    * @param fallingStar
    */
   private void drawFallingStar(Canvas canvas, FallingStar fallingStar){
       //Draw a meteor head circle
       canvas.drawCircle(fallingStar.getStarCenterX(), fallingStar.getStarCenterY(), 2 , starPaint);
       //Draw the track line arc and set the arc as a gradient
       fallingLinePaint.setShader(fallingStar.getShader());
       canvas.drawArc(fallingStar.getRect(), fallingStar.getStartAngle(), fallingStar.getSweepAngle(), false, fallingLinePaint);
   }

3. The process of drawing meteor groups and supplementing meteors

    /**
    * Draw meteor group
    */
   private void drawFallingStarGroup(Canvas canvas){
       //When the remaining meteors are less than 50% of the original total, add meteors
       if(mFallingStarList.size() <= 0){
           initFallingStarAngle();
       }else if(mFallingStarList.size() <= (FALLING_STAR_GROUP_SIZE/2)){
           addFallingStar();
       }
       //Draw meteor group
       for(int i = 0; i < mFallingStarList.size(); i++){
           drawFallingStar(canvas, mFallingStarList.get(i));
       }
   }

   /**
    * Adjust the starting point angle and sweep angle of the meteor group. The algorithm of starting point angle is counterclockwise uniform adjustment, the algorithm of sweep angle is to reduce the uniform speed above the threshold to 0, and increase the uniform speed below the threshold to the threshold
    */
   private void changeFallingStarAngle(){
       for(int i = 0; i < mFallingStarList.size(); i++){
           mFallingStarList.get(i).changeAngle();
       }
       //Adjust the rotation angle of the center picture and set it to rotate counterclockwise
       mRotateAngle -= 2;
       mRotateAngle = (mRotateAngle + 360) % 360;
       invalidate();
   }

Detailed code of supplementary meteor

    /**
    * When the number of meteors decreases and is in exhaustion, the number of meteors shall be supplemented to ensure the overall observability
    */
   private void addFallingStar(){
       int additionSize = FALLING_STAR_GROUP_SIZE - mFallingStarList.size();
       int starPathNo = 1;
       int beginRandomAngle = mRandomInt.nextInt(360);
       //Calculate the total layers of the original orbit, and allocate the meteors to be supplemented to the original orbits of each level
       int starPathCount = FALLING_STAR_GROUP_SIZE / (360 / MAX_SWEEP_ANGLE);
       if((FALLING_STAR_GROUP_SIZE % (360 / MAX_SWEEP_ANGLE)) > 0){
           starPathCount++;
       }
       //Number of meteors to be allocated in each layer
       int starCountPerPath = additionSize / starPathCount;
       for(int i = 0; i < additionSize; i++){
           starPathNo = i / starCountPerPath + 1;
           FallingStar star = new FallingStar().setRadius(mInsideImageRadius + starPathNo * 10)
                   .setStartAngle(i % (360 / MAX_SWEEP_ANGLE) * MAX_SWEEP_ANGLE + (starPathNo - 1) * 360 / PATH_COUNT_MAX + beginRandomAngle).setSweepAngle(mRandomInt.nextInt(MAX_SWEEP_ANGLE/6) + 5).build();
           mFallingStarList.add(star);
       }
   }

4. In the process of clipping to obtain a circular picture, first you need to appropriately scale the original picture, then create a drawing to prepare the output picture, draw a circle to intersect with the scaled picture, and take the intersecting background to obtain a circular picture

    private Bitmap createRoundBitmap(Bitmap inBitmap){
       Bitmap tempBitmap;
       //Determine whether scaling is required
       if(inBitmap.getWidth() == (2 * mInsideImageRadius) && inBitmap.getHeight() == inBitmap.getWidth()){
           tempBitmap = inBitmap;
       }else {
           tempBitmap = Bitmap.createScaledBitmap(inBitmap, 2*mInsideImageRadius, 2*mInsideImageRadius, false);
       }
       //Create a canvas for the picture to be output
       Bitmap result = Bitmap.createBitmap(tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888);
       Canvas canvas = new Canvas(result);
       //Set canvas transparency
       canvas.drawColor(0x00FFFFFF);
       Paint paint = new Paint();
       paint.setAntiAlias(true);
       //Draw the circle to clip
       canvas.drawCircle(tempBitmap.getWidth()/2, tempBitmap.getHeight()/2, mInsideImageRadius, paint);
       //Set intersection mode
       paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
       Rect rect = new Rect(0, 0, tempBitmap.getWidth(), tempBitmap.getHeight());
       canvas.drawBitmap(tempBitmap, rect, rect, paint);
       canvas.setBitmap(null);
       tempBitmap.recycle();
       return result;
   }

5. Set rotation and initialize meteor group for circular pictures

    @Override
   protected void onDraw(Canvas canvas) {
       //Set canvas transparency
       canvas.drawARGB(0,0,0,0);
       //Draw a circular picture in the middle
       Drawable drawable = getDrawable();
       if(null == drawable){
           return;
       }
       //Crop the original image of ImageView into a circular image
       Bitmap bitmap = ((BitmapDrawable)drawable).getBitmap();
       Bitmap roundBitmap = createRoundBitmap(bitmap);
       //Set circular Bitmap rotation through Matrix
       mMatrix.reset();
       mMatrix.setRotate(mRotateAngle);
       //Get rotated Bitmap
       Bitmap rotateBitmap = Bitmap.createBitmap(roundBitmap, 0, 0, 2*mInsideImageRadius, 2*mInsideImageRadius, mMatrix, false);
       //Draw the rotated Bitmap on the canvas. Note that the size of the rotated Bitmap based on the Matrix is not equal to the original image, so the converted Bitmap should be used to calculate the center position
       canvas.drawBitmap(rotateBitmap, width / 2 - rotateBitmap.getWidth()/2 , height / 2 - rotateBitmap.getHeight()/2, null);
       //Draw meteors
       drawFallingStarGroup(canvas);
       //Update meteor position after 33ms
       postDelayed(new Runnable() {
           @Override
           public void run() {
               changeFallingStarAngle();
           }
       }, 33);
       //Bitmap during recycling
       roundBitmap.recycle();
   }

6. Initialize meteor group
The main goal of this step is to make the meteor group relatively evenly distributed in the orbit, with a constant spacing, so as to avoid confusion

    @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       width = getMeasuredWidth();
       height = getMeasuredHeight();
       //Calculate the outermost orbit radius of meteor
       mOutSideStarRadius = Math.min(width, height) / 2 * 9 / 10;
       //Calculate the radius of the original drawing of the center
       mInsideImageRadius = mOutSideStarRadius * 2 / 3;
       //Calculate the maximum number of meteor showers that can be set
       PATH_COUNT_MAX = (mOutSideStarRadius - mInsideImageRadius) / PATH_INTERVAL_FALLING_STAR + 1;
       FALLING_STAR_GROUP_SIZE = PATH_COUNT_MAX * 360 / MAX_SWEEP_ANGLE;
       //Initialize meteor group
       initFallingStarAngle();
   }
    /**
    * Initialize the angle and radius parameters of the meteor group
    */
   private void initFallingStarAngle(){
       mFallingStarList.clear();
       int starPathNo = 1;
       int beginRandomAngle = mRandomInt.nextInt(360);
       for(int i = 0; i < FALLING_STAR_GROUP_SIZE; i++){
           starPathNo = i / (360 / MAX_SWEEP_ANGLE) + 1;
           FallingStar star = new FallingStar().setRadius(mInsideImageRadius + starPathNo * PATH_INTERVAL_FALLING_STAR)
                   .setStartAngle(i % (360 / MAX_SWEEP_ANGLE) * MAX_SWEEP_ANGLE + (starPathNo - 1) * 360 / PATH_COUNT_MAX + beginRandomAngle).setSweepAngle(mRandomInt.nextInt(MAX_SWEEP_ANGLE/6) + 5).build();
           mFallingStarList.add(star);
       }
   }

Learning experience

The above is the preliminary dynamic effect process of meteor shower, and the meteor shower rotates around the cover of the circular album. There is still some room for improvement in the process effect, which will be further improved in the future. If you need source code, please provide email message

Keywords: Android

Added by shefali on Mon, 04 Oct 2021 04:00:26 +0300