react-native-screens 4.10.0-beta.2 → 4.10.0

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 (36) hide show
  1. package/RNScreens.podspec +2 -2
  2. package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +3 -8
  3. package/android/src/fabric/java/com/swmansion/rnscreens/FabricEnabledViewGroup.kt +9 -6
  4. package/android/src/fabric/java/com/swmansion/rnscreens/NativeProxy.kt +2 -1
  5. package/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +111 -15
  6. package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +54 -17
  7. package/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +1 -1
  8. package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +125 -48
  9. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +46 -286
  10. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +94 -54
  11. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigShadowNode.kt +3 -0
  12. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt +19 -2
  13. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackViewManager.kt +2 -1
  14. package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetDelegate.kt +262 -3
  15. package/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt +2 -0
  16. package/android/src/main/java/com/swmansion/rnscreens/utils/InsetsKt.kt +31 -0
  17. package/android/src/main/java/com/swmansion/rnscreens/utils/PaddingBundle.kt +1 -0
  18. package/android/src/paper/java/com/swmansion/rnscreens/FabricEnabledHeaderConfigViewGroup.kt +14 -5
  19. package/android/src/versioned/pointerevents/77/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
  20. package/android/src/versioned/pointerevents/latest/com/swmansion/rnscreens/{ScreensCoordinatorLayoutPointerEventsImpl.kt → PointerEventsBoxNoneImpl.kt} +1 -1
  21. package/common/cpp/react/renderer/components/rnscreens/RNSModalScreenShadowNode.cpp +1 -3
  22. package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h +7 -3
  23. package/common/cpp/react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h +1 -1
  24. package/cpp/RNSScreenRemovalListener.cpp +3 -1
  25. package/ios/RNSFullWindowOverlay.mm +6 -6
  26. package/ios/RNSScreen.mm +12 -0
  27. package/ios/RNSScreenStackHeaderConfig.h +1 -1
  28. package/ios/RNSScreenStackHeaderConfig.mm +27 -22
  29. package/lib/commonjs/components/Screen.js +13 -4
  30. package/lib/commonjs/components/Screen.js.map +1 -1
  31. package/lib/module/components/Screen.js +13 -4
  32. package/lib/module/components/Screen.js.map +1 -1
  33. package/lib/typescript/components/Screen.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/components/Screen.tsx +23 -13
  36. package/android/src/main/java/com/swmansion/rnscreens/NativeDismissalObserver.kt +0 -12
@@ -7,7 +7,6 @@ import android.annotation.SuppressLint
7
7
  import android.content.Context
8
8
  import android.graphics.Color
9
9
  import android.graphics.drawable.ColorDrawable
10
- import android.os.Build
11
10
  import android.os.Bundle
12
11
  import android.view.LayoutInflater
13
12
  import android.view.Menu
@@ -16,33 +15,23 @@ import android.view.MenuItem
16
15
  import android.view.View
17
16
  import android.view.ViewGroup
18
17
  import android.view.WindowInsets
19
- import android.view.WindowManager
20
18
  import android.view.animation.Animation
21
19
  import android.view.animation.AnimationSet
22
20
  import android.view.animation.Transformation
23
- import android.view.inputmethod.InputMethodManager
24
21
  import android.widget.LinearLayout
25
- import androidx.annotation.RequiresApi
26
22
  import androidx.appcompat.widget.Toolbar
27
23
  import androidx.coordinatorlayout.widget.CoordinatorLayout
28
- import androidx.core.view.WindowInsetsCompat
29
24
  import com.facebook.react.uimanager.PixelUtil
30
25
  import com.facebook.react.uimanager.ReactPointerEventsView
31
26
  import com.facebook.react.uimanager.UIManagerHelper
32
27
  import com.google.android.material.appbar.AppBarLayout
33
28
  import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
34
29
  import com.google.android.material.bottomsheet.BottomSheetBehavior
35
- import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
36
30
  import com.google.android.material.shape.CornerFamily
37
31
  import com.google.android.material.shape.MaterialShapeDrawable
38
32
  import com.google.android.material.shape.ShapeAppearanceModel
39
33
  import com.swmansion.rnscreens.bottomsheet.DimmingViewManager
40
34
  import com.swmansion.rnscreens.bottomsheet.SheetDelegate
41
- import com.swmansion.rnscreens.bottomsheet.SheetUtils
42
- import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
43
- import com.swmansion.rnscreens.bottomsheet.useSingleDetent
44
- import com.swmansion.rnscreens.bottomsheet.useThreeDetents
45
- import com.swmansion.rnscreens.bottomsheet.useTwoDetents
46
35
  import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
47
36
  import com.swmansion.rnscreens.events.ScreenAnimationDelegate
48
37
  import com.swmansion.rnscreens.events.ScreenDismissedEvent
@@ -65,7 +54,6 @@ class KeyboardVisible(
65
54
  class ScreenStackFragment :
66
55
  ScreenFragment,
67
56
  ScreenStackFragmentWrapper {
68
- public var nativeDismissalObserver: NativeDismissalObserver? = null
69
57
  private var appBarLayout: AppBarLayout? = null
70
58
  private var toolbar: Toolbar? = null
71
59
  private var isToolbarShadowHidden = false
@@ -159,51 +147,6 @@ class ScreenStackFragment :
159
147
  }
160
148
  }
161
149
 
162
- // If the Screen has `formSheet` presentation this callback is attached to its behavior.
163
- // It is responsible for firing detent changed events & removing the sheet from the container
164
- // once it is hidden by user gesture.
165
- private val bottomSheetStateCallback =
166
- object : BottomSheetCallback() {
167
- private var lastStableState: Int =
168
- SheetUtils.sheetStateFromDetentIndex(
169
- screen.sheetInitialDetentIndex,
170
- screen.sheetDetents.count(),
171
- )
172
-
173
- override fun onStateChanged(
174
- bottomSheet: View,
175
- newState: Int,
176
- ) {
177
- if (SheetUtils.isStateStable(newState)) {
178
- lastStableState = newState
179
- screen.notifySheetDetentChange(
180
- SheetUtils.detentIndexFromSheetState(
181
- lastStableState,
182
- screen.sheetDetents.count(),
183
- ),
184
- true,
185
- )
186
- } else if (newState == BottomSheetBehavior.STATE_DRAGGING) {
187
- screen.notifySheetDetentChange(
188
- SheetUtils.detentIndexFromSheetState(
189
- lastStableState,
190
- screen.sheetDetents.count(),
191
- ),
192
- false,
193
- )
194
- }
195
-
196
- if (newState == BottomSheetBehavior.STATE_HIDDEN) {
197
- dismissSelf()
198
- }
199
- }
200
-
201
- override fun onSlide(
202
- bottomSheet: View,
203
- slideOffset: Float,
204
- ) = Unit
205
- }
206
-
207
150
  /**
208
151
  * Currently this method dispatches event to JS where state is recomputed and fragment
209
152
  * gets removed in the result of incoming state update.
@@ -241,7 +184,7 @@ class ScreenStackFragment :
241
184
  ).apply {
242
185
  behavior =
243
186
  if (screen.usesFormSheetPresentation()) {
244
- createAndConfigureBottomSheetBehaviour()
187
+ createBottomSheetBehaviour()
245
188
  } else if (isToolbarTranslucent) {
246
189
  null
247
190
  } else {
@@ -249,13 +192,8 @@ class ScreenStackFragment :
249
192
  }
250
193
  }
251
194
 
252
- if (screen.usesFormSheetPresentation()) {
253
- screen.clipToOutline = true
254
- // TODO(@kkafar): without this line there is no drawable / outline & nothing shows...? Determine what's going on here
255
- attachShapeToScreen(screen)
256
- screen.elevation = screen.sheetElevation
257
- }
258
-
195
+ // This must be called before further sheet configuration.
196
+ // Otherwise there is no enter animation -> dunno why, just observed it.
259
197
  coordinatorLayout.addView(screen.recycle())
260
198
 
261
199
  if (!screen.usesFormSheetPresentation()) {
@@ -279,7 +217,28 @@ class ScreenStackFragment :
279
217
  }
280
218
  toolbar?.let { appBarLayout?.addView(it.recycle()) }
281
219
  setHasOptionsMenu(true)
220
+ } else {
221
+ screen.clipToOutline = true
222
+ // TODO(@kkafar): without this line there is no drawable / outline & nothing shows...? Determine what's going on here
223
+ attachShapeToScreen(screen)
224
+ screen.elevation = screen.sheetElevation
225
+
226
+ // Lifecycle of sheet delegate is tied to fragment.
227
+ val sheetDelegate = requireSheetDelegate()
228
+ sheetDelegate.configureBottomSheetBehaviour(screen.sheetBehavior!!)
229
+
230
+ val dimmingDelegate = requireDimmingDelegate(forceCreation = true)
231
+ dimmingDelegate.onViewHierarchyCreated(screen, coordinatorLayout)
232
+ dimmingDelegate.onBehaviourAttached(screen, screen.sheetBehavior!!)
233
+
234
+ val container = screen.container!!
235
+ coordinatorLayout.measure(
236
+ View.MeasureSpec.makeMeasureSpec(container.width, View.MeasureSpec.EXACTLY),
237
+ View.MeasureSpec.makeMeasureSpec(container.height, View.MeasureSpec.EXACTLY),
238
+ )
239
+ coordinatorLayout.layout(0, 0, container.width, container.height)
282
240
  }
241
+
283
242
  return coordinatorLayout
284
243
  }
285
244
 
@@ -288,24 +247,6 @@ class ScreenStackFragment :
288
247
  savedInstanceState: Bundle?,
289
248
  ) {
290
249
  super.onViewCreated(view, savedInstanceState)
291
-
292
- if (!screen.usesFormSheetPresentation()) {
293
- return
294
- }
295
-
296
- sheetDelegate = SheetDelegate(screen)
297
-
298
- assert(view == coordinatorLayout)
299
- val dimmingDelegate = requireDimmingDelegate(forceCreation = true)
300
- dimmingDelegate.onViewHierarchyCreated(screen, coordinatorLayout)
301
- dimmingDelegate.onBehaviourAttached(screen, screen.sheetBehavior!!)
302
-
303
- val container = screen.container!!
304
- coordinatorLayout.measure(
305
- View.MeasureSpec.makeMeasureSpec(container.width, View.MeasureSpec.EXACTLY),
306
- View.MeasureSpec.makeMeasureSpec(container.height, View.MeasureSpec.EXACTLY),
307
- )
308
- coordinatorLayout.layout(0, 0, container.width, container.height)
309
250
  }
310
251
 
311
252
  override fun onCreateAnimation(
@@ -387,210 +328,8 @@ class ScreenStackFragment :
387
328
  return animatorSet
388
329
  }
389
330
 
390
- /**
391
- * This method might return slightly different values depending on code path,
392
- * but during testing I've found this effect negligible. For practical purposes
393
- * this is acceptable.
394
- */
395
- private fun tryResolveContainerHeight(): Int? {
396
- if (screen.container != null) {
397
- return screenStack.height
398
- }
399
-
400
- context
401
- ?.resources
402
- ?.displayMetrics
403
- ?.heightPixels
404
- ?.let { return it }
405
-
406
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
407
- (context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager)
408
- ?.currentWindowMetrics
409
- ?.bounds
410
- ?.height()
411
- ?.let { return it }
412
- }
413
- return null
414
- }
415
-
416
- private val keyboardSheetCallback =
417
- object : BottomSheetCallback() {
418
- @RequiresApi(Build.VERSION_CODES.M)
419
- override fun onStateChanged(
420
- bottomSheet: View,
421
- newState: Int,
422
- ) {
423
- if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
424
- val isImeVisible =
425
- WindowInsetsCompat
426
- .toWindowInsetsCompat(bottomSheet.rootWindowInsets)
427
- .isVisible(WindowInsetsCompat.Type.ime())
428
- if (isImeVisible) {
429
- // Does it not interfere with React Native focus mechanism? In any case I'm not aware
430
- // of different way of hiding the keyboard.
431
- // https://stackoverflow.com/questions/1109022/how-can-i-close-hide-the-android-soft-keyboard-programmatically
432
- // https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility
433
-
434
- // I want to be polite here and request focus before dismissing the keyboard,
435
- // however even if it fails I want to try to hide the keyboard. This sometimes works...
436
- bottomSheet.requestFocus()
437
- val imm = requireContext().getSystemService(InputMethodManager::class.java)
438
- imm.hideSoftInputFromWindow(bottomSheet.windowToken, 0)
439
- }
440
- }
441
- }
442
-
443
- override fun onSlide(
444
- bottomSheet: View,
445
- slideOffset: Float,
446
- ) = Unit
447
- }
448
-
449
- internal fun configureBottomSheetBehaviour(
450
- behavior: BottomSheetBehavior<Screen>,
451
- keyboardState: KeyboardState = KeyboardNotVisible,
452
- ): BottomSheetBehavior<Screen> {
453
- val containerHeight = tryResolveContainerHeight()
454
- check(containerHeight != null) {
455
- "[RNScreens] Failed to find window height during bottom sheet behaviour configuration"
456
- }
457
-
458
- behavior.apply {
459
- isHideable = true
460
- isDraggable = true
461
-
462
- // It seems that there is a guard in material implementation that will prevent
463
- // this callback from being registered multiple times.
464
- addBottomSheetCallback(bottomSheetStateCallback)
465
- }
466
-
467
- screen.footer?.registerWithSheetBehavior(behavior)
468
-
469
- return when (keyboardState) {
470
- is KeyboardNotVisible -> {
471
- when (screen.sheetDetents.count()) {
472
- 1 ->
473
- behavior.apply {
474
- val height =
475
- if (screen.isSheetFitToContents()) {
476
- screen.contentWrapper
477
- .get()
478
- ?.height
479
- .takeIf { screen.contentWrapper.get()?.isLaidOut == true }
480
- } else {
481
- (screen.sheetDetents.first() * containerHeight).toInt()
482
- }
483
- useSingleDetent(height = height)
484
- }
485
-
486
- 2 ->
487
- behavior.useTwoDetents(
488
- state =
489
- SheetUtils.sheetStateFromDetentIndex(
490
- screen.sheetInitialDetentIndex,
491
- screen.sheetDetents.count(),
492
- ),
493
- firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(),
494
- secondHeight = (screen.sheetDetents[1] * containerHeight).toInt(),
495
- )
496
-
497
- 3 ->
498
- behavior.useThreeDetents(
499
- state =
500
- SheetUtils.sheetStateFromDetentIndex(
501
- screen.sheetInitialDetentIndex,
502
- screen.sheetDetents.count(),
503
- ),
504
- firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(),
505
- halfExpandedRatio = (screen.sheetDetents[1] / screen.sheetDetents[2]).toFloat(),
506
- expandedOffsetFromTop = ((1 - screen.sheetDetents[2]) * containerHeight).toInt(),
507
- )
508
-
509
- else -> throw IllegalStateException(
510
- "[RNScreens] Invalid detent count ${screen.sheetDetents.count()}. Expected at most 3.",
511
- )
512
- }
513
- }
514
-
515
- is KeyboardVisible -> {
516
- val newMaxHeight =
517
- if (behavior.maxHeight - keyboardState.height > 1) {
518
- behavior.maxHeight - keyboardState.height
519
- } else {
520
- behavior.maxHeight
521
- }
522
- when (screen.sheetDetents.count()) {
523
- 1 ->
524
- behavior.apply {
525
- useSingleDetent(height = newMaxHeight)
526
- addBottomSheetCallback(keyboardSheetCallback)
527
- }
528
-
529
- 2 ->
530
- behavior.apply {
531
- useTwoDetents(
532
- state = BottomSheetBehavior.STATE_EXPANDED,
533
- secondHeight = newMaxHeight,
534
- )
535
- addBottomSheetCallback(keyboardSheetCallback)
536
- }
537
-
538
- 3 ->
539
- behavior.apply {
540
- useThreeDetents(
541
- state = BottomSheetBehavior.STATE_EXPANDED,
542
- )
543
- maxHeight = newMaxHeight
544
- addBottomSheetCallback(keyboardSheetCallback)
545
- }
546
-
547
- else -> throw IllegalStateException(
548
- "[RNScreens] Invalid detent count ${screen.sheetDetents.count()}. Expected at most 3.",
549
- )
550
- }
551
- }
552
-
553
- is KeyboardDidHide -> {
554
- // Here we assume that the keyboard was either closed explicitly by user,
555
- // or the user dragged the sheet down. In any case the state should
556
- // stay unchanged.
557
-
558
- behavior.removeBottomSheetCallback(keyboardSheetCallback)
559
- when (screen.sheetDetents.count()) {
560
- 1 ->
561
- behavior.useSingleDetent(
562
- height = (screen.sheetDetents.first() * containerHeight).toInt(),
563
- forceExpandedState = false,
564
- )
565
-
566
- 2 ->
567
- behavior.useTwoDetents(
568
- firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(),
569
- secondHeight = (screen.sheetDetents[1] * containerHeight).toInt(),
570
- )
571
-
572
- 3 ->
573
- behavior.useThreeDetents(
574
- firstHeight = (screen.sheetDetents[0] * containerHeight).toInt(),
575
- halfExpandedRatio = (screen.sheetDetents[1] / screen.sheetDetents[2]).toFloat(),
576
- expandedOffsetFromTop = ((1 - screen.sheetDetents[2]) * containerHeight).toInt(),
577
- )
578
-
579
- else -> throw IllegalStateException(
580
- "[RNScreens] Invalid detent count ${screen.sheetDetents.count()}. Expected at most 3.",
581
- )
582
- }
583
- }
584
- }
585
- }
586
-
587
331
  private fun createBottomSheetBehaviour(): BottomSheetBehavior<Screen> = BottomSheetBehavior<Screen>()
588
332
 
589
- // In general it would be great to create BottomSheetBehaviour only via this method as it runs some
590
- // side effects.
591
- private fun createAndConfigureBottomSheetBehaviour(): BottomSheetBehavior<Screen> =
592
- configureBottomSheetBehaviour(createBottomSheetBehaviour())
593
-
594
333
  private fun resolveBackgroundColor(screen: Screen): Int? {
595
334
  val screenColor =
596
335
  (screen.background as? ColorDrawable?)?.color
@@ -724,6 +463,13 @@ class ScreenStackFragment :
724
463
  return dimmingDelegate!!
725
464
  }
726
465
 
466
+ private fun requireSheetDelegate(): SheetDelegate {
467
+ if (sheetDelegate == null) {
468
+ sheetDelegate = SheetDelegate(screen)
469
+ }
470
+ return sheetDelegate!!
471
+ }
472
+
727
473
  private class ScreensCoordinatorLayout(
728
474
  context: Context,
729
475
  private val fragment: ScreenStackFragment,
@@ -734,7 +480,7 @@ class ScreenStackFragment :
734
480
  constructor(context: Context, fragment: ScreenStackFragment) : this(
735
481
  context,
736
482
  fragment,
737
- ScreensCoordinatorLayoutPointerEventsImpl(),
483
+ PointerEventsBoxNoneImpl(),
738
484
  )
739
485
 
740
486
  override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets = super.onApplyWindowInsets(insets)
@@ -797,6 +543,20 @@ class ScreenStackFragment :
797
543
  }
798
544
  }
799
545
 
546
+ override fun onLayout(
547
+ changed: Boolean,
548
+ l: Int,
549
+ t: Int,
550
+ r: Int,
551
+ b: Int,
552
+ ) {
553
+ super.onLayout(changed, l, t, r, b)
554
+
555
+ if (fragment.screen.usesFormSheetPresentation()) {
556
+ fragment.screen.onBottomSheetBehaviorDidLayout(changed)
557
+ }
558
+ }
559
+
800
560
  // override fun reactTagForTouch(touchX: Float, touchY: Float): Int {
801
561
  // throw IllegalStateException("Screen wrapper should never be asked for the view tag")
802
562
  // }
@@ -3,12 +3,10 @@ package com.swmansion.rnscreens
3
3
  import android.content.Context
4
4
  import android.graphics.PorterDuff
5
5
  import android.graphics.PorterDuffColorFilter
6
- import android.os.Build
7
6
  import android.text.TextUtils
8
7
  import android.util.TypedValue
9
8
  import android.view.Gravity
10
9
  import android.view.View.OnClickListener
11
- import android.view.WindowInsets
12
10
  import android.widget.ImageView
13
11
  import android.widget.TextView
14
12
  import androidx.appcompat.app.AppCompatActivity
@@ -17,19 +15,25 @@ import androidx.fragment.app.Fragment
17
15
  import com.facebook.react.ReactApplication
18
16
  import com.facebook.react.bridge.JSApplicationIllegalArgumentException
19
17
  import com.facebook.react.bridge.ReactContext
18
+ import com.facebook.react.uimanager.ReactPointerEventsView
20
19
  import com.facebook.react.uimanager.UIManagerHelper
21
20
  import com.facebook.react.views.text.ReactTypefaceUtils
22
21
  import com.swmansion.rnscreens.events.HeaderAttachedEvent
23
22
  import com.swmansion.rnscreens.events.HeaderDetachedEvent
23
+ import kotlin.math.max
24
24
 
25
25
  class ScreenStackHeaderConfig(
26
26
  context: Context,
27
- ) : FabricEnabledHeaderConfigViewGroup(context) {
27
+ private val pointerEventsImpl: ReactPointerEventsView
28
+ ) : FabricEnabledHeaderConfigViewGroup(context), ReactPointerEventsView by pointerEventsImpl {
29
+
30
+ constructor(context: Context): this(context, pointerEventsImpl = PointerEventsBoxNoneImpl())
31
+
28
32
  private val configSubviews = ArrayList<ScreenStackHeaderSubview>(3)
29
33
  val toolbar: CustomToolbar
30
34
  var isHeaderHidden = false // named this way to avoid conflict with platform's isHidden
31
- var isHeaderTranslucent = false // named this way to avoid conflict with platform's isTranslucent
32
- private var headerTopInset: Int? = null
35
+ var isHeaderTranslucent =
36
+ false // named this way to avoid conflict with platform's isTranslucent
33
37
  private var title: String? = null
34
38
  private var titleColor = 0
35
39
  private var titleFontFamily: String? = null
@@ -41,7 +45,9 @@ class ScreenStackHeaderConfig(
41
45
  private var isShadowHidden = false
42
46
  private var isDestroyed = false
43
47
  private var backButtonInCustomView = false
44
- private var isTopInsetEnabled = true
48
+ var isTopInsetEnabled = true
49
+ private set
50
+
45
51
  private var tintColor = 0
46
52
  private var isAttachedToWindow = false
47
53
  private val defaultStartInset: Int
@@ -69,19 +75,76 @@ class ScreenStackHeaderConfig(
69
75
  }
70
76
  }
71
77
 
78
+ var isTitleEmpty: Boolean = false
79
+
80
+ val preferredContentInsetStart
81
+ get() = defaultStartInset
82
+
83
+ val preferredContentInsetEnd
84
+ get() = defaultStartInset
85
+
86
+ val preferredContentInsetStartWithNavigation
87
+ get() =
88
+ // Reset toolbar insets. By default we set symmetric inset for start and end to match iOS
89
+ // implementation where both right and left icons are offset from the edge by default. We also
90
+ // reset startWithNavigation inset which corresponds to the distance between navigation icon and
91
+ // title. If title isn't set we clear that value few lines below to give more space to custom
92
+ // center-mounted views.
93
+ if (isTitleEmpty) {
94
+ 0
95
+ } else {
96
+ defaultStartInsetWithNavigation
97
+ }
98
+
99
+ fun destroy() {
100
+ isDestroyed = true
101
+ }
102
+
103
+ /**
104
+ * Native toolbar should notify the header config component that it has completed its layout.
105
+ */
106
+ fun onNativeToolbarLayout(
107
+ toolbar: Toolbar,
108
+ shouldUpdateShadowStateHint: Boolean,
109
+ ) {
110
+ if (!shouldUpdateShadowStateHint) {
111
+ return
112
+ }
113
+
114
+ val isBackButtonDisplayed = toolbar.navigationIcon != null
115
+
116
+ val contentInsetStartEstimation =
117
+ if (isBackButtonDisplayed) {
118
+ toolbar.currentContentInsetStart + toolbar.paddingStart
119
+ } else {
120
+ max(toolbar.currentContentInsetStart, toolbar.paddingStart)
121
+ }
122
+
123
+ // Assuming that there is nothing to the left of back button here, the content
124
+ // offset we're interested in in ShadowTree is the `left` of the subview left.
125
+ // In case it is not available we fallback to approximation.
126
+ val contentInsetStart =
127
+ configSubviews.firstOrNull { it.type === ScreenStackHeaderSubview.Type.LEFT }?.left
128
+ ?: contentInsetStartEstimation
129
+
130
+ val contentInsetEnd = toolbar.currentContentInsetEnd + toolbar.paddingEnd
131
+
132
+ // Note that implementation of the callee differs between architectures.
133
+ updateHeaderConfigState(
134
+ toolbar.width,
135
+ toolbar.height,
136
+ contentInsetStart,
137
+ contentInsetEnd,
138
+ )
139
+ }
140
+
72
141
  override fun onLayout(
73
142
  changed: Boolean,
74
143
  l: Int,
75
144
  t: Int,
76
145
  r: Int,
77
146
  b: Int,
78
- ) {
79
- // no-op
80
- }
81
-
82
- fun destroy() {
83
- isDestroyed = true
84
- }
147
+ ) = Unit
85
148
 
86
149
  override fun onAttachedToWindow() {
87
150
  super.onAttachedToWindow()
@@ -90,19 +153,6 @@ class ScreenStackHeaderConfig(
90
153
  UIManagerHelper
91
154
  .getEventDispatcherForReactTag(context as ReactContext, id)
92
155
  ?.dispatchEvent(HeaderAttachedEvent(surfaceId, id))
93
- // we want to save the top inset before the status bar can be hidden, which would resolve in
94
- // inset being 0
95
- if (headerTopInset == null) {
96
- headerTopInset =
97
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
98
- rootWindowInsets.getInsets(WindowInsets.Type.systemBars()).top
99
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
100
- rootWindowInsets.systemWindowInsetTop
101
- } else {
102
- // Hacky fallback for old android. Before Marshmallow, the status bar height was always 25
103
- (25 * resources.displayMetrics.density).toInt()
104
- }
105
- }
106
156
  onUpdate()
107
157
  }
108
158
 
@@ -176,32 +226,27 @@ class ScreenStackHeaderConfig(
176
226
  screenFragment?.setToolbar(toolbar)
177
227
  }
178
228
 
179
- if (isTopInsetEnabled) {
180
- headerTopInset.let {
181
- toolbar.setPadding(0, it ?: 0, 0, 0)
182
- }
183
- } else {
184
- if (toolbar.paddingTop > 0) {
185
- toolbar.setPadding(0, 0, 0, 0)
186
- }
187
- }
188
-
189
229
  activity.setSupportActionBar(toolbar)
190
230
  // non-null toolbar is set in the line above and it is used here
191
231
  val actionBar = requireNotNull(activity.supportActionBar)
192
232
 
233
+ // hide back button
234
+ actionBar.setDisplayHomeAsUpEnabled(
235
+ screenFragment?.canNavigateBack() == true && !isBackButtonHidden,
236
+ )
237
+
238
+ // title
239
+ actionBar.title = title
240
+ if (TextUtils.isEmpty(title)) {
241
+ isTitleEmpty = true
242
+ }
243
+
193
244
  // Reset toolbar insets. By default we set symmetric inset for start and end to match iOS
194
245
  // implementation where both right and left icons are offset from the edge by default. We also
195
246
  // reset startWithNavigation inset which corresponds to the distance between navigation icon and
196
247
  // title. If title isn't set we clear that value few lines below to give more space to custom
197
248
  // center-mounted views.
198
- toolbar.contentInsetStartWithNavigation = defaultStartInsetWithNavigation
199
- toolbar.setContentInsetsRelative(defaultStartInset, defaultStartInset)
200
-
201
- // hide back button
202
- actionBar.setDisplayHomeAsUpEnabled(
203
- screenFragment?.canNavigateBack() == true && !isBackButtonHidden,
204
- )
249
+ toolbar.updateContentInsets()
205
250
 
206
251
  // when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites
207
252
  // navigation click listener. The default behavior set in the wrapper is to call into
@@ -214,15 +259,6 @@ class ScreenStackHeaderConfig(
214
259
  // translucent
215
260
  screenFragment?.setToolbarTranslucent(isHeaderTranslucent)
216
261
 
217
- // title
218
- actionBar.title = title
219
- if (TextUtils.isEmpty(title)) {
220
- // if title is empty we set start navigation inset to 0 to give more space to custom rendered
221
- // views. When it is set to default it'd take up additional distance from the back button
222
- // which would impact the position of custom header views rendered at the center.
223
- toolbar.contentInsetStartWithNavigation = 0
224
- }
225
-
226
262
  val titleTextView = findTitleTextViewInToolbar(toolbar)
227
263
  if (titleColor != 0) {
228
264
  toolbar.setTitleTextColor(titleColor)
@@ -250,7 +286,8 @@ class ScreenStackHeaderConfig(
250
286
 
251
287
  // color
252
288
  if (tintColor != 0) {
253
- toolbar.navigationIcon?.colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP)
289
+ toolbar.navigationIcon?.colorFilter =
290
+ PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP)
254
291
  }
255
292
 
256
293
  // subviews
@@ -288,12 +325,14 @@ class ScreenStackHeaderConfig(
288
325
  toolbar.title = null
289
326
  params.gravity = Gravity.START
290
327
  }
328
+
291
329
  ScreenStackHeaderSubview.Type.RIGHT -> params.gravity = Gravity.END
292
330
  ScreenStackHeaderSubview.Type.CENTER -> {
293
331
  params.width = LayoutParams.MATCH_PARENT
294
332
  params.gravity = Gravity.CENTER_HORIZONTAL
295
333
  toolbar.title = null
296
334
  }
335
+
297
336
  else -> {}
298
337
  }
299
338
  view.layoutParams = params
@@ -402,7 +441,8 @@ class ScreenStackHeaderConfig(
402
441
 
403
442
  init {
404
443
  visibility = GONE
405
- toolbar = if (BuildConfig.DEBUG) DebugMenuToolbar(context, this) else CustomToolbar(context, this)
444
+ toolbar =
445
+ if (BuildConfig.DEBUG) DebugMenuToolbar(context, this) else CustomToolbar(context, this)
406
446
  defaultStartInset = toolbar.contentInsetStart
407
447
  defaultStartInsetWithNavigation = toolbar.contentInsetStartWithNavigation
408
448