diff --git a/modules/bottom-sheet/android/build.gradle b/modules/bottom-sheet/android/build.gradle index a1d423044b..c4c051b70f 100644 --- a/modules/bottom-sheet/android/build.gradle +++ b/modules/bottom-sheet/android/build.gradle @@ -44,6 +44,6 @@ android { dependencies { implementation project(':expo-modules-core') - implementation 'com.google.android.material:material:1.12.0' + implementation 'com.google.android.material:material:1.13.0' implementation "com.facebook.react:react-native:+" } diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt index 079bac3a4f..bc08c6d9c7 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -5,8 +5,10 @@ import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup import android.view.ViewStructure +import android.view.Window import android.view.accessibility.AccessibilityEvent import android.widget.FrameLayout +import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.allViews import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.ReactContext @@ -15,6 +17,7 @@ import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.EventDispatcher import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.internal.EdgeToEdgeUtils import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView @@ -30,7 +33,9 @@ class BottomSheetView( private lateinit var dialogRootViewGroup: DialogRootViewGroup private var eventDispatcher: EventDispatcher? = null - private val screenHeight = context.resources.displayMetrics.heightPixels.toFloat() + private val screenHeight = + context.resources.displayMetrics.heightPixels + .toFloat() private fun getNavigationBarHeight(): Int { val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") @@ -38,15 +43,14 @@ class BottomSheetView( } private fun getStatusBarHeight(): Int { - val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") - return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 } private val onAttemptDismiss by EventDispatcher() private val onSnapPointChange by EventDispatcher() private val onStateChange by EventDispatcher() - // Props var disableDrag = false set(value) { field = value @@ -58,48 +62,31 @@ class BottomSheetView( field = value this.dialog?.setCancelable(!value) } + var preventExpansion = false var minHeight = 0f set(value) { - field = - if (value < 0) { - 0f - } else { - dpToPx(value) - } + field = if (value < 0) 0f else dpToPx(value) } var maxHeight = this.screenHeight set(value) { val px = dpToPx(value) - field = - if (px > this.screenHeight) { - this.screenHeight - } else { - px - } + field = if (px > this.screenHeight) this.screenHeight else px } private var isOpen: Boolean = false set(value) { field = value - onStateChange( - mapOf( - "state" to if (value) "open" else "closed", - ), - ) + onStateChange(mapOf("state" to if (value) "open" else "closed")) } private var isOpening: Boolean = false set(value) { field = value if (value) { - onStateChange( - mapOf( - "state" to "opening", - ), - ) + onStateChange(mapOf("state" to "opening")) } } @@ -107,33 +94,21 @@ class BottomSheetView( set(value) { field = value if (value) { - onStateChange( - mapOf( - "state" to "closing", - ), - ) + onStateChange(mapOf("state" to "closing")) } } private var selectedSnapPoint = 0 set(value) { if (field == value) return - field = value - onSnapPointChange( - mapOf( - "snapPoint" to value, - ), - ) + onSnapPointChange(mapOf("snapPoint" to value)) } - // Lifecycle - init { (appContext.reactContext as? ReactContext)?.let { it.addLifecycleEventListener(this) this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id) - this.dialogRootViewGroup = DialogRootViewGroup(context) this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher } @@ -164,53 +139,58 @@ class BottomSheetView( when { // Full height sheets contentHeight >= screenHeight -> 0.99f - else -> - this.clampRatio(this.getTargetHeight() / screenHeight) + else -> this.clampRatio(this.getTargetHeight() / screenHeight) } private fun present() { if (this.isOpen || this.isOpening || this.isClosing) return val contentHeight = this.getContentHeight() - val dialog = BottomSheetDialog(context) + + var activityWindow: Window? = null + var currentContext = context + while (currentContext != null) { + if (currentContext is android.app.Activity) { + activityWindow = currentContext.window + break + } + currentContext = (currentContext as? android.content.ContextWrapper)?.baseContext + } + + val originalStatusBarAppearance = + activityWindow?.let { window -> + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars + } + val originalNavBarAppearance = + activityWindow?.let { window -> + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars + } + + val dialog = BottomSheetDialog(context, R.style.EdgeToEdgeBottomSheetDialogTheme) dialog.setContentView(dialogRootViewGroup) dialog.setCancelable(!preventDismiss) + dialog.setDismissWithAnimation(true) dialog.setOnDismissListener { this.isClosing = true this.destroy() } - // Configure dialog window for edge-to-edge mode - dialog.window?.apply { - // Make status bar and navigation bar transparent - setNavigationBarColor(android.graphics.Color.TRANSPARENT) - setStatusBarColor(android.graphics.Color.TRANSPARENT) - - // Set FLAG_LAYOUT_NO_LIMITS to allow layout to extend beyond system UI boundaries - // Use FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS to properly handle transparent system bars - setFlags( - android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or - android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or - android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS - ) - - // Configure system UI visibility to allow layout behind both status and navigation bars - decorView.systemUiVisibility = android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + dialog.setOnShowListener { + dialog.window?.let { window -> + val insetsController = WindowInsetsControllerCompat(window, window.decorView) + if (originalNavBarAppearance != null) { + insetsController.isAppearanceLightNavigationBars = originalNavBarAppearance + } + if (originalStatusBarAppearance != null) { + EdgeToEdgeUtils.setLightStatusBar(window, originalStatusBarAppearance) + } + } } val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) bottomSheet?.let { it.setBackgroundColor(0) - it.fitsSystemWindows = false - - // Add padding to respect the status bar but not the navigation bar - val statusBarHeight = getStatusBarHeight() - it.setPadding(0, statusBarHeight, 0, 0) - val behavior = BottomSheetBehavior.from(it) behavior.state = BottomSheetBehavior.STATE_HIDDEN behavior.isFitToContents = true @@ -219,7 +199,17 @@ class BottomSheetView( behavior.isDraggable = true behavior.isHideable = true - if (contentHeight >= this.screenHeight || this.minHeight >= this.screenHeight) { + if (preventExpansion) { + behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() + } else { + behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt() + } + + val targetHeight = this.getTargetHeight() + val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight() + val shouldBeExpanded = targetHeight >= availableHeight + + if (shouldBeExpanded) { behavior.state = BottomSheetBehavior.STATE_EXPANDED this.selectedSnapPoint = 2 } else { @@ -234,18 +224,10 @@ class BottomSheetView( newState: Int, ) { when (newState) { - BottomSheetBehavior.STATE_EXPANDED -> { - selectedSnapPoint = 2 - } - BottomSheetBehavior.STATE_COLLAPSED -> { - selectedSnapPoint = 1 - } - BottomSheetBehavior.STATE_HALF_EXPANDED -> { - selectedSnapPoint = 1 - } - BottomSheetBehavior.STATE_HIDDEN -> { - selectedSnapPoint = 0 - } + BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2 + BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1 + BottomSheetBehavior.STATE_HALF_EXPANDED -> selectedSnapPoint = 1 + BottomSheetBehavior.STATE_HIDDEN -> selectedSnapPoint = 0 } } @@ -265,43 +247,26 @@ class BottomSheetView( val dialog = this.dialog ?: return val contentHeight = this.getContentHeight() - // Ensure edge-to-edge mode settings are maintained - dialog.window?.apply { - // Maintain transparent status and navigation bars - setNavigationBarColor(android.graphics.Color.TRANSPARENT) - setStatusBarColor(android.graphics.Color.TRANSPARENT) - - // Ensure layout flags are still set - setFlags( - android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or - android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, - android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or - android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS - ) - - // Re-apply system UI visibility settings - decorView.systemUiVisibility = android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - } - val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) bottomSheet?.let { - it.fitsSystemWindows = false - - val statusBarHeight = getStatusBarHeight() - it.setPadding(0, statusBarHeight, 0, 0) - val behavior = BottomSheetBehavior.from(it) val currentState = behavior.state val oldRatio = behavior.halfExpandedRatio - var newRatio = getHalfExpandedRatio(contentHeight) + val newRatio = getHalfExpandedRatio(contentHeight) behavior.halfExpandedRatio = newRatio - if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { + if (preventExpansion) { + behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() + } + + val targetHeight = this.getTargetHeight() + val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight() + val shouldBeExpanded = targetHeight >= availableHeight + + if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) { behavior.state = BottomSheetBehavior.STATE_EXPANDED - } else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { + } else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED } else if (currentState == BottomSheetBehavior.STATE_HALF_EXPANDED && oldRatio != newRatio) { behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED @@ -329,25 +294,19 @@ class BottomSheetView( private fun getTargetHeight(): Float { val contentHeight = this.getContentHeight() - val height = - if (contentHeight > maxHeight) { - maxHeight - } else if (contentHeight < minHeight) { - minHeight - } else { - contentHeight - } - return height + return when { + contentHeight > maxHeight -> maxHeight + contentHeight < minHeight -> minHeight + else -> contentHeight + } } - private fun clampRatio(ratio: Float): Float { - if (ratio < 0.01) { - return 0.01f - } else if (ratio > 0.99) { - return 0.99f + private fun clampRatio(ratio: Float): Float = + when { + ratio < 0.01 -> 0.01f + ratio > 0.99 -> 0.99f + else -> ratio } - return ratio - } private fun setDraggable(draggable: Boolean) { val dialog = this.dialog ?: return @@ -372,9 +331,7 @@ class BottomSheetView( // View overrides to pass to DialogRootViewGroup instead override fun dispatchProvideStructure(structure: ViewStructure?) { - if (structure == null) { - return - } + if (structure == null) return dialogRootViewGroup.dispatchProvideStructure(structure) } @@ -413,7 +370,6 @@ class BottomSheetView( // https://stackoverflow.com/questions/11862391/getheight-px-or-dpi fun dpToPx(dp: Float): Float { val displayMetrics = context.resources.displayMetrics - val px = dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT) - return px + return dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT) } } diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt index 3276a07343..c43d963888 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt @@ -52,7 +52,7 @@ class DialogRootViewGroup( if (ReactFeatureFlags.dispatchPointerEvents) { jSPointerDispatcher = JSPointerDispatcher(this) } - + fitsSystemWindows = false } diff --git a/modules/bottom-sheet/android/src/main/res/values/styles.xml b/modules/bottom-sheet/android/src/main/res/values/styles.xml new file mode 100644 index 0000000000..b2a4945da7 --- /dev/null +++ b/modules/bottom-sheet/android/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt index 97b05b4d73..3f63814bcb 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt @@ -34,12 +34,15 @@ class NotificationPrefs( is Boolean -> { putBoolean(key, value) } + is String -> { putString(key, value) } + is Array<*> -> { putStringSet(key, value.map { it.toString() }.toSet()) } + is Map<*, *> -> { putStringSet(key, value.map { it.toString() }.toSet()) } diff --git a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt index 2aaee3b8ec..d7b9e0e468 100644 --- a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt +++ b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt @@ -117,7 +117,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { private fun handleImageIntents( uris: List, - text: String? + text: String?, ) { var allParams = "" @@ -145,7 +145,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { private fun handleVideoIntents( uris: List, - text: String? + text: String?, ) { val uri = uris[0] // If there is no extension for the file, substringAfterLast returns the original string - not