Skip to content

Commit 73f8548

Browse files
committed
Capture multiple frames in animation demo screenshots
1 parent 81d5ea8 commit 73f8548

File tree

1 file changed

+258
-2
lines changed

1 file changed

+258
-2
lines changed

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

Lines changed: 258 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@
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.ComponentAnimation;
15+
import com.codename1.ui.animations.Motion;
1016
import com.codename1.ui.util.ImageIO;
1117
import com.codenameone.developerguide.Demo;
1218
import com.codenameone.developerguide.DemoRegistry;
1319

1420
import java.io.IOException;
1521
import java.io.OutputStream;
22+
import java.lang.reflect.Array;
23+
import java.lang.reflect.Field;
24+
import java.lang.reflect.Modifier;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
1627
import java.util.HashMap;
1728
import java.util.HashSet;
29+
import java.util.IdentityHashMap;
30+
import java.util.LinkedHashSet;
31+
import java.util.List;
1832
import java.util.Map;
1933
import java.util.Set;
2034

@@ -26,6 +40,7 @@ public class AnimationDemosScreenshotTest extends AbstractTest {
2640
private static final String HOST_TITLE = "Demo Test Host";
2741
private static final long FORM_TIMEOUT_MS = 10000L;
2842
private static final String STORAGE_PREFIX = "developer-guide.animations.";
43+
private static final int FRAMES_PER_ANIMATION = 6;
2944

3045
private static final Map<String, String> SCREENSHOT_NAME_OVERRIDES = createScreenshotNameOverrides();
3146
private static final Set<String> OVERRIDE_FILE_NAMES = new HashSet<>(SCREENSHOT_NAME_OVERRIDES.values());
@@ -46,8 +61,18 @@ public boolean runTest() throws Exception {
4661
Form demoForm = waitForFormChange(previous);
4762
waitForFormReady(demoForm);
4863

49-
Image screenshot = capture(demoForm);
50-
saveScreenshot(storageKeyFor(demo.getTitle()), screenshot);
64+
triggerAnimationIfNeeded(demo, demoForm);
65+
Form activeForm = ensureCurrentFormReady(demoForm);
66+
AnimationContext context = waitForAnimationContext(activeForm);
67+
68+
if (context != null && context.hasMotions()) {
69+
captureAnimationFrames(demo, activeForm, context);
70+
} else {
71+
Image screenshot = capture(activeForm);
72+
saveScreenshot(storageKeyFor(demo.getTitle()), screenshot);
73+
}
74+
75+
flushAnimations(activeForm);
5176

5277
host.show();
5378
waitForHost(host);
@@ -134,6 +159,237 @@ private void waitForHost(Form host) {
134159
TestUtils.waitFor(200);
135160
}
136161

162+
private void triggerAnimationIfNeeded(Demo demo, Form form) {
163+
if (demo == null || form == null) {
164+
return;
165+
}
166+
167+
if (demo instanceof LayoutAnimationsDemo) {
168+
clickButton(form, "Fall");
169+
} else if (demo instanceof UnlayoutAnimationsDemo) {
170+
clickButton(form, "Fall");
171+
} else if (demo instanceof HiddenComponentDemo) {
172+
clickButton(form, "Hide It");
173+
} else if (demo instanceof AnimationSynchronicityDemo) {
174+
clickButton(form, "Run Sequence");
175+
} else if (demo instanceof ReplaceTransitionDemo) {
176+
clickButton(form, "Replace Pending");
177+
} else if (demo instanceof SlideTransitionsDemo) {
178+
clickButton(form, "Show");
179+
} else if (demo instanceof BubbleTransitionDemo) {
180+
clickButton(form, "+");
181+
} else if (demo instanceof SwipeBackSupportDemo) {
182+
clickButton(form, "Open Destination");
183+
}
184+
}
185+
186+
private void clickButton(Component root, String text) {
187+
Button button = findButton(root, text);
188+
if (button != null) {
189+
button.pressed();
190+
button.released();
191+
TestUtils.waitFor(200);
192+
}
193+
}
194+
195+
private Button findButton(Component component, String text) {
196+
if (component instanceof Button) {
197+
Button button = (Button) component;
198+
if (text.equals(button.getText())) {
199+
return button;
200+
}
201+
}
202+
203+
if (component instanceof Form) {
204+
return findButton(((Form) component).getContentPane(), text);
205+
}
206+
207+
if (component instanceof Container) {
208+
Container container = (Container) component;
209+
int childCount = container.getComponentCount();
210+
for (int i = 0; i < childCount; i++) {
211+
Button match = findButton(container.getComponentAt(i), text);
212+
if (match != null) {
213+
return match;
214+
}
215+
}
216+
}
217+
218+
return null;
219+
}
220+
221+
private Form ensureCurrentFormReady(Form fallback) {
222+
Component current = Display.getInstance().getCurrent();
223+
if (current instanceof Form) {
224+
Form form = (Form) current;
225+
waitForFormReady(form);
226+
return form;
227+
}
228+
return fallback;
229+
}
230+
231+
private AnimationContext waitForAnimationContext(Form form) {
232+
if (form == null) {
233+
return AnimationContext.empty();
234+
}
235+
long deadline = System.currentTimeMillis() + FORM_TIMEOUT_MS;
236+
AnimationContext context = collectAnimationContext(form);
237+
while (!context.hasMotions() && System.currentTimeMillis() <= deadline) {
238+
TestUtils.waitFor(100);
239+
context = collectAnimationContext(form);
240+
}
241+
return context;
242+
}
243+
244+
private AnimationContext collectAnimationContext(Form form) {
245+
if (form == null) {
246+
return AnimationContext.empty();
247+
}
248+
try {
249+
AnimationManager manager = form.getAnimationManager();
250+
if (manager == null) {
251+
return AnimationContext.empty();
252+
}
253+
List<ComponentAnimation> animations = getComponentAnimations(manager);
254+
if (animations.isEmpty()) {
255+
return new AnimationContext(animations, Collections.emptyList());
256+
}
257+
Set<Motion> motions = new LinkedHashSet<>();
258+
Set<Object> visited = Collections.newSetFromMap(new IdentityHashMap<>());
259+
for (ComponentAnimation animation : animations) {
260+
collectMotions(animation, motions, visited, 0);
261+
}
262+
return new AnimationContext(animations, new ArrayList<>(motions));
263+
} catch (Exception err) {
264+
return AnimationContext.empty();
265+
}
266+
}
267+
268+
private List<ComponentAnimation> getComponentAnimations(AnimationManager manager) throws NoSuchFieldException, IllegalAccessException {
269+
Field field = AnimationManager.class.getDeclaredField("anims");
270+
field.setAccessible(true);
271+
Object value = field.get(manager);
272+
if (value instanceof List) {
273+
@SuppressWarnings("unchecked")
274+
List<ComponentAnimation> animations = new ArrayList<>((List<ComponentAnimation>) value);
275+
animations.removeIf(item -> item == null);
276+
return animations;
277+
}
278+
return Collections.emptyList();
279+
}
280+
281+
private void collectMotions(Object candidate, Set<Motion> motions, Set<Object> visited, int depth) throws IllegalAccessException {
282+
if (candidate == null || visited.contains(candidate)) {
283+
return;
284+
}
285+
visited.add(candidate);
286+
287+
if (candidate instanceof Motion) {
288+
motions.add((Motion) candidate);
289+
return;
290+
}
291+
292+
if (depth > 6) {
293+
return;
294+
}
295+
296+
Class<?> type = candidate.getClass();
297+
if (type.isArray()) {
298+
int length = Array.getLength(candidate);
299+
for (int i = 0; i < length; i++) {
300+
collectMotions(Array.get(candidate, i), motions, visited, depth + 1);
301+
}
302+
return;
303+
}
304+
305+
if (candidate instanceof Iterable) {
306+
for (Object element : (Iterable<?>) candidate) {
307+
collectMotions(element, motions, visited, depth + 1);
308+
}
309+
return;
310+
}
311+
312+
if (!type.getName().startsWith("com.codename1")) {
313+
return;
314+
}
315+
316+
while (type != null && type != Object.class) {
317+
Field[] fields = type.getDeclaredFields();
318+
for (Field field : fields) {
319+
if (Modifier.isStatic(field.getModifiers())) {
320+
continue;
321+
}
322+
field.setAccessible(true);
323+
collectMotions(field.get(candidate), motions, visited, depth + 1);
324+
}
325+
type = type.getSuperclass();
326+
}
327+
}
328+
329+
private void captureAnimationFrames(Demo demo, Form form, AnimationContext context) throws IOException {
330+
String sanitized = sanitizeFileName(demo.getTitle());
331+
String baseKey = storageKeyFor(demo.getTitle());
332+
boolean baseSaved = false;
333+
334+
for (int frame = 0; frame < FRAMES_PER_ANIMATION; frame++) {
335+
double progress = FRAMES_PER_ANIMATION == 1 ? 1.0 : (double) frame / (FRAMES_PER_ANIMATION - 1);
336+
advanceMotions(context.motions, progress);
337+
refreshAnimations(context.componentAnimations);
338+
Image frameImage = capture(form);
339+
if (!baseSaved) {
340+
saveScreenshot(baseKey, frameImage);
341+
baseSaved = true;
342+
}
343+
saveScreenshot(stageStorageKeyFor(sanitized, frame), frameImage);
344+
}
345+
}
346+
347+
private void advanceMotions(List<Motion> motions, double progress) {
348+
for (Motion motion : motions) {
349+
int duration = Math.max(motion.getDuration(), 0);
350+
long targetTime = progress >= 1.0 ? duration : Math.round(duration * progress);
351+
motion.setCurrentMotionTime(targetTime);
352+
}
353+
}
354+
355+
private void refreshAnimations(List<ComponentAnimation> animations) {
356+
for (ComponentAnimation animation : animations) {
357+
animation.updateAnimationState();
358+
}
359+
}
360+
361+
private String stageStorageKeyFor(String sanitizedTitle, int frame) {
362+
return STORAGE_PREFIX + sanitizedTitle + "-frame-" + (frame + 1) + ".png";
363+
}
364+
365+
private void flushAnimations(Form form) {
366+
if (form == null) {
367+
return;
368+
}
369+
AnimationManager manager = form.getAnimationManager();
370+
if (manager != null) {
371+
manager.flush();
372+
}
373+
}
374+
375+
private static final class AnimationContext {
376+
private final List<ComponentAnimation> componentAnimations;
377+
private final List<Motion> motions;
378+
379+
private AnimationContext(List<ComponentAnimation> componentAnimations, List<Motion> motions) {
380+
this.componentAnimations = componentAnimations;
381+
this.motions = motions;
382+
}
383+
384+
static AnimationContext empty() {
385+
return new AnimationContext(Collections.emptyList(), Collections.emptyList());
386+
}
387+
388+
boolean hasMotions() {
389+
return motions != null && !motions.isEmpty();
390+
}
391+
}
392+
137393
private static Map<String, String> createScreenshotNameOverrides() {
138394
Map<String, String> map = new HashMap<>();
139395
map.put("Layout Animations", "layout-animation-1.png");

0 commit comments

Comments
 (0)