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 number | parameter | Parameter requirements |
---|---|---|
1 | Arc end angle | The end angle of the arc = start angle + sweep angle. Its value should change clockwise to show the rotation effect |
2 | Arc sweep angle | In 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 |
3 | Arc color | The 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