react-native-screens 4.6.0 → 4.7.0-beta.1

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.
@@ -5,6 +5,7 @@ import android.content.pm.ActivityInfo
5
5
  import android.graphics.Paint
6
6
  import android.os.Parcelable
7
7
  import android.util.SparseArray
8
+ import android.view.MotionEvent
8
9
  import android.view.View
9
10
  import android.view.ViewGroup
10
11
  import android.view.WindowManager
@@ -17,6 +18,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
17
18
  import com.facebook.react.bridge.GuardedRunnable
18
19
  import com.facebook.react.bridge.ReactContext
19
20
  import com.facebook.react.uimanager.PixelUtil
21
+ import com.facebook.react.uimanager.ThemedReactContext
20
22
  import com.facebook.react.uimanager.UIManagerHelper
21
23
  import com.facebook.react.uimanager.UIManagerModule
22
24
  import com.facebook.react.uimanager.events.EventDispatcher
@@ -24,13 +26,16 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
24
26
  import com.google.android.material.shape.CornerFamily
25
27
  import com.google.android.material.shape.MaterialShapeDrawable
26
28
  import com.google.android.material.shape.ShapeAppearanceModel
29
+ import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
30
+ import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
27
31
  import com.swmansion.rnscreens.events.HeaderHeightChangeEvent
28
32
  import com.swmansion.rnscreens.events.SheetDetentChangedEvent
33
+ import com.swmansion.rnscreens.ext.parentAsViewGroup
29
34
  import java.lang.ref.WeakReference
30
35
 
31
36
  @SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated.
32
37
  class Screen(
33
- val reactContext: ReactContext,
38
+ val reactContext: ThemedReactContext,
34
39
  ) : FabricEnabledViewGroup(reactContext),
35
40
  ScreenContentWrapper.OnLayoutCallback {
36
41
  val fragment: Fragment?
@@ -81,6 +86,13 @@ class Screen(
81
86
  var sheetClosesOnTouchOutside = true
82
87
  var sheetElevation: Float = 24F
83
88
 
89
+ /**
90
+ * When using form sheet presentation we want to delay enter transition **on Paper** in order
91
+ * to wait for initial layout from React, otherwise the animator-based animation will look
92
+ * glitchy. *This is not needed on Fabric*.
93
+ */
94
+ var shouldTriggerPostponedTransitionAfterLayout = false
95
+
84
96
  var footer: ScreenFooter? = null
85
97
  set(value) {
86
98
  if (value == null && field != null) {
@@ -110,7 +122,7 @@ class Screen(
110
122
  * `fitToContents` for formSheets, as this is first entry point where we can acquire
111
123
  * height of our content.
112
124
  */
113
- override fun onLayoutCallback(
125
+ override fun onContentWrapperLayout(
114
126
  changed: Boolean,
115
127
  left: Int,
116
128
  top: Int,
@@ -119,12 +131,23 @@ class Screen(
119
131
  ) {
120
132
  val height = bottom - top
121
133
 
122
- if (sheetDetents.count() == 1 && sheetDetents.first() == SHEET_FIT_TO_CONTENTS) {
134
+ if (isSheetFitToContents()) {
123
135
  sheetBehavior?.let {
124
136
  if (it.maxHeight != height) {
125
137
  it.maxHeight = height
126
138
  }
127
139
  }
140
+
141
+ if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
142
+ // On old architecture we delay enter transition in order to wait for initial frame.
143
+ shouldTriggerPostponedTransitionAfterLayout = true
144
+ val parent = parentAsViewGroup()
145
+ if (parent != null && !parent.isInLayout) {
146
+ // There are reported cases (irreproducible) when Screen is not laid out after
147
+ // maxHeight is set on behaviour.
148
+ parent.requestLayout()
149
+ }
150
+ }
128
151
  }
129
152
  }
130
153
 
@@ -162,6 +185,17 @@ class Screen(
162
185
 
163
186
  footer?.onParentLayout(changed, l, t, r, b, container!!.height)
164
187
  notifyHeaderHeightChange(t)
188
+
189
+ if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
190
+ maybeTriggerPostponedTransition()
191
+ }
192
+ }
193
+ }
194
+
195
+ private fun maybeTriggerPostponedTransition() {
196
+ if (shouldTriggerPostponedTransitionAfterLayout) {
197
+ shouldTriggerPostponedTransitionAfterLayout = false
198
+ fragment?.startPostponedEnterTransition()
165
199
  }
166
200
  }
167
201
 
@@ -377,6 +411,28 @@ class Screen(
377
411
  }
378
412
  }
379
413
 
414
+ fun endRemovalTransition() {
415
+ if (!isBeingRemoved) {
416
+ return
417
+ }
418
+ isBeingRemoved = false
419
+ endTransitionRecursive(this)
420
+ }
421
+
422
+ private fun endTransitionRecursive(parent: ViewGroup) {
423
+ parent.children.forEach { childView ->
424
+ parent.endViewTransition(childView)
425
+
426
+ if (childView is ScreenStackHeaderConfig) {
427
+ endTransitionRecursive(childView.toolbar)
428
+ }
429
+
430
+ if (childView is ViewGroup) {
431
+ endTransitionRecursive(childView)
432
+ }
433
+ }
434
+ }
435
+
380
436
  private fun startTransitionRecursive(parent: ViewGroup?) {
381
437
  parent?.let {
382
438
  for (i in 0 until it.childCount) {
@@ -407,6 +463,17 @@ class Screen(
407
463
  }
408
464
  }
409
465
 
466
+ // We do not want to perform any action, therefore do not need to override the associated method.
467
+ @SuppressLint("ClickableViewAccessibility")
468
+ override fun onTouchEvent(event: MotionEvent?): Boolean =
469
+ if (usesFormSheetPresentation()) {
470
+ // If we're a form sheet we want to consume the gestures to prevent
471
+ // DimmingView's callback from triggering when clicking on the sheet itself.
472
+ true
473
+ } else {
474
+ super.onTouchEvent(event)
475
+ }
476
+
410
477
  private fun notifyHeaderHeightChange(headerHeight: Int) {
411
478
  val screenContext = context as ReactContext
412
479
  val surfaceId = UIManagerHelper.getSurfaceId(screenContext)
@@ -17,7 +17,7 @@ class ScreenContentWrapper(
17
17
  internal var delegate: OnLayoutCallback? = null
18
18
 
19
19
  interface OnLayoutCallback {
20
- fun onLayoutCallback(
20
+ fun onContentWrapperLayout(
21
21
  changed: Boolean,
22
22
  left: Int,
23
23
  top: Int,
@@ -33,6 +33,6 @@ class ScreenContentWrapper(
33
33
  right: Int,
34
34
  bottom: Int,
35
35
  ) {
36
- delegate?.onLayoutCallback(changed, left, top, right, bottom)
36
+ delegate?.onContentWrapperLayout(changed, left, top, right, bottom)
37
37
  }
38
38
  }
@@ -15,7 +15,6 @@ import com.facebook.react.bridge.UiThreadUtil
15
15
  import com.facebook.react.uimanager.UIManagerHelper
16
16
  import com.facebook.react.uimanager.events.Event
17
17
  import com.facebook.react.uimanager.events.EventDispatcher
18
- import com.swmansion.rnscreens.bottomsheet.DimmingFragment
19
18
  import com.swmansion.rnscreens.events.HeaderBackButtonClickedEvent
20
19
  import com.swmansion.rnscreens.events.ScreenAppearEvent
21
20
  import com.swmansion.rnscreens.events.ScreenDisappearEvent
@@ -290,12 +289,7 @@ open class ScreenFragment :
290
289
  // since we subscribe to parent's animation start/end and dispatch events in child from there
291
290
  // check for `isTransitioning` should be enough since the child's animation should take only
292
291
  // 20ms due to always being `StackAnimation.NONE` when nested stack being pushed
293
- val parent =
294
- if (parentFragment is DimmingFragment) {
295
- parentFragment?.parentFragment
296
- } else {
297
- parentFragment
298
- }
292
+ val parent = parentFragment
299
293
  if (parent == null || (parent is ScreenFragment && !parent.isTransitioning)) {
300
294
  // onViewAnimationStart/End is triggered from View#onAnimationStart/End method of the fragment's root
301
295
  // view. We override an appropriate method of the StackFragment's
@@ -7,11 +7,10 @@ import android.view.View
7
7
  import com.facebook.react.bridge.ReactContext
8
8
  import com.facebook.react.uimanager.UIManagerHelper
9
9
  import com.swmansion.rnscreens.Screen.StackAnimation
10
- import com.swmansion.rnscreens.bottomsheet.DimmingFragment
10
+ import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
11
11
  import com.swmansion.rnscreens.events.StackFinishTransitioningEvent
12
12
  import java.util.Collections
13
13
  import kotlin.collections.ArrayList
14
- import kotlin.collections.HashSet
15
14
 
16
15
  class ScreenStack(
17
16
  context: Context?,
@@ -50,7 +49,7 @@ class ScreenStack(
50
49
 
51
50
  override fun adapt(screen: Screen): ScreenStackFragmentWrapper =
52
51
  when (screen.stackPresentation) {
53
- Screen.StackPresentation.FORM_SHEET -> DimmingFragment(ScreenStackFragment(screen))
52
+ Screen.StackPresentation.FORM_SHEET -> ScreenStackFragment(screen)
54
53
  else -> ScreenStackFragment(screen)
55
54
  }
56
55
 
@@ -242,7 +241,6 @@ class ScreenStack(
242
241
  }
243
242
  }
244
243
  }
245
-
246
244
  // animation logic end
247
245
  goingForward = shouldUseOpenAnimation
248
246
 
@@ -302,6 +300,12 @@ class ScreenStack(
302
300
  }
303
301
  }
304
302
  } else if (newTop != null && !newTop.fragment.isAdded) {
303
+ if (!BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && newTop.screen.isSheetFitToContents()) {
304
+ // On old architecture the content wrapper might not have received its frame yet,
305
+ // which is required to determine height of the sheet after animation. Therefore
306
+ // we delay the transition and trigger it after views receive the layout.
307
+ newTop.fragment.postponeEnterTransition()
308
+ }
305
309
  it.add(id, newTop.fragment)
306
310
  }
307
311
  topScreenWrapper = newTop as? ScreenStackFragmentWrapper
@@ -1,5 +1,8 @@
1
1
  package com.swmansion.rnscreens
2
2
 
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorSet
5
+ import android.animation.ValueAnimator
3
6
  import android.annotation.SuppressLint
4
7
  import android.content.Context
5
8
  import android.graphics.Color
@@ -16,17 +19,18 @@ import android.view.WindowInsets
16
19
  import android.view.WindowManager
17
20
  import android.view.animation.Animation
18
21
  import android.view.animation.AnimationSet
19
- import android.view.animation.AnimationUtils
20
22
  import android.view.animation.Transformation
21
23
  import android.view.inputmethod.InputMethodManager
22
24
  import android.widget.LinearLayout
23
25
  import androidx.annotation.RequiresApi
24
26
  import androidx.appcompat.widget.Toolbar
25
27
  import androidx.coordinatorlayout.widget.CoordinatorLayout
28
+ import androidx.core.animation.addListener
26
29
  import androidx.core.view.WindowInsetsCompat
27
30
  import com.facebook.react.uimanager.PixelUtil
28
31
  import com.facebook.react.uimanager.PointerEvents
29
32
  import com.facebook.react.uimanager.ReactPointerEventsView
33
+ import com.facebook.react.uimanager.UIManagerHelper
30
34
  import com.google.android.material.appbar.AppBarLayout
31
35
  import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
32
36
  import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -34,13 +38,18 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCa
34
38
  import com.google.android.material.shape.CornerFamily
35
39
  import com.google.android.material.shape.MaterialShapeDrawable
36
40
  import com.google.android.material.shape.ShapeAppearanceModel
41
+ import com.swmansion.rnscreens.bottomsheet.DimmingViewManager
42
+ import com.swmansion.rnscreens.bottomsheet.SheetDelegate
37
43
  import com.swmansion.rnscreens.bottomsheet.SheetUtils
38
44
  import com.swmansion.rnscreens.bottomsheet.isSheetFitToContents
39
45
  import com.swmansion.rnscreens.bottomsheet.useSingleDetent
40
46
  import com.swmansion.rnscreens.bottomsheet.useThreeDetents
41
47
  import com.swmansion.rnscreens.bottomsheet.useTwoDetents
42
48
  import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
49
+ import com.swmansion.rnscreens.events.ScreenDismissedEvent
50
+ import com.swmansion.rnscreens.events.ScreenEventDelegate
43
51
  import com.swmansion.rnscreens.ext.recycle
52
+ import com.swmansion.rnscreens.transition.ExternalBoundaryValuesEvaluator
44
53
  import com.swmansion.rnscreens.utils.DeviceUtils
45
54
 
46
55
  sealed class KeyboardState
@@ -76,6 +85,13 @@ class ScreenStackFragment :
76
85
  return container
77
86
  }
78
87
 
88
+ private val dimmingDelegate =
89
+ lazy(LazyThreadSafetyMode.NONE) {
90
+ DimmingViewManager(screen.reactContext, screen)
91
+ }
92
+
93
+ private var sheetDelegate: SheetDelegate? = null
94
+
79
95
  @SuppressLint("ValidFragment")
80
96
  constructor(screenView: Screen) : super(screenView)
81
97
 
@@ -131,7 +147,12 @@ class ScreenStackFragment :
131
147
 
132
148
  override fun onViewAnimationEnd() {
133
149
  super.onViewAnimationEnd()
150
+
151
+ // Rely on guards inside the callee to detect whether this was indeed appear transition.
134
152
  notifyViewAppearTransitionEnd()
153
+
154
+ // Rely on guards inside the callee to detect whether this was indeed removal transition.
155
+ screen.endRemovalTransition()
135
156
  }
136
157
 
137
158
  private fun notifyViewAppearTransitionEnd() {
@@ -176,7 +197,7 @@ class ScreenStackFragment :
176
197
  }
177
198
 
178
199
  if (newState == BottomSheetBehavior.STATE_HIDDEN) {
179
- nativeDismissalObserver?.onNativeDismiss(this@ScreenStackFragment)
200
+ dismissSelf()
180
201
  }
181
202
  }
182
203
 
@@ -186,18 +207,17 @@ class ScreenStackFragment :
186
207
  ) = Unit
187
208
  }
188
209
 
189
- override fun onCreateAnimation(
190
- transit: Int,
191
- enter: Boolean,
192
- nextAnim: Int,
193
- ): Animation? {
194
- if (screen.stackPresentation != Screen.StackPresentation.FORM_SHEET) {
195
- return null
196
- }
197
- return if (enter) {
198
- AnimationUtils.loadAnimation(context, R.anim.rns_slide_in_from_bottom)
199
- } else {
200
- AnimationUtils.loadAnimation(context, R.anim.rns_slide_out_to_bottom)
210
+ /**
211
+ * Currently this method dispatches event to JS where state is recomputed and fragment
212
+ * gets removed in the result of incoming state update.
213
+ */
214
+ internal fun dismissSelf() {
215
+ if (!this.isRemoving || !this.isDetached) {
216
+ val reactContext = screen.reactContext
217
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
218
+ UIManagerHelper
219
+ .getEventDispatcherForReactTag(reactContext, screen.id)
220
+ ?.dispatchEvent(ScreenDismissedEvent(surfaceId, screen.id))
201
221
  }
202
222
  }
203
223
 
@@ -205,6 +225,10 @@ class ScreenStackFragment :
205
225
  screen.onSheetCornerRadiusChange()
206
226
  }
207
227
 
228
+ override fun onCreate(savedInstanceState: Bundle?) {
229
+ super.onCreate(savedInstanceState)
230
+ }
231
+
208
232
  override fun onCreateView(
209
233
  inflater: LayoutInflater,
210
234
  container: ViewGroup?,
@@ -262,6 +286,90 @@ class ScreenStackFragment :
262
286
  return coordinatorLayout
263
287
  }
264
288
 
289
+ override fun onViewCreated(
290
+ view: View,
291
+ savedInstanceState: Bundle?,
292
+ ) {
293
+ super.onViewCreated(view, savedInstanceState)
294
+
295
+ if (!screen.usesFormSheetPresentation()) {
296
+ return
297
+ }
298
+
299
+ sheetDelegate = SheetDelegate(screen)
300
+
301
+ assert(view == coordinatorLayout)
302
+ dimmingDelegate.value.onViewHierarchyCreated(screen, coordinatorLayout)
303
+ dimmingDelegate.value.onBehaviourAttached(screen, screen.sheetBehavior!!)
304
+
305
+ val container = screen.container!!
306
+ coordinatorLayout.measure(
307
+ View.MeasureSpec.makeMeasureSpec(container.width, View.MeasureSpec.EXACTLY),
308
+ View.MeasureSpec.makeMeasureSpec(container.height, View.MeasureSpec.EXACTLY),
309
+ )
310
+ coordinatorLayout.layout(0, 0, container.width, container.height)
311
+ }
312
+
313
+ override fun onCreateAnimation(
314
+ transit: Int,
315
+ enter: Boolean,
316
+ nextAnim: Int,
317
+ ): Animation? {
318
+ // Ensure onCreateAnimator is called
319
+ return null
320
+ }
321
+
322
+ override fun onCreateAnimator(
323
+ transit: Int,
324
+ enter: Boolean,
325
+ nextAnim: Int,
326
+ ): Animator? {
327
+ if (!screen.usesFormSheetPresentation()) {
328
+ // Use animation defined while defining transaction in screen stack
329
+ return null
330
+ }
331
+
332
+ val animatorSet = AnimatorSet()
333
+
334
+ if (enter) {
335
+ val alphaAnimator =
336
+ ValueAnimator.ofFloat(0f, dimmingDelegate.value.maxAlpha).apply {
337
+ addUpdateListener { anim ->
338
+ val animatedValue = anim.animatedValue as? Float
339
+ animatedValue?.let { dimmingDelegate.value.dimmingView.alpha = it }
340
+ }
341
+ }
342
+ val startValueCallback = { initialStartValue: Number? -> screen.height.toFloat() }
343
+ val evaluator = ExternalBoundaryValuesEvaluator(startValueCallback, { 0f })
344
+ val slideAnimator =
345
+ ValueAnimator.ofObject(evaluator, screen.height.toFloat(), 0f).apply {
346
+ addUpdateListener { anim ->
347
+ val animatedValue = anim.animatedValue as? Float
348
+ animatedValue?.let { screen.translationY = it }
349
+ }
350
+ }
351
+ animatorSet.play(alphaAnimator).with(slideAnimator)
352
+ } else {
353
+ val alphaAnimator =
354
+ ValueAnimator.ofFloat(dimmingDelegate.value.dimmingView.alpha, 0f).apply {
355
+ addUpdateListener { anim ->
356
+ val animatedValue = anim.animatedValue as? Float
357
+ animatedValue?.let { dimmingDelegate.value.dimmingView.alpha = it }
358
+ }
359
+ }
360
+ val slideAnimator =
361
+ ValueAnimator.ofFloat(0f, (coordinatorLayout.bottom - screen.top).toFloat()).apply {
362
+ addUpdateListener { anim ->
363
+ val animatedValue = anim.animatedValue as? Float
364
+ animatedValue?.let { screen.translationY = it }
365
+ }
366
+ }
367
+ animatorSet.play(alphaAnimator).with(slideAnimator)
368
+ }
369
+ animatorSet.addListener(ScreenEventDelegate(this))
370
+ return animatorSet
371
+ }
372
+
265
373
  /**
266
374
  * This method might return slightly different values depending on code path,
267
375
  * but during testing I've found this effect negligible. For practical purposes
@@ -348,7 +456,10 @@ class ScreenStackFragment :
348
456
  behavior.apply {
349
457
  val height =
350
458
  if (screen.isSheetFitToContents()) {
351
- screen.contentWrapper.get()?.height
459
+ screen.contentWrapper
460
+ .get()
461
+ ?.height
462
+ .takeIf { screen.contentWrapper.get()?.isLaidOut == true }
352
463
  } else {
353
464
  (screen.sheetDetents.first() * containerHeight).toInt()
354
465
  }
@@ -456,10 +567,12 @@ class ScreenStackFragment :
456
567
  }
457
568
  }
458
569
 
570
+ private fun createBottomSheetBehaviour(): BottomSheetBehavior<Screen> = BottomSheetBehavior<Screen>()
571
+
459
572
  // In general it would be great to create BottomSheetBehaviour only via this method as it runs some
460
573
  // side effects.
461
- internal fun createAndConfigureBottomSheetBehaviour(): BottomSheetBehavior<Screen> =
462
- configureBottomSheetBehaviour(BottomSheetBehavior<Screen>())
574
+ private fun createAndConfigureBottomSheetBehaviour(): BottomSheetBehavior<Screen> =
575
+ configureBottomSheetBehaviour(createBottomSheetBehaviour())
463
576
 
464
577
  private fun attachShapeToScreen(screen: Screen) {
465
578
  val cornerSize = PixelUtil.toPixelFromDIP(screen.sheetCornerRadius)
@@ -12,7 +12,9 @@ internal fun <T : View> BottomSheetBehavior<T>.useSingleDetent(
12
12
  if (forceExpandedState) {
13
13
  this.state = BottomSheetBehavior.STATE_EXPANDED
14
14
  }
15
- height?.let { maxHeight = height }
15
+ height?.let {
16
+ maxHeight = height
17
+ }
16
18
  return this
17
19
  }
18
20
 
@@ -11,7 +11,7 @@ import com.facebook.react.uimanager.ReactPointerEventsView
11
11
  import com.swmansion.rnscreens.ext.equalWithRespectToEps
12
12
 
13
13
  /**
14
- * Serves as dimming view that can be used as background for some view that not fully fills
14
+ * Serves as dimming view that can be used as background for some view that does not fully fill
15
15
  * the viewport.
16
16
  *
17
17
  * This dimming view has one more additional feature: it blocks gestures if its alpha > 0.
@@ -40,6 +40,8 @@ class DimmingView(
40
40
  b: Int,
41
41
  ) = Unit
42
42
 
43
+ // We do not want to have any action defined here. We just want listeners notified that the click happened.
44
+ @SuppressLint("ClickableViewAccessibility")
43
45
  override fun onTouchEvent(event: MotionEvent?): Boolean {
44
46
  if (blockGestures) {
45
47
  callOnClick()
@@ -0,0 +1,165 @@
1
+ package com.swmansion.rnscreens.bottomsheet
2
+
3
+ import android.animation.ValueAnimator
4
+ import android.view.View
5
+ import android.view.ViewGroup
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
8
+ import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
9
+ import com.swmansion.rnscreens.Screen
10
+ import com.swmansion.rnscreens.ScreenStackFragment
11
+
12
+ /**
13
+ * Provides bulk of necessary logic for the dimming view accompanying the formSheet.
14
+ */
15
+ class DimmingViewManager(
16
+ val reactContext: ThemedReactContext,
17
+ screen: Screen,
18
+ ) {
19
+ internal val dimmingView: DimmingView = createDimmingView(screen)
20
+ internal val maxAlpha: Float = 0.3f
21
+ private var dimmingViewCallback: BottomSheetCallback? = null
22
+
23
+ /**
24
+ * Should be called when hosting fragment has its view hierarchy created.
25
+ */
26
+ fun onViewHierarchyCreated(
27
+ screen: Screen,
28
+ root: ViewGroup,
29
+ ) {
30
+ root.addView(dimmingView, 0)
31
+ if (screen.sheetInitialDetentIndex <= screen.sheetLargestUndimmedDetentIndex) {
32
+ dimmingView.alpha = 0.0f
33
+ } else {
34
+ dimmingView.alpha = maxAlpha
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Should be called after screen of hosting fragment has its behaviour attached.
40
+ */
41
+ fun onBehaviourAttached(
42
+ screen: Screen,
43
+ behavior: BottomSheetBehavior<Screen>,
44
+ ) {
45
+ behavior.addBottomSheetCallback(requireBottomSheetCallback(screen))
46
+ }
47
+
48
+ /**
49
+ * This bottom sheet callback is responsible for animating alpha of the dimming view.
50
+ */
51
+ private class AnimateDimmingViewCallback(
52
+ val screen: Screen,
53
+ val viewToAnimate: View,
54
+ val maxAlpha: Float,
55
+ ) : BottomSheetCallback() {
56
+ // largest *slide offset* that is yet undimmed
57
+ private var largestUndimmedOffset: Float =
58
+ computeOffsetFromDetentIndex(screen.sheetLargestUndimmedDetentIndex)
59
+
60
+ // first *slide offset* that should be fully dimmed
61
+ private var firstDimmedOffset: Float =
62
+ computeOffsetFromDetentIndex(
63
+ (screen.sheetLargestUndimmedDetentIndex + 1).coerceIn(
64
+ 0,
65
+ screen.sheetDetents.count() - 1,
66
+ ),
67
+ )
68
+
69
+ // interval that we interpolate the alpha value over
70
+ private var intervalLength = firstDimmedOffset - largestUndimmedOffset
71
+ private val animator =
72
+ ValueAnimator.ofFloat(0F, maxAlpha).apply {
73
+ duration = 1 // Driven manually
74
+ addUpdateListener {
75
+ viewToAnimate.alpha = it.animatedValue as Float
76
+ }
77
+ }
78
+
79
+ override fun onStateChanged(
80
+ bottomSheet: View,
81
+ newState: Int,
82
+ ) {
83
+ if (newState == BottomSheetBehavior.STATE_DRAGGING || newState == BottomSheetBehavior.STATE_SETTLING) {
84
+ largestUndimmedOffset =
85
+ computeOffsetFromDetentIndex(screen.sheetLargestUndimmedDetentIndex)
86
+ firstDimmedOffset =
87
+ computeOffsetFromDetentIndex(
88
+ (screen.sheetLargestUndimmedDetentIndex + 1).coerceIn(
89
+ 0,
90
+ screen.sheetDetents.count() - 1,
91
+ ),
92
+ )
93
+ assert(firstDimmedOffset >= largestUndimmedOffset) {
94
+ "[RNScreens] Invariant violation: firstDimmedOffset ($firstDimmedOffset) < largestDimmedOffset ($largestUndimmedOffset)"
95
+ }
96
+ intervalLength = firstDimmedOffset - largestUndimmedOffset
97
+ }
98
+ }
99
+
100
+ override fun onSlide(
101
+ bottomSheet: View,
102
+ slideOffset: Float,
103
+ ) {
104
+ if (largestUndimmedOffset < slideOffset && slideOffset < firstDimmedOffset) {
105
+ val fraction = (slideOffset - largestUndimmedOffset) / intervalLength
106
+ animator.setCurrentFraction(fraction)
107
+ }
108
+ }
109
+
110
+ /**
111
+ * This method does compute slide offset (see [BottomSheetCallback.onSlide] docs) for detent
112
+ * at given index in the detents array.
113
+ */
114
+ private fun computeOffsetFromDetentIndex(index: Int): Float =
115
+ when (screen.sheetDetents.size) {
116
+ 1 -> // Only 1 detent present in detents array
117
+ when (index) {
118
+ -1 -> -1F // hidden
119
+ 0 -> 1F // fully expanded
120
+ else -> -1F // unexpected, default
121
+ }
122
+
123
+ 2 ->
124
+ when (index) {
125
+ -1 -> -1F // hidden
126
+ 0 -> 0F // collapsed
127
+ 1 -> 1F // expanded
128
+ else -> -1F
129
+ }
130
+
131
+ 3 ->
132
+ when (index) {
133
+ -1 -> -1F // hidden
134
+ 0 -> 0F // collapsed
135
+ 1 -> screen.sheetBehavior!!.halfExpandedRatio // half
136
+ 2 -> 1F // expanded
137
+ else -> -1F
138
+ }
139
+
140
+ else -> -1F
141
+ }
142
+ }
143
+
144
+ private fun createDimmingView(screen: Screen): DimmingView =
145
+ DimmingView(reactContext, maxAlpha).apply {
146
+ // These do not guarantee fullscreen width & height, TODO: find a way to guarantee that
147
+ layoutParams =
148
+ ViewGroup.LayoutParams(
149
+ ViewGroup.LayoutParams.MATCH_PARENT,
150
+ ViewGroup.LayoutParams.MATCH_PARENT,
151
+ )
152
+ setOnClickListener {
153
+ if (screen.sheetClosesOnTouchOutside) {
154
+ (screen.fragment as ScreenStackFragment).dismissSelf()
155
+ }
156
+ }
157
+ }
158
+
159
+ private fun requireBottomSheetCallback(screen: Screen): BottomSheetCallback {
160
+ if (dimmingViewCallback == null) {
161
+ dimmingViewCallback = AnimateDimmingViewCallback(screen, dimmingView, maxAlpha)
162
+ }
163
+ return dimmingViewCallback!!
164
+ }
165
+ }
@@ -0,0 +1,119 @@
1
+ package com.swmansion.rnscreens.bottomsheet
2
+
3
+ import android.view.View
4
+ import androidx.core.graphics.Insets
5
+ import androidx.core.view.OnApplyWindowInsetsListener
6
+ import androidx.core.view.WindowInsetsCompat
7
+ import androidx.lifecycle.Lifecycle
8
+ import androidx.lifecycle.LifecycleEventObserver
9
+ import androidx.lifecycle.LifecycleOwner
10
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
11
+ import com.swmansion.rnscreens.InsetsObserverProxy
12
+ import com.swmansion.rnscreens.KeyboardDidHide
13
+ import com.swmansion.rnscreens.KeyboardNotVisible
14
+ import com.swmansion.rnscreens.KeyboardState
15
+ import com.swmansion.rnscreens.KeyboardVisible
16
+ import com.swmansion.rnscreens.Screen
17
+ import com.swmansion.rnscreens.ScreenStackFragment
18
+
19
+ class SheetDelegate(
20
+ val screen: Screen,
21
+ ) : LifecycleEventObserver,
22
+ OnApplyWindowInsetsListener {
23
+ private var isKeyboardVisible: Boolean = false
24
+ private var keyboardState: KeyboardState = KeyboardNotVisible
25
+
26
+ private val sheetBehavior: BottomSheetBehavior<Screen>?
27
+ get() = screen.sheetBehavior
28
+
29
+ private val stackFragment: ScreenStackFragment
30
+ get() = screen.fragment as ScreenStackFragment
31
+
32
+ private fun requireDecorView(): View =
33
+ checkNotNull(screen.reactContext.currentActivity) { "[RNScreens] Attempt to access activity on detached context" }
34
+ .window.decorView
35
+
36
+ init {
37
+ assert(screen.fragment is ScreenStackFragment) { "[RNScreens] Sheets are supported only in native stack" }
38
+ screen.fragment!!.lifecycle.addObserver(this)
39
+ }
40
+
41
+ // LifecycleEventObserver
42
+ override fun onStateChanged(
43
+ source: LifecycleOwner,
44
+ event: Lifecycle.Event,
45
+ ) {
46
+ when (event) {
47
+ Lifecycle.Event.ON_START -> handleHostFragmentOnStart()
48
+ Lifecycle.Event.ON_RESUME -> handleHostFragmentOnResume()
49
+ Lifecycle.Event.ON_PAUSE -> handleHostFragmentOnPause()
50
+ else -> Unit
51
+ }
52
+ }
53
+
54
+ private fun handleHostFragmentOnStart() {
55
+ InsetsObserverProxy.registerOnView(requireDecorView())
56
+ }
57
+
58
+ private fun handleHostFragmentOnResume() {
59
+ InsetsObserverProxy.addOnApplyWindowInsetsListener(this)
60
+ }
61
+
62
+ private fun handleHostFragmentOnPause() {
63
+ InsetsObserverProxy.removeOnApplyWindowInsetsListener(this)
64
+ }
65
+
66
+ // This is listener function, not the view's.
67
+ override fun onApplyWindowInsets(
68
+ v: View,
69
+ insets: WindowInsetsCompat,
70
+ ): WindowInsetsCompat {
71
+ val isImeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
72
+ val imeInset = insets.getInsets(WindowInsetsCompat.Type.ime())
73
+
74
+ if (isImeVisible) {
75
+ isKeyboardVisible = true
76
+ keyboardState = KeyboardVisible(imeInset.bottom)
77
+ sheetBehavior?.let {
78
+ stackFragment.configureBottomSheetBehaviour(it, keyboardState)
79
+ }
80
+
81
+ val prevInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
82
+ return WindowInsetsCompat
83
+ .Builder(insets)
84
+ .setInsets(
85
+ WindowInsetsCompat.Type.navigationBars(),
86
+ Insets.of(
87
+ prevInsets.left,
88
+ prevInsets.top,
89
+ prevInsets.right,
90
+ 0,
91
+ ),
92
+ ).build()
93
+ } else {
94
+ sheetBehavior?.let {
95
+ if (isKeyboardVisible) {
96
+ stackFragment.configureBottomSheetBehaviour(it, KeyboardDidHide)
97
+ } else if (keyboardState != KeyboardNotVisible) {
98
+ stackFragment.configureBottomSheetBehaviour(it, KeyboardNotVisible)
99
+ } else {
100
+ }
101
+ }
102
+
103
+ keyboardState = KeyboardNotVisible
104
+ isKeyboardVisible = false
105
+ }
106
+
107
+ val prevInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
108
+ return WindowInsetsCompat
109
+ .Builder(insets)
110
+ .setInsets(
111
+ WindowInsetsCompat.Type.navigationBars(),
112
+ Insets.of(prevInsets.left, prevInsets.top, prevInsets.right, 0),
113
+ ).build()
114
+ }
115
+
116
+ companion object {
117
+ const val TAG = "SheetDelegate"
118
+ }
119
+ }
@@ -0,0 +1,48 @@
1
+ package com.swmansion.rnscreens.events
2
+
3
+ import android.animation.Animator
4
+ import com.swmansion.rnscreens.ScreenFragmentWrapper
5
+
6
+ class ScreenEventDelegate(
7
+ private val wrapper: ScreenFragmentWrapper,
8
+ ) : Animator.AnimatorListener {
9
+ private var currentState: LifecycleState = LifecycleState.INITIALIZED
10
+
11
+ private fun progressState() {
12
+ currentState =
13
+ when (currentState) {
14
+ LifecycleState.INITIALIZED -> LifecycleState.START_DISPATCHED
15
+ LifecycleState.START_DISPATCHED -> LifecycleState.END_DISPATCHED
16
+ LifecycleState.END_DISPATCHED -> LifecycleState.END_DISPATCHED
17
+ }
18
+ }
19
+
20
+ override fun onAnimationStart(animation: Animator) {
21
+ if (currentState === LifecycleState.INITIALIZED) {
22
+ progressState()
23
+ wrapper.onViewAnimationStart()
24
+ }
25
+ }
26
+
27
+ override fun onAnimationEnd(animation: Animator) {
28
+ if (currentState === LifecycleState.START_DISPATCHED) {
29
+ progressState()
30
+ animation.removeListener(this)
31
+ wrapper.onViewAnimationEnd()
32
+ }
33
+ }
34
+
35
+ override fun onAnimationCancel(animation: Animator) = Unit
36
+
37
+ override fun onAnimationRepeat(animation: Animator) = Unit
38
+
39
+ private enum class LifecycleState {
40
+ INITIALIZED,
41
+ START_DISPATCHED,
42
+ END_DISPATCHED,
43
+ }
44
+
45
+ companion object {
46
+ const val TAG = "ScreenEventDelegate"
47
+ }
48
+ }
@@ -0,0 +1,38 @@
1
+ package com.swmansion.rnscreens.transition
2
+
3
+ import android.animation.FloatEvaluator
4
+
5
+ typealias BoundaryValueProviderFn = (Number?) -> Float?
6
+
7
+ /**
8
+ * Float type evaluator that uses boundary values provided by callbacks passed as arguments and does
9
+ * not use boundary values used during value animator construction. This allows to defer computation
10
+ * of animator boundary values to the moment when animation starts.
11
+ */
12
+ class ExternalBoundaryValuesEvaluator(val startValueProvider: BoundaryValueProviderFn, val endValueProvider: BoundaryValueProviderFn) : FloatEvaluator() {
13
+ var startValueCache: Number? = null
14
+ var endValueCache: Number? = null
15
+
16
+ private fun getStartValue(startValue: Number?): Number? {
17
+ if (startValueCache == null) {
18
+ startValueCache = startValueProvider(startValue)
19
+ }
20
+ return startValueCache
21
+ }
22
+
23
+ private fun getEndValue(endValue: Number?): Number? {
24
+ if (endValueCache == null) {
25
+ endValueCache = endValueProvider(endValue)
26
+ }
27
+ return endValueCache
28
+ }
29
+
30
+ override fun evaluate(fraction: Float, startValue: Number?, endValue: Number?): Float? {
31
+ val realStartValue = getStartValue(startValue)
32
+ val realEndValue = getEndValue(endValue)
33
+ if (realStartValue == null || realEndValue == null) {
34
+ return null
35
+ }
36
+ return super.evaluate(fraction, realStartValue, realEndValue)
37
+ }
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-screens",
3
- "version": "4.6.0",
3
+ "version": "4.7.0-beta.1",
4
4
  "description": "Native navigation primitives for your React Native app.",
5
5
  "scripts": {
6
6
  "submodules": "git submodule update --init --recursive && (cd react-navigation && yarn && yarn build && cd ../)",
@@ -1,493 +0,0 @@
1
- package com.swmansion.rnscreens.bottomsheet
2
-
3
- import android.animation.ValueAnimator
4
- import android.app.Activity
5
- import android.graphics.Color
6
- import android.os.Bundle
7
- import android.view.LayoutInflater
8
- import android.view.View
9
- import android.view.ViewGroup
10
- import android.view.animation.Animation
11
- import android.view.animation.AnimationUtils
12
- import androidx.appcompat.widget.Toolbar
13
- import androidx.core.graphics.Insets
14
- import androidx.core.view.OnApplyWindowInsetsListener
15
- import androidx.core.view.WindowInsetsCompat
16
- import androidx.fragment.app.Fragment
17
- import androidx.fragment.app.commit
18
- import androidx.lifecycle.Lifecycle
19
- import androidx.lifecycle.LifecycleEventObserver
20
- import androidx.lifecycle.LifecycleOwner
21
- import com.facebook.react.bridge.ReactContext
22
- import com.facebook.react.uimanager.UIManagerHelper
23
- import com.google.android.material.bottomsheet.BottomSheetBehavior
24
- import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
25
- import com.swmansion.rnscreens.InsetsObserverProxy
26
- import com.swmansion.rnscreens.KeyboardDidHide
27
- import com.swmansion.rnscreens.KeyboardNotVisible
28
- import com.swmansion.rnscreens.KeyboardState
29
- import com.swmansion.rnscreens.KeyboardVisible
30
- import com.swmansion.rnscreens.NativeDismissalObserver
31
- import com.swmansion.rnscreens.R
32
- import com.swmansion.rnscreens.Screen
33
- import com.swmansion.rnscreens.ScreenContainer
34
- import com.swmansion.rnscreens.ScreenFragment
35
- import com.swmansion.rnscreens.ScreenFragmentWrapper
36
- import com.swmansion.rnscreens.ScreenStack
37
- import com.swmansion.rnscreens.ScreenStackFragment
38
- import com.swmansion.rnscreens.ScreenStackFragmentWrapper
39
- import com.swmansion.rnscreens.events.ScreenDismissedEvent
40
-
41
- /**
42
- * This fragment aims to provide dimming view functionality behind the nested fragment.
43
- * Useful when nested fragment is transparent / uses some kind of non-fullscreen presentation,
44
- * such as `formSheet`.
45
- */
46
- class DimmingFragment(
47
- val nestedFragment: ScreenFragmentWrapper,
48
- ) : Fragment(),
49
- LifecycleEventObserver,
50
- ScreenStackFragmentWrapper,
51
- Animation.AnimationListener,
52
- OnApplyWindowInsetsListener,
53
- NativeDismissalObserver {
54
- private lateinit var dimmingView: DimmingView
55
- private lateinit var containerView: GestureTransparentViewGroup
56
-
57
- private val maxAlpha: Float = 0.15F
58
-
59
- private var isKeyboardVisible: Boolean = false
60
- private var keyboardState: KeyboardState = KeyboardNotVisible
61
-
62
- private var dimmingViewCallback: BottomSheetCallback? = null
63
-
64
- private val container: ScreenStack?
65
- get() = screen.container as? ScreenStack
66
-
67
- private val insetsProxy = InsetsObserverProxy
68
-
69
- init {
70
- assert(
71
- nestedFragment.fragment is ScreenStackFragment,
72
- ) { "[RNScreens] Dimming fragment is intended for use only with ScreenStackFragment" }
73
- val fragment = nestedFragment.fragment as ScreenStackFragment
74
-
75
- // We register for our child lifecycle as we want to know when it starts, because bottom sheet
76
- // behavior is attached only then & we want to attach our own callbacks to it.
77
- fragment.lifecycle.addObserver(this)
78
- fragment.nativeDismissalObserver = this
79
- }
80
-
81
- /**
82
- * This bottom sheet callback is responsible for animating alpha of the dimming view.
83
- */
84
- private class AnimateDimmingViewCallback(
85
- val screen: Screen,
86
- val viewToAnimate: View,
87
- val maxAlpha: Float,
88
- ) : BottomSheetCallback() {
89
- // largest *slide offset* that is yet undimmed
90
- private var largestUndimmedOffset: Float =
91
- computeOffsetFromDetentIndex(screen.sheetLargestUndimmedDetentIndex)
92
-
93
- // first *slide offset* that should be fully dimmed
94
- private var firstDimmedOffset: Float =
95
- computeOffsetFromDetentIndex(
96
- (screen.sheetLargestUndimmedDetentIndex + 1).coerceIn(
97
- 0,
98
- screen.sheetDetents.count() - 1,
99
- ),
100
- )
101
-
102
- // interval that we interpolate the alpha value over
103
- private var intervalLength = firstDimmedOffset - largestUndimmedOffset
104
- private val animator =
105
- ValueAnimator.ofFloat(0F, maxAlpha).apply {
106
- duration = 1 // Driven manually
107
- addUpdateListener {
108
- viewToAnimate.alpha = it.animatedValue as Float
109
- }
110
- }
111
-
112
- override fun onStateChanged(
113
- bottomSheet: View,
114
- newState: Int,
115
- ) {
116
- if (newState == BottomSheetBehavior.STATE_DRAGGING || newState == BottomSheetBehavior.STATE_SETTLING) {
117
- largestUndimmedOffset =
118
- computeOffsetFromDetentIndex(screen.sheetLargestUndimmedDetentIndex)
119
- firstDimmedOffset =
120
- computeOffsetFromDetentIndex(
121
- (screen.sheetLargestUndimmedDetentIndex + 1).coerceIn(
122
- 0,
123
- screen.sheetDetents.count() - 1,
124
- ),
125
- )
126
- assert(firstDimmedOffset >= largestUndimmedOffset) {
127
- "[RNScreens] Invariant violation: firstDimmedOffset ($firstDimmedOffset) < largestDimmedOffset ($largestUndimmedOffset)"
128
- }
129
- intervalLength = firstDimmedOffset - largestUndimmedOffset
130
- }
131
- }
132
-
133
- override fun onSlide(
134
- bottomSheet: View,
135
- slideOffset: Float,
136
- ) {
137
- if (largestUndimmedOffset < slideOffset && slideOffset < firstDimmedOffset) {
138
- val fraction = (slideOffset - largestUndimmedOffset) / intervalLength
139
- animator.setCurrentFraction(fraction)
140
- }
141
- }
142
-
143
- /**
144
- * This method does compute slide offset (see [BottomSheetCallback.onSlide] docs) for detent
145
- * at given index in the detents array.
146
- */
147
- private fun computeOffsetFromDetentIndex(index: Int): Float =
148
- when (screen.sheetDetents.size) {
149
- 1 -> // Only 1 detent present in detents array
150
- when (index) {
151
- -1 -> -1F // hidden
152
- 0 -> 1F // fully expanded
153
- else -> -1F // unexpected, default
154
- }
155
-
156
- 2 ->
157
- when (index) {
158
- -1 -> -1F // hidden
159
- 0 -> 0F // collapsed
160
- 1 -> 1F // expanded
161
- else -> -1F
162
- }
163
-
164
- 3 ->
165
- when (index) {
166
- -1 -> -1F // hidden
167
- 0 -> 0F // collapsed
168
- 1 -> screen.sheetBehavior!!.halfExpandedRatio // half
169
- 2 -> 1F // expanded
170
- else -> -1F
171
- }
172
-
173
- else -> -1F
174
- }
175
- }
176
-
177
- override fun onCreateAnimation(
178
- transit: Int,
179
- enter: Boolean,
180
- nextAnim: Int,
181
- ): Animation? =
182
- // We want dimming view to have always fade animation in current usages.
183
- AnimationUtils.loadAnimation(
184
- context,
185
- if (enter) R.anim.rns_fade_in else R.anim.rns_fade_out,
186
- )
187
-
188
- override fun onCreateView(
189
- inflater: LayoutInflater,
190
- container: ViewGroup?,
191
- savedInstanceState: Bundle?,
192
- ): View {
193
- initViewHierarchy()
194
- return containerView
195
- }
196
-
197
- override fun onViewCreated(
198
- view: View,
199
- savedInstanceState: Bundle?,
200
- ) {
201
- if (screen.sheetInitialDetentIndex <= screen.sheetLargestUndimmedDetentIndex) {
202
- dimmingView.alpha = 0.0F
203
- } else {
204
- dimmingView.alpha = maxAlpha
205
- }
206
- }
207
-
208
- override fun onStart() {
209
- // This is the earliest we can access child fragment manager & present another fragment
210
- super.onStart()
211
- insetsProxy.registerOnView(requireDecorView())
212
- presentNestedFragment()
213
- }
214
-
215
- override fun onResume() {
216
- insetsProxy.addOnApplyWindowInsetsListener(this)
217
- super.onResume()
218
- }
219
-
220
- override fun onPause() {
221
- super.onPause()
222
- insetsProxy.removeOnApplyWindowInsetsListener(this)
223
- }
224
-
225
- override fun onStateChanged(
226
- source: LifecycleOwner,
227
- event: Lifecycle.Event,
228
- ) {
229
- when (event) {
230
- Lifecycle.Event.ON_START -> {
231
- nestedFragment.screen.sheetBehavior?.let {
232
- dimmingViewCallback =
233
- AnimateDimmingViewCallback(nestedFragment.screen, dimmingView, maxAlpha)
234
- it.addBottomSheetCallback(dimmingViewCallback!!)
235
- }
236
- }
237
-
238
- else -> {}
239
- }
240
- }
241
-
242
- private fun presentNestedFragment() {
243
- childFragmentManager.commit(allowStateLoss = true) {
244
- setReorderingAllowed(true)
245
- add(requireView().id, nestedFragment.fragment, null)
246
- }
247
- }
248
-
249
- private fun cleanRegisteredCallbacks() {
250
- dimmingViewCallback?.let {
251
- nestedFragment.screen.sheetBehavior?.removeBottomSheetCallback(it)
252
- }
253
- dimmingView.setOnClickListener(null)
254
- nestedFragment.fragment.lifecycle.removeObserver(this)
255
- insetsProxy.removeOnApplyWindowInsetsListener(this)
256
- }
257
-
258
- private fun dismissSelf(emitDismissedEvent: Boolean = false) {
259
- if (!this.isRemoving) {
260
- if (emitDismissedEvent) {
261
- val reactContext = nestedFragment.screen.reactContext
262
- val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
263
- UIManagerHelper
264
- .getEventDispatcherForReactTag(reactContext, screen.id)
265
- ?.dispatchEvent(ScreenDismissedEvent(surfaceId, screen.id))
266
- }
267
- cleanRegisteredCallbacks()
268
- dismissFromContainer()
269
- }
270
- }
271
-
272
- private fun initViewHierarchy() {
273
- initContainerView()
274
- initDimmingView()
275
- containerView.addView(dimmingView)
276
- }
277
-
278
- private fun initContainerView() {
279
- containerView =
280
- GestureTransparentViewGroup(requireContext()).apply {
281
- // These do not guarantee fullscreen width & height, TODO: find a way to guarantee that
282
- layoutParams =
283
- ViewGroup.LayoutParams(
284
- ViewGroup.LayoutParams.MATCH_PARENT,
285
- ViewGroup.LayoutParams.MATCH_PARENT,
286
- )
287
- setBackgroundColor(Color.TRANSPARENT)
288
- // This is purely native view, React does not know of it, thus there should be no conflict with ids.
289
- id = View.generateViewId()
290
- }
291
- }
292
-
293
- private fun initDimmingView() {
294
- dimmingView =
295
- DimmingView(requireContext(), maxAlpha).apply {
296
- // These do not guarantee fullscreen width & height, TODO: find a way to guarantee that
297
- layoutParams =
298
- ViewGroup.LayoutParams(
299
- ViewGroup.LayoutParams.MATCH_PARENT,
300
- ViewGroup.LayoutParams.MATCH_PARENT,
301
- )
302
- setOnClickListener {
303
- if (screen.sheetClosesOnTouchOutside) {
304
- dismissSelf(true)
305
- }
306
- }
307
- }
308
- }
309
-
310
- private fun requireDecorView(): View =
311
- checkNotNull(screen.reactContext.currentActivity) { "[RNScreens] Attempt to access activity on detached context" }
312
- .window.decorView
313
-
314
- // TODO: Move these methods related to toolbar to separate interface
315
- override fun removeToolbar() = Unit
316
-
317
- override fun setToolbar(toolbar: Toolbar) = Unit
318
-
319
- override fun setToolbarShadowHidden(hidden: Boolean) = Unit
320
-
321
- override fun setToolbarTranslucent(translucent: Boolean) = Unit
322
-
323
- // Dimming view should never be bottom-most fragment
324
- override fun canNavigateBack(): Boolean = true
325
-
326
- override fun dismissFromContainer() {
327
- container?.dismiss(this)
328
- }
329
-
330
- override var screen: Screen
331
- get() = nestedFragment.screen
332
- set(value) {
333
- nestedFragment.screen = value
334
- }
335
-
336
- override val childScreenContainers: List<ScreenContainer> = nestedFragment.childScreenContainers
337
-
338
- override fun addChildScreenContainer(container: ScreenContainer) {
339
- nestedFragment.addChildScreenContainer(container)
340
- }
341
-
342
- override fun removeChildScreenContainer(container: ScreenContainer) {
343
- nestedFragment.removeChildScreenContainer(container)
344
- }
345
-
346
- override fun onContainerUpdate() {
347
- nestedFragment.onContainerUpdate()
348
- }
349
-
350
- override fun onViewAnimationStart() {
351
- nestedFragment.onViewAnimationStart()
352
- }
353
-
354
- override fun onViewAnimationEnd() {
355
- nestedFragment.onViewAnimationEnd()
356
- }
357
-
358
- override fun tryGetActivity(): Activity? = activity
359
-
360
- override fun tryGetContext(): ReactContext? = context as? ReactContext?
361
-
362
- override val fragment: Fragment
363
- get() = this
364
-
365
- override fun canDispatchLifecycleEvent(event: ScreenFragment.ScreenLifecycleEvent): Boolean {
366
- TODO("Not yet implemented")
367
- }
368
-
369
- override fun updateLastEventDispatched(event: ScreenFragment.ScreenLifecycleEvent) {
370
- TODO("Not yet implemented")
371
- }
372
-
373
- override fun dispatchLifecycleEvent(
374
- event: ScreenFragment.ScreenLifecycleEvent,
375
- fragmentWrapper: ScreenFragmentWrapper,
376
- ) {
377
- TODO("Not yet implemented")
378
- }
379
-
380
- override fun dispatchLifecycleEventInChildContainers(event: ScreenFragment.ScreenLifecycleEvent) {
381
- TODO("Not yet implemented")
382
- }
383
-
384
- override fun dispatchHeaderBackButtonClickedEvent() {
385
- TODO("Not yet implemented")
386
- }
387
-
388
- override fun dispatchTransitionProgressEvent(
389
- alpha: Float,
390
- closing: Boolean,
391
- ) {
392
- TODO("Not yet implemented")
393
- }
394
-
395
- override fun onAnimationStart(animation: Animation?) = Unit
396
-
397
- override fun onAnimationEnd(animation: Animation?) {
398
- dismissFromContainer()
399
- }
400
-
401
- override fun onAnimationRepeat(animation: Animation?) = Unit
402
-
403
- companion object {
404
- const val TAG = "DimmingFragment"
405
- }
406
-
407
- // This is View.OnApplyWindowInsetsListener method, not view's own!
408
- override fun onApplyWindowInsets(
409
- v: View,
410
- insets: WindowInsetsCompat,
411
- ): WindowInsetsCompat {
412
- val isImeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
413
- val imeInset = insets.getInsets(WindowInsetsCompat.Type.ime())
414
-
415
- if (isImeVisible) {
416
- isKeyboardVisible = true
417
- keyboardState = KeyboardVisible(imeInset.bottom)
418
- screen.sheetBehavior?.let {
419
- (nestedFragment as ScreenStackFragment).configureBottomSheetBehaviour(
420
- it,
421
- KeyboardVisible(imeInset.bottom),
422
- )
423
- }
424
-
425
- if (this.isRemoving) {
426
- return insets
427
- }
428
-
429
- val prevInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
430
- return WindowInsetsCompat
431
- .Builder(insets)
432
- .setInsets(
433
- WindowInsetsCompat.Type.navigationBars(),
434
- Insets.of(
435
- prevInsets.left,
436
- prevInsets.top,
437
- prevInsets.right,
438
- 0,
439
- ),
440
- ).build()
441
- } else {
442
- if (this.isRemoving) {
443
- val prevInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
444
- return WindowInsetsCompat
445
- .Builder(insets)
446
- .setInsets(
447
- WindowInsetsCompat.Type.navigationBars(),
448
- Insets.of(
449
- prevInsets.left,
450
- prevInsets.top,
451
- prevInsets.right,
452
- 0,
453
- ),
454
- ).build()
455
- }
456
-
457
- screen.sheetBehavior?.let {
458
- if (isKeyboardVisible) {
459
- (nestedFragment as ScreenStackFragment).configureBottomSheetBehaviour(
460
- it,
461
- KeyboardDidHide,
462
- )
463
- } else if (keyboardState != KeyboardNotVisible) {
464
- (nestedFragment as ScreenStackFragment).configureBottomSheetBehaviour(
465
- it,
466
- KeyboardNotVisible,
467
- )
468
- } else {
469
- }
470
- }
471
-
472
- keyboardState = KeyboardNotVisible
473
- isKeyboardVisible = false
474
-
475
- val prevInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
476
- return WindowInsetsCompat
477
- .Builder(insets)
478
- .setInsets(
479
- WindowInsetsCompat.Type.navigationBars(),
480
- Insets.of(
481
- prevInsets.left,
482
- prevInsets.top,
483
- prevInsets.right,
484
- 0,
485
- ),
486
- ).build()
487
- }
488
- }
489
-
490
- override fun onNativeDismiss(dismissed: ScreenStackFragmentWrapper) {
491
- dismissSelf(emitDismissedEvent = true)
492
- }
493
- }