Delightful swapping views animation

And how to do it by the simple math

Michael Spitsin
7 min readApr 24, 2018

In my previous articles I promised to write about building dynamic photosets, but then I faced with interesting task and decided to write about it.

Task: there are 2 square views with same size. We need to swap it with animation.

You might think, that this is not interesting, since there are easy and obvious solution called Linear Motion:

val v1 = ...// take first view
val v2 = ...// take second view
val v1X = v1.x
val v2X = v2.x
val v1Y = v1.y
val v2Y = v2.y
val t = ...// fraction that in range [0, 1]v1.x = v1X * (1 - t) + v2X * t
v1.y = v1Y * (1 - t) + v2Y * t
v2.x = v2X * (1 - t) + v1X * t
v2.y = v2Y * (1 - t) + v1Y * t

That’s all, but this give us an next effect. For example:

As you can see, two views are simply moving on a straight line. Thus, on some moment of time they cross with each other. For me it was not acceptable, since it only looks as too easy solution, but moreover it is not such graceful, as it can be:

As you see here, such swapping, with curve path looks more delightful and more realistic (because as you know in the real world solid objects can not intersect). So we need to invite solution that will build such curve path for us.

Note: When I stumbled on this problem, I didn’t thought about Bezier curves. It is true, that you can easy build needed curve path (especially simple one), just using quadratic or cubic Bezier curves. But when I remembered about that, I was enchanted by other problem. How can we swap views by using circle arcs as motions paths?

Gathering all and formulating task we have next statement:

How can we build an circle arc path for swapping two square views with same sizes, that they will not intersect to each other?

And this leads us to the next solution.

Solution

Step 1. Finding a circle radius d

First of all, because of task limitations, we know that both paths (motion path for first view and motion path for the second one) will just mirror each other. Thus, some dimension values will be same both for the first and for the second paths. One of such values is circle radius d.

As we already know, our views will move on arc path. This arc is a part of some circle, that has (of course) a radius d, which we want to find. Also, we know that two views will not intercept if the distance between their center will be no less that diagonal of a any of those views, since they are squares with same size.

Here you can see a sketch of a first step:

We know size, A(x, y), B(x, y), r' and we want to find d. Note that r’= size / sqrt(2) + delta, because of aforementioned statement about distance and where delta is any non-negative integer. You can play with it and insert any delta as you want, but for the simplicity I will use delta = 0.

If we will look close to the sketch and collect all previous thoughts, we will have a System of polynomial equations:

where d and r are unknown values and diff(B,C) is a dinstance between two points: B and C. If will keep in mind, that r' = size / sqrt(2) and will solve that system, we find out next solution:

where C is center point, between given points A and B.

Step 2. Finding coordinates of D point

Now, when we found radius d, we need to find coordinates of point D. It is a center of circle, on the arc of which our path is placed. Below you can see a sketch of a second step:

We know A, B, C points, radius d. Also we know, that BCD is the right angle. Now we introduce B’ and D’ points, which is result of placing center of the plane on C point:

Gathering together result of the first step and formula of angle between two vectors, we have next System of polynomial equations:

By slightly intricate calculations we find the following solution:

Step 3. Building rotation function

Now, we have center of the circle D, its radius d and begin and end points A, B of an arc. Now we need to provide a point P1 and P2 by the given t, which lies on a range [0, 1].

To do that we need to place center of a plane on the point D. Then we need to adapt or know points A, B and C and receive new A’’, B’’, C’’:

Then we need to compute angle of A’’ and angle of B’’ which are computed with help of arc tangent function. Also we need to remember two things:

  1. As arc tangent returns a value in range [-90, 90] degrees, we need to adapt it by seeing x coordinate sign. If it is less than zero, we need to add 180 to result angle. Also, we need to handle situations when x is zero.
  2. At some points the difference between to angles will be more that 180 degrees. In that case we need to sum second angle with 360 or truncate 360 from it, in order to reduce differentiation to value that is less than 180.

Thus we will have two functions:

// gets point angle by its coordinates
private fun getPointAngle(x: Float, y: Float): Float {
return if (x == 0f) {
if (y > 0f) PI.toFloat() / 2f
else - PI.toFloat() / 2f
} else {
atan(y / x) + (if ((x < 0)) PI.toFloat() else 0f)
}
}
//adjust second angle towards to the first one
private fun normalizeAngleDiff(aAngle: Float, bAngle: Float): Float {
return if (abs(aAngle - bAngle) >= PI) {
val temp = bAngle + 2 * PI.toFloat()
if (abs(aAngle - temp) >= PI) {
bAngle - 2 * PI.toFloat()
} else {
temp
}
} else {
bAngle
}
}

Also we will have a function that computes intermediate angle between aAngle and bAngle:

private fun computeAngle(fraction: Float) =
bAngle * (1f - fraction) + aAngle * fraction

Thus we will have next for retrieving both points P1 and P2:

val angle = computeAngle(t)
var x1 = dVector * cos(angle)
var y1 = dVector * sin(angle)
var x2 = 2 * (cX - dX) - x1
var y2 = 2 * (cY - dY) - y1

x1 += dX
y1 += dY
x2 += dX
y2 += dY

Where x1 and y1 are coordinates of point P1 and x2 and y2 are coordinates of point P2. Second point are received by reflecting the first one with respect to point C.

Show me the code!

Okay, as you saw last step I provided not with formulas, but with pieces of code. I myself can not wait to get started writing code. So lets start. Here is the code of Transformator that uses information of input coodinates and square size and provides a function transform that generates exchange coordinates by given fraction that is in range [0, 1]:

Here is an example of its usage:

double durationMult = diff(pickedTV, fakeTV) / max(pickedTV.getMeasuredWidth(), fakeTV.getMeasuredWidth());
final ExchangeTransformator transformator = new CurveExchangeTransformator(
new ExchangeCoords(currentTileX, currentTileY, prevTileX, prevTileY),
max(clickedTileView.getMeasuredWidth(), clickedTileView.getMeasuredHeight())
);

ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration((long) (sqrt(durationMult) * 300));
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
ExchangeCoords coords = transformator.transform(valueAnimator.getAnimatedFraction());

if (isExchangeableTile) {
fakeTV.setX(coords.getSourceX());
fakeTV.setY(coords.getSourceY());
}
pickedTV.setX(coords.getTargetX());
pickedTV.setY(coords.getTargetY());
}
});
animator.start();

Here is the result of execution:

Afterwords

As you can see, creating an swap views function is not so hard, as I expected :)

--

--

Michael Spitsin

Love being creative to solve some problems with an simple and elegant ways