diff --git a/CodenameOne/src/com/codename1/components/ImageViewer.java b/CodenameOne/src/com/codename1/components/ImageViewer.java index 8c013e682a..da4df70257 100644 --- a/CodenameOne/src/com/codename1/components/ImageViewer.java +++ b/CodenameOne/src/com/codename1/components/ImageViewer.java @@ -24,6 +24,8 @@ import com.codename1.ui.Component; import com.codename1.ui.Display; +import com.codename1.ui.Font; +import com.codename1.ui.FontImage; import com.codename1.ui.Form; import com.codename1.ui.Graphics; import com.codename1.ui.Image; @@ -36,6 +38,7 @@ import com.codename1.ui.list.DefaultListModel; import com.codename1.ui.list.ListModel; import com.codename1.ui.plaf.Style; +import com.codename1.ui.plaf.UIManager; /// ImageViewer allows zooming/panning an image and potentially flicking between multiple images /// within a list of images. @@ -66,6 +69,20 @@ /// hi.add(BorderLayout.CENTER, iv); /// ``` /// +/// Optional navigation affordances can be enabled when using an image list: +/// +/// ```java +/// iv.setNavigationArrowsVisible(true); +/// iv.setThumbnailsVisible(true); +/// iv.setThumbnailBarHeight(6f); +/// ``` +/// +/// These options can also be configured globally using theme constants: +/// +/// - `imageviewerNavigationArrowsBool` +/// - `imageviewerThumbnailsBool` +/// - `imageviewerThumbnailBarHeightMM` +/// /// You can even download image URL's dynamically into the `ImageViewer` thanks to the usage of the /// `com.codename1.ui.list.ListModel`. E.g. in this model book cover images are downloaded dynamically: /// @@ -192,12 +209,37 @@ public class ImageViewer extends Component { private boolean cycleLeft = true; private boolean cycleRight = true; private boolean isPinchZooming; + private boolean navigationArrowsVisible; + private boolean thumbnailsVisible; + private float thumbnailBarHeightMM = 6f; + private int pointerPressedAction; + private int pointerPressedThumbnailIndex = -1; /// Default constructor public ImageViewer() { setFocusable(true); setUIIDFinal("ImageViewer"); getAllStyles().setBgTransparency(0x0); + initThemeConstants(); + } + + private void initThemeConstants() { + UIManager manager = getUIManager(); + String uiidPrefix = getUIID().toLowerCase(); + navigationArrowsVisible = manager.isThemeConstant(uiidPrefix + "NavigationArrowsBool", navigationArrowsVisible); + thumbnailsVisible = manager.isThemeConstant(uiidPrefix + "ThumbnailsBool", thumbnailsVisible); + thumbnailBarHeightMM = parseFloatThemeConstant(manager.getThemeConstant(uiidPrefix + "ThumbnailBarHeightMM", Float.toString(thumbnailBarHeightMM)), thumbnailBarHeightMM); + } + + private static float parseFloatThemeConstant(String value, float defaultValue) { + if (value == null || value.length() == 0) { + return defaultValue; + } + try { + return Float.parseFloat(value); + } catch (NumberFormatException ex) { + return defaultValue; + } } /// Initializes the component with an image @@ -219,7 +261,7 @@ protected void resetFocusable() { /// {@inheritDoc} @Override public String[] getPropertyNames() { - return new String[]{"eagerLock", "image", "imageList", "swipePlaceholder"}; + return new String[]{"eagerLock", "image", "imageList", "swipePlaceholder", "navigationArrowsVisible", "thumbnailsVisible"}; } /// {@inheritDoc} @@ -232,13 +274,13 @@ protected boolean shouldBlockSideSwipe() { @Override public Class[] getPropertyTypes() { return new Class[]{Boolean.class, Image.class, - com.codename1.impl.CodenameOneImplementation.getImageArrayClass(), Image.class}; + com.codename1.impl.CodenameOneImplementation.getImageArrayClass(), Image.class, Boolean.class, Boolean.class}; } /// {@inheritDoc} @Override public String[] getPropertyTypeNames() { - return new String[]{"Boolean", "Image", "Image[]", "Image"}; + return new String[]{"Boolean", "Image", "Image[]", "Image", "Boolean", "Boolean"}; } /// {@inheritDoc} @@ -267,6 +309,12 @@ public Object getPropertyValue(String name) { if ("swipePlaceholder".equals(name)) { return getSwipePlaceholder(); } + if ("navigationArrowsVisible".equals(name)) { + return isNavigationArrowsVisible() ? Boolean.TRUE : Boolean.FALSE; + } + if ("thumbnailsVisible".equals(name)) { + return isThumbnailsVisible() ? Boolean.TRUE : Boolean.FALSE; + } return null; } @@ -293,6 +341,14 @@ public String setPropertyValue(String name, Object value) { setSwipePlaceholder((Image) value); return null; } + if ("navigationArrowsVisible".equals(name)) { + setNavigationArrowsVisible(value != null && ((Boolean) value).booleanValue()); + return null; + } + if ("thumbnailsVisible".equals(name)) { + setThumbnailsVisible(value != null && ((Boolean) value).booleanValue()); + return null; + } return super.setPropertyValue(name, value); } @@ -384,6 +440,8 @@ public void keyReleased(int key) { public void pointerPressed(int x, int y) { pressX = x; pressY = y; + pointerPressedAction = getPointerActionAt(x, y); + pointerPressedThumbnailIndex = getThumbnailIndexAt(x, y); currentZoom = zoom; getComponentForm().addComponentAwaitingRelease(this); } @@ -418,6 +476,28 @@ private Image getImageLeft() { public void pointerReleased(int x, int y) { super.pointerReleased(x, y); isPinchZooming = false; + int releaseAction = getPointerActionAt(x, y); + if (pointerPressedAction != ACTION_NONE && pointerPressedAction == releaseAction) { + if (pointerPressedAction == ACTION_LEFT) { + navigateToLeft(); + pointerPressedAction = ACTION_NONE; + return; + } + if (pointerPressedAction == ACTION_RIGHT) { + navigateToRight(); + pointerPressedAction = ACTION_NONE; + return; + } + if (pointerPressedAction == ACTION_THUMBNAIL) { + int thumbnailIndex = getThumbnailIndexAt(x, y); + if (thumbnailIndex == pointerPressedThumbnailIndex) { + navigateTo(thumbnailIndex); + } + pointerPressedAction = ACTION_NONE; + return; + } + } + pointerPressedAction = ACTION_NONE; if (panPositionX > 1) { if (panPositionX >= 1 + swipeThreshold && (cycleRight || swipeableImages.getSelectedIndex() < getImageRightPos())) { new AnimatePanX(2, getImageRight(), getImageRightPos()); @@ -448,6 +528,9 @@ protected void pinchReleased(int x, int y) { /// {@inheritDoc} @Override public void pointerDragged(int x, int y) { + if (pointerPressedAction != ACTION_NONE) { + return; + } // could be a pan float distanceX = ((float) pressX - x) / getZoom(); float distanceY = ((float) pressY - y) / getZoom(); @@ -765,6 +848,7 @@ public void paint(Graphics g) { g.drawImage(left, ((int) ratio) + getX() + prefX, getY() + prefY, prefW, prefH); g.setRenderingHints(0); } + drawNavigationChrome(g); return; } if (panPositionX > 1) { @@ -791,6 +875,7 @@ public void paint(Graphics g) { g.drawImage(right, ((int) ratio) + getX() + prefX, getY() + prefY, prefW, prefH); g.setRenderingHints(0); } + drawNavigationChrome(g); return; } // can happen in the GUI builder @@ -816,6 +901,138 @@ public void paint(Graphics g) { g.setRenderingHints(0); } + drawNavigationChrome(g); + } + + private static final int ACTION_NONE = 0; + private static final int ACTION_LEFT = 1; + private static final int ACTION_RIGHT = 2; + private static final int ACTION_THUMBNAIL = 3; + + private boolean canNavigate() { + return swipeableImages != null && swipeableImages.getSize() > 1; + } + + private void navigateToLeft() { + if (!canNavigate()) { + return; + } + if (cycleLeft || swipeableImages.getSelectedIndex() > getImageLeftPos()) { + new AnimatePanX(-1, getImageLeft(), getImageLeftPos()); + } + } + + private void navigateToRight() { + if (!canNavigate()) { + return; + } + if (cycleRight || swipeableImages.getSelectedIndex() < getImageRightPos()) { + new AnimatePanX(2, getImageRight(), getImageRightPos()); + } + } + + private void navigateTo(int index) { + if (!canNavigate() || index < 0 || index >= swipeableImages.getSize()) { + return; + } + if (index != swipeableImages.getSelectedIndex()) { + swipeableImages.setSelectedIndex(index); + } + } + + private int getPointerActionAt(int x, int y) { + if (!canNavigate()) { + return ACTION_NONE; + } + if (thumbnailsVisible && getThumbnailIndexAt(x, y) > -1) { + return ACTION_THUMBNAIL; + } + if (navigationArrowsVisible) { + int leftBound = getX() + Math.max(24, getWidth() / 8); + int rightBound = getX() + getWidth() - Math.max(24, getWidth() / 8); + if (x <= leftBound) { + return ACTION_LEFT; + } + if (x >= rightBound) { + return ACTION_RIGHT; + } + } + return ACTION_NONE; + } + + private int getThumbnailIndexAt(int x, int y) { + if (!thumbnailsVisible || !canNavigate()) { + return -1; + } + int barHeight = Math.max(24, Display.getInstance().convertToPixels(thumbnailBarHeightMM)); + int barTop = getY() + getHeight() - barHeight; + if (y < barTop || y > getY() + getHeight()) { + return -1; + } + int size = swipeableImages.getSize(); + int availableWidth = getWidth() - 8; + int perThumbWidth = Math.max(14, availableWidth / size); + int startX = getX() + (getWidth() - (perThumbWidth * size)) / 2; + if (x < startX || x > startX + perThumbWidth * size) { + return -1; + } + int index = (x - startX) / perThumbWidth; + if (index < 0 || index >= size) { + return -1; + } + return index; + } + + private void drawNavigationChrome(Graphics g) { + if (!canNavigate()) { + return; + } + if (navigationArrowsVisible) { + drawArrow(g, true); + drawArrow(g, false); + } + if (thumbnailsVisible) { + drawThumbnails(g); + } + } + + private void drawArrow(Graphics g, boolean left) { + int diameter = 28; + int radius = diameter / 2; + int centerX = left ? getX() + 16 : getX() + getWidth() - 16; + int centerY = getY() + getHeight() / 2; + g.setColor(0x66000000); + g.fillArc(centerX - radius, centerY - radius, diameter, diameter, 0, 360); + g.setColor(0xffffff); + char icon = left ? FontImage.MATERIAL_CHEVRON_LEFT : FontImage.MATERIAL_CHEVRON_RIGHT; + String iconText = String.valueOf(icon); + Font oldFont = g.getFont(); + g.setFont(FontImage.getMaterialDesignFont()); + Font iconFont = g.getFont(); + int iconX = centerX - iconFont.stringWidth(iconText) / 2; + int iconY = centerY - iconFont.getHeight() / 2; + g.drawString(iconText, iconX, iconY); + g.setFont(oldFont); + } + + private void drawThumbnails(Graphics g) { + int size = swipeableImages.getSize(); + int barHeight = Math.max(24, Display.getInstance().convertToPixels(thumbnailBarHeightMM)); + int y = getY() + getHeight() - barHeight; + g.setColor(0x66000000); + g.fillRect(getX(), y, getWidth(), barHeight); + int availableWidth = getWidth() - 8; + int perThumbWidth = Math.max(14, availableWidth / size); + int thumbMargin = 2; + int startX = getX() + (getWidth() - (perThumbWidth * size)) / 2; + int thumbHeight = barHeight - 6; + for (int i = 0; i < size; i++) { + int tx = startX + i * perThumbWidth; + Image thumb = swipeableImages.getItemAt(i); + g.drawImage(thumb, tx + thumbMargin, y + 3, perThumbWidth - thumbMargin * 2, thumbHeight); + g.setColor(i == swipeableImages.getSelectedIndex() ? 0xffffff : 0x99ffffff); + g.drawRect(tx + thumbMargin, y + 3, perThumbWidth - thumbMargin * 2, thumbHeight); + } } /// {@inheritDoc} @@ -938,6 +1155,63 @@ public boolean isAnimatedZoom() { return animateZoom; } + /// Indicates if side navigation arrows should be painted for moving between images. + /// + /// #### Returns + /// + /// `true` if side navigation arrows are visible. + public boolean isNavigationArrowsVisible() { + return navigationArrowsVisible; + } + + /// Enables side navigation arrows (material font icons) for moving between images. + /// + /// #### Parameters + /// + /// - `navigationArrowsVisible`: `true` to show side navigation arrows. + public void setNavigationArrowsVisible(boolean navigationArrowsVisible) { + this.navigationArrowsVisible = navigationArrowsVisible; + repaint(); + } + + /// Indicates if thumbnails should be painted in a strip at the bottom for direct image selection. + /// + /// #### Returns + /// + /// `true` if the thumbnail strip is visible. + public boolean isThumbnailsVisible() { + return thumbnailsVisible; + } + + /// Enables a bottom thumbnail strip for direct image selection. + /// + /// #### Parameters + /// + /// - `thumbnailsVisible`: `true` to show thumbnails. + public void setThumbnailsVisible(boolean thumbnailsVisible) { + this.thumbnailsVisible = thumbnailsVisible; + repaint(); + } + + /// Gets the thumbnail strip height in millimeters. + /// + /// #### Returns + /// + /// Height of the thumbnail strip in millimeters. + public float getThumbnailBarHeight() { + return thumbnailBarHeightMM; + } + + /// Sets the thumbnail strip height in millimeters. + /// + /// #### Parameters + /// + /// - `thumbnailBarHeight`: Height of the thumbnail strip in millimeters. + public void setThumbnailBarHeight(float thumbnailBarHeight) { + this.thumbnailBarHeightMM = Math.max(1f, thumbnailBarHeight); + repaint(); + } + /// Manipulate the zoom level of the application /// /// #### Returns diff --git a/docs/developer-guide/Advanced-Theming.asciidoc b/docs/developer-guide/Advanced-Theming.asciidoc index 61e5b3753a..47c5bf243d 100644 --- a/docs/developer-guide/Advanced-Theming.asciidoc +++ b/docs/developer-guide/Advanced-Theming.asciidoc @@ -190,6 +190,15 @@ SoftKey, Touch, Bar, Title, Right, Native |iconUiid |Allows theming the icon component used by `SpanLabel`, `SpanButton`, `MultiButton`, and `SpanMultiButton` when they fetch icon styling via theme constants. +|imageviewerNavigationArrowsBool +|Enables side navigation arrows by default for `ImageViewer` instances using the `ImageViewer` UIID (or a lowercased custom UIID prefix). + +|imageviewerThumbnailBarHeightMM +|Sets the default thumbnail strip height (in millimeters) for `ImageViewer` navigation thumbnails. + +|imageviewerThumbnailsBool +|Enables the bottom thumbnail strip by default for `ImageViewer` instances using the `ImageViewer` UIID (or a lowercased custom UIID prefix). + |textUiid |Overrides the text component UIID for `SpanLabel`, `SpanButton`, `MultiButton`, and `SpanMultiButton` instances when provided as a theme constant. diff --git a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc index 76c910b107..3e8c743241 100644 --- a/docs/developer-guide/The-Components-Of-Codename-One.asciidoc +++ b/docs/developer-guide/The-Components-Of-Codename-One.asciidoc @@ -2455,6 +2455,26 @@ image::img/components-imageviewer-multi.png[An ImageViewer with multiple element Notice that we use a https://www.codenameone.com/javadoc/com/codename1/ui/list/ListModel.html[ListModel] to allow swiping between images. +From Codename One 9.0, `ImageViewer` also supports optional side arrows (material font icons) and an optional thumbnail strip for direct image navigation: + +[source,java] +---- +ImageViewer iv = new ImageViewer(red); +iv.setImageList(new DefaultListModel<>(red, green, blue, gray)); +iv.setNavigationArrowsVisible(true); +iv.setThumbnailsVisible(true); +iv.setThumbnailBarHeight(6f); // Optional, defaults to 6mm +---- + +When arrows are enabled, tapping near the left/right edge will move to the previous/next image. +When thumbnails are enabled, tapping a thumbnail jumps directly to that image. + +All three options can be configured with theme constants (using the `ImageViewer` UIID prefix): + +- `imageviewerNavigationArrowsBool=true|false` +- `imageviewerThumbnailsBool=true|false` +- `imageviewerThumbnailBarHeightMM=` + TIP: `EncodedImage's` aren't always fully loaded and so when you swipe if the images are really large you might see delays! You can dynamically download images directly into the `ImageViewer` with a custom list model like this: diff --git a/maven/core-unittests/src/test/java/com/codename1/components/ImageViewerTest.java b/maven/core-unittests/src/test/java/com/codename1/components/ImageViewerTest.java index e4396b0923..4c182b4fc9 100644 --- a/maven/core-unittests/src/test/java/com/codename1/components/ImageViewerTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/components/ImageViewerTest.java @@ -2,6 +2,7 @@ import com.codename1.junit.FormTest; import com.codename1.junit.UITestBase; +import com.codename1.ui.Display; import com.codename1.ui.DisplayTest; import com.codename1.ui.Form; import com.codename1.ui.Image; @@ -86,13 +87,49 @@ void propertyAccessorsExposeConfiguration() { viewer.setCycleLeft(false); viewer.setCycleRight(false); viewer.setSwipeThreshold(0.6f); + viewer.setNavigationArrowsVisible(true); + viewer.setThumbnailsVisible(true); - assertArrayEquals(new String[]{"eagerLock", "image", "imageList", "swipePlaceholder"}, viewer.getPropertyNames()); + assertArrayEquals(new String[]{"eagerLock", "image", "imageList", "swipePlaceholder", "navigationArrowsVisible", "thumbnailsVisible"}, viewer.getPropertyNames()); assertSame(placeholder, viewer.getPropertyValue("swipePlaceholder")); assertFalse(viewer.isEagerLock()); assertFalse(viewer.isCycleLeft()); assertFalse(viewer.isCycleRight()); assertEquals(0.6f, viewer.getSwipeThreshold()); + assertTrue(viewer.isNavigationArrowsVisible()); + assertTrue(viewer.isThumbnailsVisible()); + viewer.setThumbnailBarHeight(7.5f); + assertEquals(7.5f, viewer.getThumbnailBarHeight()); + } + + @FormTest + void thumbnailTapNavigatesToSpecificImage() { + Image first = Image.createImage(16, 16, 0xff112233); + Image second = Image.createImage(16, 16, 0xff445566); + Image third = Image.createImage(16, 16, 0xff778899); + DefaultListModel model = new DefaultListModel<>(first, second, third); + ImageViewer viewer = new ImageViewer(first); + viewer.setImageList(model); + viewer.setThumbnailsVisible(true); + + Form f = new Form(new BorderLayout()); + f.add(BorderLayout.CENTER, viewer); + f.show(); + f.setSize(new com.codename1.ui.geom.Dimension(240, 320)); + f.layoutContainer(); + f.revalidate(); + viewer.setSize(new com.codename1.ui.geom.Dimension(240, 220)); + viewer.setX(0); + viewer.setY(0); + + int y = viewer.getY() + viewer.getHeight() - 8; + int x = viewer.getWidth() - 20; + Display.getInstance().pointerPressed(new int[]{x}, new int[]{y}); + Display.getInstance().pointerReleased(new int[]{x}, new int[]{y}); + flushSerialCalls(); + + assertEquals(2, model.getSelectedIndex()); + assertSame(third, viewer.getImage()); } @FormTest diff --git a/scripts/android/screenshots/ImageViewerNavigationModes.png b/scripts/android/screenshots/ImageViewerNavigationModes.png new file mode 100644 index 0000000000..56805503e3 Binary files /dev/null and b/scripts/android/screenshots/ImageViewerNavigationModes.png differ diff --git a/scripts/android/screenshots/Sheet.png b/scripts/android/screenshots/Sheet.png index 5878c2eb4e..86b9e67602 100644 Binary files a/scripts/android/screenshots/Sheet.png and b/scripts/android/screenshots/Sheet.png differ diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index 9a880dc4b9..84b2e16e8a 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -70,6 +70,7 @@ public final class Cn1ssDeviceRunner extends DeviceRunner { new MediaPlaybackScreenshotTest(), new OrientationLockScreenshotTest(), new SheetScreenshotTest(), + new ImageViewerNavigationScreenshotTest(), new InPlaceEditViewTest(), new BytecodeTranslatorRegressionTest(), new BackgroundThreadUiAccessTest(), diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ImageViewerNavigationScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ImageViewerNavigationScreenshotTest.java new file mode 100644 index 0000000000..76424ca65a --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/ImageViewerNavigationScreenshotTest.java @@ -0,0 +1,50 @@ +package com.codenameone.examples.hellocodenameone.tests; + +import com.codename1.components.ImageViewer; +import com.codename1.ui.Container; +import com.codename1.ui.Form; +import com.codename1.ui.Image; +import com.codename1.ui.Label; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.GridLayout; +import com.codename1.ui.list.DefaultListModel; + +public class ImageViewerNavigationScreenshotTest extends BaseTest { + + @Override + public boolean runTest() { + Form form = createForm("ImageViewer Navigation", new BorderLayout(), "ImageViewerNavigationModes"); + form.add(BorderLayout.CENTER, createModesGrid()); + form.show(); + return true; + } + + private Container createModesGrid() { + Container grid = new Container(new GridLayout(3, 2)); + grid.add(createMode("Default", false, false, 6f)); + grid.add(createMode("Arrows", true, false, 6f)); + grid.add(createMode("Thumbnails", false, true, 4f)); + grid.add(createMode("Arrows + Thumbnails", true, true, 6f)); + grid.add(createMode("Arrows + Tall Thumbs", true, true, 8f)); + grid.add(createMode("Thumbnails Only (Tall)", false, true, 9f)); + return grid; + } + + private Container createMode(String title, boolean arrows, boolean thumbnails, float thumbnailBarHeightMM) { + Image red = Image.createImage(120, 80, 0xffcc4444); + Image green = Image.createImage(120, 80, 0xff44cc44); + Image blue = Image.createImage(120, 80, 0xff4444cc); + + DefaultListModel images = new DefaultListModel<>(red, green, blue); + ImageViewer viewer = new ImageViewer(red); + viewer.setImageList(images); + viewer.setNavigationArrowsVisible(arrows); + viewer.setThumbnailsVisible(thumbnails); + viewer.setThumbnailBarHeight(thumbnailBarHeightMM); + + Container mode = new Container(new BorderLayout()); + mode.add(BorderLayout.NORTH, new Label(title)); + mode.add(BorderLayout.CENTER, viewer); + return mode; + } +} diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java index a21f48c0de..272a7b4564 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/OrientationLockScreenshotTest.java @@ -1,6 +1,5 @@ package com.codenameone.examples.hellocodenameone.tests; -import com.codename1.testing.TestUtils; import com.codename1.ui.CN; import com.codename1.ui.Form; import com.codename1.ui.Label; @@ -8,17 +7,36 @@ import com.codename1.ui.util.UITimer; public class OrientationLockScreenshotTest extends BaseTest { + private static final int ORIENTATION_POLL_INTERVAL_MS = 100; + private static final int ORIENTATION_POLL_ATTEMPTS = 25; + @Override - public boolean runTest() throws Exception { - Form hi = createForm("Orientation Lock", new BoxLayout(BoxLayout.Y_AXIS), "landscape"); + public boolean runTest() { + Form hi = new Form("Orientation Lock", new BoxLayout(BoxLayout.Y_AXIS)) { + @Override + protected void onShowCompleted() { + CN.lockOrientation(false); + UITimer.timer(300, false, this, () -> { + Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot("landscape"); + CN.lockOrientation(true); + waitForOrientation(this, true, OrientationLockScreenshotTest.this::done); + }); + } + }; hi.add(new Label("Testing orientation lock...")); - hi.show(); - - CN.lockOrientation(false); - - TestUtils.waitFor(250); - return true; } + + private void waitForOrientation(Form form, boolean portrait, Runnable onDone) { + waitForOrientation(form, portrait, ORIENTATION_POLL_ATTEMPTS, onDone); + } + + private void waitForOrientation(Form form, boolean portrait, int attemptsLeft, Runnable onDone) { + if (CN.isPortrait() == portrait || attemptsLeft <= 0) { + onDone.run(); + return; + } + UITimer.timer(ORIENTATION_POLL_INTERVAL_MS, false, form, () -> waitForOrientation(form, portrait, attemptsLeft - 1, onDone)); + } } diff --git a/scripts/ios/screenshots/ImageViewerNavigationModes.png b/scripts/ios/screenshots/ImageViewerNavigationModes.png new file mode 100644 index 0000000000..8cd5b01f97 Binary files /dev/null and b/scripts/ios/screenshots/ImageViewerNavigationModes.png differ diff --git a/scripts/ios/screenshots/Sheet.png b/scripts/ios/screenshots/Sheet.png index c920c91f6c..c79875356f 100644 Binary files a/scripts/ios/screenshots/Sheet.png and b/scripts/ios/screenshots/Sheet.png differ diff --git a/scripts/ios/screenshots/landscape.png b/scripts/ios/screenshots/landscape.png index 5cb599cd20..527af7a8dc 100644 Binary files a/scripts/ios/screenshots/landscape.png and b/scripts/ios/screenshots/landscape.png differ