diff --git a/README.md b/README.md index 4394cd32..27a4562a 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,13 @@ [How to become a sponsor?](https://reactnativeunistyles.vercel.app/other/for-sponsors/) - + codemask - + galaxies-dev + + + kmartinezmedia diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index f438e6bb..03424aaa 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -6,6 +6,7 @@ add_library(unistyles SHARED ../cxx/UnistylesRuntime.cpp ./src/main/cxx/cpp-adapter.cpp + ./src/main/cxx/helpers.cpp ) include_directories( diff --git a/android/src/main/cxx/cpp-adapter.cpp b/android/src/main/cxx/cpp-adapter.cpp index d0f13d7f..abc22302 100644 --- a/android/src/main/cxx/cpp-adapter.cpp +++ b/android/src/main/cxx/cpp-adapter.cpp @@ -1,34 +1,26 @@ #include #include +#include #include "UnistylesRuntime.h" +#include "helpers.h" using namespace facebook; static jobject unistylesModule = nullptr; std::shared_ptr unistylesRuntime = nullptr; -void throwKotlinException( - JNIEnv *env, - const char *message -) { - jclass runtimeExceptionClass = env->FindClass("java/lang/RuntimeException"); - - if (runtimeExceptionClass != nullptr) { - env->ThrowNew(runtimeExceptionClass, message); - env->DeleteLocalRef(runtimeExceptionClass); - } -} - extern "C" JNIEXPORT void JNICALL Java_com_unistyles_UnistylesModule_nativeInstall( JNIEnv *env, jobject thiz, jlong jsi, - jint screenWidth, - jint screenHeight, + jobject screen, jstring colorScheme, - jstring contentSizeCategory + jstring contentSizeCategory, + jobject insets, + jobject statusBar, + jobject navigationBar ) { auto runtime = reinterpret_cast(jsi); @@ -49,10 +41,12 @@ Java_com_unistyles_UnistylesModule_nativeInstall( env->ReleaseStringUTFChars(contentSizeCategory, contentSizeCategoryChars); unistylesRuntime = std::make_shared( - screenWidth, - screenHeight, + jobjectToDimensions(env, screen), colorSchemeStr, - contentSizeCategoryStr + contentSizeCategoryStr, + jobjectToInsets(env, insets), + jobjectToDimensions(env, statusBar), + jobjectToDimensions(env, navigationBar) ); unistylesRuntime->onThemeChange([=](const std::string &theme) { @@ -65,13 +59,23 @@ Java_com_unistyles_UnistylesModule_nativeInstall( env->DeleteLocalRef(cls); }); - unistylesRuntime->onLayoutChange([=](const std::string &breakpoint, const std::string &orientation, int width, int height) { + unistylesRuntime->onLayoutChange([=](const std::string &breakpoint, const std::string &orientation, Dimensions& screen, Dimensions& statusBar, Insets& insets, Dimensions& navigationBar) { jstring breakpointStr = env->NewStringUTF(breakpoint.c_str()); jstring orientationStr = env->NewStringUTF(orientation.c_str()); jclass cls = env->GetObjectClass(unistylesModule); - jmethodID methodId = env->GetMethodID(cls, "onLayoutChange", "(Ljava/lang/String;Ljava/lang/String;II)V"); - - env->CallVoidMethod(unistylesModule, methodId, breakpointStr, orientationStr, width, height); + jclass dimensionsClass = env->FindClass("com/unistyles/Dimensions"); + jmethodID dimensionsConstructor = env->GetMethodID(dimensionsClass, "", "(II)V"); + jobject screenObj = env->NewObject(dimensionsClass, dimensionsConstructor, screen.width, screen.height); + jobject statusBarObj = env->NewObject(dimensionsClass, dimensionsConstructor, statusBar.width, statusBar.height); + jobject navigationBarObj = env->NewObject(dimensionsClass, dimensionsConstructor, navigationBar.width, navigationBar.height); + + jclass insetsClass = env->FindClass("com/unistyles/Insets"); + jmethodID insetsConstructor = env->GetMethodID(insetsClass, "", "(IIII)V"); + jobject insetsObj = env->NewObject(insetsClass, insetsConstructor, insets.left, insets.top, insets.right, insets.bottom); + jmethodID methodId = env->GetMethodID(cls, "onLayoutChange", + "(Ljava/lang/String;Ljava/lang/String;Lcom/unistyles/Dimensions;Lcom/unistyles/Dimensions;Lcom/unistyles/Insets;Lcom/unistyles/Dimensions;)V"); + + env->CallVoidMethod(unistylesModule, methodId, breakpointStr, orientationStr, screenObj, statusBarObj, insetsObj, navigationBarObj); env->DeleteLocalRef(breakpointStr); env->DeleteLocalRef(orientationStr); env->DeleteLocalRef(cls); @@ -109,9 +113,14 @@ Java_com_unistyles_UnistylesModule_nativeDestroy(JNIEnv *env, jobject thiz) { extern "C" JNIEXPORT void JNICALL -Java_com_unistyles_UnistylesModule_nativeOnOrientationChange(JNIEnv *env, jobject thiz, jint width, jint height) { +Java_com_unistyles_UnistylesModule_nativeOnOrientationChange(JNIEnv *env, jobject thiz, jobject screen, jobject insets, jobject statusBar, jobject navigationBar) { if (unistylesRuntime != nullptr) { - unistylesRuntime->handleScreenSizeChange(width, height); + Dimensions screenDimensions = jobjectToDimensions(env, screen); + Dimensions statusBarDimensions = jobjectToDimensions(env, statusBar); + Insets screenInsets = jobjectToInsets(env, insets); + Dimensions navigationBarDimensions = jobjectToDimensions(env, navigationBar); + + unistylesRuntime->handleScreenSizeChange(screenDimensions, screenInsets, statusBarDimensions, navigationBarDimensions); } } diff --git a/android/src/main/cxx/helpers.cpp b/android/src/main/cxx/helpers.cpp new file mode 100644 index 00000000..66d4ab7c --- /dev/null +++ b/android/src/main/cxx/helpers.cpp @@ -0,0 +1,44 @@ +#include "helpers.h" +#include "UnistylesRuntime.h" + +Dimensions jobjectToDimensions(JNIEnv *env, jobject dimensionObj) { + jclass dimensionClass = env->FindClass("com/unistyles/Dimensions"); + jfieldID widthFieldID = env->GetFieldID(dimensionClass, "width", "I"); + jfieldID heightFieldID = env->GetFieldID(dimensionClass, "height", "I"); + + int width = env->GetIntField(dimensionObj, widthFieldID); + int height = env->GetIntField(dimensionObj, heightFieldID); + + env->DeleteLocalRef(dimensionClass); + + return Dimensions{width, height}; +} + +Insets jobjectToInsets(JNIEnv *env, jobject insetsObj) { + jclass insetsClass = env->FindClass("com/unistyles/Insets"); + jfieldID leftFieldID = env->GetFieldID(insetsClass, "left", "I"); + jfieldID topFieldID = env->GetFieldID(insetsClass, "top", "I"); + jfieldID rightFieldID = env->GetFieldID(insetsClass, "right", "I"); + jfieldID bottomFieldID = env->GetFieldID(insetsClass, "bottom", "I"); + + int left = env->GetIntField(insetsObj, leftFieldID); + int top = env->GetIntField(insetsObj, topFieldID); + int right = env->GetIntField(insetsObj, rightFieldID); + int bottom = env->GetIntField(insetsObj, bottomFieldID); + + env->DeleteLocalRef(insetsClass); + + return Insets{top, bottom, left, right}; +} + +void throwKotlinException( + JNIEnv *env, + const char *message +) { + jclass runtimeExceptionClass = env->FindClass("java/lang/RuntimeException"); + + if (runtimeExceptionClass != nullptr) { + env->ThrowNew(runtimeExceptionClass, message); + env->DeleteLocalRef(runtimeExceptionClass); + } +} diff --git a/android/src/main/cxx/helpers.h b/android/src/main/cxx/helpers.h new file mode 100644 index 00000000..973955fa --- /dev/null +++ b/android/src/main/cxx/helpers.h @@ -0,0 +1,9 @@ +#include +#include +#include +#include + +Dimensions jobjectToDimensions(JNIEnv *env, jobject dimensionObj); +Insets jobjectToInsets(JNIEnv *env, jobject insetsObj); + +void throwKotlinException(JNIEnv *env, const char *message); diff --git a/android/src/main/java/com/unistyles/Config.kt b/android/src/main/java/com/unistyles/Config.kt new file mode 100644 index 00000000..2fdee2b8 --- /dev/null +++ b/android/src/main/java/com/unistyles/Config.kt @@ -0,0 +1,116 @@ +package com.unistyles + +import android.annotation.SuppressLint +import android.content.res.Configuration +import com.facebook.react.bridge.ReactApplicationContext + +class UnistylesConfig(private val reactApplicationContext: ReactApplicationContext) { + private val insets: UnistylesInsets = UnistylesInsets(reactApplicationContext) + private val density: Float = reactApplicationContext.resources.displayMetrics.density + private var lastConfig: Config = this.getAppConfig() + private var lastLayoutConfig: LayoutConfig = this.getAppLayoutConfig() + + fun hasNewConfig(): Boolean { + val newConfig = this.getAppConfig() + val newContentSizeCategory = newConfig.contentSizeCategory != lastConfig.contentSizeCategory + val newColorScheme = newConfig.colorScheme != lastConfig.colorScheme + + if (!newContentSizeCategory && !newColorScheme) { + return false + } + + lastConfig = newConfig + lastConfig.hasNewContentSizeCategory = newContentSizeCategory + lastConfig.hasNewColorScheme = newColorScheme + + return true + } + + fun hasNewLayoutConfig(): Boolean { + val newConfig = this.getAppLayoutConfig() + + if (newConfig.isEqual(lastLayoutConfig)) { + return false + } + + lastLayoutConfig = newConfig + + return true + } + + fun getConfig(): Config { + return this.lastConfig + } + + fun getLayoutConfig(): LayoutConfig { + return this.lastLayoutConfig + } + + private fun getAppConfig(): Config { + val fontScale = reactApplicationContext.resources.configuration.fontScale + + return Config( + this.getColorScheme(), + this.getContentSizeCategory(fontScale), + ) + } + + private fun getAppLayoutConfig(): LayoutConfig { + val displayMetrics = reactApplicationContext.resources.displayMetrics + val screenWidth = (displayMetrics.widthPixels / density).toInt() + val screenHeight = (displayMetrics.heightPixels / density).toInt() + + return LayoutConfig( + Dimensions(screenWidth, screenHeight), + this.insets.get(), + Dimensions(screenWidth, getStatusBarHeight()), + Dimensions(screenWidth, getNavigationBarHeight()) + ) + } + + private fun getContentSizeCategory(fontScale: Float): String { + val contentSizeCategory = when { + fontScale <= 0.85f -> "Small" + fontScale <= 1.0f -> "Default" + fontScale <= 1.15f -> "Large" + fontScale <= 1.3f -> "ExtraLarge" + fontScale <= 1.5f -> "Huge" + fontScale <= 1.8 -> "ExtraHuge" + else -> "ExtraExtraHuge" + } + + return contentSizeCategory + } + + private fun getColorScheme(): String { + val colorScheme = when (reactApplicationContext.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) { + Configuration.UI_MODE_NIGHT_YES -> "dark" + Configuration.UI_MODE_NIGHT_NO -> "light" + else -> "unspecified" + } + + return colorScheme + } + + @SuppressLint("InternalInsetResource", "DiscouragedApi") + private fun getStatusBarHeight(): Int { + val heightResId = reactApplicationContext.resources.getIdentifier("status_bar_height", "dimen", "android") + + if (heightResId > 0) { + return (reactApplicationContext.resources.getDimensionPixelSize(heightResId) / density).toInt() + } + + return 0 + } + + @SuppressLint("InternalInsetResource", "DiscouragedApi") + private fun getNavigationBarHeight(): Int { + val heightResId = reactApplicationContext.resources.getIdentifier("navigation_bar_height", "dimen", "android") + + if (heightResId > 0) { + return (reactApplicationContext.resources.getDimensionPixelSize(heightResId) / density).toInt() + } + + return 0 + } +} diff --git a/android/src/main/java/com/unistyles/Insets.kt b/android/src/main/java/com/unistyles/Insets.kt new file mode 100644 index 00000000..c5dee4e1 --- /dev/null +++ b/android/src/main/java/com/unistyles/Insets.kt @@ -0,0 +1,138 @@ +package com.unistyles + +import android.content.Context +import android.graphics.Rect +import android.os.Build +import android.view.View +import android.view.Window +import android.view.WindowInsets +import android.view.WindowManager +import com.facebook.react.bridge.ReactApplicationContext +import kotlin.math.roundToInt + +class UnistylesInsets(private val reactApplicationContext: ReactApplicationContext) { + private val density: Float = reactApplicationContext.resources.displayMetrics.density + + fun get(): Insets { + return this.getCurrentInsets() + } + + private fun getCurrentInsets(): Insets { + val baseInsets = this.getBaseInsets() + val statusBarTranslucent = this.hasTranslucentStatusBar(baseInsets) ?: return baseInsets + val window = reactApplicationContext.currentActivity?.window ?: return baseInsets + val shouldModifyLandscapeInsets = this.forceLandscapeInsets(baseInsets, window) + + val topInset = this.getTopInset(baseInsets, statusBarTranslucent, window) + val bottomInset = this.getBottomInset(baseInsets, window) + val leftInset = if (shouldModifyLandscapeInsets) 0 else baseInsets.left + val rightInset = if (shouldModifyLandscapeInsets) 0 else baseInsets.right + + return Insets(topInset, bottomInset, leftInset, rightInset) + } + + @Suppress("DEPRECATION") + private fun getBaseInsets(): Insets { + // this is the best API, but it's available from Android 11 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val windowManager = reactApplicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val systemBarsInsets = windowManager.currentWindowMetrics.windowInsets.getInsets(WindowInsets.Type.systemBars()) + + val top = (systemBarsInsets.top / density).roundToInt() + val bottom = (systemBarsInsets.bottom / density).roundToInt() + val left = (systemBarsInsets.left / density).roundToInt() + val right = (systemBarsInsets.right / density).roundToInt() + + return Insets(top, bottom, left, right) + } + + // available from Android 6.0 + val window = reactApplicationContext.currentActivity?.window ?: return Insets(0, 0, 0, 0) + val systemInsets = window.decorView.rootWindowInsets + + val top = (systemInsets.systemWindowInsetTop / density).roundToInt() + val bottom = (systemInsets.systemWindowInsetBottom / density).roundToInt() + val left = (systemInsets.systemWindowInsetLeft / density).roundToInt() + val right = (systemInsets.systemWindowInsetRight / density).roundToInt() + + return Insets(top, bottom, left, right) + } + + // this function helps to detect statusBar translucent, as React Native is using weird API instead of windows flags + private fun hasTranslucentStatusBar(baseInsets: Insets): Boolean? { + val window = reactApplicationContext.currentActivity?.window ?: return null + val contentView = window.decorView.findViewById(android.R.id.content) ?: return null + val decorViewLocation = IntArray(2) + val contentViewLocation = IntArray(2) + + // decor view is a top level view with navigationBar and statusBar + window.decorView.getLocationOnScreen(decorViewLocation) + // content view is view without navigationBar and statusBar + contentView.getLocationOnScreen(contentViewLocation) + + val statusBarHeight = contentViewLocation[1] - decorViewLocation[1] + + // if positions are the same, then the status bar is translucent + return statusBarHeight == 0 + } + + private fun getTopInset(baseInsets: Insets, statusBarTranslucent: Boolean, window: Window): Int { + if (!statusBarTranslucent) { + return 0 + } + + return baseInsets.top + } + + @Suppress("DEPRECATION") + private fun getBottomInset(baseInsets: Insets, window: Window): Int { + val translucentNavigation = hasTranslucentNavigation(window) + + // Android 11 has inconsistent FLAG_LAYOUT_NO_LIMITS, which alone does nothing + // https://stackoverflow.com/questions/64153785/android-11-window-setdecorfitssystemwindow-doesnt-show-screen-behind-status-a + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + if ((hasFullScreenMode(window) && translucentNavigation) || translucentNavigation) { + return baseInsets.bottom + } + + return 0 + } + + return if (hasTranslucentNavigation(window) || hasFullScreenMode(window)) baseInsets.bottom else 0 + } + + private fun forceLandscapeInsets(baseInsets: Insets, window: Window): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return false + } + + val displayMetrics = reactApplicationContext.resources.displayMetrics + val isLandscape = displayMetrics.widthPixels > displayMetrics.heightPixels + + if (!isLandscape) { + return false + } + + val contentView = window.decorView.findViewById(android.R.id.content) ?: return false + val visibleRect = Rect() + val drawingRect = Rect() + + window.decorView.getGlobalVisibleRect(visibleRect) + contentView.getDrawingRect(drawingRect) + + // width is equal to navigationBarHeight + statusBarHeight (in landscape) + val width = ((visibleRect.width() - contentView.width) / density).roundToInt() + + // we should correct landscape if insets are equal to width + return (baseInsets.left + baseInsets.right) == width + } + + @Suppress("DEPRECATION") + private fun hasTranslucentNavigation(window: Window): Boolean { + return (window.attributes.flags and WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) == WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION + } + + private fun hasFullScreenMode(window: Window): Boolean { + return (window.attributes.flags and WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) == WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + } +} diff --git a/android/src/main/java/com/unistyles/Models.kt b/android/src/main/java/com/unistyles/Models.kt new file mode 100644 index 00000000..e39ac397 --- /dev/null +++ b/android/src/main/java/com/unistyles/Models.kt @@ -0,0 +1,85 @@ +package com.unistyles + +class Dimensions(var width: Int, var height: Int) { + fun isEqual(dimensions: Dimensions): Boolean { + if (this.width != dimensions.width) { + return false + } + + return this.height == dimensions.height + } + + override fun toString(): String { + return "${width}x${height}" + } +} + +class Insets(var top: Int, var bottom: Int, var left: Int, var right: Int) { + fun isEqual(insets: Insets): Boolean { + if (this.top != insets.top) { + return false + } + + if (this.bottom != insets.bottom) { + return false + } + + if (this.left != insets.left) { + return false + } + + return this.right == insets.right + } + + override fun toString(): String { + return "T:${top}B:${bottom}L:${left}R:${right}" + } +} + +class LayoutConfig( + val screen: Dimensions, + val insets: Insets, + val statusBar: Dimensions, + val navigationBar: Dimensions +) { + fun isEqual(config: LayoutConfig): Boolean { + if (!this.screen.isEqual(config.screen)) { + return false + } + + if (!this.insets.isEqual(config.insets)) { + return false + } + + if (!this.statusBar.isEqual(config.statusBar)) { + return false + } + + return this.navigationBar.isEqual(config.navigationBar) + } + + override fun toString(): String { + return buildString { + append("screen=") + append(screen) + append(" insets=") + append(insets) + append(" statusBar=") + append(statusBar) + append(" navigationBar=") + append(navigationBar) + } + } +} + +class Config( + val colorScheme: String, + val contentSizeCategory: String, +) { + var hasNewColorScheme: Boolean = false + var hasNewContentSizeCategory: Boolean = false + + override fun toString(): String { + return "colorScheme=${colorScheme} contentSizeCategory:${contentSizeCategory}" + } +} diff --git a/android/src/main/java/com/unistyles/Platform.kt b/android/src/main/java/com/unistyles/Platform.kt new file mode 100644 index 00000000..8adfa6e4 --- /dev/null +++ b/android/src/main/java/com/unistyles/Platform.kt @@ -0,0 +1,23 @@ +package com.unistyles + +import com.facebook.react.bridge.ReactApplicationContext + +class Platform(reactApplicationContext: ReactApplicationContext) { + private val config: UnistylesConfig = UnistylesConfig(reactApplicationContext) + + fun hasNewLayoutConfig(): Boolean { + return this.config.hasNewLayoutConfig() + } + + fun hasNewConfig(): Boolean { + return this.config.hasNewConfig() + } + + fun getConfig(): Config { + return this.config.getConfig() + } + + fun getLayoutConfig(): LayoutConfig { + return this.config.getLayoutConfig() + } +} diff --git a/android/src/main/java/com/unistyles/UnistylesModule.kt b/android/src/main/java/com/unistyles/UnistylesModule.kt index 573f3e4c..db4a8872 100644 --- a/android/src/main/java/com/unistyles/UnistylesModule.kt +++ b/android/src/main/java/com/unistyles/UnistylesModule.kt @@ -4,10 +4,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.res.Configuration import android.os.Handler import android.os.Looper import android.util.Log +import android.view.ViewTreeObserver import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.ReactApplicationContext @@ -15,11 +15,28 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.modules.core.DeviceEventManagerModule - class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener { + private val drawHandler = Handler(Looper.getMainLooper()) + private val debounceDuration = 250L + private var runnable: Runnable? = null + + private var isCxxReady: Boolean = false + private val platform: Platform = Platform(reactContext) + private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { + if (this.isCxxReady) { + runnable?.let { drawHandler.removeCallbacks(it) } + + runnable = Runnable { + this@UnistylesModule.onLayoutConfigChange() + }.also { + drawHandler.postDelayed(it, debounceDuration) + } + } + } + private val configurationChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Intent.ACTION_CONFIGURATION_CHANGED) { + if (intent.action == Intent.ACTION_CONFIGURATION_CHANGED && this@UnistylesModule.isCxxReady) { Handler(Looper.getMainLooper()).postDelayed({ this@UnistylesModule.onConfigChange() }, 10) @@ -37,50 +54,56 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ reactApplicationContext.registerReceiver(configurationChangeReceiver, IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)) } + private fun setupLayoutListener() { + val activity = currentActivity ?: return + activity.window.decorView.rootView.viewTreeObserver.addOnGlobalLayoutListener(layoutListener) + } + + @Deprecated("Deprecated in Java") override fun onCatalystInstanceDestroy() { + val activity = currentActivity ?: return + + activity.window.decorView.rootView.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener) reactApplicationContext.unregisterReceiver(configurationChangeReceiver) + runnable?.let { drawHandler.removeCallbacks(it) } this.nativeDestroy() } //endregion //region Event handlers private fun onConfigChange() { - val config = this.getConfig() + if (!platform.hasNewConfig()) { + return + } + + val config = platform.getConfig() reactApplicationContext.runOnJSQueueThread { - this.nativeOnOrientationChange( - config["width"] as Int, - config["height"] as Int - ) - this.nativeOnAppearanceChange( - config["colorScheme"] as String - ) - this.nativeOnContentSizeCategoryChange(config["contentSizeCategory"] as String) + if (config.hasNewColorScheme) { + this.nativeOnAppearanceChange(config.colorScheme) + } + + if (config.hasNewContentSizeCategory) { + this.nativeOnContentSizeCategoryChange(config.contentSizeCategory) + } } } - private fun getConfig(): Map { - val displayMetrics = reactApplicationContext.resources.displayMetrics - val colorScheme = when (reactApplicationContext.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) { - Configuration.UI_MODE_NIGHT_YES -> "dark" - Configuration.UI_MODE_NIGHT_NO -> "light" - else -> "unspecified" - } - val fontScale = reactApplicationContext.resources.configuration.fontScale - val contentSizeCategory = when { - fontScale <= 0.85f -> "Small" - fontScale <= 1.0f -> "Default" - fontScale <= 1.15f -> "Large" - fontScale <= 1.3f -> "ExtraLarge" - else -> "Huge" + private fun onLayoutConfigChange() { + if (!platform.hasNewLayoutConfig()) { + return } - return mapOf( - "width" to (displayMetrics.widthPixels / displayMetrics.density).toInt(), - "height" to (displayMetrics.heightPixels / displayMetrics.density).toInt(), - "colorScheme" to colorScheme, - "contentSizeCategory" to contentSizeCategory - ) + val config = platform.getLayoutConfig() + + reactApplicationContext.runOnJSQueueThread { + this.nativeOnOrientationChange( + config.screen, + config.insets, + config.statusBar, + config.navigationBar + ) + } } //endregion @@ -89,16 +112,21 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ fun install(): Boolean { return try { System.loadLibrary("unistyles") - val config = this.getConfig() + val config = platform.getConfig() + val layoutConfig = platform.getLayoutConfig() + this.setupLayoutListener() this.reactApplicationContext.javaScriptContextHolder?.let { this.nativeInstall( it.get(), - config["width"] as Int, - config["height"] as Int, - config["colorScheme"] as String, - config["contentSizeCategory"] as String + layoutConfig.screen, + config.colorScheme, + config.contentSizeCategory, + layoutConfig.insets, + layoutConfig.statusBar, + layoutConfig.navigationBar ) + this.isCxxReady = true Log.i(NAME, "Installed Unistyles \uD83E\uDD84!") @@ -111,23 +139,45 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ } } - private external fun nativeInstall(jsi: Long, width: Int, height: Int, colorScheme: String, contentSizeCategory: String) + private external fun nativeInstall( + jsi: Long, + screen: Dimensions, + colorScheme: String, + contentSizeCategory: String, + insets: Insets, + statusBar: Dimensions, + navigationBar: Dimensions + ) private external fun nativeDestroy() - private external fun nativeOnOrientationChange(width: Int, height: Int) + private external fun nativeOnOrientationChange(screen: Dimensions, insets: Insets, statusBar: Dimensions, navigationBar: Dimensions) private external fun nativeOnAppearanceChange(colorScheme: String) private external fun nativeOnContentSizeCategoryChange(contentSizeCategory: String) //endregion //region Event emitter - private fun onLayoutChange(breakpoint: String, orientation: String, width: Int, height: Int) { + private fun onLayoutChange(breakpoint: String, orientation: String, screen: Dimensions, statusBar: Dimensions, insets: Insets, navigationBar: Dimensions) { val body = Arguments.createMap().apply { putString("type", "layout") putMap("payload", Arguments.createMap().apply { putString("breakpoint", breakpoint) putString("orientation", orientation) putMap("screen", Arguments.createMap().apply { - putInt("width", width) - putInt("height", height) + putInt("width", screen.width) + putInt("height", screen.height) + }) + putMap("statusBar", Arguments.createMap().apply { + putInt("width", statusBar.width) + putInt("height", statusBar.height) + }) + putMap("insets", Arguments.createMap().apply { + putInt("top", insets.top) + putInt("bottom", insets.bottom) + putInt("left", insets.left) + putInt("right", insets.right) + }) + putMap("navigationBar", Arguments.createMap().apply { + putInt("width", navigationBar.width) + putInt("height", navigationBar.height) }) }) } @@ -180,6 +230,7 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ fun removeListeners(count: Double) = Unit override fun onHostResume() { this.onConfigChange() + this.onLayoutConfigChange() } override fun onHostPause() {} diff --git a/cxx/UnistylesRuntime.cpp b/cxx/UnistylesRuntime.cpp index 4e9bd87a..0abd766b 100644 --- a/cxx/UnistylesRuntime.cpp +++ b/cxx/UnistylesRuntime.cpp @@ -25,6 +25,9 @@ std::vector UnistylesRuntime::getPropertyNames(jsi::Runtime& ru properties.push_back(jsi::PropNameID::forUtf8(runtime, std::string("addPlugin"))); properties.push_back(jsi::PropNameID::forUtf8(runtime, std::string("removePlugin"))); properties.push_back(jsi::PropNameID::forUtf8(runtime, std::string("enabledPlugins"))); + properties.push_back(jsi::PropNameID::forUtf8(runtime, std::string("insets"))); + properties.push_back(jsi::PropNameID::forUtf8(runtime, std::string("statusBar"))); + properties.push_back(jsi::PropNameID::forUtf8(runtime, std::string("navigationBar"))); // setters properties.push_back(jsi::PropNameID::forUtf8(runtime, std::string("themes"))); @@ -36,17 +39,17 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p std::string propName = propNameId.utf8(runtime); if (propName == "screenWidth") { - return jsi::Value(this->screenWidth); + return jsi::Value(this->screen.width); } if (propName == "screenHeight") { - return jsi::Value(this->screenHeight); + return jsi::Value(this->screen.height); } if (propName == "hasAdaptiveThemes") { return jsi::Value(this->hasAdaptiveThemes); } - + if (propName == "contentSizeCategory") { return jsi::Value(jsi::String::createFromUtf8(runtime, this->contentSizeCategory)); } @@ -66,14 +69,14 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p if (propName == "colorScheme") { return jsi::Value(jsi::String::createFromUtf8(runtime, this->colorScheme)); } - + if (propName == "enabledPlugins") { auto jsiArray = facebook::jsi::Array(runtime, this->pluginNames.size()); - + for (size_t i = 0; i < this->pluginNames.size(); i++) { jsiArray.setValueAtIndex(runtime, i, facebook::jsi::String::createFromUtf8(runtime, this->pluginNames[i])); } - + return jsiArray; } @@ -91,7 +94,7 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p return jsi::Value(runtime, *sortedBreakpointEntriesArray); } - + if (propName == "addPlugin") { return jsi::Function::createFromHostFunction( runtime, @@ -100,19 +103,19 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p [this](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *arguments, size_t count) -> jsi::Value { std::string pluginName = arguments[0].asString(runtime).utf8(runtime); bool notify = arguments[1].asBool(); - + this->pluginNames.push_back(pluginName); - + // registry enabled plugins won't notify listeners if (notify) { this->onPluginChangeCallback(); } - + return jsi::Value::undefined(); } ); } - + if (propName == "removePlugin") { return jsi::Function::createFromHostFunction( runtime, @@ -120,14 +123,14 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p 1, [this](jsi::Runtime &runtime, const jsi::Value &thisVal, const jsi::Value *arguments, size_t count) -> jsi::Value { std::string pluginName = arguments[0].asString(runtime).utf8(runtime); - + auto it = std::find(this->pluginNames.begin(), this->pluginNames.end(), pluginName); - + if (it != this->pluginNames.end()) { this->pluginNames.erase(it); this->onPluginChangeCallback(); } - + return jsi::Value::undefined(); } ); @@ -169,7 +172,7 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p this->sortedBreakpointPairs = sortedBreakpointEntriesVec; - std::string breakpoint = this->getBreakpointFromScreenWidth(this->screenWidth, sortedBreakpointEntriesVec); + std::string breakpoint = this->getBreakpointFromScreenWidth(this->screen.width, sortedBreakpointEntriesVec); this->breakpoint = breakpoint; @@ -189,12 +192,12 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p this->themeName = themeName; this->onThemeChangeCallback(themeName); } - + return jsi::Value::undefined(); } ); } - + if (propName == "updateTheme") { return jsi::Function::createFromHostFunction(runtime, jsi::PropNameID::forAscii(runtime, "updateTheme"), @@ -205,7 +208,7 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p if (this->themeName == themeName) { this->onThemeChangeCallback(themeName); } - + return jsi::Value::undefined(); } ); @@ -223,7 +226,7 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p if (!enableAdaptiveThemes || !this->supportsAutomaticColorScheme) { return jsi::Value::undefined(); } - + if (this->themeName != this->colorScheme) { this->themeName = this->colorScheme; this->onThemeChangeCallback(this->themeName); @@ -234,6 +237,35 @@ jsi::Value UnistylesRuntime::get(jsi::Runtime& runtime, const jsi::PropNameID& p ); } + if (propName == "insets") { + auto insets = jsi::Object(runtime); + + insets.setProperty(runtime, "top", this->insets.top); + insets.setProperty(runtime, "bottom", this->insets.bottom); + insets.setProperty(runtime, "left", this->insets.left); + insets.setProperty(runtime, "right", this->insets.right); + + return insets; + } + + if (propName == "statusBar") { + auto statusBar = jsi::Object(runtime); + + statusBar.setProperty(runtime, "width", this->statusBar.width); + statusBar.setProperty(runtime, "height", this->statusBar.height); + + return statusBar; + } + + if (propName == "navigationBar") { + auto navigationBarValue = jsi::Object(runtime); + + navigationBarValue.setProperty(runtime, "width", this->navigationBar.width); + navigationBarValue.setProperty(runtime, "height", this->navigationBar.height); + + return navigationBarValue; + } + return jsi::Value::undefined(); } @@ -286,20 +318,27 @@ std::string UnistylesRuntime::getBreakpointFromScreenWidth(int width, const std: return sortedBreakpointPairs.empty() ? "" : sortedBreakpointPairs.back().first; } -void UnistylesRuntime::handleScreenSizeChange(int width, int height) { - std::string breakpoint = this->getBreakpointFromScreenWidth(width, this->sortedBreakpointPairs); - bool shouldNotify = this->breakpoint != breakpoint || this->screenWidth != width || this->screenHeight != height; - +void UnistylesRuntime::handleScreenSizeChange(Dimensions& screen, Insets& insets, Dimensions& statusBar, Dimensions& navigationBar) { + std::string breakpoint = this->getBreakpointFromScreenWidth(screen.width, this->sortedBreakpointPairs); + bool hasDifferentBreakpoint = this->breakpoint != breakpoint; + bool hasDifferentScreenDimensions = this->screen.width != screen.width || this->screen.height != screen.height; + bool hasDifferentInsets = this->insets.top != insets.top || this->insets.bottom != insets.bottom || this->insets.left != insets.left || this->insets.right != insets.right; + + // we don't need to check statusBar/navigationBar as they will only change on orientation change witch is equal to hasDifferentScreenDimensions + bool shouldNotify = hasDifferentBreakpoint || hasDifferentScreenDimensions || hasDifferentInsets; + this->breakpoint = breakpoint; - this->screenWidth = width; - this->screenHeight = height; + this->screen = {screen.width, screen.height}; + this->insets = {insets.top, insets.bottom, insets.left, insets.right}; + this->statusBar = {statusBar.width, statusBar.height}; + this->navigationBar = {navigationBar.width, navigationBar.height}; - std::string orientation = width > height + std::string orientation = screen.width > screen.height ? UnistylesOrientationLandscape : UnistylesOrientationPortrait; if (shouldNotify) { - this->onLayoutChangeCallback(breakpoint, orientation, width, height); + this->onLayoutChangeCallback(breakpoint, orientation, screen, statusBar, insets, navigationBar); } } @@ -309,7 +348,7 @@ void UnistylesRuntime::handleAppearanceChange(std::string colorScheme) { if (!this->supportsAutomaticColorScheme || !this->hasAdaptiveThemes) { return; } - + if (this->themeName != this->colorScheme) { this->onThemeChangeCallback(this->colorScheme); this->themeName = this->colorScheme; diff --git a/cxx/UnistylesRuntime.h b/cxx/UnistylesRuntime.h index 4ad93f72..26446ca2 100644 --- a/cxx/UnistylesRuntime.h +++ b/cxx/UnistylesRuntime.h @@ -2,6 +2,7 @@ #include #include +#include using namespace facebook; @@ -16,25 +17,46 @@ const std::string UnistylesErrorBreakpointsCannotBeEmpty = "You are trying to re const std::string UnistylesErrorBreakpointsMustStartFromZero = "You are trying to register breakpoints that don't start from 0"; const std::string UnistylesErrorThemesCannotBeEmpty = "You are trying to register empty themes object"; +struct Dimensions { + int width; + int height; +}; + +struct Insets { + int top; + int bottom; + int left; + int right; +}; + class JSI_EXPORT UnistylesRuntime : public jsi::HostObject { private: std::function onThemeChangeCallback; - std::function onLayoutChangeCallback; + std::function onLayoutChangeCallback; std::function onContentSizeCategoryChangeCallback; std::function onPluginChangeCallback; - int screenWidth; - int screenHeight; + Dimensions screen; + Dimensions statusBar; + Dimensions navigationBar; + Insets insets; std::string colorScheme; std::string contentSizeCategory; public: UnistylesRuntime( - int screenWidth, - int screenHeight, + Dimensions screen, std::string colorScheme, - std::string contentSizeCategory - ): screenWidth(screenWidth), screenHeight(screenHeight), colorScheme(colorScheme), contentSizeCategory(contentSizeCategory) {} + std::string contentSizeCategory, + Insets insets, + Dimensions statusBar, + Dimensions navigationBar + ): screen(screen), + colorScheme(colorScheme), + contentSizeCategory(contentSizeCategory), + insets(insets), + statusBar(statusBar), + navigationBar(navigationBar) {} bool hasAdaptiveThemes; bool supportsAutomaticColorScheme; @@ -49,14 +71,14 @@ class JSI_EXPORT UnistylesRuntime : public jsi::HostObject { this->onThemeChangeCallback = callback; } - void onLayoutChange(std::function callback) { + void onLayoutChange(std::function callback) { this->onLayoutChangeCallback = callback; } void onPluginChange(std::function callback) { this->onPluginChangeCallback = callback; } - + void onContentSizeCategoryChange(std::function callback) { this->onContentSizeCategoryChangeCallback = callback; } @@ -65,7 +87,7 @@ class JSI_EXPORT UnistylesRuntime : public jsi::HostObject { void set(jsi::Runtime& runtime, const jsi::PropNameID& propNameId, const jsi::Value& value) override; std::vector getPropertyNames(jsi::Runtime& runtime) override; - void handleScreenSizeChange(int width, int height); + void handleScreenSizeChange(Dimensions& screen, Insets& insets, Dimensions& statusBar, Dimensions& navigationBar); void handleAppearanceChange(std::string colorScheme); void handleContentSizeCategoryChange(std::string contentSizeCategory); diff --git a/docs/.astro/types.d.ts b/docs/.astro/types.d.ts index 7567eb13..1190ddfc 100644 --- a/docs/.astro/types.d.ts +++ b/docs/.astro/types.d.ts @@ -248,6 +248,13 @@ declare module 'astro:content' { collection: "docs"; data: InferEntrySchema<"docs"> } & { render(): Render[".mdx"] }; +"reference/dimensions.mdx": { + id: "reference/dimensions.mdx"; + slug: "reference/dimensions"; + body: string; + collection: "docs"; + data: InferEntrySchema<"docs"> +} & { render(): Render[".mdx"] }; "reference/dynamic-functions.mdx": { id: "reference/dynamic-functions.mdx"; slug: "reference/dynamic-functions"; diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 2aaeab26..2173934f 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -87,17 +87,24 @@ export default defineConfig({ label: 'Compound variants', link: '/reference/compound-variants/' }, + { + label: 'Dimensions', + link: '/reference/dimensions/', + badge: 'New' + }, { label: 'Unistyles Registry', link: '/reference/unistyles-registry/' }, { label: 'Unistyles Runtime', - link: '/reference/unistyles-runtime/' + link: '/reference/unistyles-runtime/', + badge: 'New' }, { label: 'Content size category', - link: '/reference/content-size-category/' + link: '/reference/content-size-category/', + badge: 'New' }, { label: 'Plugins', @@ -156,7 +163,8 @@ export default defineConfig({ }, { label: 'Sponsors', - link: 'other/sponsors/' + link: 'other/sponsors/', + badge: 'New' }, { label: 'For Sponsors', diff --git a/docs/src/content/docs/other/sponsors.mdx b/docs/src/content/docs/other/sponsors.mdx index 34bb7129..e5aa6fee 100644 --- a/docs/src/content/docs/other/sponsors.mdx +++ b/docs/src/content/docs/other/sponsors.mdx @@ -52,7 +52,15 @@ Do you want to become a sponsor? Read a guide [for sponsors](/other/for-sponsors ### Individuals -🙋‍♂️🙋‍♀️ +
+ +
### Contributors diff --git a/docs/src/content/docs/reference/content-size-category.mdx b/docs/src/content/docs/reference/content-size-category.mdx index 8ad69a07..f87da317 100644 --- a/docs/src/content/docs/reference/content-size-category.mdx +++ b/docs/src/content/docs/reference/content-size-category.mdx @@ -28,6 +28,12 @@ and the available values are: `xSmall`, `Small`, `Medium`, `Large`, `xLarge`, `xxLarge`, `xxxLarge`, `unspecified` + + +In addition to above categories, you can also use the [Accessibility sizes](https://developer.apple.com/documentation/uikit/uicontentsizecategory#2901207), available values are: + +`accessibilityMedium`, `accessibilityLarge`, `accessibilityExtraLarge`, `accessibilityExtraExtraLarge`, `accessibilityExtraExtraExtraLarge` + ### Android There is no direct equivalent to the iOS content size category on Android. The implementation is based on [Font Scale](https://developer.android.com/reference/android/content/res/Configuration#fontScale) @@ -37,13 +43,15 @@ and the available values are: Mapping is based on the following table: -| Value | Font Scale | -| ----- | ---------- | -| Small | \<= 0.85 | -| Default | \<= 1.0 | -| Large | \<= 1.15 | -| ExtraLarge | \<= 1.3 | -| Huge | >1.3 | +| Version | Value | Font Scale | +| ----- | ----- | ---------- | +| - | Small | \<= 0.85 | +| - | Default | \<= 1.0 | +| - | Large | \<= 1.15 | +| - | ExtraLarge | \<= 1.3 | +| - | Huge | \<=1.5 | +| | ExtraHuge | \<=1.8 | +| | ExtraExtraHuge | >1.8 | :::tip[Your app restarts?] If you change the font scale in your Android settings, your app will restart by default. diff --git a/docs/src/content/docs/reference/dimensions.mdx b/docs/src/content/docs/reference/dimensions.mdx new file mode 100644 index 00000000..160f03fe --- /dev/null +++ b/docs/src/content/docs/reference/dimensions.mdx @@ -0,0 +1,134 @@ +--- +title: Dimensions +--- + +import Seo from '../../../components/Seo.astro' +import Badge from '../../../components/Badge.astro' + + + +Unistyles provides rich metadata about your device dimensions. This is useful for creating responsive designs as well as avoiding additional hooks that require passing values to stylesheets. +Every prop can be accessed with [UnistylesRuntime](/reference/unistyles-runtime). +Dimensions are always up to date and are updated based on Unistyles' core logic, e.g., when the device orientation changes. + +### Accessing dimensions + +In order to start using the dimensions metadata, you need to import `UnistylesRuntime`: + +```tsx /UnistylesRuntime/ +import { UnistylesRuntime } from 'react-native-unistyles' +``` + +`UnistylesRuntime` can be used in your component as well as directly in the stylesheet. + +:::tip[UnistylesRuntime] +Using UnistylesRuntime in your stylesheet has additional benefit, it will update (re-render) your stylesheets when the dimensions change. +::: + +### Screen dimensions + + + + +The most basic dimensions are the screen dimensions. These are the dimensions of the screen that your app is running on. +You can access them with `screen` prop: + +```tsx /screen/ +import { UnistylesRuntime } from 'react-native-unistyles' + +UnistylesRuntime.screen.width // eg. 400 +UnistylesRuntime.screen.height // eg. 760 +``` + +### Status bar + + + + + +You can access status bar dimensions with `statusBar` prop: + +```tsx /statusBar/ +import { UnistylesRuntime } from 'react-native-unistyles' + +UnistylesRuntime.statusBar.width // eg. 400 +UnistylesRuntime.statusBar.height // eg. 24 +``` + +This prop may be useful for creating custom headers. In most of the cases status bar height is equal to the top inset, but on some devices it may be different. +Status bar height is not dynamic and won't cover hiding it. + +### Navigation bar + + + + +You can access navigation bar dimensions with `navigationBar` prop: + +```tsx /navigationBar/ +import { UnistylesRuntime } from 'react-native-unistyles' + +UnistylesRuntime.navigationBar.width // eg. 400 +UnistylesRuntime.navigationBar.height // eg. 24 +``` + +This prop may be useful for creating custom bottom bars. In most of the cases navigation bar height is equal to the bottom inset, but on some devices it may be different. +Navigation bar height is not dynamic. + +### Insets + + + + + +Insets are the safe areas of the screen. They are used to avoid overlapping with system UI elements such as the status bar, navigation bar, and home indicator. +You can access them with `insets` prop: + +```tsx /insets/ +import { UnistylesRuntime } from 'react-native-unistyles' + +UnistylesRuntime.insets.top // eg. 42 +UnistylesRuntime.insets.bottom // eg. 24 +UnistylesRuntime.insets.left // eg. 0, or in vertical orientation can be top inset +UnistylesRuntime.insets.right // eg. 0 +``` + +Insets can be used directly in your stylesheets to avoid passing values from `useSafeAreaInsets` hook from [react-native-safe-area-context](https://github.com/th3rdwave/react-native-safe-area-context?tab=readme-ov-file#usesafeareainsets) library. + +Insets on Android respect following setups: + +##### Modifying dynamicaly StatusBar API: + +```tsx + + diff --git a/docs/src/content/docs/reference/dynamic-functions.mdx b/docs/src/content/docs/reference/dynamic-functions.mdx index 49464505..842533f4 100644 --- a/docs/src/content/docs/reference/dynamic-functions.mdx +++ b/docs/src/content/docs/reference/dynamic-functions.mdx @@ -60,8 +60,7 @@ const stylesheet = createStyleSheet(theme => ({ ``` :::caution -It's worth mentioning that while using dynamic functions may be convenient, -you should limit their number to a minimum. There are a few downsides to be aware of: +It's worth mentioning that while using dynamic functions may be convenient, there are a few downsides to be aware of: - On the web, styles created with dynamic functions are always inlined in the `style` attribute - The `container` style from the example above will recompute the styles every time the component re-renders ::: diff --git a/docs/src/content/docs/reference/unistyles-runtime.mdx b/docs/src/content/docs/reference/unistyles-runtime.mdx index 023b48d3..c855c8c6 100644 --- a/docs/src/content/docs/reference/unistyles-runtime.mdx +++ b/docs/src/content/docs/reference/unistyles-runtime.mdx @@ -36,17 +36,20 @@ and use it anywhere in your code, even outside a component. ## Getters -| Name | Type | Description | -| ---- | ---- | ----------- | -| colorScheme | string | Get your device's color scheme. Available options `dark`, `light` or `unspecified` | -| hasAdaptiveThemes | boolean | Indicates if you have enabled [adaptive themes](/reference/theming#adaptive-themes) | -| themeName | string | Name of the selected theme or an empty string if you don't use themes | -| breakpoint | string / undefined | Current breakpoint or always undefined if you don't use breakpoints | -| breakpoints | Object | Your breakpoints registered with [UnistylesRegistry](/reference/unistyles-registry/) | -| enabledPlugins | string[] | Names of currently enabled plugins | -| screen | Object | Screen dimensions | -| orientation | ScreenOrientation | Your device's orientation | -| contentSizeCategory | IOSContentSizeCategory or AndroidContentSizeCategory | Your device's [content size category](/reference/content-size-category/) | +| Version | Name | Type | Description | +| ----| ---- | ---- | ----------- | +| - | colorScheme | string | Get your device's color scheme. Available options `dark`, `light` or `unspecified` | +| - | hasAdaptiveThemes | boolean | Indicates if you have enabled [adaptive themes](/reference/theming#adaptive-themes) | +| - | themeName | string | Name of the selected theme or an empty string if you don't use themes | +| - | breakpoint | string / undefined | Current breakpoint or always undefined if you don't use breakpoints | +| - | breakpoints | Object | Your breakpoints registered with [UnistylesRegistry](/reference/unistyles-registry/) | +| - | enabledPlugins | string[] | Names of currently enabled plugins | +| - | screen | \{width: number, height: number\} | Screen dimensions | +| - | orientation | ScreenOrientation | Your device's orientation | +| - | contentSizeCategory | IOSContentSizeCategory or AndroidContentSizeCategory | Your device's [content size category](/reference/content-size-category/) | +| | insets | \{ top: number, bottom: number, left: number, right: number \} | Device insets which are safe to put content into | +| | statusBar | \{width: number, height: number\} | Status bar dimensions | +| | navigationBar | \{width: number, height: number\} | Navigation bar dimensions (Android) | ## Setters diff --git a/docs/src/content/docs/reference/variants.mdx b/docs/src/content/docs/reference/variants.mdx index 430b0b79..a873eba4 100644 --- a/docs/src/content/docs/reference/variants.mdx +++ b/docs/src/content/docs/reference/variants.mdx @@ -176,6 +176,11 @@ const stylesheet = createStyleSheet(theme => ({ If you specify a boolean variants like "true", there is no requirement to specify a "false" variant (and vice versa). You can mix boolean variants with other variants as well. +:::caution +Boolean variants respects other rules, eg. `false` is not equal to `default`. To select `false` variant you need to pass `false` as a value, +to fallback to `default` variant you need to pass `undefined` or `{}`. +::: + ## Default variant You can define a `default` variant that will be used when you don't pass any variant to the `useStyles` hook: diff --git a/examples/expo/src/examples/ContentSizeCategoryScreen.tsx b/examples/expo/src/examples/ContentSizeCategoryScreen.tsx index 0559c26f..744b4cf3 100644 --- a/examples/expo/src/examples/ContentSizeCategoryScreen.tsx +++ b/examples/expo/src/examples/ContentSizeCategoryScreen.tsx @@ -8,7 +8,9 @@ enum AppContentSizeCategory { Small = 'Small', Medium = 'Medium', Large = 'Large', - xLarge = 'xLarge' + xLarge = 'xLarge', + xxLarge = 'xxLarge', + xxxLarge = 'xxxLarge' } const getUnifiedContentSizeCategory = (contentSizeCategory: IOSContentSizeCategory | AndroidContentSizeCategory) => { @@ -19,11 +21,19 @@ const getUnifiedContentSizeCategory = (contentSizeCategory: IOSContentSizeCatego case IOSContentSizeCategory.Small: return AppContentSizeCategory.Small case IOSContentSizeCategory.Medium: + case IOSContentSizeCategory.AccessibilityMedium: return AppContentSizeCategory.Medium case IOSContentSizeCategory.Large: + case IOSContentSizeCategory.AccessibilityLarge: return AppContentSizeCategory.Large - default: + case IOSContentSizeCategory.ExtraLarge: + case IOSContentSizeCategory.AccessibilityExtraLarge: return AppContentSizeCategory.xLarge + case IOSContentSizeCategory.ExtraExtraLarge: + case IOSContentSizeCategory.AccessibilityExtraExtraLarge: + return AppContentSizeCategory.xxLarge + default: + return AppContentSizeCategory.xxxLarge } } @@ -34,8 +44,12 @@ const getUnifiedContentSizeCategory = (contentSizeCategory: IOSContentSizeCatego return AppContentSizeCategory.Medium case AndroidContentSizeCategory.Large: return AppContentSizeCategory.Large - default: + case AndroidContentSizeCategory.ExtraLarge: return AppContentSizeCategory.xLarge + case AndroidContentSizeCategory.Huge: + return AppContentSizeCategory.xxLarge + default: + return AppContentSizeCategory.xxxLarge } } @@ -83,6 +97,14 @@ const stylesheet = createStyleSheet(theme => ({ xLarge: { padding: 32, backgroundColor: '#833471' + }, + xxLarge: { + padding: 64, + backgroundColor: '#6F1E51' + }, + xxxLarge: { + padding: 128, + backgroundColor: '#5B2C6F' } } } diff --git a/examples/expo/src/examples/RuntimeScreen.tsx b/examples/expo/src/examples/RuntimeScreen.tsx index 47f1c394..6c03e3ed 100644 --- a/examples/expo/src/examples/RuntimeScreen.tsx +++ b/examples/expo/src/examples/RuntimeScreen.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { View, Text, ScrollView } from 'react-native' +import { View, Text, ScrollView, StatusBar } from 'react-native' import { UnistylesRuntime, createStyleSheet, useStyles } from 'react-native-unistyles' import { Button, DemoScreen } from '../components' import { autoGuidelinePlugin } from '../plugins' @@ -18,12 +18,16 @@ export const RuntimeScreen: React.FunctionComponent = () => { removePlugin, addPlugin, setTheme, - setAdaptiveThemes + setAdaptiveThemes, + insets, + statusBar, + navigationBar } = UnistylesRuntime const { styles, theme } = useStyles(stylesheet) return ( + @@ -37,6 +41,30 @@ export const RuntimeScreen: React.FunctionComponent = () => { {screen.width}x{screen.height} + + + Status bar dimensions: + + + {statusBar.width}x{statusBar.height} + + + + + Navigation bar dimensions: + + + {navigationBar.width}x{navigationBar.height} + + + + + Insets: + + + T:{insets.top} B:{insets.bottom} R:{insets.right} L:{insets.left} + + Selected theme: @@ -147,6 +175,10 @@ export const RuntimeScreen: React.FunctionComponent = () => { + + + + ) } @@ -193,5 +225,37 @@ const stylesheet = createStyleSheet(theme => ({ }, fakeSpacer: { height: 100 + }, + topInset: { + position: 'absolute', + top: UnistylesRuntime.insets.top, + left: 0, + right: 0, + height: 1, + backgroundColor: theme.colors.accent + }, + bottomInset: { + position: 'absolute', + bottom: UnistylesRuntime.insets.bottom, + left: 0, + right: 0, + height: 1, + backgroundColor: theme.colors.accent + }, + leftInset: { + position: 'absolute', + top: 0, + bottom: 0, + left: UnistylesRuntime.insets.left, + width: 1, + backgroundColor: theme.colors.accent + }, + rightInset: { + position: 'absolute', + top: 0, + bottom: 0, + right: UnistylesRuntime.insets.right, + width: 1, + backgroundColor: theme.colors.accent } })) diff --git a/examples/macos/App.tsx b/examples/macos/App.tsx index ebe42229..e2561929 100644 --- a/examples/macos/App.tsx +++ b/examples/macos/App.tsx @@ -21,7 +21,10 @@ export const App: React.FunctionComponent = () => { enabledPlugins, addPlugin, removePlugin, - updateTheme + updateTheme, + insets, + statusBar, + navigationBar } = UnistylesRuntime return ( @@ -48,6 +51,30 @@ export const App: React.FunctionComponent = () => { {screen.width}x{screen.height}
+ + + Status bar dimensions: + + + {statusBar.width ?? '-'}x{statusBar.height ?? '-'} + + + + + Navigation bar dimensions: + + + {navigationBar.width ?? '-'}x{navigationBar.height ?? '-'} + + + + + Insets: + + + T:{insets.top ?? '-'} B:{insets.bottom ?? '-'} R:{insets.right ?? '-'} L:{insets.left ?? '-'} + + Current breakpoint: diff --git a/examples/win/App.tsx b/examples/win/App.tsx index c541f78e..db79f95d 100644 --- a/examples/win/App.tsx +++ b/examples/win/App.tsx @@ -1,5 +1,10 @@ import React from 'react' -import { createStyleSheet, useStyles, UnistylesRuntime, mq } from 'react-native-unistyles' +import { + createStyleSheet, + useStyles, + UnistylesRuntime, + mq +} from 'react-native-unistyles' import { ScrollView, View, Text, Image, Pressable } from 'react-native' import { highContrastPlugin } from './highContrastPlugin' import './styles' @@ -21,93 +26,80 @@ export const App: React.FunctionComponent = () => { enabledPlugins, addPlugin, removePlugin, - updateTheme + updateTheme, + insets, + statusBar, + navigationBar } = UnistylesRuntime return ( - - - - Welcome to Unistyles! - + + + Welcome to Unistyles! - - Runtime values: - + Runtime values: - - Screen dimensions: - + Screen dimensions: {screen.width}x{screen.height} - - Current breakpoint: - + Status bar dimensions: - {breakpoint} + {statusBar.width}x{statusBar.height} - - Color scheme: - + Navigation bar: - {colorScheme} + {navigationBar.width}x{navigationBar.height} - - Content size category: - + Insets: - {contentSizeCategory} + T:{insets.top} B:{insets.bottom} R:{insets.right} L: + {insets.left} - - Selected theme: - - - {themeName} - + Current breakpoint: + {breakpoint} - - Orientation: - - - {orientation} - + Color scheme: + {colorScheme} - - Has adaptive themes: - + Content size category: + {contentSizeCategory} + + + Selected theme: + {themeName} + + + Orientation: + {orientation} + + + Has adaptive themes: {hasAdaptiveThemes ? 'Yes' : 'No'} - - Enabled plugins: - + Enabled plugins: {enabledPlugins.length > 0 ? enabledPlugins.join(', ') : '-'} - - Actions: - + Actions: styles.button(event.pressed)} onPress={() => { @@ -122,17 +114,13 @@ export const App: React.FunctionComponent = () => { } }} > - - Change theme - + Change theme styles.button(event.pressed)} onPress={() => setAdaptiveThemes(!hasAdaptiveThemes)} > - - Toggle adaptive themes - + Toggle adaptive themes styles.button(event.pressed)} @@ -142,9 +130,7 @@ export const App: React.FunctionComponent = () => { : addPlugin(highContrastPlugin) }} > - - Toggle plugin - + Toggle plugin styles.button(event.pressed)} @@ -155,9 +141,10 @@ export const App: React.FunctionComponent = () => { ...theme, colors: { ...theme.colors, - typography: theme.colors.typography === '#000000' - ? '#00d2d3' - : '#000000' + typography: + theme.colors.typography === '#000000' + ? '#00d2d3' + : '#000000' } })) case 'dark': @@ -165,9 +152,10 @@ export const App: React.FunctionComponent = () => { ...theme, colors: { ...theme.colors, - typography: theme.colors.typography === '#ffffff' - ? '#00d2d3' - : '#ffffff' + typography: + theme.colors.typography === '#ffffff' + ? '#00d2d3' + : '#ffffff' } })) case 'premium': @@ -176,17 +164,16 @@ export const App: React.FunctionComponent = () => { ...theme, colors: { ...theme.colors, - typography: theme.colors.typography === '#76278f' - ? '#000000' - : '#76278f' + typography: + theme.colors.typography === '#76278f' + ? '#000000' + : '#76278f' } })) } }} > - - Update theme - + Update theme diff --git a/ios/UnistylesModule.mm b/ios/UnistylesModule.mm index f3fa01ce..31777b76 100644 --- a/ios/UnistylesModule.mm +++ b/ios/UnistylesModule.mm @@ -72,10 +72,12 @@ - (void)emitEvent:(NSString *)eventName withBody:(NSDictionary *)body { void registerUnistylesHostObject(jsi::Runtime &runtime, UnistylesModule* weakSelf) { auto unistylesRuntime = std::make_shared( - (int)weakSelf.platform.initialWidth, - (int)weakSelf.platform.initialHeight, + weakSelf.platform.initialScreen, weakSelf.platform.initialColorScheme, - weakSelf.platform.initialContentSizeCategory + weakSelf.platform.initialContentSizeCategory, + weakSelf.platform.initialInsets, + weakSelf.platform.initialStatusBar, + weakSelf.platform.initialNavigationBar ); unistylesRuntime.get()->onThemeChange([=](std::string theme) { @@ -89,15 +91,29 @@ void registerUnistylesHostObject(jsi::Runtime &runtime, UnistylesModule* weakSel [weakSelf emitEvent:@"__unistylesOnChange" withBody:body]; }); - unistylesRuntime.get()->onLayoutChange([=](std::string breakpoint, std::string orientation, int width, int height) { + unistylesRuntime.get()->onLayoutChange([=](std::string breakpoint, std::string orientation, Dimensions& screen, Dimensions& statusBar, Insets& insets, Dimensions& navigationBar) { NSDictionary *body = @{ @"type": @"layout", @"payload": @{ @"breakpoint": cxxStringToNSString(breakpoint), @"orientation": cxxStringToNSString(orientation), @"screen": @{ - @"width": @(width), - @"height": @(height) + @"width": @(screen.width), + @"height": @(screen.height) + }, + @"statusBar": @{ + @"width": @(statusBar.width), + @"height": @(statusBar.height) + }, + @"navigationBar": @{ + @"width": @(navigationBar.width), + @"height": @(navigationBar.height) + }, + @"insets": @{ + @"top": @(insets.top), + @"bottom": @(insets.bottom), + @"left": @(insets.left), + @"right": @(insets.right) } } }; diff --git a/ios/platform/Platform_iOS.h b/ios/platform/Platform_iOS.h index 57abebd4..95bbd62f 100644 --- a/ios/platform/Platform_iOS.h +++ b/ios/platform/Platform_iOS.h @@ -1,11 +1,15 @@ #include +#include +#include @interface Platform : NSObject -@property (nonatomic, assign) CGFloat initialWidth; -@property (nonatomic, assign) CGFloat initialHeight; +@property (nonatomic, assign) Dimensions initialScreen; @property (nonatomic, assign) std::string initialColorScheme; @property (nonatomic, assign) std::string initialContentSizeCategory; +@property (nonatomic, assign) Insets initialInsets; +@property (nonatomic, assign) Dimensions initialStatusBar; +@property (nonatomic, assign) Dimensions initialNavigationBar; @property (nonatomic, assign) void* unistylesRuntime; - (instancetype)init; diff --git a/ios/platform/Platform_iOS.mm b/ios/platform/Platform_iOS.mm index ce9b8273..c7fbb975 100644 --- a/ios/platform/Platform_iOS.mm +++ b/ios/platform/Platform_iOS.mm @@ -12,10 +12,11 @@ - (instancetype)init { UIScreen *screen = [UIScreen mainScreen]; UIContentSizeCategory contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; - self.initialWidth = screen.bounds.size.width; - self.initialHeight = screen.bounds.size.height; + self.initialScreen = {(int)screen.bounds.size.width, (int)screen.bounds.size.height}; self.initialColorScheme = [self getColorScheme]; self.initialContentSizeCategory = [self getContentSizeCategory:contentSizeCategory]; + self.initialStatusBar = [self getStatusBarDimensions]; + self.initialInsets = [self getInsets]; [self setupListeners]; } @@ -71,12 +72,14 @@ - (void)onContentSizeCategoryChange:(NSNotification *)notification { - (void)onOrientationChange:(NSNotification *)notification { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - UIScreen *screen = [UIScreen mainScreen]; - CGFloat screenWidth = screen.bounds.size.width; - CGFloat screenHeight = screen.bounds.size.height; + UIScreen *mainScreen = [UIScreen mainScreen]; + Dimensions screen = {(int)mainScreen.bounds.size.width, (int)mainScreen.bounds.size.height}; + Insets insets = [self getInsets]; + Dimensions statusBar = [self getStatusBarDimensions]; + Dimensions navigationBar = [self getNavigationBarDimensions]; if (self.unistylesRuntime != nullptr) { - ((UnistylesRuntime*)self.unistylesRuntime)->handleScreenSizeChange((int)screenWidth, (int)screenHeight); + ((UnistylesRuntime*)self.unistylesRuntime)->handleScreenSizeChange(screen, insets, statusBar, navigationBar); } }); } @@ -95,6 +98,23 @@ - (void)onOrientationChange:(NSNotification *)notification { } } +- (Insets)getInsets { + UIWindow *window = UIApplication.sharedApplication.windows.firstObject; + UIEdgeInsets safeArea = window.safeAreaInsets; + + return {(int)safeArea.top, (int)safeArea.bottom, (int)safeArea.left, (int)safeArea.right}; +} + +- (Dimensions)getStatusBarDimensions { + CGRect statusBarFrame = UIApplication.sharedApplication.statusBarFrame; + + return {(int)statusBarFrame.size.width, (int)statusBarFrame.size.height}; +} + +- (Dimensions)getNavigationBarDimensions { + return {0, 0}; +} + - (std::string)getContentSizeCategory:(UIContentSizeCategory)contentSizeCategory { if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { return std::string([@"xxxLarge" UTF8String]); @@ -124,6 +144,26 @@ - (void)onOrientationChange:(NSNotification *)notification { return std::string([@"xSmall" UTF8String]); } + if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { + return std::string([@"accessibilityMedium" UTF8String]); + } + + if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { + return std::string([@"accessibilityLarge" UTF8String]); + } + + if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { + return std::string([@"accessibilityExtraLarge" UTF8String]); + } + + if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { + return std::string([@"accessibilityExtraExtraLarge" UTF8String]); + } + + if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { + return std::string([@"accessibilityExtraExtraExtraLarge" UTF8String]); + } + return std::string([@"unspecified" UTF8String]); } diff --git a/ios/platform/Platform_macOS.h b/ios/platform/Platform_macOS.h index a9c6a81c..72e93dde 100644 --- a/ios/platform/Platform_macOS.h +++ b/ios/platform/Platform_macOS.h @@ -1,11 +1,15 @@ #include +#include +#include @interface Platform : NSObject -@property (nonatomic, assign) CGFloat initialWidth; -@property (nonatomic, assign) CGFloat initialHeight; +@property (nonatomic, assign) Dimensions initialScreen; @property (nonatomic, assign) std::string initialColorScheme; @property (nonatomic, assign) std::string initialContentSizeCategory; +@property (nonatomic, assign) Insets initialInsets; +@property (nonatomic, assign) Dimensions initialStatusBar; +@property (nonatomic, assign) Dimensions initialNavigationBar; @property (nonatomic, assign) void* unistylesRuntime; - (instancetype)init; diff --git a/ios/platform/Platform_macOS.mm b/ios/platform/Platform_macOS.mm index dc9df780..64e5e658 100644 --- a/ios/platform/Platform_macOS.mm +++ b/ios/platform/Platform_macOS.mm @@ -11,10 +11,12 @@ - (instancetype)init { if (self) { NSWindow *window = RCTSharedApplication().mainWindow; - self.initialWidth = window.frame.size.width; - self.initialHeight = window.frame.size.height; + self.initialScreen = {(int)window.frame.size.width, (int)window.frame.size.height}; self.initialContentSizeCategory = std::string([@"unspecified" UTF8String]); self.initialColorScheme = [self getColorScheme]; + self.initialStatusBar = [self getStatusBarDimensions]; + self.initialNavigationBar = [self getNavigationBarDimensions]; + self.initialInsets = [self getInsets]; [self setupListeners]; } @@ -59,12 +61,18 @@ - (void)onAppearanceChange { - (void)onWindowResize { NSWindow *window = RCTSharedApplication().mainWindow; - - CGFloat screenWidth = window.frame.size.width; - CGFloat screenHeight = window.frame.size.height; + Dimensions screen = {(int)window.frame.size.width, (int)window.frame.size.height}; + Insets insets = [self getInsets]; + Dimensions statusBar = [self getStatusBarDimensions]; + Dimensions navigationBar = [self getNavigationBarDimensions]; if (self.unistylesRuntime != nullptr) { - ((UnistylesRuntime*)self.unistylesRuntime)->handleScreenSizeChange((int)screenWidth, (int)screenHeight); + ((UnistylesRuntime*)self.unistylesRuntime)->handleScreenSizeChange( + screen, + insets, + statusBar, + navigationBar + ); } } @@ -78,6 +86,18 @@ - (void)onWindowResize { return UnistylesLightScheme; } +- (Insets)getInsets { + return {0, 0, 0, 0}; +} + +- (Dimensions)getStatusBarDimensions { + return {0, 0}; +} + +- (Dimensions)getNavigationBarDimensions { + return {0, 0}; +} + @end #endif diff --git a/package.json b/package.json index 9f061682..0fcd671d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-unistyles", - "version": "2.3.0", + "version": "2.4.0-rc.2", "description": "Level up your React Native StyleSheet", "scripts": { "test": "jest", diff --git a/src/common.ts b/src/common.ts index 359742fd..efd14fac 100644 --- a/src/common.ts +++ b/src/common.ts @@ -16,6 +16,11 @@ export const ScreenOrientation = { } as const export enum IOSContentSizeCategory { + AccessibilityExtraExtraExtraLarge = 'accessibilityExtraExtraExtraLarge', + AccessibilityExtraExtraLarge = 'accessibilityExtraExtraLarge', + AccessibilityExtraLarge = 'accessibilityExtraLarge', + AccessibilityLarge = 'accessibilityLarge', + AccessibilityMedium = 'accessibilityMedium', ExtraExtraExtraLarge = 'xxxLarge', ExtraExtraLarge = 'xxLarge', ExtraLarge = 'xLarge', @@ -31,7 +36,9 @@ export enum AndroidContentSizeCategory { Default = 'Default', Large = 'Large', ExtraLarge = 'ExtraLarge', - Huge = 'Huge' + Huge = 'Huge', + ExtraHuge = 'ExtraHuge', + ExtraExtraHuge = 'ExtraExtraHuge' } export enum UnistylesEventType { diff --git a/src/core/UnistylesModule.ts b/src/core/UnistylesModule.ts index bf4eb0fe..91f6be49 100644 --- a/src/core/UnistylesModule.ts +++ b/src/core/UnistylesModule.ts @@ -1,6 +1,6 @@ import { NativeEventEmitter, NativeModules } from 'react-native' import type { UnistylesThemes, UnistylesBreakpoints } from 'react-native-unistyles' -import type { ColorSchemeName } from '../types' +import type { ColorSchemeName, ScreenInsets, ScreenDimensions } from '../types' import { normalizeWebStylesPlugin } from '../plugins' import { isServer } from '../common' @@ -19,6 +19,20 @@ export class UnistylesBridgeWeb { #sortedBreakpointPairs: Array<[keyof UnistylesBreakpoints, number]> = [] #breakpoint: keyof UnistylesBreakpoints = '' as keyof UnistylesBreakpoints #contentSizeCategory: string = 'unspecified' + #insets: ScreenInsets = { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + #statusBar: ScreenDimensions = { + height: 0, + width: 0 + } + #navigationBar: ScreenDimensions = { + height: 0, + width: 0 + } constructor() { if (!isServer) { @@ -54,6 +68,12 @@ export class UnistylesBridgeWeb { return this.#enabledPlugins case 'colorScheme': return this.#colorScheme + case 'insets': + return this.#insets + case 'statusBar': + return this.#statusBar + case 'navigationBar': + return this.#navigationBar case 'useTheme': return (themeName: keyof UnistylesThemes) => this.useTheme(themeName) case 'updateTheme': diff --git a/src/core/UnistylesRuntime.ts b/src/core/UnistylesRuntime.ts index c5953aed..99c8b2d7 100644 --- a/src/core/UnistylesRuntime.ts +++ b/src/core/UnistylesRuntime.ts @@ -76,6 +76,30 @@ export class UnistylesRuntime { } } + /** + * Get the safe area insets + * @returns - The safe area insets { top, bottom, left, right } + */ + public get insets() { + return this.unistylesBridge.insets + } + + /** + * Get the status bar info + * @returns - The status bar size { width, height } + */ + public get statusBar() { + return this.unistylesBridge.statusBar + } + + /** + * Get the navigation bar info (Android) + * @returns - The navigation bar size { width, height } + */ + public get navigationBar() { + return this.unistylesBridge.navigationBar + } + /** * Get the screen orientation * @returns - The screen orientation diff --git a/src/hooks/useUnistyles.ts b/src/hooks/useUnistyles.ts index a2fde2bc..64e205af 100644 --- a/src/hooks/useUnistyles.ts +++ b/src/hooks/useUnistyles.ts @@ -16,6 +16,20 @@ export const useUnistyles = () => { screenSize: { width: unistyles.runtime.screen.width, height: unistyles.runtime.screen.height + }, + statusBar: { + width: unistyles.runtime.statusBar.width, + height: unistyles.runtime.statusBar.height + }, + navigationBar: { + width: unistyles.runtime.navigationBar.width, + height: unistyles.runtime.navigationBar.height + }, + insets: { + top: unistyles.runtime.insets.top, + bottom: unistyles.runtime.insets.bottom, + left: unistyles.runtime.insets.left, + right: unistyles.runtime.insets.right } }) @@ -35,7 +49,10 @@ export const useUnistyles = () => { return setLayout({ breakpoint: layoutEvent.payload.breakpoint, orientation: layoutEvent.payload.orientation, - screenSize: layoutEvent.payload.screen + screenSize: layoutEvent.payload.screen, + statusBar: layoutEvent.payload.statusBar, + insets: layoutEvent.payload.insets, + navigationBar: layoutEvent.payload.navigationBar }) } case UnistylesEventType.Plugin: { diff --git a/src/types/unistyles.ts b/src/types/unistyles.ts index b1dfdbfc..03887983 100644 --- a/src/types/unistyles.ts +++ b/src/types/unistyles.ts @@ -5,6 +5,18 @@ import type { UnistylesPlugin } from './plugin' export type ColorSchemeName = 'light' | 'dark' | 'unspecified' +export type ScreenInsets = { + top: number, + right: number, + bottom: number, + left: number +} + +export type ScreenDimensions = { + height: number, + width: number +} + export type UnistylesConfig = { adaptiveThemes?: boolean, initialTheme?: keyof UnistylesThemes, @@ -23,6 +35,9 @@ export type UnistylesBridge = { colorScheme: ColorSchemeName, contentSizeCategory: IOSContentSizeCategory | AndroidContentSizeCategory, sortedBreakpointPairs: Array<[keyof UnistylesBreakpoints, UnistylesBreakpoints[keyof UnistylesBreakpoints]]>, + insets: ScreenInsets, + statusBar: ScreenDimensions, + navigationBar: ScreenDimensions // setters themes: Array, @@ -45,6 +60,9 @@ export type UnistylesMobileLayoutEvent = { type: UnistylesEventType.Layout, payload: { screen: ScreenSize, + statusBar: ScreenDimensions, + navigationBar: ScreenDimensions, + insets: ScreenInsets, breakpoint: keyof UnistylesBreakpoints, orientation: typeof ScreenOrientation[keyof typeof ScreenOrientation] } diff --git a/windows/ReactNativeUnistyles/ReactNativeUnistyles.h b/windows/ReactNativeUnistyles/ReactNativeUnistyles.h index fc3ccdef..52e36fff 100644 --- a/windows/ReactNativeUnistyles/ReactNativeUnistyles.h +++ b/windows/ReactNativeUnistyles/ReactNativeUnistyles.h @@ -18,8 +18,7 @@ using namespace winrt::Windows::UI::Core; using namespace facebook; struct UIInitialInfo { - int screenWidth; - int screenHeight; + Dimensions screen; std::string colorScheme; std::string contentSizeCategory; }; @@ -39,7 +38,9 @@ struct Unistyles { auto bounds = Window::Current().Bounds(); if (this->unistylesRuntime != nullptr) { - ((UnistylesRuntime*)this->unistylesRuntime)->handleScreenSizeChange((int)bounds.Width, (int)bounds.Height); + Dimensions screenDimensions = Dimensions{(int)bounds.Width, (int)bounds.Height}; + + ((UnistylesRuntime*)this->unistylesRuntime)->handleScreenSizeChange(screenDimensions, this->getInsets(), this->getStatusBarDimensions(), this->getNavigationBarDimensions()); } })); @@ -80,8 +81,7 @@ struct Unistyles { UIInitialInfo uiMetadata; auto bounds = Window::Current().Bounds(); - uiMetadata.screenWidth = bounds.Width; - uiMetadata.screenHeight = bounds.Height; + uiMetadata.screen = Dimensions{(int)bounds.Width, (int)bounds.Height}; uiMetadata.colorScheme = this->getColorScheme(); uiMetadata.contentSizeCategory = UnistylesUnspecifiedScheme; @@ -91,10 +91,12 @@ struct Unistyles { UIInitialInfo uiInfo = uiInfoFuture.get(); auto unistylesRuntime = std::make_shared( - uiInfo.screenWidth, - uiInfo.screenHeight, + uiInfo.screen, uiInfo.colorScheme, - uiInfo.contentSizeCategory + uiInfo.contentSizeCategory, + this->getInsets(), + this->getStatusBarDimensions(), + this->getNavigationBarDimensions() ); unistylesRuntime->onThemeChange([this](std::string theme) { @@ -108,16 +110,30 @@ struct Unistyles { this->OnThemeChange(payload); }); - unistylesRuntime.get()->onLayoutChange([this](std::string breakpoint, std::string orientation, int width, int height) { + unistylesRuntime.get()->onLayoutChange([this](std::string breakpoint, std::string orientation, Dimensions& screen, Dimensions& statusBar, Insets& insets, Dimensions& navigationBar) { auto payload = winrt::Microsoft::ReactNative::JSValueObject{ {"type", "layout"}, {"payload", winrt::Microsoft::ReactNative::JSValueObject{ {"breakpoint", breakpoint}, {"orientation", orientation}, {"screen", winrt::Microsoft::ReactNative::JSValueObject{ - {"width", width}, - {"height", height} - }} + {"width", screen.width}, + {"height", screen.height} + }}, + {"statusBar", winrt::Microsoft::ReactNative::JSValueObject{ + {"width", statusBar.width}, + {"height", statusBar.height} + }}, + {"navigationBar", winrt::Microsoft::ReactNative::JSValueObject{ + {"width", navigationBar.width}, + {"height", navigationBar.height} + }}, + {"insets", winrt::Microsoft::ReactNative::JSValueObject{ + {"top", insets.top}, + {"bottom", insets.bottom}, + {"left", insets.left}, + {"right", insets.right} + }}, }} }; @@ -192,6 +208,18 @@ struct Unistyles { return UnistylesUnspecifiedScheme; } + + Insets getInsets() { + return Insets{ 0, 0, 0, 0 }; + } + + Dimensions getStatusBarDimensions() { + return {0, 0}; + } + + Dimensions getNavigationBarDimensions() { + return { 0, 0 }; + } }; } // namespace winrt::ReactNativeUnistyles diff --git a/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj b/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj index c83f8adb..925e9596 100644 --- a/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj +++ b/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj @@ -102,6 +102,8 @@ _DEBUG;%(PreprocessorDefinitions) ../../cxx;%(AdditionalIncludeDirectories) + ../../cxx;%(AdditionalIncludeDirectories) + ../../cxx;%(AdditionalIncludeDirectories) @@ -119,6 +121,11 @@ NotUsing + NotUsing + NotUsing + NotUsing + NotUsing + NotUsing Create @@ -147,4 +154,4 @@ - \ No newline at end of file + diff --git a/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj.filters b/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj.filters index a4e4bc96..f79f66db 100644 --- a/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj.filters +++ b/windows/ReactNativeUnistyles/ReactNativeUnistyles.vcxproj.filters @@ -7,6 +7,7 @@ + @@ -16,4 +17,4 @@ - \ No newline at end of file +