Skip to content

Commit 7bbb440

Browse files
committed
feat: add support for headers and change the layout approach
This commit adds support for header components. However, the major improvement is the changed approach to doing the layout for the bottom sheet. Prior to this commit, there was an inherent conflict between the native iOS/Android layout systems and the React layout manager. In particular, this resulted in issues with the on-screen keyboard overlapping controls. To solve this, this commit introduces an intermediate view that sits between the native OS dialog and the hosted React view. This intermediate view handles the keyboard avoidance using OS-specific mechanism: keyboardLayoutGuide on iOS and rootView.padding on Android. We then communicate the visible area back to React that then lays out components as usual. So there is no need to have special handling for scrolling views or to do manual footer positioning.
1 parent 3b6c84d commit 7bbb440

34 files changed

+439
-525
lines changed

android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt

Lines changed: 41 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,23 @@ package com.lodev09.truesheet
22

33
import android.annotation.SuppressLint
44
import android.graphics.Color
5-
import android.graphics.drawable.ShapeDrawable
6-
import android.graphics.drawable.shapes.RoundRectShape
5+
import android.graphics.Rect
76
import android.view.View
87
import android.view.ViewGroup
98
import android.view.WindowManager
9+
import androidx.constraintlayout.widget.ConstraintLayout
10+
import androidx.core.graphics.drawable.toDrawable
1011
import com.facebook.react.uimanager.ThemedReactContext
1112
import com.google.android.material.bottomsheet.BottomSheetBehavior
1213
import com.google.android.material.bottomsheet.BottomSheetDialog
13-
import com.lodev09.truesheet.core.KeyboardManager
14-
import com.lodev09.truesheet.core.RootSheetView
1514
import com.lodev09.truesheet.core.Utils
1615

1716
data class SizeInfo(val index: Int, val value: Float)
1817

1918
@SuppressLint("ClickableViewAccessibility")
20-
class TrueSheetDialog(private val reactContext: ThemedReactContext, private val rootSheetView: RootSheetView) :
19+
class TrueSheetDialog(private val reactContext: ThemedReactContext, private val rootSheetView: ViewGroup) :
2120
BottomSheetDialog(reactContext) {
2221

23-
private var keyboardManager = KeyboardManager(reactContext)
2422
private var windowAnimation: Int = 0
2523

2624
// First child of the rootSheetView
@@ -31,8 +29,8 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
3129
null
3230
}
3331

34-
private val sheetContainerView: ViewGroup?
35-
get() = rootSheetView.parent?.let { it as? ViewGroup }
32+
val sheetContainerView: ViewGroup?
33+
get() = proxyView.parent?.let { it as? ViewGroup }
3634

3735
/**
3836
* Specify whether the sheet background is dimmed.
@@ -52,6 +50,7 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
5250
var maxScreenHeight = 0
5351

5452
var contentHeight = 0
53+
var headerHeight = 0
5554
var footerHeight = 0
5655
var maxSheetHeight: Int? = null
5756

@@ -70,24 +69,25 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
7069
behavior.isHideable = value
7170
}
7271

73-
var cornerRadius: Float = 0f
74-
var backgroundColor: Int = Color.WHITE
72+
// 1st child is the header view
73+
val headerView: ViewGroup?
74+
get() = containerView?.getChildAt(0) as? ViewGroup
7575

76-
// 1st child is the content view
76+
// 2nd child is the content view
7777
val contentView: ViewGroup?
78-
get() = containerView?.getChildAt(0) as? ViewGroup
78+
get() = containerView?.getChildAt(1) as? ViewGroup
7979

80-
// 2nd child is the footer view
80+
// 3rd child is the footer view
8181
val footerView: ViewGroup?
82-
get() = containerView?.getChildAt(1) as? ViewGroup
82+
get() = containerView?.getChildAt(2) as? ViewGroup
8383

8484
var sizes: Array<Any> = arrayOf("medium", "large")
8585

86-
init {
87-
setContentView(rootSheetView)
86+
private val proxyView = ConstraintLayout(reactContext)
8887

89-
sheetContainerView?.setBackgroundColor(backgroundColor)
90-
sheetContainerView?.clipToOutline = true
88+
init {
89+
proxyView.addView(rootSheetView)
90+
setContentView(proxyView)
9191

9292
// Setup window params to adjust layout based on Keyboard state
9393
window?.apply {
@@ -99,45 +99,37 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
9999
maxScreenHeight = Utils.screenHeight(reactContext, edgeToEdge)
100100
}
101101

102+
fun getVisibleContentDimensions() : Rect {
103+
val rect = Rect()
104+
proxyView.getGlobalVisibleRect(rect)
105+
return rect
106+
}
107+
102108
override fun getEdgeToEdgeEnabled(): Boolean = edgeToEdge || super.getEdgeToEdgeEnabled()
103109

104110
override fun onStart() {
105111
super.onStart()
106112

113+
// We don't want any of the background to be rendered, it should be completely
114+
// handled by the React side of things.
115+
val transparent = Color.TRANSPARENT.toDrawable()
116+
sheetContainerView?.background = transparent
117+
proxyView.background = transparent
118+
rootSheetView.background = transparent
119+
107120
if (edgeToEdge) {
121+
super.getEdgeToEdgeEnabled()
122+
108123
window?.apply {
109124
setFlags(
110125
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
111126
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
112127
)
113-
114128
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
115129
}
116130
}
117131
}
118132

119-
/**
120-
* Setup background color and corner radius.
121-
*/
122-
fun setupBackground() {
123-
val outerRadii = floatArrayOf(
124-
cornerRadius,
125-
cornerRadius,
126-
cornerRadius,
127-
cornerRadius,
128-
0f,
129-
0f,
130-
0f,
131-
0f
132-
)
133-
134-
val background = ShapeDrawable(RoundRectShape(outerRadii, null, null))
135-
136-
// Use current background color
137-
background.paint.color = backgroundColor
138-
sheetContainerView?.background = background
139-
}
140-
141133
/**
142134
* Setup dimmed sheet.
143135
* `dimmedIndex` will further customize the dimming behavior.
@@ -199,14 +191,6 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
199191
}
200192
}
201193

202-
fun positionFooter() {
203-
footerView?.let { footer ->
204-
sheetContainerView?.let { container ->
205-
footer.y = (maxScreenHeight - container.top - footerHeight).toFloat()
206-
}
207-
}
208-
}
209-
210194
/**
211195
* Set the state based for the given size index.
212196
*/
@@ -226,7 +210,7 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
226210

227211
is String -> {
228212
when (size) {
229-
"auto" -> contentHeight + footerHeight
213+
"auto" -> minOf(contentHeight + headerHeight + footerHeight, maxScreenHeight*10/9)
230214

231215
"large" -> maxScreenHeight
232216

@@ -287,34 +271,6 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
287271
else -> BottomSheetBehavior.STATE_HIDDEN
288272
}
289273

290-
/**
291-
* Handle keyboard state changes and adjust maxScreenHeight (sheet max height) accordingly.
292-
* Also update footer's Y position.
293-
*/
294-
fun registerKeyboardManager() {
295-
keyboardManager.registerKeyboardListener(object : KeyboardManager.OnKeyboardChangeListener {
296-
override fun onKeyboardStateChange(isVisible: Boolean, visibleHeight: Int?) {
297-
maxScreenHeight = when (isVisible) {
298-
true -> visibleHeight ?: 0
299-
else -> Utils.screenHeight(reactContext, edgeToEdge)
300-
}
301-
302-
positionFooter()
303-
}
304-
})
305-
}
306-
307-
fun setOnSizeChangeListener(listener: (w: Int, h: Int) -> Unit) {
308-
rootSheetView.sizeChangeListener = listener
309-
}
310-
311-
/**
312-
* Remove keyboard listener.
313-
*/
314-
fun unregisterKeyboardManager() {
315-
keyboardManager.unregisterKeyboardListener()
316-
}
317-
318274
/**
319275
* Configure the sheet based from the size preference.
320276
*/
@@ -344,10 +300,16 @@ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val
344300

345301
setPeekHeight(getSizeHeight(sizes[0]), isShowing)
346302

347-
halfExpandedRatio = minOf(getSizeHeight(sizes[1]).toFloat() / maxScreenHeight.toFloat(), 1.0f)
303+
// Android crashes if 1.0f is specified for the half-expanded ratio
304+
val ratio = minOf(getSizeHeight(sizes[1]).toFloat() / maxScreenHeight.toFloat(), 0.99f)
305+
halfExpandedRatio = ratio
348306
maxHeight = getSizeHeight(sizes[2])
349307
}
350308
}
309+
// Since the React content no longer drives the height calculations, update
310+
// the proxy view's height to take all the available space.
311+
proxyView.minHeight = maxHeight
312+
proxyView.maxHeight = maxHeight
351313
}
352314
}
353315

0 commit comments

Comments
 (0)