In the second part we implement a simple line chart. This assumes that you have read the previous article. Below we will continue to contribute to this line chart.
I want to add three buttons to the top of the graph so that users can click on different buttons to view different categories of data. For example, users can view walking, running, and cycling. The user points different buttons, we will be different with the motion data displayed in the graph.
We implemented a button click, set different coordinate point data, and then run the app. You will find that, although the method setChartData()
has been called, the graph does not change at all. Why is it? Because we didn't notice the line chart redraw. This can be achieved by invoking a invalidate()
method. However, such a different category of data switching is very abrupt, if there is a transition of the animation will be much better.
If we want to add a transition animation of different categories of data to a line chart, there are two issues to resolve:
1. We need to change the value of the line chart from old to new step.
2. We need to update the view after the modification of each step when the value of the previous step is modified.
Let's start with the first question. There are many ways to change the value of a point. The simplest one is a simple linear interpolator, supplemented by some advanced interpolator. What we're going to do here is slightly different.
How to move it.
We put the logic mentioned above in a Dynamics
class called. An Dynamics
object contains the position of a point, the speed of the point, and the target position of the point. Use this object's update()
method to update the position and speed of the current point. update()
The method looks like this:
fun update(now: Long) { val dt = Math.min(now - lastTime, 50) velocity += (targetPosition - position) * springiness velocity *= 1 - damping position += velocity * dt / 1000 now}
The first thing we need to do in this method is to calculate the time step, which is basically the time from the last update to the present. and ensure that the longest time is not longer than 50 milliseconds. This is done because the transition delays the animation's update time by avoiding what happens during the animation.
We then update the speed based on the distance from the current point to the target point. At the same time, the animation to achieve a spring effect, so at the time of updating the speed will be considered spring "elastic constant." The speed is reduced by a constant of "damping factor (greater than 0, less than 1)" and finally to 0.
We then use the speed to update the location of the point and record the current update time to facilitate the calculation of the next time step.
In this way, the trajectory of the point is as if it were tied to a spring. This point will rush to the target position and oscillate near that position. If we increase the damping coefficient , the acceleration of the point becomes smaller and if the damping coefficient is large enough, the points will not oscillate at the target position.
This animation and the use of the interpolator are slightly different. The interpolator needs to be set to a duration (duration) when used. The interpolation operation executes within the specified time. However, we only care about the final end time of the animation execution, or under what conditions it ends. Therefore, we add the following method:
fun isAtRest(): Boolean { val standingStill = Math.abs(velocity) < TOLERANCE val isAtTarget = targetPosition - position < TOLERANCE return standingStill && isAtTarget}
Returns trueif the point is already in the target position and the speed is 0. Compared to floating-point numbers is not a good idea, so we detect whether the speed value is close to 0. So TOLERANCE
the value is 0.01, which in our case is a reasonable threshold.
Using Dynamics
LineChartView
It is very easy to update the code before you put Dynamics
it in. However, I'm still trying to create a line chart, although the code for this line chart is exactly the same as the code in the previous section. This is mainly to facilitate the reader to view the different chapters of the code. The custom of this heart is called AnimLineChartView
the attempt. So, the features of this animation are focused on AnimLineChartView
this class.
In the previous section, the last code we drew is this:
var maxValue = getMax(this.points)var path = Path()path.moveTo(getXPos(0), getYPos(this.points[0], maxValue))forin1.1)) { path.lineTo(getXPos(i), getYPos(points[i], maxValue))}
After use, Dynamics
this is the following:
var maxValue = getMax(this.points)var path = Path()path.moveTo(getXPos(0), getYPos(this.points[0in1..(points.count1)) { path.lineTo(getXPos(i), getYPos(points[i].position, maxValue))}
The main reason is that the point is no longer represented by float
an array, but by Dynamics
an array of types:
privatevarnull// private var _points: List<Dynamics>? = null// var points: List<Dynamics>// get() = if (_points == null) listOf<Dynamics>() else _points!!// set(value) {// _points = value// }
_dynamicPoints: ArrayList<Dynamics>?
Instead of var points: List<Dynamics>
. The value of the value of the object that was previously used directly in the float type would need to be replaced Dynamics
position
.
Start processing animations
What we need to do now is to constantly invoke upate()
the method to update _dynamicPoints
and trigger the redraw of the view. We use it Runnable
to implement the above functions. An runnable example is an executable command that is typically used to perform some task on another thread. But we used it to update the view on the UI thread.
The runnable we're going to use is this:
private var animator: Runnable = object : Runnable {override Fun Run () { var neednewframe = false var now = Animationutils.currentanimationtimemillis () for (d in this @AnimLineChartView. _dynamicpoints!!) {d.update (now) if (D.isatrest ()) {neednewframe = true } } if (neednewframe) {postdelayed (this, 20 )} invalidate ()}}
In the Runnable
only way run()
, we traverse _dynamicPoints
all the points (which are now Dynamics
types) and invoke the update()
method. If there is a "dot" that does not stop, we set a new animation (Schedulenewframe). Setting up a new animation is through this sentence: postDelayed(this, 20)
to achieve. That is, whenever a new animation needs to be set, it is called after a certain period of time Runnable
. Finally, the method is called invalidate()
to trigger the redraw.
So what if you animator
do it again before the next draw? After all, it's more than 15ms before we start the next draw, we can't control it. It is interesting to note that the Runnable
object is wrapped in a message and added to MessageQueue
(Message Queuing), where the message queue is in the UI thread Looper
. invalidate()
method is also the case. The UI thread Looper
then distributes the various messages and ensures that the redraw and execution of the Runnable objects is performed sequentially. Essentially, in the UI thread, the Looper
sequential distribution executes all Message
, so each Message
object is executed in a different order from the time of the post.
Dynamics
and Runnable
The combination is a very good choice for dealing with animations. It's easy to animate custom views that were previously animated. I always start with the drawing and interacting code, add Dynamic
properties, and Runnable
animate the view.
Look at the setChartData()
method:
Fun Setchartdata (newpoints:list<float>) {varnow = Animationutils.currentanimationtimemillis ()if( This. _dynamicpoints = =NULL|| This. _dynamicpoints?. COUNT ()! = Newpoints.count ()) { This. _dynamicpoints =NULL This. _dynamicpoints = arraylist<dynamics> () for(I:intinch 0.. (Newpoints.count ()-1)) {varDynamicpoint = Dynamics ( -F0.30f) dynamicpoint.setposition (Newpoints[i], now) dynamicpoint.settargetposition (Newpoints[i], now) This. _dynamicpoints?. Add (Dynamicpoint)} invalidate ()}Else{ for(I:intinch 0.. (Newpoints.count ()-1)) { This. _dynamicpoints?.Get(i)?. Settargetposition (Newpoints[i], now) removecallbacks (animator) post (Animator)}}}
There are two situations in which we need to deal with:
1. If we do not have the data before, or the previous data has expired (and the current number of new data is different). At this point we'll create a new Dynamics
array and initialize them. We position
specify the value as the Y value of the point and velocity
specify 0 (the default). We then targetPosition
specify the same value. The last Call invalidate()
method triggers the redraw.
2. Another situation is that we already have some data. All we need to do is targetPosition
replace the new value and start the animation. We post(r: Runnable)
can start the animation by invoking the method. However, the animation may already be running, so remove the runnable that might have been added before post a runnable animation. This is also easy to debug some. The only value modified in this method is targetPosition
. The current position update()
will not change until the method is called.
The results are as follows:
Smooth as silk
There is one more thing to deal with, and that is the figure is too angular. We substituted the method of drawing the line chart path.lineTo(x, y)
cublicTo()
. This will draw from one point to another using Bezier curves. Of course, we also need to calculate the coordinates of the other two control points required by the Bezier curve.
How the control point coordinates are calculated. The main calculation is the control point of the current point and the next point. So assuming the current point is the I point, the next point of I is the (i+i) point, and the first point of I is the (i-1) point. This is easy to understand. At the time of calculation, the control point of I points is the x+ of the I point (X-Point (i-1) of the Point (i+1)) * The smooth constant, the Y value is similar. Point (I+i) control points are: X of Point (i+1)-(X of Point (i+2)) * Smooth constant. The Y-value of the point (i+1) control points is similarly available.
Let's go back to the animation section again, assuming you have an app with a button and a picture. After you click the button, the image will blur until it disappears (fade out). Then click the button picture in the Blur to full display (fade in). This can be achieved entirely using Alpha animation . But what happens if you click the button first to make the picture fade in, and then click the button fade out without waiting for the animation to execute completely? This image will be displayed immediately alpha=1 and then executed fade out animation.
Then look at our custom Line chart animation, arbitrarily switch between different categories, the individual data lines will not suddenly change, but very smooth animation into the next category of data.
Stay tuned to my next episode!
Android Custom View three: Add "smooth" animations to custom views