expo-dev-menu 56.0.5 → 56.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/android/build.gradle +2 -2
- package/android/src/debug/java/expo/modules/devmenu/fab/FabState.kt +12 -39
- package/android/src/debug/java/expo/modules/devmenu/fab/FabUtils.kt +10 -0
- package/android/src/debug/java/expo/modules/devmenu/fab/MovableFloatingActionButton.kt +29 -10
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 56.0.7 — 2026-05-13
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- Fix FAB safe-area bounds ([#45647](https://github.com/expo/expo/pull/45647) by [@Wenszel](https://github.com/Wenszel))
|
|
18
|
+
|
|
19
|
+
## 56.0.6 — 2026-05-11
|
|
20
|
+
|
|
21
|
+
_This version does not introduce any user-facing changes._
|
|
22
|
+
|
|
13
23
|
## 56.0.5 — 2026-05-08
|
|
14
24
|
|
|
15
25
|
### 💡 Others
|
package/android/build.gradle
CHANGED
|
@@ -12,7 +12,7 @@ apply plugin: 'expo-module-gradle-plugin'
|
|
|
12
12
|
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
13
13
|
|
|
14
14
|
group = 'host.exp.exponent'
|
|
15
|
-
version = '56.0.
|
|
15
|
+
version = '56.0.7'
|
|
16
16
|
|
|
17
17
|
def hasDevLauncher = findProject(":expo-dev-launcher") != null
|
|
18
18
|
def configureInRelease = findProperty("expo.devmenu.configureInRelease") == "true"
|
|
@@ -29,7 +29,7 @@ android {
|
|
|
29
29
|
|
|
30
30
|
defaultConfig {
|
|
31
31
|
versionCode 10
|
|
32
|
-
versionName '56.0.
|
|
32
|
+
versionName '56.0.7'
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
buildTypes {
|
|
@@ -4,8 +4,6 @@ import android.content.Context
|
|
|
4
4
|
import android.content.SharedPreferences
|
|
5
5
|
import androidx.compose.animation.core.Animatable
|
|
6
6
|
import androidx.compose.animation.core.VectorConverter
|
|
7
|
-
import androidx.compose.foundation.layout.WindowInsets
|
|
8
|
-
import androidx.compose.foundation.layout.systemBars
|
|
9
7
|
import androidx.compose.runtime.Composable
|
|
10
8
|
import androidx.compose.runtime.LaunchedEffect
|
|
11
9
|
import androidx.compose.runtime.getValue
|
|
@@ -15,7 +13,6 @@ import androidx.compose.runtime.remember
|
|
|
15
13
|
import androidx.compose.runtime.setValue
|
|
16
14
|
import androidx.compose.ui.geometry.Offset
|
|
17
15
|
import androidx.compose.ui.platform.LocalContext
|
|
18
|
-
import androidx.compose.ui.platform.LocalDensity
|
|
19
16
|
import androidx.core.content.edit
|
|
20
17
|
import kotlinx.coroutines.delay
|
|
21
18
|
|
|
@@ -25,17 +22,9 @@ private const val FAB_POSITION_X = "fabPositionX"
|
|
|
25
22
|
private const val FAB_POSITION_Y = "fabPositionY"
|
|
26
23
|
private const val FAB_POSITION_UNSET = -1f
|
|
27
24
|
|
|
28
|
-
data class FabBounds(
|
|
29
|
-
val screen: Offset,
|
|
30
|
-
val safe: Offset,
|
|
31
|
-
val safeMinY: Float,
|
|
32
|
-
val drag: Offset,
|
|
33
|
-
val halfFab: Offset
|
|
34
|
-
)
|
|
35
|
-
|
|
36
25
|
class FabState(
|
|
37
26
|
initialOffset: Offset,
|
|
38
|
-
|
|
27
|
+
var fabAreaBounds: Offset,
|
|
39
28
|
val prefs: SharedPreferences
|
|
40
29
|
) {
|
|
41
30
|
val animatedOffset = Animatable(initialOffset, Offset.VectorConverter)
|
|
@@ -51,12 +40,9 @@ class FabState(
|
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
fun savePosition(offset: Offset) {
|
|
54
|
-
val safeWidth = bounds.safe.x
|
|
55
|
-
val safeHeight = bounds.safe.y - bounds.safeMinY
|
|
56
|
-
|
|
57
43
|
// Store position as 0–1 ratios within the safe area so it survives screen size changes
|
|
58
|
-
val normalizedX = if (
|
|
59
|
-
val normalizedY = if (
|
|
44
|
+
val normalizedX = if (fabAreaBounds.x > 0f) offset.x / fabAreaBounds.x else 0f
|
|
45
|
+
val normalizedY = if (fabAreaBounds.y > 0f) offset.y / fabAreaBounds.y else 0f
|
|
60
46
|
|
|
61
47
|
prefs.edit {
|
|
62
48
|
putFloat(FAB_POSITION_X, normalizedX)
|
|
@@ -66,40 +52,27 @@ class FabState(
|
|
|
66
52
|
}
|
|
67
53
|
|
|
68
54
|
@Composable
|
|
69
|
-
fun rememberFabState(
|
|
70
|
-
val density = LocalDensity.current
|
|
71
|
-
val systemBarInsets = WindowInsets.systemBars
|
|
72
|
-
val safeInsetTop = with(density) { systemBarInsets.getTop(this).toFloat() }
|
|
73
|
-
val safeInsetBottom = with(density) { systemBarInsets.getBottom(this).toFloat() }
|
|
74
|
-
val halfFab = Offset(totalFabSizePx.x / 2f, totalFabSizePx.y / 2f)
|
|
75
|
-
|
|
76
|
-
val fabBounds = FabBounds(
|
|
77
|
-
screen = screenBounds,
|
|
78
|
-
safe = Offset(screenBounds.x, screenBounds.y - safeInsetBottom),
|
|
79
|
-
safeMinY = safeInsetTop,
|
|
80
|
-
drag = Offset(screenBounds.x + halfFab.x, screenBounds.y + halfFab.y),
|
|
81
|
-
halfFab = halfFab
|
|
82
|
-
)
|
|
83
|
-
|
|
55
|
+
fun rememberFabState(fabAreaBounds: Offset): FabState {
|
|
84
56
|
val context = LocalContext.current
|
|
85
57
|
val prefs = remember { context.getSharedPreferences(FAB_PREFS, Context.MODE_PRIVATE) }
|
|
86
58
|
|
|
87
|
-
val initialOffset = remember
|
|
59
|
+
val initialOffset = remember {
|
|
88
60
|
val savedX = prefs.getFloat(FAB_POSITION_X, FAB_POSITION_UNSET)
|
|
89
61
|
val savedY = prefs.getFloat(FAB_POSITION_Y, FAB_POSITION_UNSET)
|
|
90
|
-
val safeMaxX = maxOf(0f, fabBounds.safe.x)
|
|
91
|
-
val safeMaxY = maxOf(fabBounds.safeMinY, fabBounds.safe.y)
|
|
92
62
|
if (savedX != FAB_POSITION_UNSET && savedY != FAB_POSITION_UNSET) {
|
|
93
63
|
Offset(
|
|
94
|
-
x = (savedX *
|
|
95
|
-
y = (savedY *
|
|
64
|
+
x = (savedX * fabAreaBounds.x).coerceIn(0f, fabAreaBounds.x),
|
|
65
|
+
y = (savedY * fabAreaBounds.y).coerceIn(0f, fabAreaBounds.y)
|
|
96
66
|
)
|
|
97
67
|
} else {
|
|
98
|
-
Offset(
|
|
68
|
+
Offset(fabAreaBounds.x, 0f)
|
|
99
69
|
}
|
|
100
70
|
}
|
|
101
71
|
|
|
102
|
-
val state = remember { FabState(initialOffset,
|
|
72
|
+
val state = remember { FabState(initialOffset, fabAreaBounds, prefs) }
|
|
73
|
+
// We can't simply update the entire state when the bounds change,
|
|
74
|
+
// because it would result in resetting the interaction state (isPressed, isDragging).
|
|
75
|
+
state.fabAreaBounds = fabAreaBounds
|
|
103
76
|
|
|
104
77
|
LaunchedEffect(state.lastInteractionTime) {
|
|
105
78
|
state.isIdle = false
|
|
@@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|
|
5
5
|
import androidx.compose.runtime.mutableStateOf
|
|
6
6
|
import androidx.compose.runtime.remember
|
|
7
7
|
import androidx.compose.ui.geometry.Offset
|
|
8
|
+
import androidx.compose.ui.geometry.Rect
|
|
8
9
|
import androidx.compose.ui.unit.IntOffset
|
|
9
10
|
import expo.modules.devmenu.fab.ExpoVelocityTracker.PointF
|
|
10
11
|
import kotlin.math.roundToInt
|
|
@@ -49,6 +50,15 @@ internal fun Offset.coerceIn(minX: Float = 0f, maxX: Float, minY: Float = 0f, ma
|
|
|
49
50
|
)
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
internal fun Offset.coerceIn(rect: Rect): Offset {
|
|
54
|
+
return this.coerceIn(
|
|
55
|
+
minX = rect.left,
|
|
56
|
+
maxX = rect.right,
|
|
57
|
+
minY = rect.top,
|
|
58
|
+
maxY = rect.bottom
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
@Composable
|
|
53
63
|
internal fun <T> rememberPrevious(current: T): T? {
|
|
54
64
|
val previousRef = remember { mutableStateOf<T?>(null) }
|
|
@@ -14,12 +14,14 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
|
|
14
14
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
15
15
|
import androidx.compose.foundation.layout.offset
|
|
16
16
|
import androidx.compose.foundation.layout.padding
|
|
17
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
17
18
|
import androidx.compose.foundation.layout.size
|
|
18
19
|
import androidx.compose.runtime.Composable
|
|
19
20
|
import androidx.compose.runtime.LaunchedEffect
|
|
20
21
|
import androidx.compose.runtime.remember
|
|
21
22
|
import androidx.compose.ui.Modifier
|
|
22
23
|
import androidx.compose.ui.geometry.Offset
|
|
24
|
+
import androidx.compose.ui.geometry.Rect
|
|
23
25
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
24
26
|
import androidx.compose.ui.input.pointer.positionChange
|
|
25
27
|
import androidx.compose.ui.platform.LocalDensity
|
|
@@ -48,7 +50,11 @@ fun MovableFloatingActionButton(
|
|
|
48
50
|
margin: Dp = Margin,
|
|
49
51
|
onPress: () -> Unit = {}
|
|
50
52
|
) {
|
|
51
|
-
BoxWithConstraints(
|
|
53
|
+
BoxWithConstraints(
|
|
54
|
+
modifier = modifier
|
|
55
|
+
.safeDrawingPadding()
|
|
56
|
+
.fillMaxSize()
|
|
57
|
+
) {
|
|
52
58
|
val totalFabSize = DpSize(fabSize.width + margin * 2, fabSize.height + margin * 2)
|
|
53
59
|
val totalFabSizePx = with(LocalDensity.current) {
|
|
54
60
|
Offset(totalFabSize.width.toPx(), totalFabSize.height.toPx())
|
|
@@ -58,7 +64,16 @@ fun MovableFloatingActionButton(
|
|
|
58
64
|
y = constraints.maxHeight - totalFabSizePx.y
|
|
59
65
|
)
|
|
60
66
|
|
|
61
|
-
val
|
|
67
|
+
val halfFab = Offset(totalFabSizePx.x / 2f, totalFabSizePx.y / 2f)
|
|
68
|
+
val dragBounds = Rect(
|
|
69
|
+
left = -halfFab.x,
|
|
70
|
+
top = -halfFab.y,
|
|
71
|
+
right = bounds.x + halfFab.x,
|
|
72
|
+
bottom = bounds.y + halfFab.y
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
val fab = rememberFabState(bounds)
|
|
76
|
+
|
|
62
77
|
val isFabDisplayable = state.showFab &&
|
|
63
78
|
!state.isInPictureInPictureMode &&
|
|
64
79
|
bounds.x >= 0f &&
|
|
@@ -70,7 +85,7 @@ fun MovableFloatingActionButton(
|
|
|
70
85
|
LaunchedEffect(state.isOpen) {
|
|
71
86
|
if (state.isOpen) {
|
|
72
87
|
fab.restingOffset = fab.animatedOffset.value
|
|
73
|
-
val isOnLeftSide = fab.animatedOffset.value.x < fab.
|
|
88
|
+
val isOnLeftSide = fab.animatedOffset.value.x < fab.fabAreaBounds.x / 2f
|
|
74
89
|
val offScreenX = if (isOnLeftSide) -totalFabSizePx.x else constraints.maxWidth.toFloat()
|
|
75
90
|
fab.animatedOffset.animateTo(
|
|
76
91
|
targetValue = Offset(offScreenX, fab.animatedOffset.value.y),
|
|
@@ -97,14 +112,13 @@ fun MovableFloatingActionButton(
|
|
|
97
112
|
previousBounds?.let {
|
|
98
113
|
val oldX = fab.animatedOffset.value.x
|
|
99
114
|
val oldY = fab.animatedOffset.value.y
|
|
100
|
-
val newX = (oldX / previousBounds.x) * fab.
|
|
101
|
-
val newY = (oldY / previousBounds.y) * fab.
|
|
115
|
+
val newX = (oldX / previousBounds.x) * fab.fabAreaBounds.x
|
|
116
|
+
val newY = (oldY / previousBounds.y) * fab.fabAreaBounds.y
|
|
102
117
|
val newTarget = calculateTargetPosition(
|
|
103
118
|
currentPosition = Offset(newX, newY),
|
|
104
119
|
velocity = ExpoVelocityTracker.PointF(0f, 0f),
|
|
105
|
-
bounds = fab.
|
|
106
|
-
totalFabWidth = totalFabSizePx.x
|
|
107
|
-
minY = fab.bounds.safeMinY
|
|
120
|
+
bounds = fab.fabAreaBounds,
|
|
121
|
+
totalFabWidth = totalFabSizePx.x
|
|
108
122
|
)
|
|
109
123
|
|
|
110
124
|
fab.animatedOffset.snapTo(newTarget)
|
|
@@ -140,7 +154,7 @@ fun MovableFloatingActionButton(
|
|
|
140
154
|
|
|
141
155
|
drag(pointerId) { change ->
|
|
142
156
|
dragOffset = (dragOffset + change.positionChange())
|
|
143
|
-
.coerceIn(
|
|
157
|
+
.coerceIn(dragBounds)
|
|
144
158
|
dragDistance += change.positionChange().getDistance()
|
|
145
159
|
velocityTracker.registerPosition(dragOffset.x, dragOffset.y)
|
|
146
160
|
|
|
@@ -187,7 +201,12 @@ private fun CoroutineScope.handleRelease(
|
|
|
187
201
|
totalFabSizePx: Offset
|
|
188
202
|
) {
|
|
189
203
|
val velocity = velocityTracker.calculateVelocity()
|
|
190
|
-
val newOffset = calculateTargetPosition(
|
|
204
|
+
val newOffset = calculateTargetPosition(
|
|
205
|
+
currentPosition = fab.animatedOffset.value,
|
|
206
|
+
velocity = velocity,
|
|
207
|
+
bounds = fab.fabAreaBounds,
|
|
208
|
+
totalFabWidth = totalFabSizePx.x
|
|
209
|
+
)
|
|
191
210
|
|
|
192
211
|
velocityTracker.clear()
|
|
193
212
|
launch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-dev-menu",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.7",
|
|
4
4
|
"description": "Expo/React Native module with the developer menu.",
|
|
5
5
|
"main": "build/DevMenu.js",
|
|
6
6
|
"types": "build/DevMenu.d.ts",
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"@types/node": "^22.14.0",
|
|
33
33
|
"react": "19.2.3",
|
|
34
34
|
"react-native": "0.85.3",
|
|
35
|
-
"expo": "56.0.
|
|
36
|
-
"
|
|
37
|
-
"expo
|
|
35
|
+
"babel-preset-expo": "56.0.7",
|
|
36
|
+
"expo-module-scripts": "56.0.2",
|
|
37
|
+
"expo": "56.0.0-preview.10"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"expo": "*",
|
|
41
41
|
"react-native": "*"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "40f0a6f6711d93762e0506b37e6e077e4bd9a541",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "expo-module build",
|
|
46
46
|
"clean": "expo-module clean",
|