From ac1425f94ef2a7445bf3b716d7dcd829fd00f1ea Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 5 Feb 2021 10:42:12 +0000 Subject: [PATCH] Added Display::safeAreaInsets and implementations for iOS and Android --- .../juce_gui_basics/desktop/juce_Displays.h | 11 +- .../native/juce_android_Windowing.cpp | 128 +++++++++++++++++- .../native/juce_ios_Windowing.mm | 36 +++-- 3 files changed, 164 insertions(+), 11 deletions(-) diff --git a/modules/juce_gui_basics/desktop/juce_Displays.h b/modules/juce_gui_basics/desktop/juce_Displays.h index 7e1281bbe28e..5c268b021ca7 100644 --- a/modules/juce_gui_basics/desktop/juce_Displays.h +++ b/modules/juce_gui_basics/desktop/juce_Displays.h @@ -45,7 +45,8 @@ 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 totalArea; /** The total area of this display in logical pixels which isn't covered by OS-dependent @@ -53,6 +54,14 @@ class JUCE_API Displays */ Rectangle 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 safeAreaInsets; + /** The top-left of this display in physical coordinates. */ Point topLeftPhysical; diff --git a/modules/juce_gui_basics/native/juce_android_Windowing.cpp b/modules/juce_gui_basics/native/juce_android_Windowing.cpp index ae6959d0ef7e..b50eee4f6337 100644 --- a/modules/juce_gui_basics/native/juce_android_Windowing.cpp +++ b/modules/juce_gui_basics/native/juce_android_Windowing.cpp @@ -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 { @@ -244,6 +259,30 @@ namespace constexpr int FLAG_NOT_FOCUSABLE = 0x8; } +//============================================================================== +static bool supportsDisplayCutout() +{ + return getAndroidSDKVersion() >= 28; +} + +static BorderSize androidDisplayCutoutToBorderSize (LocalRef 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 @@ -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", @@ -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(); } @@ -856,6 +907,61 @@ class AndroidComponentPeer : public ComponentPeer, static void JNICALL handleAppPausedJni (JNIEnv*, jobject /*view*/, jlong host) { if (auto* myself = reinterpret_cast (host)) myself->handleAppPausedCallback(); } static void JNICALL handleAppResumedJni (JNIEnv*, jobject /*view*/, jlong host) { if (auto* myself = reinterpret_cast (host)) myself->handleAppResumedCallback(); } + //============================================================================== + struct ViewWindowInsetsListener : public juce::AndroidInterfaceImplementer + { + jobject onApplyWindowInsets (LocalRef v, LocalRef insets) + { + auto* env = getEnv(); + + LocalRef 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).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 windowView (env->GetObjectArrayElement (args, 0)); + LocalRef 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 { @@ -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 insets (env->CallObjectMethod (contentView.get(), getRootWindowInsetsMethodId)); + + if (insets != nullptr) + { + LocalRef displayCutout (env->CallObjectMethod (insets.get(), AndroidWindowInsets.getDisplayCutout)); + + if (displayCutout.get() != nullptr) + d.safeAreaInsets = androidDisplayCutoutToBorderSize (displayCutout, d.scale); + } + } + } + static bool hasAddedMainActivityListener = false; if (! hasAddedMainActivityListener) diff --git a/modules/juce_gui_basics/native/juce_ios_Windowing.mm b/modules/juce_gui_basics/native/juce_ios_Windowing.mm index b7b2d68a2ba0..27ebe7b2e684 100644 --- a/modules/juce_gui_basics/native/juce_ios_Windowing.mm +++ b/modules/juce_gui_basics/native/juce_ios_Windowing.mm @@ -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 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 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) @@ -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;