Skip to content

Commit 9ed10e0

Browse files
authored
Avoid calling updateState() for completed animations and add tests (#4737)
* Fix redundant updateState call for finished animations * Narrow finished-animation fix to AnimationManager and expand lifecycle tests
1 parent ec05ede commit 9ed10e0

4 files changed

Lines changed: 174 additions & 16 deletions

File tree

CodenameOne/src/com/codename1/ui/AnimationManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ void updateAnimations() {
6969
if (c.isInProgress()) {
7070
c.updateAnimationState();
7171
} else {
72-
c.updateAnimationState();
72+
c.completeAnimation();
7373
anims.remove(c);
7474
}
7575
} else {

CodenameOne/src/com/codename1/ui/animations/ComponentAnimation.java

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -126,24 +126,35 @@ public int getMaxSteps() {
126126
public final void updateAnimationState() {
127127
updateState();
128128
if (!isInProgress()) {
129-
if (!completed) {
130-
completed = true;
131-
if (notifyLock != null) {
132-
synchronized (notifyLock) {
133-
notifyLock.notifyAll();
134-
}
135-
}
136-
if (onCompletion != null) {
137-
onCompletion.run();
129+
completeIfNeeded();
130+
} else { //ensure completed would be set to false if animation has been restarted
131+
completed = false;
132+
}
133+
}
134+
135+
/// Completes the animation if needed, without forcing an additional updateState() call.
136+
public final void completeAnimation() {
137+
if (!isInProgress()) {
138+
completeIfNeeded();
139+
}
140+
}
141+
142+
private void completeIfNeeded() {
143+
if (!completed) {
144+
completed = true;
145+
if (notifyLock != null) {
146+
synchronized (notifyLock) {
147+
notifyLock.notifyAll();
138148
}
139-
if (post != null) {
140-
for (Runnable p : post) {
141-
p.run();
142-
}
149+
}
150+
if (onCompletion != null) {
151+
onCompletion.run();
152+
}
153+
if (post != null) {
154+
for (Runnable p : post) {
155+
p.run();
143156
}
144157
}
145-
} else { //ensure completed would be set to false if animation has been restarted
146-
completed = false;
147158
}
148159
}
149160

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.codename1.ui;
2+
3+
import com.codename1.junit.FormTest;
4+
import com.codename1.junit.UITestBase;
5+
import com.codename1.ui.animations.ComponentAnimation;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertFalse;
12+
13+
class AnimationManagerTest extends UITestBase {
14+
15+
@FormTest
16+
void testFinishedAnimationDoesNotUpdateStateAgainBeforeRemoval() {
17+
Form form = CN.getCurrentForm();
18+
AnimationManager manager = form.getAnimationManager();
19+
AtomicInteger updateStateCalls = new AtomicInteger();
20+
AtomicInteger completionCalls = new AtomicInteger();
21+
22+
ComponentAnimation animation = new ComponentAnimation() {
23+
private int remainingSteps = 1;
24+
25+
@Override
26+
public boolean isInProgress() {
27+
return remainingSteps > 0;
28+
}
29+
30+
@Override
31+
protected void updateState() {
32+
updateStateCalls.incrementAndGet();
33+
remainingSteps--;
34+
}
35+
};
36+
37+
manager.addAnimation(animation, completionCalls::incrementAndGet);
38+
39+
manager.updateAnimations();
40+
assertEquals(1, updateStateCalls.get(), "Animation should update once while in progress");
41+
assertEquals(1, completionCalls.get(), "Completion callback should run exactly once");
42+
assertFalse(manager.isAnimating(), "Animation should report as not animating once completed");
43+
44+
manager.updateAnimations();
45+
assertEquals(1, updateStateCalls.get(), "Finished animation should not receive another update");
46+
assertEquals(1, completionCalls.get(), "Completion callback should not run a second time");
47+
}
48+
49+
@Test
50+
void testAlreadyFinishedAnimationRunsCompletionWithoutUpdateState() {
51+
Form form = new Form();
52+
AnimationManager manager = form.getAnimationManager();
53+
AtomicInteger updateStateCalls = new AtomicInteger();
54+
AtomicInteger completionCalls = new AtomicInteger();
55+
56+
ComponentAnimation animation = new ComponentAnimation() {
57+
@Override
58+
public boolean isInProgress() {
59+
return false;
60+
}
61+
62+
@Override
63+
protected void updateState() {
64+
updateStateCalls.incrementAndGet();
65+
}
66+
};
67+
68+
manager.addAnimation(animation, completionCalls::incrementAndGet);
69+
assertFalse(manager.isAnimating(), "Already-finished animation should not mark manager as animating");
70+
71+
manager.updateAnimations();
72+
73+
assertEquals(0, updateStateCalls.get(), "Already-finished animation must not mutate state");
74+
assertEquals(1, completionCalls.get(), "Completion callback should still run once");
75+
}
76+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.codename1.ui.animations;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.concurrent.atomic.AtomicInteger;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
class ComponentAnimationLifecycleTest {
10+
11+
@Test
12+
void testUpdateAnimationStateStillInvokesUpdateStateWhenAlreadyFinished() {
13+
AtomicInteger updateStateCalls = new AtomicInteger();
14+
AtomicInteger completionCalls = new AtomicInteger();
15+
16+
ComponentAnimation animation = new ComponentAnimation() {
17+
@Override
18+
public boolean isInProgress() {
19+
return false;
20+
}
21+
22+
@Override
23+
protected void updateState() {
24+
updateStateCalls.incrementAndGet();
25+
}
26+
};
27+
animation.setOnCompletion(completionCalls::incrementAndGet);
28+
29+
animation.updateAnimationState();
30+
31+
assertEquals(1, updateStateCalls.get(), "Direct updateAnimationState() should still invoke updateState()");
32+
assertEquals(1, completionCalls.get(), "Completion callback should run when animation is finished");
33+
}
34+
35+
@Test
36+
void testRestartedAnimationResetsCompletionLifecycle() {
37+
AtomicInteger completionCalls = new AtomicInteger();
38+
39+
class RestartableAnimation extends ComponentAnimation {
40+
private int remainingSteps = 2;
41+
42+
@Override
43+
public boolean isInProgress() {
44+
return remainingSteps > 0;
45+
}
46+
47+
@Override
48+
protected void updateState() {
49+
remainingSteps--;
50+
}
51+
52+
public void restart() {
53+
remainingSteps = 2;
54+
}
55+
}
56+
RestartableAnimation animation = new RestartableAnimation();
57+
animation.setOnCompletion(completionCalls::incrementAndGet);
58+
59+
animation.updateAnimationState();
60+
animation.updateAnimationState();
61+
assertEquals(1, completionCalls.get(), "Completion should run for first lifecycle");
62+
63+
animation.updateAnimationState();
64+
assertEquals(1, completionCalls.get(), "No extra completion should fire without restart");
65+
66+
animation.restart();
67+
animation.updateAnimationState();
68+
animation.updateAnimationState();
69+
assertEquals(2, completionCalls.get(), "Completion should run again after restart");
70+
}
71+
}

0 commit comments

Comments
 (0)