@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.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.
Files changed (236) hide show
  1. package/ApexNavigation.podspec +2 -1
  2. package/CHANGELOG.md +7 -0
  3. package/android/CLAUDE.md +19 -10
  4. package/android/CORE.md +3 -2
  5. package/android/consumer-rules.pro +6 -0
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
  8. package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
  12. package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
  14. package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
  15. package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
  16. package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
  19. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
  20. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
  23. package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
  24. package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +36 -38
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
  46. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
  47. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
  48. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
  50. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
  52. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
  53. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
  56. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
  57. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
  58. package/android/src/main/jni/ANNContentHeights.cpp +82 -0
  59. package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
  60. package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
  61. package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
  62. package/android/src/main/res/values/styles.xml +1 -1
  63. package/android/src/main/res/values-night/styles.xml +1 -1
  64. package/cli/doctor/checks/app.js +16 -1
  65. package/cli/project.js +4 -0
  66. package/cli/setup/index.js +5 -0
  67. package/cli/setup/steps/metro.js +46 -0
  68. package/docs-md/architecture/native-screens.md +4 -0
  69. package/docs-md/architecture/theme-and-context.md +131 -12
  70. package/docs-md/architecture/transitions.md +43 -4
  71. package/docs-md/flows/bottom-sheets.md +4 -2
  72. package/docs-md/fundamentals/screens-and-props.md +9 -1
  73. package/docs-md/guides/cli.md +3 -1
  74. package/docs-md/help/troubleshooting.md +1 -1
  75. package/docs-md/index.json +29 -1
  76. package/docs-md/screen-ui/back-and-gestures.md +3 -1
  77. package/docs-md/screen-ui/bar-buttons.md +1 -1
  78. package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
  79. package/docs-md/start/installation.md +16 -1
  80. package/ios/Animations/ANNFrameDriver.h +55 -15
  81. package/ios/Animations/ANNFrameDriver.mm +214 -60
  82. package/ios/Animations/ANNTransitionAnimator.h +26 -5
  83. package/ios/Animations/ANNTransitionAnimator.mm +49 -3
  84. package/ios/Animations/ANNTransitionPlan.h +2 -2
  85. package/ios/Animations/ANNTransitionPlan.mm +10 -0
  86. package/ios/Animations/Elements/ANNElementFinder.h +2 -2
  87. package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
  88. package/ios/Animations/Elements/ANNElementTransition.h +3 -3
  89. package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
  90. package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
  92. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
  93. package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
  94. package/ios/CLAUDE.md +15 -7
  95. package/ios/CORE.md +1 -1
  96. package/ios/Core/ANNAppDelegate.mm +0 -2
  97. package/ios/Core/ANNDeepLinkQueue.h +6 -5
  98. package/ios/Core/ANNDeepLinkQueue.mm +22 -10
  99. package/ios/Core/ANNSceneDelegate.mm +2 -2
  100. package/ios/Core/ApexNavigation.mm +6 -2
  101. package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
  102. package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
  103. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
  104. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
  105. package/ios/Layouts/Stack/ANNStackController.h +8 -0
  106. package/ios/Layouts/Stack/ANNStackController.mm +153 -11
  107. package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
  108. package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
  109. package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
  110. package/ios/Options/ANNAnimationOptions.h +2 -1
  111. package/ios/Options/ANNAnimationOptions.mm +1 -1
  112. package/ios/Options/ANNOptions.h +7 -0
  113. package/ios/Options/ANNOptions.mm +45 -0
  114. package/ios/Options/ANNOptionsReader.mm +2 -4
  115. package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
  116. package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
  117. package/ios/Options/Params/ColorParser.h +4 -3
  118. package/ios/Options/Params/ColorParser.mm +22 -4
  119. package/ios/React/ANNSurfaceHostingView.h +8 -0
  120. package/ios/React/ANNSurfaceHostingView.mm +28 -1
  121. package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
  122. package/ios/TurboModules/ANNTurboModule.mm +46 -0
  123. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
  124. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
  125. package/lib/module/ARCHITECTURE.md +9 -3
  126. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  127. package/lib/module/api/app/AppRegistry.js +14 -2
  128. package/lib/module/api/app/AppRegistry.js.map +1 -1
  129. package/lib/module/api/app/createNavigation.js +188 -66
  130. package/lib/module/api/app/createNavigation.js.map +1 -1
  131. package/lib/module/api/events/navigationEvents.js +6 -2
  132. package/lib/module/api/events/navigationEvents.js.map +1 -1
  133. package/lib/module/api/linking/linking.js +11 -0
  134. package/lib/module/api/linking/linking.js.map +1 -1
  135. package/lib/module/api/screens/defineScreen.js +45 -3
  136. package/lib/module/api/screens/defineScreen.js.map +1 -1
  137. package/lib/module/api/screens/nativeScreens.js +31 -6
  138. package/lib/module/api/screens/nativeScreens.js.map +1 -1
  139. package/lib/module/api/screens/registerScreens.js +172 -35
  140. package/lib/module/api/screens/registerScreens.js.map +1 -1
  141. package/lib/module/api/theme/theme.js +5 -0
  142. package/lib/module/api/theme/theme.js.map +1 -1
  143. package/lib/module/engine/commands.js +21 -1
  144. package/lib/module/engine/commands.js.map +1 -1
  145. package/lib/module/engine/layout.js +7 -2
  146. package/lib/module/engine/layout.js.map +1 -1
  147. package/lib/module/engine/nativeScreens.js +9 -0
  148. package/lib/module/engine/nativeScreens.js.map +1 -1
  149. package/lib/module/engine/options.js +5 -4
  150. package/lib/module/engine/options.js.map +1 -1
  151. package/lib/module/engine/optionsChange.js +102 -0
  152. package/lib/module/engine/optionsChange.js.map +1 -0
  153. package/lib/module/engine/refresh.js +85 -0
  154. package/lib/module/engine/refresh.js.map +1 -0
  155. package/lib/module/engine/rememberedOptions.js +60 -0
  156. package/lib/module/engine/rememberedOptions.js.map +1 -1
  157. package/lib/module/engine/screens.js +26 -0
  158. package/lib/module/engine/screens.js.map +1 -1
  159. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
  160. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  161. package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
  162. package/lib/typescript/api/app/createNavigation.d.ts +5 -0
  163. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  164. package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
  165. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  166. package/lib/typescript/api/index.d.ts +1 -1
  167. package/lib/typescript/api/index.d.ts.map +1 -1
  168. package/lib/typescript/api/linking/linking.d.ts +8 -0
  169. package/lib/typescript/api/linking/linking.d.ts.map +1 -1
  170. package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
  171. package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
  172. package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
  173. package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
  174. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  175. package/lib/typescript/api/theme/theme.d.ts +2 -0
  176. package/lib/typescript/api/theme/theme.d.ts.map +1 -1
  177. package/lib/typescript/engine/commands.d.ts +5 -0
  178. package/lib/typescript/engine/commands.d.ts.map +1 -1
  179. package/lib/typescript/engine/layout.d.ts.map +1 -1
  180. package/lib/typescript/engine/nativeScreens.d.ts +5 -0
  181. package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
  182. package/lib/typescript/engine/optionsChange.d.ts +28 -0
  183. package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
  184. package/lib/typescript/engine/refresh.d.ts +35 -0
  185. package/lib/typescript/engine/refresh.d.ts.map +1 -0
  186. package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
  187. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  188. package/lib/typescript/engine/screens.d.ts +7 -0
  189. package/lib/typescript/engine/screens.d.ts.map +1 -1
  190. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  191. package/lib/typescript/interfaces/options/animations.d.ts +40 -15
  192. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  193. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
  194. package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
  195. package/lib/typescript/interfaces/options/modals.d.ts +6 -7
  196. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/options/stack.d.ts +8 -3
  198. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
  200. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  201. package/metro/acceptPlugin.js +57 -0
  202. package/metro/babelTransformer.js +45 -0
  203. package/metro/index.js +36 -0
  204. package/package.json +3 -1
  205. package/src/ARCHITECTURE.md +9 -3
  206. package/src/adapters/NativeANNTurboModule.ts +7 -0
  207. package/src/api/app/AppRegistry.ts +16 -2
  208. package/src/api/app/createNavigation.ts +198 -66
  209. package/src/api/events/navigationEvents.ts +9 -3
  210. package/src/api/index.ts +1 -0
  211. package/src/api/linking/linking.ts +22 -1
  212. package/src/api/screens/defineScreen.ts +49 -5
  213. package/src/api/screens/nativeScreens.ts +40 -4
  214. package/src/api/screens/registerScreens.tsx +170 -42
  215. package/src/api/theme/theme.ts +5 -0
  216. package/src/engine/commands.ts +21 -0
  217. package/src/engine/layout.ts +11 -2
  218. package/src/engine/nativeScreens.ts +10 -1
  219. package/src/engine/options.ts +6 -8
  220. package/src/engine/optionsChange.ts +107 -0
  221. package/src/engine/refresh.ts +97 -0
  222. package/src/engine/rememberedOptions.ts +70 -0
  223. package/src/engine/screens.tsx +24 -0
  224. package/src/interfaces/ComponentEvents.ts +2 -2
  225. package/src/interfaces/options/animations.ts +40 -15
  226. package/src/interfaces/options/bottomTabs.ts +2 -2
  227. package/src/interfaces/options/buttons.ts +2 -2
  228. package/src/interfaces/options/modals.ts +6 -7
  229. package/src/interfaces/options/stack.ts +8 -3
  230. package/src/interfaces/options/topBar.ts +14 -0
  231. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
  232. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
  233. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
  234. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
  235. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
  236. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
4
4
  import android.content.Context
5
5
  import android.graphics.Color
6
6
  import android.graphics.Outline
7
+ import android.os.SystemClock
7
8
  import android.view.GestureDetector
8
9
  import android.view.Gravity
9
10
  import android.view.MotionEvent
@@ -18,6 +19,7 @@ import android.widget.ScrollView
18
19
  import androidx.activity.BackEventCompat
19
20
  import androidx.coordinatorlayout.widget.CoordinatorLayout
20
21
  import androidx.core.graphics.Insets
22
+ import androidx.core.view.OneShotPreDrawListener
21
23
  import androidx.core.view.ViewCompat
22
24
  import androidx.core.view.WindowInsetsCompat
23
25
  import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -25,23 +27,33 @@ import com.google.android.material.bottomsheet.BottomSheetDragHandleView
25
27
  import dev.apexnative.navigation.options.SheetDetent
26
28
  import dev.apexnative.navigation.options.SheetOptions
27
29
  import dev.apexnative.navigation.utils.MaterialTheme
30
+ import dev.apexnative.navigation.utils.SystemAnimations
28
31
  import dev.apexnative.navigation.utils.TestIds
29
32
  import dev.apexnative.navigation.utils.dp
33
+ import dev.apexnative.navigation.viewcontrollers.viewcontroller.IReactView
30
34
  import kotlin.math.roundToInt
31
35
 
32
36
  /**
33
37
  * A modal shown as a Material bottom sheet (`pageSheet`, `formSheet`, `popover`, or `modal.sheet` without a style): the
34
38
  * modal's view in a container with a [BottomSheetBehavior], in the modals' CoordinatorLayout, over a scrim. It stops at
35
39
  * the detents of its options — `medium` at half the height, `large` at the full height under the status bar, a number
36
- * at that height in dp — and the user drags it between them. In a window wider than [MAX_WIDTH_DP] it stands in the
37
- * middle at that width. A drag down past the smallest one closes it, unless `swipeToDismiss` is false: then the sheet
38
- * stays, and the try is told. The screen under it stays in place. On Android 14+ the back gesture previews its hide
39
- * ([SheetBack]): the Material behavior shrinks it with the progress and slides it down at the release.
40
+ * at that height in dp, `content` at the height of the shown screen's content — and the user drags it between them; at
41
+ * three of them at most, the Material behavior's resting states ([SheetStops]). The content is measured without a
42
+ * height limit, as on iOS ([IReactView.contentSize]), again after each of its mounts, and the stops follow it. In a
43
+ * window wider than [MAX_WIDTH_DP] it stands in the middle at that width. While the keyboard shows, the sheet stands
44
+ * on it at every stop, as on iOS, unless its screen keeps its size for it. A drag down past the smallest one closes
45
+ * it, unless `swipeToDismiss` is false: then the sheet stays, and the try is told. The screen under it stays in place.
46
+ * On Android 14+ the back gesture previews its hide ([SheetBack]): the Material behavior shrinks it with the progress
47
+ * and slides it down at the release.
40
48
  */
41
49
  internal class ModalSheet(
42
50
  private val layout: CoordinatorLayout,
43
51
  private val content: View,
44
52
  private val events: Events,
53
+ /** The content of the screen the modal shows now, which the `content` detent measures; none without one. */
54
+ private val shownContent: () -> IReactView?,
55
+ /** The screen the modal shows now moves its content out of the keyboard's way (`layout.adjustResize`). */
56
+ private val adjustsForKeyboard: () -> Boolean,
45
57
  ) : SheetBack {
46
58
  interface Events {
47
59
  /** The user dragged the sheet to another detent. */
@@ -63,6 +75,19 @@ internal class ModalSheet(
63
75
  private var options = SheetOptions()
64
76
  private var swipeToDismiss = true
65
77
  private var detents: List<SheetDetent> = listOf(SheetDetent.Large)
78
+ // The detents the sheet rests at, from the lowest (`SheetStops.stops`): the behavior's states.
79
+ private var stops: List<SheetDetent> = listOf(SheetDetent.Large)
80
+ // The heights the behavior has for the stops, in pixels: lifted by the keyboard (`SheetStops.lifted`).
81
+ private var stopHeights: List<Int> = emptyList()
82
+ // The part of the keyboard over the navigation bar, in pixels; 0 without the keyboard.
83
+ private var keyboard = 0
84
+ // The stops' heights changed while the sheet moved (a drag, a slide): it takes them once it rests, and goes to the
85
+ // `selectedDetent` a merge named meanwhile ([pendingArrived]).
86
+ private var restopPending = false
87
+ // The sheet showed while its `content` detent had no height yet: once it has one, the sheet goes where the show
88
+ // would have taken it ([showTarget]), unless the user moved it first.
89
+ private var awaiting = false
90
+ private var pendingArrived: SheetDetent? = null
66
91
  private var selected: SheetDetent? = null
67
92
  private var topGap = 0
68
93
  private var dragged = false
@@ -94,7 +119,31 @@ internal class ModalSheet(
94
119
  // A list inside the sheet scrolls with it, as the Material behavior expects: up at half height the sheet goes up
95
120
  // first, down at the top of the list the sheet comes down. The behavior follows views that scroll nested.
96
121
  private val scrollViews = ViewTreeObserver.OnGlobalLayoutListener {
97
- if (behavior.dragAnywhere) enableNestedScrolling(container)
122
+ turnScrollViews()
123
+ // The bars above the content may have taken their height.
124
+ if (followed != null) contentChanged()
125
+ }
126
+
127
+ // The shown screen's content that the `content` detent follows, its height without a height limit (pixels, none
128
+ // before it is measured), and the height of the detent the behavior has now.
129
+ private var followed: IReactView? = null
130
+ // The shown screen's content whose mounts turn the lists in it to scroll nested ([hearContent]).
131
+ private var heard: IReactView? = null
132
+ private var contentBody: Int? = null
133
+ private var appliedContentHeight: Int? = null
134
+ // When the content was measured last after a mount, and whether a measure waits ([contentMounted]).
135
+ private var lastMeasure = 0L
136
+ private var measureWaits = false
137
+ private val measureLater = Runnable {
138
+ measureWaits = false
139
+ lastMeasure = SystemClock.uptimeMillis()
140
+ measureContent()
141
+ contentChanged()
142
+ }
143
+
144
+ // The stops stand in pixels: they follow the layout's height (a rotation, a resize).
145
+ private val layoutHeight = View.OnLayoutChangeListener { _, _, top, _, bottom, _, oldTop, _, oldBottom ->
146
+ if (bottom - top != oldBottom - oldTop) container.post { restop() }
98
147
  }
99
148
 
100
149
  /** The sheet is down, out of sight: the modal can go at once. */
@@ -113,6 +162,19 @@ internal class ModalSheet(
113
162
  }
114
163
  })
115
164
  scrim.setOnTouchListener { _, event -> taps.onTouchEvent(event) }
165
+ // The keyboard covers the bottom of the window: the sheet stands on it, by the keyboard's part over the
166
+ // navigation bar (the detents count that bar already), unless the screen in it keeps its size for the keyboard
167
+ // (`layout.adjustResize: false`). The scrim, under the sheet, hears the window's insets.
168
+ ViewCompat.setOnApplyWindowInsetsListener(scrim) { _, insets ->
169
+ val ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
170
+ val bar = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
171
+ val lift = (ime - bar).coerceAtLeast(0)
172
+ if (lift != keyboard) {
173
+ keyboard = lift
174
+ restop()
175
+ }
176
+ insets
177
+ }
116
178
  // The sheet keeps a touch that lands on it and no view in it takes (its bar, an empty part), as Material's
117
179
  // BottomSheetDialog does: else the touch falls to the scrim under it, and a tap there closes a dimmed sheet;
118
180
  // past an undimmed scrim it reaches the behavior, which takes the sheet into its drag state at the touch's
@@ -151,8 +213,11 @@ internal class ModalSheet(
151
213
  container.viewTreeObserver.addOnGlobalLayoutListener(scrollViews)
152
214
  }
153
215
 
154
- /** Slides the sheet up to its selected detent; [onShown] once it is there. */
155
- fun show(sheetOptions: SheetOptions, swipeToDismiss: Boolean, onShown: () -> Unit) {
216
+ /**
217
+ * Slides the sheet up to its selected detent; [onShown] once it is there. Not [animated], or with the system's
218
+ * animations off ([SystemAnimations]), it stands there at once.
219
+ */
220
+ fun show(sheetOptions: SheetOptions, swipeToDismiss: Boolean, animated: Boolean, onShown: () -> Unit) {
156
221
  topGap = ViewCompat.getRootWindowInsets(layout)?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
157
222
  layout.addView(scrim, CoordinatorLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
158
223
  // As tall as its largest detent (maxHeight), not moved down by a margin: the behavior counts its states from
@@ -163,42 +228,48 @@ internal class ModalSheet(
163
228
  params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
164
229
  params.behavior = behavior
165
230
  layout.addView(container, params)
231
+ layout.addOnLayoutChangeListener(layoutHeight)
166
232
  shown = onShown
167
233
  update(sheetOptions, swipeToDismiss, merge = null)
168
234
  selected = sheetOptions.selectedDetent?.takeIf { it in detents }
235
+ awaiting = SheetDetent.Content in detents && contentHeight() == null
236
+ if (!animated || SystemAnimations.areOff(context)) {
237
+ // The behavior lays the sheet out at the state it has before its first layout, with no slide, and tells
238
+ // no state then: the sheet stands at its detent once laid out.
239
+ behavior.state = stateOf(showTarget())
240
+ container.post { if (closing == null) onState(behavior.state) }
241
+ return
242
+ }
169
243
  // Laid out hidden first, so it slides up from the bottom.
170
- container.post { if (closing == null) behavior.state = stateOf(selected ?: detents.first()) }
244
+ container.post { if (closing == null) behavior.state = stateOf(showTarget()) }
171
245
  }
172
246
 
247
+ // The stop the show goes to: the selected one when it is a stop already, else the lowest.
248
+ private fun showTarget(): SheetDetent = selected?.takeIf { it in stops } ?: stops.first()
249
+
173
250
  /**
174
251
  * New resolved options: the detents and the look. The sheet moves only as [SheetStops] says, after a `mergeOptions`
175
252
  * of [merge]; without one (the shown screen changed, the show) it stays where it is.
176
253
  */
177
254
  fun update(sheetOptions: SheetOptions, swipeToDismiss: Boolean, merge: SheetOptions?) {
255
+ // The stop it rests at, by the stops it has now: the new ones may give its state another detent.
256
+ val settled = detentOf(behavior.state)
178
257
  options = sheetOptions
179
258
  this.swipeToDismiss = swipeToDismiss
180
- detents = sheetOptions.detents?.takeIf { it.isNotEmpty() }?.sortedBy { heightOf(it) } ?: listOf(SheetDetent.Large)
259
+ detents = sheetOptions.detents?.takeIf { it.isNotEmpty() } ?: listOf(SheetDetent.Large)
181
260
  grabber.visibility = if (sheetOptions.grabber.isTrue) View.VISIBLE else View.GONE
182
261
  behavior.dragAnywhere = sheetOptions.expandsOnScroll.get(true)
183
262
  container.invalidateOutline()
184
- configure()
185
- if (merge == null || closing != null || backFinishing) return
186
- val settled = detentOf(behavior.state)
187
- if (settled != null) behavior.isHideable = swipeToDismiss && settled == detents.first()
188
- val target = SheetStops.target(settled, detents, merge.selectedDetent, sheetOptions.selectedDetent)
189
- // A merge that moves the sheet or keeps it from going down ends a preview of back on it.
190
- if (target != null || !behavior.isHideable) stopBackPreview()
191
- if (target != null) {
192
- selected = target
193
- behavior.state = stateOf(target)
194
- }
195
- updateScrim()
263
+ hearContent()
264
+ followContent()
265
+ restop(settled, merge?.selectedDetent)
196
266
  }
197
267
 
198
- /** Slides the sheet down; [onClosed] once it is out of sight. */
199
- fun close(onClosed: () -> Unit) {
268
+ /** Slides the sheet down; [onClosed] once it is out of sight. Not [animated], or with animations off, at once. */
269
+ fun close(animated: Boolean, onClosed: () -> Unit) {
200
270
  closing = onClosed
201
- if (isClosed) {
271
+ if (isClosed || !animated || SystemAnimations.areOff(context)) {
272
+ stopBackPreview()
202
273
  finishClosing()
203
274
  return
204
275
  }
@@ -216,6 +287,12 @@ internal class ModalSheet(
216
287
  stopBackPreview()
217
288
  stopBackFrames()
218
289
  container.viewTreeObserver.removeOnGlobalLayoutListener(scrollViews)
290
+ layout.removeOnLayoutChangeListener(layoutHeight)
291
+ followed?.setOnContentMounted(CONTENT_KEY, null)
292
+ followed = null
293
+ container.removeCallbacks(measureLater)
294
+ heard?.setOnContentMounted(SCROLL_KEY, null)
295
+ heard = null
219
296
  container.removeView(content)
220
297
  layout.removeView(container)
221
298
  layout.removeView(scrim)
@@ -296,52 +373,208 @@ internal class ModalSheet(
296
373
  events.onCloseAttempted()
297
374
  return
298
375
  }
376
+ // With animations off the modal goes at once, and its close takes the sheet away with no slide.
377
+ if (SystemAnimations.areOff(context)) {
378
+ if (closing != null) return
379
+ closing = {}
380
+ return events.onClosedByUser()
381
+ }
299
382
  behavior.isHideable = true
300
383
  behavior.state = BottomSheetBehavior.STATE_HIDDEN
301
384
  }
302
385
 
303
- // How the Material behavior stops at the detents: with half and full height both, at its three states (the
304
- // smallest at the collapsed one); otherwise at the collapsed state (the smaller detent) and the expanded one, which
305
- // is only as tall as the largest detent.
306
- private fun configure() {
307
- val height = detents.filterIsInstance<SheetDetent.Height>().firstOrNull()
308
- val halfAndFull = SheetDetent.Medium in detents && SheetDetent.Large in detents
309
- behavior.isFitToContents = !halfAndFull
310
- behavior.expandedOffset = topGap
311
- behavior.maxHeight = heightOf(detents.last())
312
- val collapsed = if (halfAndFull) height else detents.takeIf { it.size > 1 }?.first()
313
- behavior.skipCollapsed = collapsed == null
314
- // Without a detent of its own the collapsed state is the smallest detent: a sheet that isn't hideable settles
315
- // there, not at Material's peek height.
316
- behavior.peekHeight = heightOf(collapsed ?: detents.first())
386
+ // The stops of the detents and how the Material behavior rests at them (`SheetStops.material`): at one or two it
387
+ // fits the sheet to its highest stop, at three it stops half expanded between them. The detent the options select
388
+ // stays the middle one when there are more.
389
+ // A new height of the sheet (`maxHeight`), of its collapsed state, or a change of the mode is laid out: the
390
+ // behavior measures the sheet and counts a fitted sheet's top in its layout. It says whether a layout comes.
391
+ private fun configure(): Boolean {
392
+ appliedContentHeight = contentHeight()
393
+ // A `content` detent not measured yet has no height: it stays out of the stops until it has one, or it would
394
+ // count as the full height and take the place of `large`.
395
+ val measured = detents.filter { it != SheetDetent.Content || contentHeight() != null }.ifEmpty { detents }
396
+ stops = SheetStops.stops(measured, ::heightOf, options.selectedDetent)
397
+ val lift = if (adjustsForKeyboard()) keyboard else 0
398
+ stopHeights = SheetStops.lifted(stops.map(::heightOf), lift, fullHeight())
399
+ val material = SheetStops.material(stopHeights, parentHeight())
400
+ val relayout = behavior.maxHeight != material.maxHeight || behavior.peekHeight != material.peekHeight ||
401
+ behavior.isFitToContents != material.fitToContents
402
+ behavior.isFitToContents = material.fitToContents
403
+ material.halfExpandedRatio?.takeIf { it > 0f && it < 1f }?.let { behavior.halfExpandedRatio = it }
404
+ behavior.expandedOffset = material.expandedOffset
405
+ behavior.maxHeight = material.maxHeight
406
+ behavior.skipCollapsed = material.skipCollapsed
407
+ behavior.peekHeight = material.peekHeight
408
+ if (relayout) container.requestLayout()
409
+ return relayout
317
410
  }
318
411
 
319
- private fun stateOf(detent: SheetDetent): Int = when {
320
- detent == detents.last() -> BottomSheetBehavior.STATE_EXPANDED
321
- detent == SheetDetent.Medium && !behavior.isFitToContents -> BottomSheetBehavior.STATE_HALF_EXPANDED
322
- else -> BottomSheetBehavior.STATE_COLLAPSED
412
+ /**
413
+ * The stops or their heights changed (the options, the layout, the content, the keyboard): the behavior takes them,
414
+ * and a sheet at rest goes to [arrived] (a `selectedDetent` a merge names) when it is a stop, else stays at the
415
+ * stop it rested at ([settled], by the stops before the change) where it stands now, else goes to the selected one
416
+ * or the lowest (`SheetStops.target`). A sheet on the move takes them once it rests.
417
+ */
418
+ private fun restop(settled: SheetDetent? = detentOf(behavior.state), arrived: SheetDetent? = null) {
419
+ if (container.parent == null) return
420
+ val relayout = configure()
421
+ updateScrim()
422
+ if (closing != null || backFinishing) return
423
+ if (isMoving()) {
424
+ restopPending = true
425
+ if (arrived != null) pendingArrived = arrived
426
+ return
427
+ }
428
+ // Not shown yet: the show slides it to the stop a merge names.
429
+ if (settled == null) {
430
+ if (arrived != null && arrived in stops) selected = arrived
431
+ return
432
+ }
433
+ val shows = awaiting && contentHeight() != null
434
+ if (shows) awaiting = false
435
+ val wanted = arrived ?: if (shows) showTarget() else null
436
+ val target = SheetStops.target(settled, stops, wanted, options.selectedDetent) ?: settled
437
+ behavior.isHideable = swipeToDismiss && target == stops.first()
438
+ // A move, or a sheet that can no longer go down, ends a preview of back on it.
439
+ if (target != settled || !behavior.isHideable) stopBackPreview()
440
+ if (backPreview != null) return
441
+ if (target != settled) selected = target
442
+ if (!relayout) {
443
+ behavior.state = stateOf(target)
444
+ return
445
+ }
446
+ // The new height comes with the sheet's next layout, where the behavior puts a sheet at rest at its state's
447
+ // offset at once; a move to another stop follows it.
448
+ afterNextLayout {
449
+ val gone = closing != null || backFinishing || container.parent == null
450
+ if (gone || backPreview != null) return@afterNextLayout
451
+ if (isMoving()) {
452
+ restopPending = true
453
+ if (target != settled) pendingArrived = target
454
+ return@afterNextLayout
455
+ }
456
+ behavior.state = stateOf(target)
457
+ }
458
+ }
459
+
460
+ private fun isMoving(): Boolean =
461
+ behavior.state == BottomSheetBehavior.STATE_DRAGGING || behavior.state == BottomSheetBehavior.STATE_SETTLING
462
+
463
+ // Runs [step] on the pre-draw of the frame of the sheet's next layout: the behavior has placed it by then.
464
+ private fun afterNextLayout(step: () -> Unit) {
465
+ container.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
466
+ override fun onLayoutChange(
467
+ view: View, left: Int, top: Int, right: Int, bottom: Int,
468
+ oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int,
469
+ ) {
470
+ view.removeOnLayoutChangeListener(this)
471
+ OneShotPreDrawListener.add(view, step)
472
+ }
473
+ })
474
+ }
475
+
476
+ private fun stateOf(detent: SheetDetent): Int = when (detent) {
477
+ stops.last() -> BottomSheetBehavior.STATE_EXPANDED
478
+ stops.first() -> BottomSheetBehavior.STATE_COLLAPSED
479
+ else -> BottomSheetBehavior.STATE_HALF_EXPANDED
323
480
  }
324
481
 
325
482
  private fun detentOf(state: Int): SheetDetent? = when (state) {
326
- BottomSheetBehavior.STATE_EXPANDED -> detents.last()
327
- BottomSheetBehavior.STATE_HALF_EXPANDED -> SheetDetent.Medium
328
- BottomSheetBehavior.STATE_COLLAPSED -> detents.first()
483
+ BottomSheetBehavior.STATE_EXPANDED -> stops.last()
484
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> stops.getOrNull(1)?.takeIf { stops.size == SheetStops.MAX_STOPS }
485
+ BottomSheetBehavior.STATE_COLLAPSED -> stops.first()
329
486
  else -> null
330
487
  }
331
488
 
489
+ private fun parentHeight(): Int = if (layout.height > 0) layout.height else layout.rootView.height
490
+
491
+ // The tallest a sheet is: the layout under the status bar.
492
+ private fun fullHeight(): Int = parentHeight() - topGap
493
+
332
494
  /** The height of the sheet at the detent, in px. */
333
495
  private fun heightOf(detent: SheetDetent): Int {
334
- val full = (if (layout.height > 0) layout.height else layout.rootView.height) - topGap
496
+ val full = fullHeight()
335
497
  return when (detent) {
336
498
  SheetDetent.Large -> full
337
499
  SheetDetent.Medium -> (full + topGap) / 2
500
+ SheetDetent.Content -> contentHeight()?.coerceAtMost(full) ?: full
338
501
  is SheetDetent.Height -> detent.dp.toFloat().dp.coerceAtMost(full)
339
502
  }
340
503
  }
341
504
 
505
+ // The `content` detent follows the content of the screen shown now, and measures it after each of its mounts; a
506
+ // sheet without the detent follows nothing.
507
+ private fun followContent() {
508
+ val content = if (SheetDetent.Content in detents) shownContent() else null
509
+ if (content !== followed) {
510
+ followed?.setOnContentMounted(CONTENT_KEY, null)
511
+ followed = content
512
+ // The last height stays until the new content has one: a push in the sheet doesn't jump it to the top.
513
+ content?.setOnContentMounted(CONTENT_KEY) { contentMounted() }
514
+ }
515
+ measureContent()
516
+ }
517
+
518
+ // Fabric tells every mount of the app: the content is measured at most once per [MEASURE_INTERVAL_MS], the last
519
+ // mount of a burst included, so a screen that renders each frame elsewhere doesn't lay the sheet's tree out each
520
+ // frame too.
521
+ private fun contentMounted() {
522
+ val wait = lastMeasure + MEASURE_INTERVAL_MS - SystemClock.uptimeMillis()
523
+ if (wait > 0) {
524
+ if (!measureWaits) {
525
+ measureWaits = true
526
+ container.postDelayed(measureLater, wait)
527
+ }
528
+ return
529
+ }
530
+ measureLater.run()
531
+ }
532
+
533
+ // The content's own height at the width it has (the sheet's, before it is laid out); kept until a measure gives
534
+ // one again.
535
+ private fun measureContent() {
536
+ val content = followed ?: return
537
+ val width = content.asView().width.takeIf { it > 0 } ?: container.width.takeIf { it > 0 } ?: sheetWidth()
538
+ content.contentSize(width)?.let { contentBody = it.height }
539
+ }
540
+
541
+ // The height of the `content` detent: the content's own, the bars above it in the sheet (a stack's top bar), and
542
+ // the navigation bar below it, which a detent leaves room for as UIKit adds the bottom safe area; none before the
543
+ // content is measured.
544
+ private fun contentHeight(): Int? {
545
+ val body = contentBody ?: return null
546
+ val insets = ViewCompat.getRootWindowInsets(layout)
547
+ val bottom = insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
548
+ return offsetAbove() + body + bottom
549
+ }
550
+
551
+ // How far below the sheet's top the content stands, by layout: the views between them, without their transforms.
552
+ private fun offsetAbove(): Int {
553
+ var view: View = followed?.asView() ?: return 0
554
+ var top = 0
555
+ while (view !== container) {
556
+ top += view.top
557
+ view = view.parent as? View ?: return 0
558
+ }
559
+ return top
560
+ }
561
+
562
+ // The content may have changed its height: the stops follow it.
563
+ private fun contentChanged() {
564
+ if (contentHeight() != appliedContentHeight) restop()
565
+ }
566
+
567
+ private fun sheetWidth(): Int {
568
+ val width = if (layout.width > 0) layout.width else layout.rootView.width
569
+ return width.coerceAtMost(MAX_WIDTH_DP.dp)
570
+ }
571
+
342
572
  private fun onState(state: Int) {
343
573
  when (state) {
344
- BottomSheetBehavior.STATE_DRAGGING -> dragged = true
574
+ BottomSheetBehavior.STATE_DRAGGING -> {
575
+ dragged = true
576
+ awaiting = false
577
+ }
345
578
  BottomSheetBehavior.STATE_HIDDEN -> {
346
579
  backFinishing = false
347
580
  when {
@@ -357,7 +590,7 @@ internal class ModalSheet(
357
590
  if (closing != null) return
358
591
  val detent = detentOf(state)
359
592
  // A drag down closes the sheet from its smallest detent only; from a larger one it stops at the next.
360
- behavior.isHideable = swipeToDismiss && detent == detents.first()
593
+ behavior.isHideable = swipeToDismiss && detent == stops.first()
361
594
  shown?.let {
362
595
  shown = null
363
596
  it()
@@ -367,6 +600,12 @@ internal class ModalSheet(
367
600
  events.onDetentChanged(detent)
368
601
  }
369
602
  dragged = false
603
+ if (restopPending) {
604
+ val arrived = pendingArrived
605
+ restopPending = false
606
+ pendingArrived = null
607
+ container.post { restop(arrived = arrived) }
608
+ }
370
609
  }
371
610
  }
372
611
  updateScrim()
@@ -384,9 +623,9 @@ internal class ModalSheet(
384
623
  // Hidden at the end of back's slide, it still stands at its detent until the next layout.
385
624
  if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) return setDim(0f)
386
625
  val height = layout.height - sheetTop()
387
- val undimmed = options.largestUndimmedDetent?.takeIf { it in detents }
388
- val from = undimmed?.let { heightOf(it) } ?: 0
389
- val to = detents.firstOrNull { heightOf(it) > from }?.let { heightOf(it) } ?: return run { setDim(0f) }
626
+ val undimmed = stops.indexOf(options.largestUndimmedDetent).takeIf { it >= 0 }
627
+ val from = undimmed?.let { stopHeights.getOrNull(it) } ?: 0
628
+ val to = stopHeights.firstOrNull { it > from } ?: return run { setDim(0f) }
390
629
  setDim(((height - from).toFloat() / (to - from)).coerceIn(0f, 1f))
391
630
  }
392
631
 
@@ -406,9 +645,35 @@ internal class ModalSheet(
406
645
 
407
646
  private fun cornerRadius(): Int = if (options.cornerRadius.hasValue()) options.cornerRadius.get().toFloat().dp else 28.dp
408
647
 
409
- private fun enableNestedScrolling(view: View) {
410
- if (view is ScrollView && !view.isNestedScrollingEnabled) view.isNestedScrollingEnabled = true
411
- if (view is ViewGroup) for (i in 0 until view.childCount) enableNestedScrolling(view.getChildAt(i))
648
+ // React Native mounts the lists of the shown screen after the sheet's layout, and its views take no layout of their
649
+ // own: the sheet hears each mount of the content and turns a new list to scroll nested there.
650
+ private fun hearContent() {
651
+ val content = if (behavior.dragAnywhere) shownContent() else null
652
+ if (content !== heard) {
653
+ heard?.setOnContentMounted(SCROLL_KEY, null)
654
+ heard = content
655
+ content?.setOnContentMounted(SCROLL_KEY) { turnScrollViews() }
656
+ }
657
+ turnScrollViews()
658
+ }
659
+
660
+ // The behavior finds the lists that scroll nested only in its layout of the sheet: a list turned so gets the sheet
661
+ // one more, else the behavior knows no list and takes a drag down on a scrolled one for a drag of the sheet.
662
+ private fun turnScrollViews() {
663
+ if (behavior.dragAnywhere && enableNestedScrolling(container)) container.requestLayout()
664
+ }
665
+
666
+ // Whether a list was turned to scroll nested now.
667
+ private fun enableNestedScrolling(view: View): Boolean {
668
+ var turned = false
669
+ if (view is ScrollView && !view.isNestedScrollingEnabled) {
670
+ view.isNestedScrollingEnabled = true
671
+ turned = true
672
+ }
673
+ if (view is ViewGroup) {
674
+ for (i in 0 until view.childCount) turned = enableNestedScrolling(view.getChildAt(i)) || turned
675
+ }
676
+ return turned
412
677
  }
413
678
 
414
679
  /** With `expandsOnScroll: false` only the top of the sheet, around the grabber, drags it; the rest scrolls. */
@@ -424,7 +689,7 @@ internal class ModalSheet(
424
689
  when (event.actionMasked) {
425
690
  MotionEvent.ACTION_DOWN -> {
426
691
  downY = event.rawY
427
- downAtSmallest = detentOf(state) == detents.first()
692
+ downAtSmallest = detentOf(state) == stops.first()
428
693
  }
429
694
  MotionEvent.ACTION_UP -> {
430
695
  if (!swipeToDismiss && closing == null && downAtSmallest && event.rawY - downY > ATTEMPT_DISTANCE_DP.dp) {
@@ -467,5 +732,14 @@ internal class ModalSheet(
467
732
  private const val SCRIM_ALPHA = 0.32f
468
733
  private const val GRAB_AREA_DP = 48
469
734
  private const val ATTEMPT_DISTANCE_DP = 48
735
+
736
+ // The key of the sheet's handler of its content's mounts.
737
+ private const val CONTENT_KEY = "sheet.content"
738
+
739
+ // The least time between two measures of the content after its mounts.
740
+ private const val MEASURE_INTERVAL_MS = 100L
741
+
742
+ // The key of the sheet's handler that turns its content's lists to scroll nested.
743
+ private const val SCROLL_KEY = "sheet.scroll"
470
744
  }
471
745
  }
@@ -14,10 +14,13 @@ import android.widget.FrameLayout
14
14
  import androidx.coordinatorlayout.widget.CoordinatorLayout
15
15
  import androidx.core.view.ViewCompat
16
16
  import androidx.core.view.WindowInsetsCompat
17
+ import dev.apexnative.navigation.options.DefaultOptions
17
18
  import dev.apexnative.navigation.options.ModalAnimationOptions
18
19
  import dev.apexnative.navigation.options.Options
19
20
  import dev.apexnative.navigation.utils.TestIds
20
21
  import dev.apexnative.navigation.utils.dp
22
+ import dev.apexnative.navigation.viewcontrollers.component.ComponentViewController
23
+ import dev.apexnative.navigation.viewcontrollers.parent.ParentController
21
24
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
22
25
  import dev.apexnative.navigation.views.animations.AnimationLayer
23
26
  import dev.apexnative.navigation.views.animations.AnimationRules
@@ -96,7 +99,7 @@ internal class ModalViews(
96
99
  val container = parts.container
97
100
  container.isWaiting = false
98
101
  container.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
99
- if (record.isSheet) return presentSheet(record, parts, options, seen, events, onEnd)
102
+ if (record.isSheet) return presentSheet(record, parts, options, animated, seen, events, onEnd)
100
103
  applyTestIds(record, parts, options)
101
104
  if (!animated || !seen) {
102
105
  SideAnimator.rest(container)
@@ -118,11 +121,13 @@ internal class ModalViews(
118
121
  }
119
122
 
120
123
  // The root's view leaves the container and goes into the sheet's, in one turn; the sheet slides to its first
121
- // detent. Nobody sees the activity: the show counts as done now, the sheet's later end does nothing.
124
+ // detent, or stands there at once when not [animated]. Nobody sees the activity: the show counts as done now, the
125
+ // sheet's later end does nothing.
122
126
  private fun presentSheet(
123
127
  record: ModalRecord,
124
128
  parts: Parts,
125
129
  options: Options,
130
+ animated: Boolean,
126
131
  seen: Boolean,
127
132
  events: ModalSheet.Events,
128
133
  onEnd: () -> Unit,
@@ -130,7 +135,16 @@ internal class ModalViews(
130
135
  val container = parts.container
131
136
  val view = record.root.view
132
137
  container.removeView(view)
133
- val sheet = ModalSheet(container, view, events)
138
+ val sheet = ModalSheet(
139
+ container,
140
+ view,
141
+ events,
142
+ shownContent = { (shownScreen(record.root) as? ComponentViewController)?.content },
143
+ adjustsForKeyboard = {
144
+ val screen = shownScreen(record.root)
145
+ screen == null || !screen.resolveCurrentOptions(DefaultOptions.current).layout.adjustResize.isFalse
146
+ },
147
+ )
134
148
  parts.sheet = sheet
135
149
  container.alpha = 1f
136
150
  applyTestIds(record, parts, options)
@@ -141,11 +155,18 @@ internal class ModalViews(
141
155
  onEnd()
142
156
  }
143
157
  }
144
- sheet.show(options.modal.sheet, swipeToDismissOf(options)) { shown() }
158
+ sheet.show(options.modal.sheet, swipeToDismissOf(options), animated) { shown() }
145
159
  if (!seen) shown()
146
160
  return { told = true }
147
161
  }
148
162
 
163
+ // The screen a layout shows now: down through the child each parent shows.
164
+ private fun shownScreen(root: ViewController<*>): ViewController<*>? {
165
+ var node = root
166
+ while (node is ParentController<*>) node = node.currentChild ?: return null
167
+ return node
168
+ }
169
+
149
170
  override fun dismiss(
150
171
  record: ModalRecord,
151
172
  animated: Boolean,
@@ -159,7 +180,7 @@ internal class ModalViews(
159
180
  if (sheet != null) {
160
181
  if (!seen) return none(onEnd)
161
182
  var told = false
162
- sheet.close {
183
+ sheet.close(animated) {
163
184
  if (!told) {
164
185
  told = true
165
186
  onEnd()