Hi there!
First of all I’d like to thank the JUCE team for getting the Android windowing, insets, etc. working properly. One would expect this to be almost trivial but the amount of tedious work needed on Android for this stuff is beyond all measure. So many thanks once again!
That being said, there are a few things which are still not working perfectly, both on Android as on iOS.
One issue is being able to toggle freely between kiok mode and normal mode (that is, showing and hiding the status and navigation bars). The problem is that after doing so, rotating the device causes the wrong bounds to be set. One would expect that by setting the top level component as fullscreen the app’s bounds would be handled automatically, but this is not the case.
Below is a very simple test app which includes a simple workaround for this problem. I have tested it on different Android and iOS devices, using split mode or not, and the workaround seems to work fine. But it is clearly more of a hack, so perhaps the JUCE team could look into this and have the bounds set properly without any special intervention.
#include <JuceHeader.h>
class MainComponent : public juce::Component
{
public:
MainComponent() {}
void resized() override { repaint(); }
void paint (juce::Graphics& g) override
{
#if JUCE_IOS || JUCE_ANDROID
const auto* display = juce::Desktop::getInstance().getDisplays().getDisplayForRect (getScreenBounds());
auto safeBounds = display->safeAreaInsets.subtractedFrom (display->keyboardInsets.subtractedFrom (getLocalBounds()));
#else
auto safeBounds = getLocalBounds();
#endif
g.fillAll (isDarkStyle() ? juce::Colours::darkblue : juce::Colours::lightblue);
g.setColour (isDarkStyle() ? juce::Colours::darkgreen : juce::Colours::lightgreen);
g.fillRect (safeBounds);
g.setColour (juce::Colours::red);
g.drawRect (safeBounds.reduced (4), 3.0f);
g.setColour (isDarkStyle() ? juce::Colours::white : juce::Colours::black);
g.drawFittedText (getDisplayInfo(), safeBounds.reduced (10), juce::Justification::topLeft, 30);
}
juce::String getDisplayInfo()
{
auto* display = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay();
auto totalArea = display->totalArea.toString();
auto userArea = display->userArea.toString();
auto localBounds = getLocalBounds().toString();
auto insets = display->safeAreaInsets;
auto top = juce::String (insets.getTop());
auto bottom = juce::String (insets.getBottom());
auto left = juce::String (insets.getLeft());
auto right = juce::String (insets.getRight());
auto parentBounds = getTopLevelComponent()->getBounds().toString();
auto peerNonFullscreen = getTopLevelComponent()->getPeer()->getNonFullScreenBounds().toString();
auto kioskMode = isKioskMode() ? "on" : "off";
auto appStyle = isDarkStyle() ? "dark" : "light";
auto nl = juce::newLine;
return juce::String ("totalArea: " + totalArea + nl +
"userArea: " + userArea + nl +
"localBounds: " + localBounds + nl +
"insets:" + " top " + top + " bottom " + bottom + " left " + left + " right " + right + nl +
"parentBounds: " + parentBounds + nl +
"peer non-fullscreen bounds: " + peerNonFullscreen + nl +
"kiosk mode: " + kioskMode + " (tap to change)" + nl +
"app style: " + appStyle + " (hold to change)" );
}
bool isKioskMode()
{
return juce::Desktop::getInstance().getKioskModeComponent() != nullptr;
}
void toggleKioskMode()
{
juce::Desktop::getInstance().setKioskModeComponent (isKioskMode() ? nullptr : getTopLevelComponent());
// This ensures that disabling kiosk mode sets the correct bounds in all situations.
// See the top level component's parentSizeChanged() function.
getTopLevelComponent()->parentSizeChanged();
}
bool isDarkStyle()
{
return getTopLevelComponent()->getPeer()->getAppStyle() == juce::ComponentPeer::Style::dark;
}
void toggleAppStyle()
{
getTopLevelComponent()->getPeer()->setAppStyle (isDarkStyle() ? juce::ComponentPeer::Style::light
: juce::ComponentPeer::Style::dark);
}
void mouseUp (const juce::MouseEvent& e) override
{
if (e.mouseWasClicked() && e.getNumberOfClicks() == 1)
toggleKioskMode();
else if (e.mouseWasDraggedSinceMouseDown() && e.getDistanceFromDragStart() < 10)
toggleAppStyle();
repaint();
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
class NewProjectApplication : public juce::JUCEApplication
{
public:
NewProjectApplication() {}
const juce::String getApplicationName() override { return ProjectInfo::projectName; }
const juce::String getApplicationVersion() override { return ProjectInfo::versionString; }
bool moreThanOneInstanceAllowed() override { return false; }
void initialise (const juce::String& commandLine) override { mainWindow.reset (new MainWindow (getApplicationName())); }
void shutdown() override { mainWindow = nullptr; }
void systemRequestedQuit() override { quit(); }
class MainWindow : public juce::DocumentWindow
{
public:
MainWindow (juce::String name)
: DocumentWindow (name, juce::Colours::grey, DocumentWindow::allButtons)
{
setUsingNativeTitleBar (true);
setContentOwned (new MainComponent(), false);
#if JUCE_IOS || JUCE_ANDROID
setFullScreen (true);
#else
centreWithSize (600, 400);
#endif
setResizable (true, true);
setVisible (true);
}
#if JUCE_IOS || JUCE_ANDROID
void parentSizeChanged() override
{
DocumentWindow::parentSizeChanged();
// Without this, setting kiosk mode on and off and then rotating the device
// leads to incorrect bounds being set, both on Android and on iOS.
// This hack assumes that the app is fullscreen (checking isFullScreen()
// here doen't work). Also, a small time delay is necessary.
juce::Timer::callAfterDelay (50, [this]
{
const auto* display = juce::Desktop::getInstance().getDisplays().getDisplayForRect (getScreenBounds());
setBounds (display->userArea);
// It can be that the bounds have not changed but the safe area insets have,
// so it's best to call resized() anyways.
getContentComponent()->resized();
});
}
#endif
MainComponent& getMainComponent() { return *dynamic_cast<MainComponent*> (getContentComponent()); }
void closeButtonPressed() override { JUCEApplication::getInstance()->systemRequestedQuit(); }
~MainWindow() { clearContentComponent(); }
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
};
private:
std::unique_ptr<MainWindow> mainWindow;
};
START_JUCE_APPLICATION (NewProjectApplication)
/*
NOTE:
There is a difference between iPhones and other devices regarding the safe area insets.
On iPhones, if the status bar is displayed on landscape mode (i.e. when kiosk mode is
disabled), the reported top safe area inset is 0. This is intentional as Apple consider the
status bar as being non-intrusive in that scenario, and the status bar usually gets hidden
when entering landscape mode (note that when using JUCE you have to do so manually!). On iPads
and on Android devices, on the other hand, when the status bar is displayed on landscape mode,
the top area inset *does* reflect the height of the status bar.
*/


