Skip to content

Commit a3d1257

Browse files
committed
Use slow motion for animation screenshots
1 parent 01ad037 commit a3d1257

File tree

2 files changed

+251
-15
lines changed

2 files changed

+251
-15
lines changed

.github/workflows/developer-guide-docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
mkdir -p "$HOME/.codenameone"
5252
touch "$HOME/.codenameone/guibuilder.jar"
5353
cp maven/CodeNameOneBuildClient.jar "$HOME/.codenameone/CodeNameOneBuildClient.jar"
54-
xvfb-run -a mvn -B -ntp -Dgenerate-gui-sources-done=true -pl common -am -f docs/demos/pom.xml install
54+
xvfb-run -a mvn -B -ntp -Dgenerate-gui-sources-done=true -am -f docs/demos/pom.xml install
5555
5656
- name: Install ImageMagick for screenshot comparison
5757
if: github.event_name != 'pull_request' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true'

docs/demos/common/src/test/java/com/codenameone/developerguide/animations/AnimationDemosScreenshotTest.java

Lines changed: 250 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,24 @@
44
import com.codename1.io.Util;
55
import com.codename1.testing.AbstractTest;
66
import com.codename1.testing.TestUtils;
7+
import com.codename1.ui.AnimationManager;
8+
import com.codename1.ui.Button;
9+
import com.codename1.ui.Component;
10+
import com.codename1.ui.Container;
711
import com.codename1.ui.Display;
812
import com.codename1.ui.Form;
913
import com.codename1.ui.Image;
14+
import com.codename1.ui.animations.Motion;
1015
import com.codename1.ui.util.ImageIO;
1116
import com.codenameone.developerguide.Demo;
1217
import com.codenameone.developerguide.DemoRegistry;
1318

1419
import java.io.IOException;
1520
import java.io.OutputStream;
21+
import java.util.ArrayList;
1622
import java.util.HashMap;
1723
import java.util.HashSet;
24+
import java.util.List;
1825
import java.util.Map;
1926
import java.util.Set;
2027

@@ -26,6 +33,11 @@ public class AnimationDemosScreenshotTest extends AbstractTest {
2633
private static final String HOST_TITLE = "Demo Test Host";
2734
private static final long FORM_TIMEOUT_MS = 10000L;
2835
private static final String STORAGE_PREFIX = "developer-guide.animations.";
36+
private static final int FRAMES_PER_ANIMATION = 6;
37+
private static final long ANIMATION_CAPTURE_TIMEOUT_MS = 2000L;
38+
private static final long ANIMATION_SETTLE_TIMEOUT_MS = 1500L;
39+
private static final int ANIMATION_FRAME_DELAY_MS = 180;
40+
private static final String FRAME_MANIFEST_SUFFIX = "-frames.manifest";
2941

3042
private static final Map<String, String> SCREENSHOT_NAME_OVERRIDES = createScreenshotNameOverrides();
3143
private static final Set<String> OVERRIDE_FILE_NAMES = new HashSet<>(SCREENSHOT_NAME_OVERRIDES.values());
@@ -36,24 +48,40 @@ public class AnimationDemosScreenshotTest extends AbstractTest {
3648
public boolean runTest() throws Exception {
3749
clearPreviousScreenshots();
3850

39-
Form host = new Form(HOST_TITLE);
40-
host.show();
41-
TestUtils.waitForFormTitle(HOST_TITLE, FORM_TIMEOUT_MS);
51+
boolean previousSlowMotion = Motion.isSlowMotion();
52+
Motion.setSlowMotion(true);
53+
try {
54+
Form host = new Form(HOST_TITLE);
55+
host.show();
56+
TestUtils.waitForFormTitle(HOST_TITLE, FORM_TIMEOUT_MS);
4257

43-
for (Demo demo : DemoRegistry.getDemos()) {
44-
Form previous = Display.getInstance().getCurrent();
45-
demo.show(host);
46-
Form demoForm = waitForFormChange(previous);
47-
waitForFormReady(demoForm);
58+
for (Demo demo : DemoRegistry.getDemos()) {
59+
Form previous = Display.getInstance().getCurrent();
60+
demo.show(host);
61+
Form demoForm = waitForFormChange(previous);
62+
waitForFormReady(demoForm);
4863

49-
Image screenshot = capture(demoForm);
50-
saveScreenshot(storageKeyFor(demo.getTitle()), screenshot);
64+
waitForAnimationsToFinish(demoForm);
5165

52-
host.show();
53-
waitForHost(host);
54-
}
66+
triggerAnimationIfNeeded(demo, demoForm);
67+
Form activeForm = ensureCurrentFormReady(demoForm);
5568

56-
return true;
69+
if (waitForAnimationStart(activeForm, ANIMATION_CAPTURE_TIMEOUT_MS)) {
70+
captureAnimationFrames(demo, activeForm);
71+
finalizeAnimations(activeForm);
72+
} else {
73+
Image screenshot = capture(activeForm);
74+
saveScreenshot(storageKeyFor(demo.getTitle()), screenshot);
75+
}
76+
77+
host.show();
78+
waitForHost(host);
79+
}
80+
81+
return true;
82+
} finally {
83+
Motion.setSlowMotion(previousSlowMotion);
84+
}
5785
}
5886

5987
private void clearPreviousScreenshots() {
@@ -134,6 +162,169 @@ private void waitForHost(Form host) {
134162
TestUtils.waitFor(200);
135163
}
136164

165+
private void triggerAnimationIfNeeded(Demo demo, Form form) {
166+
if (demo == null || form == null) {
167+
return;
168+
}
169+
170+
if (demo instanceof LayoutAnimationsDemo) {
171+
clickButton(form, "Fall");
172+
} else if (demo instanceof UnlayoutAnimationsDemo) {
173+
clickButton(form, "Fall");
174+
} else if (demo instanceof HiddenComponentDemo) {
175+
clickButton(form, "Hide It");
176+
} else if (demo instanceof AnimationSynchronicityDemo) {
177+
clickButton(form, "Run Sequence");
178+
} else if (demo instanceof ReplaceTransitionDemo) {
179+
clickButton(form, "Replace Pending");
180+
} else if (demo instanceof SlideTransitionsDemo) {
181+
clickButton(form, "Show");
182+
} else if (demo instanceof BubbleTransitionDemo) {
183+
clickButton(form, "+");
184+
} else if (demo instanceof SwipeBackSupportDemo) {
185+
clickButton(form, "Open Destination");
186+
}
187+
}
188+
189+
private void clickButton(Component root, String text) {
190+
Button button = findButton(root, text);
191+
if (button != null) {
192+
button.pressed();
193+
button.released();
194+
TestUtils.waitFor(200);
195+
}
196+
}
197+
198+
private Button findButton(Component component, String text) {
199+
if (component instanceof Button) {
200+
Button button = (Button) component;
201+
if (text.equals(button.getText())) {
202+
return button;
203+
}
204+
}
205+
206+
if (component instanceof Form) {
207+
return findButton(((Form) component).getContentPane(), text);
208+
}
209+
210+
if (component instanceof Container) {
211+
Container container = (Container) component;
212+
int childCount = container.getComponentCount();
213+
for (int i = 0; i < childCount; i++) {
214+
Button match = findButton(container.getComponentAt(i), text);
215+
if (match != null) {
216+
return match;
217+
}
218+
}
219+
}
220+
221+
return null;
222+
}
223+
224+
private Form ensureCurrentFormReady(Form fallback) {
225+
Component current = Display.getInstance().getCurrent();
226+
if (current instanceof Form) {
227+
Form form = (Form) current;
228+
waitForFormReady(form);
229+
return form;
230+
}
231+
return fallback;
232+
}
233+
234+
private void waitForAnimationsToFinish(Form form) {
235+
if (form == null) {
236+
return;
237+
}
238+
long deadline = System.currentTimeMillis() + ANIMATION_SETTLE_TIMEOUT_MS;
239+
while (System.currentTimeMillis() <= deadline) {
240+
AnimationManager manager = form.getAnimationManager();
241+
if (manager == null || !manager.isAnimating()) {
242+
break;
243+
}
244+
form.animate();
245+
TestUtils.waitFor(50);
246+
}
247+
}
248+
249+
private void captureAnimationFrames(Demo demo, Form form) throws IOException {
250+
String sanitized = sanitizeFileName(demo.getTitle());
251+
String baseKey = storageKeyFor(demo.getTitle());
252+
boolean baseSaved = false;
253+
List<String> frameKeys = new ArrayList<>(FRAMES_PER_ANIMATION);
254+
Image finalFrameImage = null;
255+
256+
for (int frameIndex = 0; frameIndex < FRAMES_PER_ANIMATION; frameIndex++) {
257+
if (!isAnimating(form) && frameIndex == 0) {
258+
break;
259+
}
260+
261+
Image frameImage = capture(form);
262+
if (!baseSaved) {
263+
saveScreenshot(baseKey, frameImage);
264+
baseSaved = true;
265+
}
266+
267+
String frameKey = stageStorageKeyFor(sanitized, frameIndex);
268+
saveScreenshot(frameKey, frameImage);
269+
frameKeys.add(frameKey);
270+
finalFrameImage = frameImage;
271+
272+
if (frameIndex >= FRAMES_PER_ANIMATION - 1) {
273+
break;
274+
}
275+
276+
if (!advanceAnimation(form)) {
277+
finalFrameImage = capture(form);
278+
break;
279+
}
280+
}
281+
282+
if (!baseSaved) {
283+
Image screenshot = capture(form);
284+
saveScreenshot(baseKey, screenshot);
285+
finalFrameImage = screenshot;
286+
}
287+
288+
if (finalFrameImage == null) {
289+
finalFrameImage = capture(form);
290+
}
291+
292+
while (frameKeys.size() < FRAMES_PER_ANIMATION) {
293+
String frameKey = stageStorageKeyFor(sanitized, frameKeys.size());
294+
saveScreenshot(frameKey, finalFrameImage);
295+
frameKeys.add(frameKey);
296+
}
297+
298+
if (!frameKeys.isEmpty()) {
299+
recordFrameManifest(sanitized, frameKeys);
300+
}
301+
}
302+
303+
private String stageStorageKeyFor(String sanitizedTitle, int frame) {
304+
return STORAGE_PREFIX + sanitizedTitle + "-frame-" + (frame + 1) + ".png";
305+
}
306+
307+
private void recordFrameManifest(String sanitizedTitle, List<String> frameKeys) {
308+
if (sanitizedTitle == null || frameKeys == null || frameKeys.isEmpty()) {
309+
return;
310+
}
311+
312+
String manifestKey = STORAGE_PREFIX + sanitizedTitle + FRAME_MANIFEST_SUFFIX;
313+
storage.deleteStorageFile(manifestKey);
314+
315+
Map<String, Object> manifest = new HashMap<>();
316+
manifest.put("frames", new ArrayList<>(frameKeys));
317+
318+
List<Integer> comparableFrames = new ArrayList<>(2);
319+
comparableFrames.add(Integer.valueOf(0));
320+
if (frameKeys.size() > 1) {
321+
comparableFrames.add(Integer.valueOf(frameKeys.size() - 1));
322+
}
323+
manifest.put("compareFrames", comparableFrames);
324+
325+
storage.writeObject(manifestKey, manifest);
326+
}
327+
137328
private static Map<String, String> createScreenshotNameOverrides() {
138329
Map<String, String> map = new HashMap<>();
139330
map.put("Layout Animations", "layout-animation-1.png");
@@ -148,6 +339,51 @@ private String sanitizeFileName(String value) {
148339
return sanitized.isEmpty() ? "demo-screenshot" : sanitized;
149340
}
150341

342+
private void finalizeAnimations(Form form) {
343+
if (form == null) {
344+
return;
345+
}
346+
form.animate();
347+
waitForAnimationsToFinish(form);
348+
}
349+
350+
private boolean waitForAnimationStart(Form form, long timeoutMs) {
351+
if (form == null) {
352+
return false;
353+
}
354+
long deadline = System.currentTimeMillis() + timeoutMs;
355+
while (System.currentTimeMillis() <= deadline) {
356+
if (isAnimating(form)) {
357+
return true;
358+
}
359+
form.animate();
360+
TestUtils.waitFor(50);
361+
}
362+
return isAnimating(form);
363+
}
364+
365+
private boolean isAnimating(Form form) {
366+
if (form == null) {
367+
return false;
368+
}
369+
AnimationManager manager = form.getAnimationManager();
370+
return manager != null && manager.isAnimating();
371+
}
372+
373+
private boolean advanceAnimation(Form form) {
374+
if (form == null) {
375+
return false;
376+
}
377+
long deadline = System.currentTimeMillis() + ANIMATION_FRAME_DELAY_MS;
378+
boolean animating = isAnimating(form);
379+
while (System.currentTimeMillis() <= deadline && animating) {
380+
form.animate();
381+
TestUtils.waitFor(20);
382+
animating = isAnimating(form);
383+
}
384+
return animating;
385+
}
386+
151387
@Override
152388
public boolean shouldExecuteOnEDT() {
153389
return true;

0 commit comments

Comments
 (0)