Skip to content

Commit

Permalink
Added Display::safeAreaInsets and implementations for iOS and Android
Browse files Browse the repository at this point in the history
  • Loading branch information
ed95 committed Feb 8, 2021
1 parent 6d8c0b2 commit ac1425f
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 11 deletions.
11 changes: 10 additions & 1 deletion modules/juce_gui_basics/desktop/juce_Displays.h
Expand Up @@ -45,14 +45,23 @@ class JUCE_API Displays
bool isMain;

/** The total area of this display in logical pixels including any OS-dependent objects
like the taskbar, menu bar, etc. */
like the taskbar, menu bar, etc.
*/
Rectangle<int> totalArea;

/** The total area of this display in logical pixels which isn't covered by OS-dependent
objects like the taskbar, menu bar, etc.
*/
Rectangle<int> userArea;

/** Represents the area of this display in logical pixels that is not functional for
displaying content.
On mobile devices this may be the area covered by display cutouts and notches, where
you still want to draw a background but should not position important content.
*/
BorderSize<int> safeAreaInsets;

/** The top-left of this display in physical coordinates. */
Point<int> topLeftPhysical;

Expand Down
128 changes: 127 additions & 1 deletion modules/juce_gui_basics/native/juce_android_Windowing.cpp
Expand Up @@ -225,6 +225,21 @@ DECLARE_JNI_CLASS (AndroidWindowManagerLayoutParams, "android/view/WindowManager
DECLARE_JNI_CLASS (AndroidWindow, "android/view/Window")
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getDisplayCutout, "getDisplayCutout", "()Landroid/view/DisplayCutout;")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidWindowInsets, "android/view/WindowInsets", 28)
#undef JNI_CLASS_MEMBERS

#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getSafeInsetBottom, "getSafeInsetBottom", "()I") \
METHOD (getSafeInsetLeft, "getSafeInsetLeft", "()I") \
METHOD (getSafeInsetRight, "getSafeInsetRight", "()I") \
METHOD (getSafeInsetTop, "getSafeInsetTop", "()I")

DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidDisplayCutout, "android/view/DisplayCutout", 28)
#undef JNI_CLASS_MEMBERS

//==============================================================================
namespace
{
Expand All @@ -244,6 +259,30 @@ namespace
constexpr int FLAG_NOT_FOCUSABLE = 0x8;
}

//==============================================================================
static bool supportsDisplayCutout()
{
return getAndroidSDKVersion() >= 28;
}

static BorderSize<int> androidDisplayCutoutToBorderSize (LocalRef<jobject> displayCutout, double displayScale)
{
if (displayCutout.get() == nullptr)
return {};

auto* env = getEnv();

auto getInset = [&] (jmethodID methodID)
{
return roundToInt (env->CallIntMethod (displayCutout.get(), methodID) / displayScale);
};

return { getInset (AndroidDisplayCutout.getSafeInsetTop),
getInset (AndroidDisplayCutout.getSafeInsetLeft),
getInset (AndroidDisplayCutout.getSafeInsetBottom),
getInset (AndroidDisplayCutout.getSafeInsetRight) };
}

//==============================================================================
class AndroidComponentPeer : public ComponentPeer,
private Timer
Expand Down Expand Up @@ -309,7 +348,7 @@ class AndroidComponentPeer : public ComponentPeer,
env->SetIntField (windowLayoutParams.get(), AndroidWindowManagerLayoutParams.gravity, GRAVITY_LEFT | GRAVITY_TOP);
env->SetIntField (windowLayoutParams.get(), AndroidWindowManagerLayoutParams.windowAnimations, 0x01030000 /* android.R.style.Animation */);

if (getAndroidSDKVersion() >= 28)
if (supportsDisplayCutout())
{
jfieldID layoutInDisplayCutoutModeFieldId = env->GetFieldID (AndroidWindowManagerLayoutParams,
"layoutInDisplayCutoutMode",
Expand All @@ -333,6 +372,18 @@ class AndroidComponentPeer : public ComponentPeer,
env->CallVoidMethod (viewGroup.get(), AndroidViewManager.addView, view.get(), windowLayoutParams.get());
}

if (supportsDisplayCutout())
{
jmethodID setOnApplyWindowInsetsListenerMethodId = env->GetMethodID (AndroidView,
"setOnApplyWindowInsetsListener",
"(Landroid/view/View$OnApplyWindowInsetsListener;)V");

if (setOnApplyWindowInsetsListenerMethodId != nullptr)
env->CallVoidMethod (view.get(), setOnApplyWindowInsetsListenerMethodId,
CreateJavaInterface (new ViewWindowInsetsListener,
"android/view/View$OnApplyWindowInsetsListener").get());
}

if (isFocused())
handleFocusGain();
}
Expand Down Expand Up @@ -856,6 +907,61 @@ class AndroidComponentPeer : public ComponentPeer,
static void JNICALL handleAppPausedJni (JNIEnv*, jobject /*view*/, jlong host) { if (auto* myself = reinterpret_cast<AndroidComponentPeer*> (host)) myself->handleAppPausedCallback(); }
static void JNICALL handleAppResumedJni (JNIEnv*, jobject /*view*/, jlong host) { if (auto* myself = reinterpret_cast<AndroidComponentPeer*> (host)) myself->handleAppResumedCallback(); }

//==============================================================================
struct ViewWindowInsetsListener : public juce::AndroidInterfaceImplementer
{
jobject onApplyWindowInsets (LocalRef<jobject> v, LocalRef<jobject> insets)
{
auto* env = getEnv();

LocalRef<jobject> displayCutout (env->CallObjectMethod (insets.get(), AndroidWindowInsets.getDisplayCutout));

if (displayCutout != nullptr)
{
auto& displays = Desktop::getInstance().getDisplays();
auto& mainDisplay = *displays.getPrimaryDisplay();

auto newSafeAreaInsets = androidDisplayCutoutToBorderSize (displayCutout, mainDisplay.scale);

if (newSafeAreaInsets != mainDisplay.safeAreaInsets)
const_cast<Displays&> (displays).refresh();

auto* fieldId = env->GetStaticFieldID (AndroidWindowInsets, "CONSUMED", "Landroid/view/WindowInsets");
jassert (fieldId != nullptr);

return env->GetStaticObjectField (AndroidWindowInsets, fieldId);
}

jmethodID onApplyWindowInsetsMethodId = env->GetMethodID (AndroidView,
"onApplyWindowInsets",
"(Landroid/view/WindowInsets;)Landroid/view/WindowInsets;");

jassert (onApplyWindowInsetsMethodId != nullptr);

return env->CallObjectMethod (v.get(), onApplyWindowInsetsMethodId, insets.get());
}

private:
jobject invoke (jobject proxy, jobject method, jobjectArray args) override
{
auto* env = getEnv();
auto methodName = juce::juceString ((jstring) env->CallObjectMethod (method, JavaMethod.getName));

if (methodName == "onApplyWindowInsets")
{
jassert (env->GetArrayLength (args) == 2);

LocalRef<jobject> windowView (env->GetObjectArrayElement (args, 0));
LocalRef<jobject> insets (env->GetObjectArrayElement (args, 1));

return onApplyWindowInsets (std::move (windowView), std::move (insets));
}

// invoke base class
return AndroidInterfaceImplementer::invoke (proxy, method, args);
}
};

//==============================================================================
struct PreallocatedImage : public ImagePixelData
{
Expand Down Expand Up @@ -1457,6 +1563,26 @@ void Displays::findDisplays (float masterScale)
if (! activityArea.isEmpty())
d.userArea = activityArea / d.scale;

if (supportsDisplayCutout())
{
jmethodID getRootWindowInsetsMethodId = env->GetMethodID (AndroidView,
"getRootWindowInsets",
"()Landroid/view/WindowInsets;");

if (getRootWindowInsetsMethodId != nullptr)
{
LocalRef<jobject> insets (env->CallObjectMethod (contentView.get(), getRootWindowInsetsMethodId));

if (insets != nullptr)
{
LocalRef<jobject> displayCutout (env->CallObjectMethod (insets.get(), AndroidWindowInsets.getDisplayCutout));

if (displayCutout.get() != nullptr)
d.safeAreaInsets = androidDisplayCutoutToBorderSize (displayCutout, d.scale);
}
}
}

static bool hasAddedMainActivityListener = false;

if (! hasAddedMainActivityListener)
Expand Down
36 changes: 27 additions & 9 deletions modules/juce_gui_basics/native/juce_ios_Windowing.mm
Expand Up @@ -661,18 +661,35 @@ Image juce_createIconForFile (const File&)
return Orientations::convertToJuce (orientation);
}

// The most straightforward way of retrieving the screen area available to an iOS app
// seems to be to create a new window (which will take up all available space) and to
// query its frame.
struct TemporaryWindow
{
UIWindow* window = [[UIWindow alloc] init];
~TemporaryWindow() noexcept { [window release]; }
};

static Rectangle<int> getRecommendedWindowBounds()
{
// The most straightforward way of retrieving the screen area available to an iOS app
// seems to be to create a new window (which will take up all available space) and to
// query its frame.
struct TemporaryWindow
{
UIWindow* window = [[UIWindow alloc] init];
~TemporaryWindow() noexcept { [window release]; }
};
return convertToRectInt (TemporaryWindow().window.frame);
}

return convertToRectInt (TemporaryWindow{}.window.frame);
static BorderSize<int> getSafeAreaInsets (float masterScale)
{
#if defined (__IPHONE_11_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_11_0
UIEdgeInsets safeInsets = TemporaryWindow().window.safeAreaInsets;

auto getInset = [&] (float original) { return roundToInt (original / masterScale); };

return { getInset (safeInsets.top), getInset (safeInsets.left),
getInset (safeInsets.bottom), getInset (safeInsets.right) };
#else
auto statusBarSize = [UIApplication sharedApplication].statusBarFrame.size;
auto statusBarHeight = jmin (statusBarSize.width, statusBarSize.height);

return { roundToInt (statusBarHeight / masterScale), 0, 0, 0 };
#endif
}

void Displays::findDisplays (float masterScale)
Expand All @@ -684,6 +701,7 @@ Image juce_createIconForFile (const File&)
Display d;
d.totalArea = convertToRectInt ([s bounds]) / masterScale;
d.userArea = getRecommendedWindowBounds() / masterScale;
d.safeAreaInsets = getSafeAreaInsets (masterScale);
d.isMain = true;
d.scale = masterScale * s.scale;
d.dpi = 160 * d.scale;
Expand Down

0 comments on commit ac1425f

Please sign in to comment.