Take you to achieve girlfriend can not stop netease cloud music cosmic dust special effects
This is the last article in the custom View series. The last article was on custom Views, and this article is on custom viewGroups.
preface
This is the Github address for this special effects: github.com/MlxChange/W…
I’ll admit I bragged. The headline was nearly a thousand stars (though it was only 700) and it became Trending (# 3 Kotlin at the time). But I still have an aspirant heart, boast is not guilty!
Everyone and first put down the hands of 40m broadsword, let me run 39 meters, see what I can give you the whole flower work and then decide whether to cut me.
In the same way, I know you can’t fool people without pictures, so let’s put pictures first and see if the result is really that cool.
How is it still calculated to have so a drop drop cool feeling?
This effect is a custom ViewGroup, I believe there are many friends usually write custom View or more, but the custom ViewGroup is less. Mainly ah, custom ViewGroup to consider too many things, and what measurement, and what layout, sometimes also have to consider sliding conflict, really is much more trouble.
However, this article will take you through a bit of analysis of the effects, to implement the effects, and then to deal with the hassle, so that you can easily learn to customize viewgroups. Mom doesn’t have to worry about me not being able to customize viewgroups anymore.
In addition, you can also look at my last article, the custom View master is not skilled enough to consider my way of implementation, is a little bit of implementation, and then slowly modify, increase the effect.
No more talking. Put the code in.
The special effects analysis
Since it is a custom ViewGroup, the effect is quite complicated. When I first saw the original renderings, I refused to say that your UI design was so cool. I did not consider the hard work of our programmers and resolutely did not do this kind of thing.
So I got into a fight with UI, and whoever wins is up to him. Now that I see the result, I just want to say it smells good.
Okay, I’m done with the skin. Let’s take a closer look at the effect.
First of all, this effect can see a lot of pages, just like a list, remember I saw an effect before is to explore that effect, left like, right don’t like and similar to this. So I thought I could start with a custom LayoutManager or a custom View inherited from RecyclerView, but I gave it up because it wasn’t easy to preview to the next interface, so I decided to create a custom ViewGroup.
In my opinion, the most special effect of this effect is that you can see the content of the next interface by dragging and dropping, just like pulling a curtain. After moving across the center, you can automatically slide to the other side, and have a rebound effect, and then display the next page. When the next page is fully displayed, the drag button is automatically generated to preview the next page again. The button can also be pressed back, and the opposite direction will generate a new button, showing the previous page.
To sum up, I summarize the following points:
- Is a custom
ViewGroup
- The child View is displayed as a stack, like a list. You can customize the contents of the child View and preview the contents of the next or previous page.
- There is a drag button, which gets bigger as it is pulled out and smaller as it is retracted, and it is a very smooth effect
- It bounces a few more times as it slides completely to the other side
In fact, I think the other ones are easier to implement, but the drag button is a bit more troublesome, so I decided to start with the drag button.
Drag the button
Although talking about custom ViewGroup, but do custom View can not start dry, I first write a custom View, first see if the drag button can be implemented, if this can not be implemented, then the back to find an excuse to dump the pan to UI is not need to write it.
Let me see how this drag-and-drop button works.
Start with the button on the left, because the origin of the screen is in the upper left corner.
First of all, it’s a little bit to the left, so it’s a line from top to bottom, but it has a little bit of a protruding point in the middle. I don’t care about the bump, but let me draw this line. Because it’s a test, I’m creating a TestView
class TestView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {}
Copy the code
And then you draw lines. So let’s think about how to draw this line. It does look like a line in the rendering, but it also has content to the left, so I think it’s more like a long, thin rectangle with a traitor sticking out of it. So drawing a line becomes drawing a rectangle, and due to the existence of the traitor, the simple canvas drawing rectangle obviously cannot add the traitor to the subsequent rectangle, so we have to draw the rectangle in another way, then what is it?
That’s right, Path, Path can do the same thing. So start drying.
Draw a rectangular
Define a brush and Path
var path= Path()
var paint =Paint()
init {
paint.color=Color.RED // For easy identification, we define red
paint.style=Paint.Style.STROKE
paint.isAntiAlias=true
}
Copy the code
How do I draw a rectangle? It’s exactly the same as what we did on the paper, you go from the original point and you draw a line to the right, and then you draw a line down, and then you draw a line to the left, and then you close with the origin. So a rectangle comes out. The soul painter is me.
In fact, Path is very good for us to implement, and for convenience, we still define a center point centerX and centerY
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
path.lineTo(100f.0f)//100 is just a test distance
path.lineTo(100f,centerY*2)// Draw step 2
path.lineTo(0f,centerY*2)// Draw step 3
path.close()// Close, i.e. draw step 4
canvas.drawPath(path,paint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX= (w/2).toFloat()// Define the screen center X value
centerY= (h/2).toFloat()// Define the screen center Y value
}
Copy the code
So let’s see what happens
Emmm looks interesting, but what the hell is the status bar? Forget about it. It’s just a transparent status bar.
Painting processes
So now let’s think about how do we draw this projection?
Actually, the bumps, when you draw them on paper, look like this
You see if we were to draw it on a piece of paper it would be very easy, just a bunch of bumps on the right side of the line. Three more steps, let’s see what the code does
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
path.lineTo(100f.0f)// Step 1
path.lineTo(100f,centerY-200)// Step 2
path.lineTo(180f,centerY)/ / the third step
path.lineTo(100f,centerY+200)/ / step 4
path.lineTo(100f,centerY*2)/ / step 5
path.lineTo(0f,centerY*2)/ / step 6
path.close()/ / step 7
canvas.drawPath(path,paint)
}
Copy the code
What is the effect?
It looks a little bit interesting, but the effect looks very smooth, you are pointy, a little bit different.
Dude, what you say makes sense, but I can’t draw a round one. Do how? Only the pointy can make a living like this, inside the elder brother all is talented and sound, ah wait no, wrong set.
What is a circle? It’s a curve, right? How do we draw curves in a computer? Let’s see if anyone knows. I’ll wait ten minutes
Ah, ten minutes later, no one knows, so I said the answer
That’s right, bezier curves!
What, you’ve never heard of bezier curves? Dude, you’re out.
A Bessel curve is an algorithm for simulating curves in a computer, by balabalabla, omits a bunch of definitions…..
In short, bezier curves help us draw curves, and determine curves by position points and control points. For those of you who don’t, I suggest you learn the basics of bezier curves.
Let’s look at the second order Bezier curve
/** * From the last point, draw the second Bezier curve * (x1,y1) as the control point, (x2,y2) as the end point */
public void quadTo(float x1, float y1, float x2, float y2) ;
Copy the code
So, using bezier curves, how do you draw them? Let’s keep trying to draw it on paper
The Bezier curve corresponds to step three. The starting point would be point A, the control point would be point B, and the end point would be point C.
What are the coordinates of this point? Let’s assume that step 1 has a length of 100, the X offset from POINT A to point B is 100, and if the Y coordinate of point B is the center coordinate, the coordinate of point B is (100+100,centerY). And then the distance from A to C we’re going to set at 200, which should make sense
Let’s do it in code
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
path.lineTo(100f.0f)/ / step 1
path.lineTo(100f,centerY-100) step2
path.quadTo(200f,centerY,100f,centerY+100)/ / step 3
path.lineTo(100f,centerY*2)/ / step 4
path.lineTo(0f,centerY*2)/ / step 5
path.close()/ / step 6
canvas.drawPath(path,paint)
}
Copy the code
What is the effect? Let’s take a look at
The EMMM doesn’t look so hot anymore, but the bump is a bit of an obtrusion. I don’t know, it looks like they stuffed money into the show.
You say so, I compare with the effect drawing, I feel I make it like this, the boss is not afraid to immediately fire me. But how did the slick effects come about?
In fact, it is very simple, the second order Bezier curve does not work, so let its brother bezier curve of the third order, the third order Bezier curve does not work, the fourth and fifth order…
The end result is a sixth-order Bezier curve, as shown below
P1 and P8 are position points, and the rest are control points. The red line is the resulting curve, doesn’t it look smooth? Is the solution simple?
Oh, don’t drop the machete! I’m sorry! It’s not easy at all. I was thinking, is thinking for a long time to come out.
Order six is hard, but we can break it down. Wouldn’t it be OK to decompose it into two third-order Bezier curves?
We start at P1, we end at P4, and P2 and P3 are the position points, so it’s easy to draw half and half
The top half looks like this:
So let’s look at the method of third-order Bezier curves
/* * x1,y1 is the position of the first control point, corresponding to P2 * x2,y2 is the position of the second control point, corresponding to P3 * x3,y3 is the position of the last position point, corresponding to p4 */
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
Copy the code
So how do they determine their coordinates?
Let’s try drawing more on paper
P1,P4 are position points, P2,P3 are control points. So how do we determine the distance between P2,P3, and P4?
There are a lot of online sites that generate Bezier curves, where you generate four points, you’re pretty close, and you look at their coordinates and you get a rough idea of what they are.
I have tried it before. If P1 is (0,0), the coordinates of several points are as follows:
P2 (0,100), p3(90,75), p4(100,150). Since the y-coordinate of P4 has been determined to be centerY, P1 can only be (100,centerY-150). The code is as follows:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
path.lineTo(100f.0f)/ / the first step
path.lineTo(100f,centerY-150)/ / the second step
path.cubicTo(100f,centerY-150f+100.100f+90f,centerY-150+75f.100f+100f,centerY)/ / the third step
path.lineTo(100f,centerY*2)/ / step 5
path.lineTo(0f,centerY*2)/ / step 6
path.close()/ / step 7
canvas.drawPath(path,paint)
}
Copy the code
Since we only spent the top half, the fourth step in the figure is not drawn. We just want to see if the top half is what we want
It looks like the first half of the effect is a lot smoother, so I’ll follow suit and finish the second half. So for the second half, you start with P4
P1 is the position of p4 of the upper half, and the coordinates of P2, P3, and P4 are easy to determine by comparing the positions of the upper half
Here’s the code:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
path.lineTo(100f.0f)/ / the first step
path.lineTo(100f,centerY-150)/ / the second step
path.cubicTo(100f,centerY-150f+100.100f+90f,centerY-150+75f.100f+100f,centerY)/ / the third step
path.cubicTo(100f+90f,centerY+90f.100f,centerY+75f.100f,centerY+150f)/ / step 4
path.lineTo(100f,centerY*2)/ / step 5
path.lineTo(0f,centerY*2)/ / step 6
path.close()/ / step 7
canvas.drawPath(path,paint)
}
Copy the code
No proof, to see the effect is the absolute truth
What do you think? It’s nice and smooth. It’s not sharp anymore. Mlx, YES!
Now that this effect has been implemented, let’s move on to the next step, animation!
Drag and drop the animation
First of all, by analyzing the animation above, we can see that the small bump will follow the finger up and down, and when the finger slides to the right, the bump will become larger. So let’s ignore the left and right situation and consider the up and down situation first.
Up and down the animation
In the bump that we draw above, the highest point of the bump we set the Y value to be centerY. If the bulge moves, the relative positions of these points should not change. The only thing that changes is the top of the bulge.
So the question is, what should happen to the top of the bulge?
That’s right, follow your finger up and down. Now that’s the case. The highest point of the protuberance is the point where the upper and lower parts of the protuberance are located. The Y value is centerY and should now follow the finger while the other relative distances remain the same.
So again, if you look at this picture, if you change the Y value of P4, you don’t change the X value of P1, P2, P3, and you start with P4. So you can make the bump just change up and down.
In other words,
The only variable here is Y, and since Y follows the finger it’s the value of Y where the finger presses. How do I record it?
Hey, hey, hey, that’s the onTouchEvent method
We’ll start by defining a variable currentY to record the current finger position, which defaults to the center of the screen if there is no touch
var currentY=0f// Record the current finger touch position
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
centerX= (w/2).toFloat()
centerY= (h/2).toFloat()
currentY=centerY // The default is screen center
}
Copy the code
Now we need to record the position of the touch
override fun onTouchEvent(event: MotionEvent): Boolean {
when(event.action){
MotionEvent.ACTION_MOVE-> {
currentY=event.y
invalidate()// Redraw the interface}}return true
}
Copy the code
That’s not enough. We haven’t changed the drawing place and haven’t adopted P4 as the standard. Let’s go change it now. It was centerY. Now it’s a new guy, currentY, I think
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
path.lineTo(100f.0f)
path.lineTo(100f,currentY-150)
// Draw the top half
path.cubicTo(100f,currentY-150f+100.100f+90f,currentY-150+75f.100f+100f,currentY)
// Draw the bottom half
path.cubicTo(100f+90f,currentY+90f.100f,currentY+75f.100f,currentY+150f)
path.lineTo(100f,centerY*2)
path.lineTo(0f,centerY*2)
path.close()
canvas.drawPath(path,paint)
}
Copy the code
Aha, let’s have a look at the effect
????
Dude, I’m not kidding. We had a little slip up. You see, path is a path, right? We keep adding paths, but the previous paths are still there, which means that the paths are superimposed. So we need to delete the original path every time we draw the path.
Something like this:
override fun onDraw(canvas: Canvas){... canvas.drawPath(path,paint)/ / path
path.reset()// Delete the previous path after drawing
}
Copy the code
So, let’s see, right?
Is that all right? It’s perfect! I didn’t lie to you
About the animation
Now that we’re done with the up and down, is it time to slide left and right?
Let’s analyze the effect again
When we look at the renderings again, we see
The bump gets bigger as it drags outward, but when it reaches the center of the screen, it’s the largest and doesn’t get any bigger.
What do you mean, a bulge? I suspect you’re driving…
It gets bigger, and it makes a lot of sense to draw it, but let’s keep drawing it on paper
It starts out like this, and then as it gets bigger, it looks like this
Let’s look at what has changed?
And you can obviously see that the distance from P1 to P4, let’s call it the distance from P1 to P4 in the Y direction the radius of this bump. Because the distance from P1 to P4 is half of the way in the Y direction
So it’s pretty clear that this radius is getting bigger, and P1 to P4 is getting bigger in the X direction,
Let me draw another picture
You can see that there is a blue rectangle, and simply speaking, the bump is getting bigger, which means that the blue rectangle is getting bigger and bigger, and the length of the rectangle is the length of the bump, and the width of the rectangle is the width of the bump. Of course, I didn’t draw it very well here, after all, the soul artist.
So the coordinates of P1,P2,P3, and P4 can be represented by this rectangle. We can define a length dragHeight and a width dragWidth for this rectangle, length in the Y direction, width in the X direction.
Something like this:
The width of the rectangle above is defined as dragWidth and the length of the rectangle is defined as dragHeight. After many tests and the best results are obtained, the coordinate relationship is as follows. And we already know that the Y coordinate of P4 is the finger touch coordinate currentY. Then the final coordinates are as follows:
Moreover,
See, the dragHeight is determined by the dragWidth. The coordinates of all four points are determined by dragHeight.
So how do I determine dragWidth? The dragWidth is determined by how far the finger drags left and right. However, for good user experience, do not let the finger block the drag button, so the highest point of the protrusion should be a little left of the finger press. CurrentX is defined as the X value of the finger touch point, then the following formula is used:
Some friends may ask, you are all what what what, up the whole pile of formulas, but also let people happy to play.
In fact, I also do not have a way, these parameters ah is really I adjusted you can also directly take to use, if you feel bad, you can also manually modify the following parameters
So now that we have these parameters, let’s modify the previous code and first define the length and width of the rectangle
var dragWidth=0
var dragHeight=0
Copy the code
Then modify the coordinates of the control points and data points before, but it is not very troublesome to directly fill in the data here, and do not know which point is which point. So, according to our diagram, seven points are defined, corresponding to point 1 to point 8 in the following figure
As usual we can not define in Ondraw oh ~ in order to facilitate you can compare the picture to understand, I directly named the point in the picture, you see my red point, that is the real point. And please don’t make fun of my name. Notice that our curve is made up of two third-order Bezier curves,
// Top half
private val point1=PointF(0f.0f)
private val point2=PointF(0f.0f)
private val point3=PointF(0f.0f)
/ / most the right side
private val point4=PointF(0f.0f)
// Lower half
private val point5=PointF(0f.0f)
private val point6=PointF(0f.0f)
private val point7=PointF(0f.0f)
Copy the code
So let’s modify the drawing code:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Set the coordinates of the corresponding seven points
point1.x=100f
point1.y=currentY-dragHeight
point2.x=100f
point2.y=(currentY+point1.y)/2 + 30
point3.x=(100f+dragWidth)*0.94 f
point3.y=(point1.y+currentY)/2
point4.x=100f+dragWidth
point4.y=currentY
point7.x=100f
point7.y=currentY+dragHeight
point5.x=(100f+dragWidth)*0.94 f
point5.y=(currentY+point7.y)/2
point6.x=100f
point6.y=currentY+dragHeight/2 - 30
/ / the first step
path.lineTo(100f.0f)
/ / the second step
path.lineTo(point1.x,point1.y)
// Step 3: Draw the top half
path.cubicTo(point2.x,point2.y,point3.x,point3.y,point4.x,point4.y)
// Step 4, draw the bottom half
path.cubicTo(point5.x,point5.y,point6.x,point6.y,point7.x,point7.y)
/ / step 5
path.lineTo(100f,centerY*2)
/ / step 6
path.lineTo(0f,centerY*2)
/ / step 7
path.close()
canvas.drawPath(path,paint)
path.reset()
}
Copy the code
Ok, perfect. Let’s run it and see what happens
You’ll find nothing. Ha ha ha, everything is I cheated you, stupid?
Just a little fun to liven things up, because everyone’s been watching this for so long, and they’re probably tired, so why don’t I give you something to eat?
Well, it doesn’t have any effect, because all of our formulas are based on the length and width of this rectangle, and notice that rectangle refers to the half raised rectangle, which is the rectangle in the formula.
The length and width of our rectangle are both 0, so of course it doesn’t work. Therefore, we need to set an initial value for the length and width of the rectangle. We know that the length of a rectangle is 1.5 times the width, so we only need to set an initial value for the width. The initial value is set to 100 for the moment, i.e
var dragWidth=100f
var dragHeight=0f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
dragHeight = dragWidth*1.5 f. }Copy the code
That’s not the same thing, is it? Have much use
Now the size of the bump is determined by the length and width of the rectangle, and the length of the rectangle is determined by the width of the rectangle. In other words, the width of the rectangle determines the size of the bump. So let’s recall that if the right slide gets bigger, it’s essentially the width of the rectangle gets bigger. We talked about that above.
So just change the width of the rectangle in the touch event, and the bump will change. Let’s try that
override fun onTouchEvent(event: MotionEvent): Boolean {
when(event.action){
MotionEvent.ACTION_MOVE-> {
currentY=event.y
// Update the rectangle width
dragWidth=event.x-30
invalidate()
}
}
return true
}
Copy the code
See how it works:
Gee, this is interesting. It’s pretty much the same, but we still need two points.
- The most you can do is slide to the center of the screen
- If you do not slide to the center of the screen, it automatically returns to the starting position.
We’ll take it one at a time
Limited slip distance
If the X value of the current touch event exceeds centerX, it remains centerX
override fun onTouchEvent(event: MotionEvent): Boolean {
when(event.action){
MotionEvent.ACTION_MOVE-> {
currentY=event.y
// Determine if the X value of the current touch event exceeds the center of the screen
dragWidth = if(event.x>centerX){
centerX-30
}else{
event.x-30
}
invalidate()
}
}
return true
}
Copy the code
It’s easy to change, so I’m not going to put the effect here. I just can’t slide through the center of the screen anymore.
Let go and return to the starting position
When we don’t get to the center of the screen, if we let go, we go back to the starting position. Why not set the width of the rectangle to the initial value of 100?
That’s right. Let’s judge and let go. Where does letting go judge, surely in the way of touching events
override fun onTouchEvent(event: MotionEvent): Boolean {
when(event.action){
...
// Take your finger off the screen
MotionEvent.ACTION_UP->{
// If the rectangle width is smaller than the center of the screen
if(dragWidth < centerX){
dragWidth=100f // return to the initial position
invalidate() // Update the interface}}}return true
}
Copy the code
Isn’t that easy? Easy as pie
Emmm is back to its original position, but the other one is going back a little bit. Your sudden return has caught me off guard. Without a bit of defense ~~~
How do you go back a little bit?
Ah, our old friend property animation pops up again. That’s right, relying on our old friend property animation. We are all familiar with property animation. Let’s first customize a property animation and then let it slowly return to its original position according to time. One thing to note here is that the original effect also has a backward-looking effect, which is essentially an OvershootInterpolator. Custom estimators and interpolators I won’t cover these two customizations due to space and effectiveness, and they are very simple.
Let’s start by defining such a rebound animation
private val dragReboundAnimator = ValueAnimator.ofFloat(0f.1f)
private var reboundLength = 0f // How far does it need to bounce back
private var dragReboundX = 0f // The X value of the initial rebound location
init{... dragReboundAnimator.doOnStart { reboundLength = dragWidth -100
dragReboundX = dragWidth
}
dragReboundAnimator.duration = 700
dragReboundAnimator.interpolator = OvershootInterpolator(3f)
dragReboundAnimator.addUpdateListener {
dragWidth = dragReboundX - it.animatedValue as Float * reboundLength
invalidate()
}
}
Copy the code
First, let me explain what these variables mean
dragReboundAnimator
Define a property animation from 0 to 1, such as 0, 0.1, 0.2, all the way to 1.reboundLength
It’s the distance that I need to bounce back, so let’s say I have my dragWidth which is the width of the rectangle is 400, and my initial position is 100, so the distance that I need to bounce back is 300, so I need to bounce back a little bit and if it’s 600, I need to bounce back a little bit 500.dragReboundX
When you first let go, I need to know where you started bouncing back.
The whole process is like this: when it needs to rebound, I first record the initial position of rebound and the distance to rebound, and then update the initial position of rebound minus the distance to rebound * animation value. What does that mean? The initial distance is 600, the distance to bounce back is 600-100=500, and the initial animation value is 0. Which is the following
So that’s a little bit of rebound. And we have applied the OvershootInterpolator(3f) interpolator, which allows you to overshoot and then interpolate back to the correct position.
So let’s see what happens
Well! The effect is already very good! It looks pretty much like the original. In the optimization of some details I believe it must be no problem. I won’t optimize it for the time being in the next article.
summary
Yangzasa also wrote more than 7000 words, but the custom ViewGroup is too much, need to say a lot of content. So I’ve tried to be as concise as possible, and it’s not really practical to finish an article. And, I believe that friends see here estimated also tired, need a rest.
So LET me make a quick summary here
Custom View, do need a little bit of analysis effect, first do a similar, and then slowly modify, slowly optimize. This kind of thing is very important especially for beginners. So, we can slowly experience according to my train of thought.
So the final effect is ViewGroup, but most of this is a custom View, this is because the ultimate goal of a custom ViewGroup can still have the effect of a custom View, if the effect can not do everything is empty talk ~
In the next video, I’m going to show you how to flip it to the other side, and how to customize the ViewGroup so if you like, you can like the nuggets, follow me and give me something more, okay
Welcome to follow me, a little kitten who likes to customize View and NDK and study source code