Deep anatomy of UIScrollView and damped animation
Abstract
UIScrollView is an essential and most used basic component in iOS development; Commonly used Feed streams, Pager, carousel graphs and so on are closely related to UIScrollView. In daily development, we are often limited to the few necessary call interfaces and proxies, without exploring the subtle design behind those simple interfaces, such as: how can a scroll view display infinite content in a limited area? What happens every time you touch the screen in a scrolling area? How does it look physically in the real world? This article starts with basic parameter observations and uses some basic methods and concepts in mathematics, physics and optimization methods as tools to explore the laws behind the smooth interaction of UIScrollView and appreciate the subtle design made by Apple engineers when implementing a basic component.
Partial display principle of UIScrollView
To illustrate this point, here’s an excerpt from Apple’s official documentation:
*Documents: UIScrollView is the superclass of several UIKit classes, including UITableView and UITextView
An scroll view is A view with an origin that’s adjustable over the content view. A scroll view tracks the movements of fingers, and adjusts the origin accordingly[1]. The view that shows its content through the scroll view draws that portion of itself according to the new origin, which is pinned to an offset[2] in the content view. By default, it bounces[3] back when scrolling exceeds the bounds of the content. *
The gist of the passage is as follows:
-
UIScrollView tracks finger movements and appropriately adjusts the origin of the content displayed (the coordinates of the top left corner of a rectangle).
-
Display different contents according to the origin coordinates of the adjusted rectangle.
-
Default bounces on contact with boundaries.
-
Also, UIScrollView is the parent of UITableView, UITextView (and UICollectionView).
From the perspective of UIScrollView’s parent UIView, UIView’s property: bounds.origin(x,y) marks the reference origin that all children of a UIView depend on. All child views added to the UIView are drawn with reference to this origin, which means: If the origin is labeled {-40, -40.f}, then all the children of the view will be drawn at the point (-40, -40). For example, in this case, a subview with frame = {20.f,20.f,100.f,100.f} would be drawn from the point (-20.f, -20.f). The source of -20 is the origin of the subview (20,20) plus the offset (-40,-40), so, In this case, you can only see a small part of the child element that is 20*20 in the lower right corner. The rest of the child element is invisible.
In UIScrollView, to distinguish this feature from regular UIView, bounds.origin is called separately: ContentOffset, both of which contain two two-dimensional vectors (CGpoints) with values X and y, allows scrolling effects to be achieved by constantly changing the contentOffset according to user gestures and animation patterns.
Tuning contentOffset is not difficult, but there are many things to consider to make scrolling smooth; It’s a sign of how well Apple has provided the basic components that allow us to operate UIScrollView without knowing any details, while encapsulating most of the complex logic in the understatement “Accordingly”.
UIScrollView interaction details
We have compiled all the things to consider before setContentOffset:
-
UIScrollView usually moves in the same position and direction when panGesture is in effect within the container’s defined scope (contentSize), and the panGesture displacement distance is 1:1 between contentOffset and panGesture.
-
If the Gesture is still in effect after panGesture, then it continues to slow down for a while.
-
After the end of panGesture, if the current contentOffset exceeds contentSize, there will be a period of need to rebound and restore to the boundary.
-
(2) and (3) combined, after the end of panGesture, there is still speed in effect, deceleration in the following period of time, deceleration did not end of the case touched the boundary, began to rebound, and rebound to the non-stretching state.
-
Because orthogonal vectors do not affect each other, contentOffset needs to operate independently in both x and Y directions.
-
As panGestures are made beyond the boundary, the effective ratio of panGesture conversion to contentOffset distance decreases, presenting a pull-to-limit effect.
(if we setContentOffset only while panGesture is in effect, the effect is no different from drawing a trackable View on the screen, which is similar to dragging a folder around on your normal Windows desktop).
It is these complex interactive features that make UIScrollView a great interactive experience in our hands.
Activity activity
Numerical observations
Animation. Let’s start with the easy activity animation. To see how it works, you’ll need to create a regular UIScrollView. Print statistics in the following agents:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
NSLog(@"DecelerateVelocity:%lf", velocity.y);
NSLog(@"DecelerateDistance:%lf", targetContentOffset->y - scrollView.contentOffset.y);
}
Copy the code
-
Activity atevelocity records the instantaneous speed at which the panGesture gesture ends, the activity’s starting speed.
-
Activity AteDistance tracks the total distance traveled by natural deceleration between the end of panGesture and the end of the activity
We captured the data captured at the end of several panGesture gestures. In order not to be affected by Bounces and boundaries, we should try to ensure that the boundaries are not touched during these decelerations. The data is as follows:
The serial number | Initial deceleration speed | The distance traveled until it stops |
---|---|---|
1 | 5.0270956 | 2506.5 |
2 | 1.802126 | 895.0 |
3 | 1.412374 | 700.5 |
4 | 1.687861 | 838.0 |
- It’s not hard to see the connection: regardless of 1000 times, the initial speed of deceleration is always twice the total distance it has traveled by inertia.
About this 1000 times: We can see from this agent’s comment:
Called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
Millisecond (points/millisecond) the speed is millisecond (points/second); Therefore, by default, we multiplied the speed by 1000 to facilitate later calculation. Meanwhile, we regarded the width/height of 1point as a unit and the length was 1 meter, so that the unit of speed was unified as the standard unit of speed: m/s.
Results analysis
Based on the above observation, we try to find a common function whose derivative (v) with respect to time (t) seems to be twice its own (y).
So obviously the function and its derivatives are:
These two functions are exponential acceleration, and Decelerate, so you need to add a negative sign to ensure that the deceleration activity will always decrease over time.
(Careful students can find that in addition to increasing the negative sign of the exponential, C2 constant is also added with a negative sign. This is because we directly use the initial velocity v0 in the above observation value of velocity, while the final shift is used to subtract the initial displacement. The correct thing to do is to use the change in velocity, which is zero minus v0, because the final state is a stop state; So the real conclusion should be that the change in velocity should always be -2 times the change in displacement, but the positive and negative relationship has not been emphasized for numerical convenience.)
So according to the known conditions: when the deceleration starts t=0, v=v0, therefore, the constant coefficient C2 on the right can be fixed as v0; C1 is a constant that depends on where we started, so we don’t care.
Then the velocity attenuation formula we finally get is:
Relevant interpretation
Here is the normal derivation (to explain the abrupt exponential function) :
Deceleration effects at different speeds v1 and V2 start to meet the same motion law, let v2>v1, then V1 is the necessary state when V2 is reduced to 0, then according to the previous observation results:
Subtract two formulas:
When v1 and v2 are infinitely close:
Divide both sides by the time interval dy:
Integrate both sides:
EC =v0, e^C = v_0eC=v0;
conclusion
The deceleration motion of UIScrollView is the damped motion with a damping coefficient of 2, which corresponds to the real low-speed fluid resistance (regardless of compression losses).
other
-
Q: Why is the multiples of y and v approximately equal to?
-
A: this is not the error, Apple is intentional, because the damping reduction is exponential decay, so the process of infinite approximating 0 is very long, if you look at the relationship between the v/t will find that even the t is infinite, can’t reduced to 0 v, because the index is always positive, so as to omit that behind A long slow slow, Apple has to cut it. The cutoff is something like v = (12 ~ 13)pt/s, and anything less than that gets cutoff, so y is always a little bit less than v/2. So it’s a concession to the goal of improving the user experience by strictly following the laws of nature.
Bounces sports exploration
Numerical observations
Activity activity, in the same way, will mainly observe Bounces’ movement rule from two dimensions of time and space. The observed values include:
-
The initial velocity v0 when UIScrollView first touches the boundary, an intuitive feeling is that when the v0 is larger, the furthest distance can be reached in the rebound process will be longer, which can be understood as the greater the inertia.
-
So the second value worth looking at is the maximum distance that Bounces can travel beyond the boundary.
-
The third value is the total duration of the Bounces motion, which begins with the initial contact with the boundary and ends with the natural recovery to the boundary.
Specific statistical methods are only briefly introduced here, and readers can write their own Demo for practice:
-
To make this distinction easier, we make UIScrollView bump the top boundary each time, so that when contentoffset. y is negative, Bounces animation starts, and a start time t0 is recorded here.
-
In order to get an accurate initial speed, we stopped dragging each time the UIScrollView reached the boundary and made it bounce back depending on its inertia. This way, we could calculate the initial speed when reaching the boundary by relying on the laws described in activity above :(speed when stopping dragging – distance from the top when stopping dragging *2).
-
The maximum distance, obtained by keeping the minimum captured in the scrollViewDidScroll proxy.
-
End time, captured by the EndActivity agent (the end of Bounces is also considered the end of deceleration).
After these records, we managed to capture several sets of data:
The serial number | Initial velocity at contact boundary (P /s) | Bounces maximum distance (p) | Bounces duration (s) |
---|---|---|---|
1 | 986.497 | 32.5 | 0.6668 |
2 | 2404.116 | 80.5 | 0.7509 |
3 | 1793.594 | – 60 | 0.7337 |
4 | 1251.628 | 41.5 | 0.6836 |
Observed phenomena:
- V0 is always directly proportional to the farthest distance y, which is about 30.
- The total duration of Bounces motion does not change much with the change of inertia, and the value is basically stable between 0.6s and 0.8s. This is typical of damped motion, and an algorithm similar to log(n) time complexity does not produce large increments of n.
So it’s obvious that Bounces has two forces: elasticity and resistance; The elastic force is used to ensure a rebound on contact with the boundary, and the resistance is used to limit the harmonic vibration of the elastic force.
Results analysis
We list the mathematical model after the above two forces:
- In the first term, K is the stiffness coefficient k in hooke’s Law elastic model, and Y is the length of spring compression, that is, the distance beyond the boundary of UIScrollView. Because the spring is always the opposite of y, the sign is minus.
- In the subterm, C is the damping coefficient c of resistance (see the value 2 in Activity activity). The resistance and velocity v are always in the opposite direction, so the sign is negative.
- The whole thing is what we know as Newton’s second law: the net force is equal to mass times acceleration.
Another well-known conclusion is that the instantaneous velocity v is the derivative of displacement y with respect to time t; The instantaneous acceleration a is the derivative of velocity V with respect to time t, which is the second derivative of displacement y with respect to time t. Then the above equation can be written as follows:
Divide both sides of this equation by mass M:
This is a second-order linear homogeneous differential equation, and there are three general solutions in different forms according to the number of return solutions of its characteristic roots. In order to facilitate the discussion of characteristic roots, we make some substitutions for its coefficients, let’s say:
Note that the damping coefficient C and the progress coefficient K are both positive here because signs are already taken into account in the equation; In terms of mass M, we don’t consider mass less than or equal to zero; So these parameters are all positive, and they’re also positive after the substitution.
So this simplifies to:
Its characteristic root equation is:
Discuss its solution form
Case 1:4δ2−4ω2>04δ^2-4ω 2>04δ2−4ω2>0
The characteristic root equation has two distinct real roots A and B, and its general solution form is:
Case 2:4δ2−4ω2=04δ^2-4ω 2=04δ2−4ω2=0
A pair of identical real roots of the characteristic root equation, both a, and its general solution form is:
Case 3:4δ2−4ω2<04δ^2-4ω 2<04δ2−4ω2<0
The characteristic root equation has no roots in the real number field and a pair of conjugate complex roots in the complex number field: A − BIa-BIA − BI, a+ BIA + BIA +bi, and its general solution form is:
These three correspond respectively to the three forms of damping vibration: overdamping, critical damping and underdamping
From a user experience perspective, the best feel is usually achieved by bouncing back to the boundary as quickly as possible without shock. Therefore, apple must choose the critical damping for its general component boundary damping, so we choose the general solution form under the critical damping condition 4δ2−4ω2=04δ^2-4ω 2=04δ2−4ω2=0:
At this point the delta delta omega = delta omega = = omega, we take the delta, then the solutions of characteristic root equation: lambda 2 + 2 delta lambda + delta 2 = 0 lambda ^ 2 + 2 delta delta ^ 2 = 0 lambda lambda + 2 + 2 delta lambda + delta 2 = 0, get: lambda = – the delta = – omega lambda = – delta = – omega lambda = – the delta = – omega
Substitute the solution of λ into a of the substitution general solution, and obtain:
Bounces starts with t=0,y=0, and C1=0.
C and δ are two undetermined coefficients, and the measurement method will be introduced later. It can be directly given here: C is the velocity v0 when the boundary is first contacted, and δ is a constant 10.9. Therefore, Bounces formula is:
Relevant interpretation
The relationship between characteristic root equation and differential equation:
For the second-order homogeneous equation: y “+py’+yp=0y” +py’+yp=0y “+py’+yp=0, consider a function Y is the easiest to gather the second derivative, the first derivative, the most easy to extract the common factor containing variables, and can cancel the remaining constants by addition and subtraction.
Then the function should be exponential, because the derivative of an exponential always extracts itself:
If the solution form is confirmed, the equation can be written as:
The exponent cannot be zero, so the solution of the left polynomial λ2+pλ+q=0λ^2+pλ+q=0λ2+pλ+q=0 is the solution of the differential equation, so this simplified equation is the characteristic root equation.
conclusion
Bounces motion is a damped vibration that relies on elastic K and damping C to satisfy a special quantitative relationship that makes Bounces follow critically damped vibrations, thus exhibiting a phenomenon that “Bounces never bounce over boundaries or vibrate repeatedly”. After the above discussion, we tried to build the UIScrollView content display area as follows:
-
The contentSize of UIScrollView is filled with fluid c=2 with low resistance, which can be understood as water, as shown in blue.
-
The outside of contentSize is filled with fluid C =21.8 with relatively high resistance, which can be understood as oil, shown in yellow in the figure.
-
Four springs are installed at the contentSize boundary of UIScrollView, each with a progress coefficient of 119, shown in purple.
-
The content area displayed by UIScrollView is a smooth iron plate with a mass of 1kg, which can move freely in the whole area following gestures and receive corresponding forces when in different areas.
Parameter measurement method
Due to the existence of two undetermined coefficients, C and δ, and the main observed values are contentOffset (displacement), the influence factors of C and δ are higher level parameters such as velocity and acceleration, it is difficult to determine A and δ by observing displacement. So here we use a simple optimization method to make the predicted motion trend gradually match the actual trend.
Gradient descent idea
-
Given a set of UIScrollView impacting boundaries that triggers Bounces’ real data set S, S contains all the contentOffset values for the time Bounces is animated: [y0, y1, y2, y3…] .
-
Given a set of undetermined coefficients (C, delta, phi), the solution formula by y = C + phi (t) e – delta (t + phi) y = C + phi (t) e ^ {- delta (t + phi)} y = C + phi (t) e – delta (t + phi) to calculate all the theoretical value [y0 ‘, y1 ‘, y2, y3 ‘… .
-
Calculate the theoretical value and practical value of variance: Sum = ∑ ∗ n = 0 n (yn – ‘y ∗ n) 2 Sum = {n = 0} \ sum_ * ^ n (y_n – y’ * _n) ^ 2 Sum = ∑ ∗ n = 0 n (yn – y ‘∗ n) 2, the Sum function is smaller, the closer the theoretical curve and the actual curve.
-
If I change any of the values of A, δ, or φ independently to make the value of the objective function smaller, then I affirm the change and change the original value to the changed value. For the three undetermined coefficients, we have six changes, A+△A, A-△A, δ+△δ, δ-△δ, φ+△φ, φ-△φ. When any of the six changes can make the result of the objective function smaller, we take the change that becomes the smallest, so this method is also called the fastest descent method.
-
When the objective function is optimized to 0, it means that our target curve has completely overlaps with the actual observed curve. And what we see on our machine is always discrete, so in fact, it’s pretty close when you optimize it down to the single digit.
The following is an optimization process for a set of Bounces data
-
Y =Cte−δty=Cte^{-δt}y=Cte−δt It is always assumed that the motion starts with t = 0, but Bounces’ y value may not be exactly 0, because the screen refresh signal in the phone is discrete and refreshed every 0.0167s, so most of the time it changes directly from a positive number such as y=10 to a negative number: Y is equal to -10, instead of falling exactly at 0, and the first value we get is -10, so the curve as a whole is slightly offset in time, so by adding this variable φ, it converges more precisely.
-
In general, the didScroll callback frequency is less than CADisplayLink’s callback frequency, and discrete integration may cause contentOffset to remain constant during a refresh under slow scrolling, that is, the didScroll interval may be greater than 0.0167s. It is 2 or 3 refresh cycles, so CADisplayLink is the safest method, but after practice, there is little impact on this aspect, because the difference of y value in low speed state is not big, and the influence on sum function is very small, so use didScroll dot, the default interval is 0.0167s.
Finally, we measured several sets of data similar to the above data, and δ value was always close to a number of 10.9. And C changes depending on how strong Bounces are.
The determination of parameter C
Since we found that the size is affected by the strength of each Bounces, we found a relationship containing two parameters to explore the specific algorithm, using this formula:
F is the resultant force and t is the duration. Since the two parts of the elastic force and resistance in the resultant force F change with time, both of them are written in integral form (small C is the damping coefficient and large C is the constant to be fixed).
Then take the formula for y and v up there:
And plug that into the left-hand integral:
It is found that there is always one CCC in the left integral, there is no CCC in it, only a bunch of known quantities, and at this time there happens to be v0V_0v0 on the right, so CCC is proportional to the initial velocity v0V_0v0, and the scaling coefficient is m divided by the rest of the integral. Instead of calculating this integral, we just need to find a set of Bounces data, use deceleration to get v0v_0v0, and use the optimization method above to optimize the C that Bounces is closest to the actual value. (Second variable in the above gifs: A), the ratio of CCC to V0v_0v0 in Bounces is the fixed ratio in all cases, which happens to be 1. So the whole equation becomes:
Other relevant values:
Stiffness ratio: km=119\ FRAc {k}{m}=119mk=119
Damping ratio: CM =21.8\frac{c}{m}=21.8 MC =21.8
Bounces indicates the maximum distance
According to y=v0te−10.9ty=v_0te^{-10.9t}y=v0te−10.9t, the point satisfying y’=0y ‘=0y ‘=0 is the furthest point, which can be obtained:
Get:
Two conclusions:
-
Bounces always reaches its maximum distance at 110.9\frac{1}{10.9}10.91 seconds after the start. The maximum distance is reached at a fixed time.
-
The farthest distance is indeed proportional to the initial velocity, with a scaling coefficient of 1δe\frac{1}{δe}δ E1, these two: 2.71828×10.9, approximately 30, which we observed earlier in the table.
Bounces recursively
Considering that we implement a CustomScrollView ourselves, all we know when we perform Bounces animation with CADisplayLink is the instantaneous speed V and the current position Y; And this y doesn’t have to be 0, v doesn’t have to be v0; So here we provide the method of converting any instantaneous state {v,y} to v0:
Divide by:
The right-hand side of the equation simplifies to the expression T:
Substitute y for v0v_0v0:
T =t+△t(△t=0.0167). Use y and y’ to calculate {y, v} at the next refresh. I’m going to implement a Bounces change like UIScrollView.
Practical use in development
Use Activity to reduce energy transfer in both uiscrollViews
Activity you can use it in a complex structure, nested in multiple layers of UIScrollView. The outer layer of the activity is a vertical scroll view that supports the header, the collapsible top area, and the paging container. The paging container is a horizontal scrolling view; The interior is divided into several vertical scroll views, thus forming three nested uiscrollViews, as follows:
The vertical view of the outermost blue layer takes effect after the user drags up the head and top area. When the blue layer hits the bottom, there is a clear pause effect. Because the superview gestures work, the remaining inertia is not transferred to the internal orange longitudinal view; To compensate for this and make the vertical list look more integrated, we have assigned a mechanism for residual velocity (or impulse) detection to the outer blue view:
-
The Detector uses restVelocity=(Velocity −2∗ distance) as mentioned above (velocity-2**distance)restVelocity=(Velocity −2∗∗distance) the remaining velocity after touching the boundary is obtained. *
-
Multiplied by its own mass m, it is passed through the blue path in the figure to the orange view corresponding to the currently selected TAB.
-
The orange view is configured with an impulse-receiving mechanism that removes the received MVMVMV’s mass from their inner speed.
-
The orange view uses this speed as the initial speed to perform a deceleration animation that scrolls down.
Q: About why mv is transmitted
A: We assign an extra attribute M to each Detector and Impulser, so that these effects can be sent in different proportions between them. It looks like: A dense sphere crashing into A less dense sphere. The default mass is usually 1, so the remaining speed between different UiscrollViews is passed in a 1:1 ratio.
Use Bounces to implement the POP type barrage
You can use Bounces animation to make a bullet-screen track of POP type. We use a similar parameter relationship to Bounces to build critical damping and use this curve to control the zoom of the bullet-screen:
To save performance, you can take a few good set of configuration parameters of critical damping curve dot performed interval is 0.0167 s, and the point values stored in a static array, barrage orbit is executed directly from the fixed a few obtain the response values in the array, so the barrage of each POP track all work in the same rule, at the same time, There is no need to repeatedly calculate those tedious exponentials, reducing the large performance cost of numerical calculations during the execution of ordinary POP animations.
conclusion
The subtleties of Apple engineers in building a harmonious society for developers are everywhere revealed. As iOS engineers, we can see the tremendous efforts behind apple’s pursuit of the ultimate user experience through a few simple interfaces and agents. If you have the chance to do it again, I believe you will not hesitate to buy an iPhone13XSProPlusMax, or become an iOS engineer who is down-to-earth in the pursuit of perfection.
This article is fairly formal, and I learned to use markdown’s formula syntax. It is based on a crude version I wrote earlier (the formula was too bad to read HHH) : UIScrollView scroll feature
Both the Demo used in this article and the hand-implemented CustomScrollView can be downloaded from my Github: Github
If you feel good, please give me a Star! Thank you!