44import com .codename1 .io .Util ;
55import com .codename1 .testing .AbstractTest ;
66import 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 ;
711import com .codename1 .ui .Display ;
812import com .codename1 .ui .Form ;
913import com .codename1 .ui .Image ;
14+ import com .codename1 .ui .animations .ComponentAnimation ;
15+ import com .codename1 .ui .animations .Motion ;
1016import com .codename1 .ui .util .ImageIO ;
1117import com .codenameone .developerguide .Demo ;
1218import com .codenameone .developerguide .DemoRegistry ;
1319
1420import java .io .IOException ;
1521import 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 ;
1627import java .util .HashMap ;
1728import java .util .HashSet ;
29+ import java .util .IdentityHashMap ;
30+ import java .util .LinkedHashSet ;
31+ import java .util .List ;
1832import java .util .Map ;
1933import 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