Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,11 @@ export const MultipleExample = () => {
| `topLeftRightCornerRadius` | `number` | `20` | Radius for top corners |
| `backgroundColor` | `string` | `'white'` | Background color of sheet |
| `isSystemUILight` | `boolean` | `undefined` | Android only - status bar styling |
| `presentationStyle` | `'bottom' \| 'center'` | `'bottom'` | `'center'` shows the sheet as a dialog card in the vertical center with a full-screen dim; dismiss by swiping down or tapping the dim. Ignores `useInlinePresentation`. |
| `presentationStyle` | `'bottom' \| 'center' \| 'footer'` | `'bottom'` | `'center'` shows the sheet as a dialog card in the vertical center with a full-screen dim; dismiss by swiping down or tapping the dim. Ignores `useInlinePresentation`. `'footer'` (with `useInlinePresentation`) pins the content to the bottom of the screen above inline sheets, with no gestures or dim — for a buy button that must stay put while an inline sheet is dragged, or for full-screen overlays that must cover it. |
| `centerAnimation` | `'fade' \| 'slide'` | `'fade'` | Enter animation for `presentationStyle: 'center'`. `'fade'` = fade + scale in; `'slide'` = slide up into center. |
| `dismissOnOverlayTap` | `boolean` | `dismissable` | Tap on the dimmed background dismisses the sheet. `false` with `dismissable: true` keeps swipe-to-dismiss but ignores taps. |
| `overlayOpacity` | `number` | platform default | Background dim opacity, 0..1. `0` with `useInlinePresentation` removes the background and lets touches reach the screen behind. |
| `collapsedHeight` | `number` | — | `useInlinePresentation` only. Lower snap point; the sheet opens there and can be dragged up to its full height. |

### FittedSheetRef Methods

Expand Down
66 changes: 63 additions & 3 deletions android/src/main/java/com/sheet2/AppFittedSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.uimanager.PointerEvents
import com.facebook.react.uimanager.ReactPointerEventsView
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.EventDispatcher
Expand All @@ -35,7 +37,13 @@ internal fun AppFittedSheet.onSheetDismiss() {

private var presentedSheets: MutableList<String> = mutableListOf()

open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEventListener {
open class AppFittedSheet(context: Context) :
ViewGroup(context),
LifecycleEventListener,
ReactPointerEventsView {
// The sheet content lives in its own window or overlay; this placeholder must never take touches.
override val pointerEvents: PointerEvents = PointerEvents.NONE

private var stacked = true
private val fragmentTag = "CCBottomSheet-${System.currentTimeMillis()}"
var mHostView = DialogRootViewGroup(context)
Expand All @@ -47,6 +55,9 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent
field = value
inlinePresenter.setDismissable(value)
}
var dismissOnOverlayTap = true
var overlayOpacity = -1F
var collapsedHeight = 0F
var topLeftRightCornerRadius: Float = 0F
var _backgroundColor: Int = Color.TRANSPARENT
var isSystemUILight: Boolean = false
Expand Down Expand Up @@ -121,6 +132,10 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent
InlineSheetPresenter(this, mHostView)
}

private val footerPresenter: InlineFooterPresenter by lazy {
InlineFooterPresenter(this, mHostView)
}

init {
mHostView.onSheetLayoutChanged = { pushContentOriginOffset() }
}
Expand Down Expand Up @@ -190,9 +205,19 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent
return
}

if (presentationStyle == "footer") {
footerPresenter.show()
return
}

if (useInlinePresentation) {
if (!inlinePresenter.isShown) {
inlinePresenter.show(dismissable) { onSheetDismiss() }
inlinePresenter.show(
dismissable = dismissable,
dismissOnOverlayTap = dismissOnOverlayTap,
overlayOpacity = overlayOpacity,
collapsedHeight = collapsedHeight.toInt(),
) { onSheetDismiss() }
}
return
}
Expand All @@ -208,9 +233,17 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent
val fragment = FragmentModalBottomSheet(
modalView = mHostView,
dismissable = dismissable,
dismissOnOverlayTap = dismissOnOverlayTap,
overlayOpacity = overlayOpacity,
isSystemUILight = isSystemUILight
) { dismissAll ->
val parent = mHostView.parent as? ViewGroup
SheetTreeLog.log(
context,
"sheet.detachHost",
"host=${SheetTreeLog.tag(mHostView)} parent=${SheetTreeLog.tag(parent)}" +
" parentChildren=${SheetTreeLog.childTags(parent)}",
)
parent?.removeViewAt(0)
onSheetDismiss()
if (dismissAll) {
Expand Down Expand Up @@ -262,6 +295,16 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent

override fun addView(child: View, index: Int) {
UiThreadUtil.assertOnUiThread()
// An overlay child is kept out of mHostView while getChildCount keeps
// counting it, so from here on this view's children and React Native's idea
// of them are two different lists.
SheetTreeLog.log(
context,
"sheet.addView",
"child=${SheetTreeLog.tag(child)} index=$index self=${SheetTreeLog.tag(this)}" +
" hostChildren=${SheetTreeLog.childTags(mHostView)}" +
" inline=$useInlinePresentation",
)
if (useInlinePresentation && index > 0) {
inlineOverlayView = child
inlinePresenter.setOverlayView(child)
Expand Down Expand Up @@ -289,6 +332,12 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent

override fun removeView(child: View) {
UiThreadUtil.assertOnUiThread()
SheetTreeLog.log(
context,
"sheet.removeView",
"child=${SheetTreeLog.tag(child)} self=${SheetTreeLog.tag(this)}" +
" hostChildren=${SheetTreeLog.childTags(mHostView)}",
)
if (child == inlineOverlayView) {
inlineOverlayView = null
inlinePresenter.setOverlayView(null)
Expand All @@ -304,6 +353,12 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent

override fun removeViewAt(index: Int) {
UiThreadUtil.assertOnUiThread()
SheetTreeLog.log(
context,
"sheet.removeViewAt",
"index=$index self=${SheetTreeLog.tag(this)}" +
" hostChildren=${SheetTreeLog.childTags(mHostView)}",
)
if (useInlinePresentation && index >= mHostView.childCount && inlineOverlayView != null) {
inlineOverlayView = null
inlinePresenter.setOverlayView(null)
Expand All @@ -317,7 +372,7 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent
dismiss()
}

private fun onDropInstance() {
internal fun onDropInstance() {
(context as ReactContext).removeLifecycleEventListener(this)
dismiss()
}
Expand All @@ -329,6 +384,11 @@ open class AppFittedSheet(context: Context) : ViewGroup(context), LifecycleEvent
centeredDialog = null
return
}
if (presentationStyle == "footer") {
footerPresenter.dismiss()
onSheetDismiss()
return
}
if (useInlinePresentation) {
inlinePresenter.dismiss()
return
Expand Down
5 changes: 3 additions & 2 deletions android/src/main/java/com/sheet2/CustomBottomSheetDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class CustomBottomSheetDialog extends AppCompatDialog {
private FrameLayout container;
boolean dismissWithAnimation;
boolean cancelable = true;
public boolean dismissOnOverlayTap = true;

protected int startState = BottomSheetBehavior.STATE_EXPANDED;

Expand Down Expand Up @@ -191,7 +192,7 @@ private View wrapInBottomSheet(
coordinator
.findViewById(R.id.touch_outside)
.setOnClickListener(
view12 -> {if (cancelable && isShowing()) cancel();});
view12 -> {if (cancelable && dismissOnOverlayTap && isShowing()) cancel();});
// The bottom sheet container spans the full width, but the card content can be
// narrower and horizontally centered (maxWidth / landscape). A blanket
// `event -> true` here swallows taps that land in the dim strips beside the
Expand All @@ -202,7 +203,7 @@ private View wrapInBottomSheet(
final View card = view;
bottomSheet.setOnTouchListener((view1, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_UP
&& cancelable && isShowing() && card != null
&& cancelable && dismissOnOverlayTap && isShowing() && card != null
&& !isPointInsideCard(card, event.getRawX(), event.getRawY())) {
cancel();
}
Expand Down
26 changes: 22 additions & 4 deletions android/src/main/java/com/sheet2/DialogRootViewGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.graphics.Outline
import android.os.Looper
import android.view.View
import android.view.ViewOutlineProvider
import com.facebook.react.uimanager.PixelUtil.pxToDp
import kotlin.math.max
import kotlin.math.min

Expand Down Expand Up @@ -86,7 +85,6 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) {
if (sheetMaxHeightSize == Float.MAX_VALUE) return
val newHeight = allowedHeight
val newWidth = allowedWidth
println("😀 DialogRootViewGroup.setVirtualHeight ${newHeight.pxToDp()} :${newWidth.pxToDp()}")
ensureLayoutParams()
layoutParams?.height = newHeight
translationX = centeringTranslationX(newWidth)
Expand All @@ -105,19 +103,39 @@ class DialogRootViewGroup(context: Context) : BaseRNView(context) {
}

override fun addView(child: View, index: Int, params: LayoutParams) {
println("😀 DialogRootViewGroup.addView ${child.id}")
if (reactView != null) removeView(reactView)
// Dropping the previous child here is a removal React Native never hears
// about: it goes on counting that child, and its next insert lands at an
// index the group no longer has.
val replaced = reactView
SheetTreeLog.log(
context,
"host.addView",
"child=${SheetTreeLog.tag(child)} index=$index children=${SheetTreeLog.childTags(this)}" +
" replaces=${SheetTreeLog.tag(replaced)}",
)
if (replaced != null) removeView(replaced)
super.addView(child, index, params)
reactView = child
setVirtualHeight(sheetMaxHeightSize)
}

override fun removeView(view: View?) {
SheetTreeLog.log(
context,
"host.removeView",
"child=${SheetTreeLog.tag(view)} children=${SheetTreeLog.childTags(this)}",
)
if (view == reactView) releaseReactView()
super.removeView(view)
}

override fun removeViewAt(index: Int) {
SheetTreeLog.log(
context,
"host.removeViewAt",
"index=$index child=${SheetTreeLog.tag(getChildAt(index))}" +
" children=${SheetTreeLog.childTags(this)}",
)
if (getChildAt(index) === reactView) releaseReactView()
super.removeViewAt(index)
}
Expand Down
19 changes: 18 additions & 1 deletion android/src/main/java/com/sheet2/FragmentModalBottomSheet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,24 @@ class FragmentModalBottomSheet() : BottomSheetDialogFragment() {
private var overlayHost: ViewGroup? = null
private var overlayView: View? = null
private var dismissable: Boolean = true
private var dismissOnOverlayTap: Boolean = true
private var overlayOpacity: Float = -1F
private var isSystemUILight: Boolean = false
private var onDismiss: ((dismissAll: Boolean) -> Unit)? = null
var dismissAll = false

constructor(
modalView: ViewGroup,
dismissable: Boolean,
dismissOnOverlayTap: Boolean,
overlayOpacity: Float,
isSystemUILight: Boolean,
onDismiss: (dismissAll: Boolean) -> Unit
) : this() {
this.modalView = modalView
this.dismissable = dismissable
this.dismissOnOverlayTap = dismissOnOverlayTap
this.overlayOpacity = overlayOpacity
this.isSystemUILight = isSystemUILight
this.onDismiss = onDismiss
}
Expand All @@ -56,6 +62,8 @@ class FragmentModalBottomSheet() : BottomSheetDialogFragment() {
this.isCancelable = dismissable
val dialog = CustomBottomSheetDialog(requireContext(), R.style.AppBottomSheetDialog)
dialog.setSheetBackgroundColor(Color.TRANSPARENT)
dialog.dismissOnOverlayTap = dismissOnOverlayTap
if (overlayOpacity >= 0) dialog.window?.setDimAmount(overlayOpacity)
dialog.window?.let {
presentedWindow = WeakReference(it)
it.setStatusBarStyle(isSystemUILight)
Expand Down Expand Up @@ -84,7 +92,16 @@ class FragmentModalBottomSheet() : BottomSheetDialogFragment() {
val view = overlayView ?: return
val decorView = dialog?.window?.decorView as? ViewGroup ?: return
detachOverlayHost()
(view.parent as? ViewGroup)?.removeView(view)
// Same as the inline presenter: the overlay is taken out of whatever parent
// React Native gave it and put into the dialog's own host.
val previousParent = view.parent as? ViewGroup
SheetTreeLog.log(
view.context,
"dialog.attachOverlay",
"overlay=${SheetTreeLog.tag(view)} from=${SheetTreeLog.tag(previousParent)}" +
" fromChildren=${SheetTreeLog.childTags(previousParent)}",
)
previousParent?.removeView(view)

val host = DialogPassThroughFrameLayout(decorView.context).apply {
layoutParams = ViewGroup.LayoutParams(
Expand Down
74 changes: 74 additions & 0 deletions android/src/main/java/com/sheet2/InlineFooterPresenter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.sheet2

import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import android.widget.FrameLayout

internal class InlineFooterPresenter(
private val anchor: AppFittedSheet,
private val hostView: View,
) {

private var overlay: FrameLayout? = null

val isShown: Boolean get() = overlay != null

fun show() {
if (isShown) return
val root = findInlineRoot(anchor) ?: return

val overlayRoot = PassThroughFrameLayout(anchor.context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
isClickable = false
isFocusable = false
}

(hostView.parent as? ViewGroup)?.removeView(hostView)
val size = hostView.layoutParams
hostView.layoutParams = FrameLayout.LayoutParams(
size?.width ?: FrameLayout.LayoutParams.MATCH_PARENT,
size?.height ?: 0,
Gravity.BOTTOM,
)
overlayRoot.addView(hostView)

root.addView(overlayRoot)
overlay = overlayRoot
footers.add(overlayRoot)
hostView.post { anchor.pushContentOriginOffset() }
}

fun dismiss() {
val layout = overlay ?: return
footers.remove(layout)
overlay = null
(layout.parent as? ViewGroup)?.removeView(layout)
(hostView.parent as? ViewGroup)?.removeView(hostView)
}

companion object {
private val footers = mutableListOf<View>()

fun bringFootersToFront(root: ViewGroup) {
footers.filter { it.parent === root }.forEach { it.bringToFront() }
}
}
}

internal fun findInlineRoot(anchor: View): ViewGroup? {
var current: ViewParent? = anchor.parent
var lastGroup: ViewGroup? = null
while (current != null) {
if (current is ViewGroup) lastGroup = current
if (current.javaClass.name == "com.swmansion.rnscreens.Screen") {
return (current.parent as? ViewGroup) ?: (current as? ViewGroup)
}
current = current.parent
}
return lastGroup
}
Loading
Loading