@lodev09/react-native-true-sheet 3.3.5 → 3.4.0-beta.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.
- package/README.md +1 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +2 -0
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +184 -121
- package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +2 -2
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +4 -3
- package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +72 -0
- package/android/src/main/res/values/styles.xml +2 -3
- package/package.json +2 -2
- package/android/src/main/res/anim/true_sheet_fade_in.xml +0 -6
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
The true native bottom sheet experience for your React Native Apps. 💩
|
|
10
10
|
|
|
11
|
-
<img alt="React Native True Sheet - IOS" src="docs/static/img/preview-ios.gif" width="
|
|
11
|
+
<img alt="React Native True Sheet - IOS" src="docs/static/img/preview-ios.gif" width="248" height="500" /><img alt="React Native True Sheet - Android" src="docs/static/img/preview-android.gif" width="248" height="500" /><img alt="React Native True Sheet - Web" src="docs/static/img/preview-web.gif" width="248" height="500" />
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
@@ -251,6 +251,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
251
251
|
viewController.dimmed = dimmed
|
|
252
252
|
if (viewController.isPresented) {
|
|
253
253
|
viewController.setupDimmedBackground(viewController.currentDetentIndex)
|
|
254
|
+
viewController.updateDimAmount()
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
|
|
@@ -259,6 +260,7 @@ class TrueSheetView(private val reactContext: ThemedReactContext) :
|
|
|
259
260
|
viewController.dimmedDetentIndex = index
|
|
260
261
|
if (viewController.isPresented) {
|
|
261
262
|
viewController.setupDimmedBackground(viewController.currentDetentIndex)
|
|
263
|
+
viewController.updateDimAmount()
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.lodev09.truesheet
|
|
2
2
|
|
|
3
|
+
import android.animation.ValueAnimator
|
|
3
4
|
import android.annotation.SuppressLint
|
|
4
5
|
import android.graphics.Color
|
|
5
6
|
import android.graphics.drawable.ShapeDrawable
|
|
@@ -28,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
|
28
29
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
29
30
|
import com.lodev09.truesheet.core.GrabberOptions
|
|
30
31
|
import com.lodev09.truesheet.core.RNScreensFragmentObserver
|
|
32
|
+
import com.lodev09.truesheet.core.TrueSheetDimView
|
|
31
33
|
import com.lodev09.truesheet.core.TrueSheetGrabberView
|
|
32
34
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserver
|
|
33
35
|
import com.lodev09.truesheet.core.TrueSheetKeyboardObserverDelegate
|
|
@@ -72,7 +74,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
72
74
|
private const val DEFAULT_CORNER_RADIUS = 16 // dp
|
|
73
75
|
|
|
74
76
|
// Animation durations from res/anim/true_sheet_slide_in.xml and true_sheet_slide_out.xml
|
|
75
|
-
private const val PRESENT_ANIMATION_DURATION =
|
|
77
|
+
private const val PRESENT_ANIMATION_DURATION = 250L
|
|
76
78
|
private const val DISMISS_ANIMATION_DURATION = 150L
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -87,6 +89,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
87
89
|
// ====================================================================
|
|
88
90
|
|
|
89
91
|
private var dialog: BottomSheetDialog? = null
|
|
92
|
+
private var dimView: TrueSheetDimView? = null
|
|
93
|
+
private var parentDimView: TrueSheetDimView? = null
|
|
90
94
|
|
|
91
95
|
private val behavior: BottomSheetBehavior<FrameLayout>?
|
|
92
96
|
get() = dialog?.behavior
|
|
@@ -144,6 +148,8 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
144
148
|
get() = ScreenUtils.getScreenHeight(reactContext)
|
|
145
149
|
val screenWidth: Int
|
|
146
150
|
get() = ScreenUtils.getScreenWidth(reactContext)
|
|
151
|
+
val realScreenHeight: Int
|
|
152
|
+
get() = ScreenUtils.getRealScreenHeight(reactContext)
|
|
147
153
|
|
|
148
154
|
var maxSheetHeight: Int? = null
|
|
149
155
|
var detents = mutableListOf(0.5, 1.0)
|
|
@@ -152,7 +158,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
152
158
|
var dimmedDetentIndex = 0
|
|
153
159
|
var grabber: Boolean = true
|
|
154
160
|
var grabberOptions: GrabberOptions? = null
|
|
155
|
-
var sheetCornerRadius: Float =
|
|
161
|
+
var sheetCornerRadius: Float = DEFAULT_CORNER_RADIUS.dpToPx()
|
|
162
|
+
set(value) {
|
|
163
|
+
field = if (value < 0) DEFAULT_CORNER_RADIUS.dpToPx() else value
|
|
164
|
+
setupBackground()
|
|
165
|
+
}
|
|
156
166
|
var sheetBackgroundColor: Int? = null
|
|
157
167
|
var edgeToEdgeFullScreen: Boolean = false
|
|
158
168
|
|
|
@@ -261,6 +271,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
261
271
|
|
|
262
272
|
cleanupKeyboardObserver()
|
|
263
273
|
cleanupModalObserver()
|
|
274
|
+
dimView?.detach()
|
|
275
|
+
dimView = null
|
|
276
|
+
parentDimView?.detach()
|
|
277
|
+
parentDimView = null
|
|
264
278
|
sheetContainer?.removeView(this)
|
|
265
279
|
|
|
266
280
|
dialog = null
|
|
@@ -281,16 +295,18 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
281
295
|
setupGrabber()
|
|
282
296
|
setupKeyboardObserver()
|
|
283
297
|
|
|
298
|
+
val toTop = getExpectedSheetTop(currentDetentIndex)
|
|
299
|
+
setupTransitionTracker(realScreenHeight, toTop, PRESENT_ANIMATION_DURATION)
|
|
300
|
+
animateDimAlpha(show = true)
|
|
301
|
+
|
|
284
302
|
sheetContainer?.post {
|
|
285
|
-
bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
|
|
286
303
|
positionFooter()
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
sheetContainer?.postDelayed({
|
|
290
|
-
val
|
|
291
|
-
val detent = getDetentValueForIndex(detentInfo.index)
|
|
307
|
+
val (index, position, detent) = getDetentInfoWithValue(currentDetentIndex)
|
|
292
308
|
|
|
293
|
-
delegate?.viewControllerDidPresent(
|
|
309
|
+
delegate?.viewControllerDidPresent(index, position, detent)
|
|
294
310
|
parentSheetView?.viewControllerDidBlur()
|
|
295
311
|
delegate?.viewControllerDidFocus()
|
|
296
312
|
|
|
@@ -303,8 +319,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
303
319
|
if (isDismissing) return@setOnCancelListener
|
|
304
320
|
|
|
305
321
|
isDismissing = true
|
|
322
|
+
val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
|
|
323
|
+
setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
|
|
324
|
+
animateDimAlpha(show = false)
|
|
306
325
|
emitWillDismissEvents()
|
|
307
|
-
emitDismissedPosition()
|
|
308
326
|
}
|
|
309
327
|
|
|
310
328
|
dialog.setOnDismissListener {
|
|
@@ -321,7 +339,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
321
339
|
override fun onSlide(sheetView: View, slideOffset: Float) {
|
|
322
340
|
val behavior = behavior ?: return
|
|
323
341
|
|
|
324
|
-
emitChangePositionDelegate(sheetView
|
|
342
|
+
emitChangePositionDelegate(sheetView.top)
|
|
325
343
|
|
|
326
344
|
when (behavior.state) {
|
|
327
345
|
BottomSheetBehavior.STATE_DRAGGING,
|
|
@@ -331,6 +349,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
331
349
|
}
|
|
332
350
|
|
|
333
351
|
positionFooter(slideOffset)
|
|
352
|
+
updateDimAmount()
|
|
334
353
|
}
|
|
335
354
|
|
|
336
355
|
override fun onStateChanged(sheetView: View, newState: Int) {
|
|
@@ -386,14 +405,21 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
386
405
|
reactContext = reactContext,
|
|
387
406
|
onModalPresented = {
|
|
388
407
|
if (isPresented && isDialogVisible) {
|
|
389
|
-
|
|
408
|
+
isDialogVisible = false
|
|
409
|
+
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFadeOutAnimation)
|
|
410
|
+
dialog?.window?.decorView?.visibility = INVISIBLE
|
|
390
411
|
wasHiddenByModal = true
|
|
391
412
|
}
|
|
392
413
|
},
|
|
393
414
|
onModalDismissed = {
|
|
394
415
|
// Only show if we were the one hidden by modal, not by sheet stacking
|
|
395
416
|
if (isPresented && wasHiddenByModal) {
|
|
396
|
-
|
|
417
|
+
isDialogVisible = true
|
|
418
|
+
dialog?.window?.decorView?.visibility = VISIBLE
|
|
419
|
+
// Restore animation after visibility change to avoid slide animation
|
|
420
|
+
sheetContainer?.post {
|
|
421
|
+
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
422
|
+
}
|
|
397
423
|
wasHiddenByModal = false
|
|
398
424
|
}
|
|
399
425
|
}
|
|
@@ -424,6 +450,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
424
450
|
dismissPromise = null
|
|
425
451
|
}
|
|
426
452
|
|
|
453
|
+
/** Helper to get detent info with its screen fraction value. */
|
|
454
|
+
private fun getDetentInfoWithValue(index: Int): Triple<Int, Float, Float> {
|
|
455
|
+
val detentInfo = getDetentInfoForIndex(index)
|
|
456
|
+
val detent = getDetentValueForIndex(detentInfo.index)
|
|
457
|
+
return Triple(detentInfo.index, detentInfo.position, detent)
|
|
458
|
+
}
|
|
459
|
+
|
|
427
460
|
// ====================================================================
|
|
428
461
|
// MARK: - Dialog Visibility (for stacking)
|
|
429
462
|
// ====================================================================
|
|
@@ -439,35 +472,22 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
439
472
|
|
|
440
473
|
fun getExpectedSheetTop(detentIndex: Int): Int {
|
|
441
474
|
if (detentIndex < 0 || detentIndex >= detents.size) return screenHeight
|
|
442
|
-
|
|
443
|
-
return screenHeight - detentHeight
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/** Hides without dismissing. Used for sheet stacking and RN Screens modals. */
|
|
447
|
-
fun hideDialog(emitPosition: Boolean = false, animated: Boolean = false) {
|
|
448
|
-
isDialogVisible = false
|
|
449
|
-
if (animated) {
|
|
450
|
-
dialog?.window?.setWindowAnimations(com.lodev09.truesheet.R.style.TrueSheetFadeAnimation)
|
|
451
|
-
}
|
|
452
|
-
dialog?.window?.decorView?.visibility = INVISIBLE
|
|
453
|
-
if (emitPosition) {
|
|
454
|
-
emitDismissedPosition()
|
|
455
|
-
}
|
|
475
|
+
return realScreenHeight - getDetentHeight(detents[detentIndex])
|
|
456
476
|
}
|
|
457
477
|
|
|
458
|
-
/**
|
|
459
|
-
fun
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
470
|
-
|
|
478
|
+
/** Translates the sheet when stacking. Pass 0 to reset. */
|
|
479
|
+
fun translateDialog(translationY: Int) {
|
|
480
|
+
val bottomSheet = bottomSheetView ?: return
|
|
481
|
+
val duration = if (translationY > 0) PRESENT_ANIMATION_DURATION else DISMISS_ANIMATION_DURATION
|
|
482
|
+
|
|
483
|
+
bottomSheet.animate()
|
|
484
|
+
.translationY(translationY.toFloat())
|
|
485
|
+
.setDuration(duration)
|
|
486
|
+
.setUpdateListener {
|
|
487
|
+
val effectiveTop = bottomSheet.top + bottomSheet.translationY.toInt()
|
|
488
|
+
emitChangePositionDelegate(effectiveTop)
|
|
489
|
+
}
|
|
490
|
+
.start()
|
|
471
491
|
}
|
|
472
492
|
|
|
473
493
|
// ====================================================================
|
|
@@ -490,11 +510,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
490
510
|
setupSheetDetents()
|
|
491
511
|
setStateForDetentIndex(detentIndex)
|
|
492
512
|
|
|
493
|
-
val
|
|
494
|
-
val detent = getDetentValueForIndex(detentInfo.index)
|
|
513
|
+
val (index, position, detent) = getDetentInfoWithValue(detentIndex)
|
|
495
514
|
|
|
496
515
|
parentSheetView?.viewControllerWillBlur()
|
|
497
|
-
delegate?.viewControllerWillPresent(
|
|
516
|
+
delegate?.viewControllerWillPresent(index, position, detent)
|
|
498
517
|
delegate?.viewControllerWillFocus()
|
|
499
518
|
|
|
500
519
|
if (!animated) {
|
|
@@ -509,8 +528,9 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
509
528
|
if (isDismissing) return
|
|
510
529
|
|
|
511
530
|
isDismissing = true
|
|
531
|
+
val fromTop = bottomSheetView?.top ?: getExpectedSheetTop(currentDetentIndex)
|
|
532
|
+
setupTransitionTracker(fromTop, realScreenHeight, DISMISS_ANIMATION_DURATION)
|
|
512
533
|
emitWillDismissEvents()
|
|
513
|
-
emitDismissedPosition()
|
|
514
534
|
|
|
515
535
|
if (!animated) {
|
|
516
536
|
dialog?.window?.setWindowAnimations(0)
|
|
@@ -528,14 +548,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
528
548
|
val behavior = this.behavior ?: return
|
|
529
549
|
|
|
530
550
|
isReconfiguring = true
|
|
531
|
-
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
532
551
|
val edgeToEdgeTopInset: Int = if (!edgeToEdgeFullScreen) topInset else 0
|
|
533
552
|
|
|
534
553
|
behavior.apply {
|
|
535
554
|
isFitToContents = false
|
|
536
555
|
maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
|
|
537
556
|
|
|
538
|
-
val maxAvailableHeight =
|
|
557
|
+
val maxAvailableHeight = realScreenHeight - edgeToEdgeTopInset
|
|
539
558
|
|
|
540
559
|
setPeekHeight(getDetentHeight(detents[0]), isPresented)
|
|
541
560
|
|
|
@@ -547,13 +566,13 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
547
566
|
val maxDetentHeight = getDetentHeight(detents.last())
|
|
548
567
|
|
|
549
568
|
val adjustedHalfExpandedHeight = minOf(halfExpandedDetentHeight, maxAvailableHeight)
|
|
550
|
-
halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() /
|
|
569
|
+
halfExpandedRatio = minOf(adjustedHalfExpandedHeight.toFloat() / realScreenHeight.toFloat(), MAX_HALF_EXPANDED_RATIO)
|
|
551
570
|
|
|
552
|
-
expandedOffset = maxOf(edgeToEdgeTopInset,
|
|
571
|
+
expandedOffset = maxOf(edgeToEdgeTopInset, realScreenHeight - maxDetentHeight)
|
|
553
572
|
isFitToContents = detents.size < 3 && expandedOffset == 0
|
|
554
573
|
|
|
555
574
|
val offset = if (expandedOffset == 0) topInset else 0
|
|
556
|
-
val newHeight =
|
|
575
|
+
val newHeight = realScreenHeight - expandedOffset - offset
|
|
557
576
|
val newWidth = minOf(screenWidth, maxWidth)
|
|
558
577
|
|
|
559
578
|
if (lastStateWidth != newWidth || lastStateHeight != newHeight) {
|
|
@@ -584,10 +603,10 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
584
603
|
}
|
|
585
604
|
|
|
586
605
|
bottomSheet.addView(grabberView)
|
|
587
|
-
grabberView.bringToFront()
|
|
588
606
|
}
|
|
589
607
|
|
|
590
608
|
private var keyboardObserver: TrueSheetKeyboardObserver? = null
|
|
609
|
+
private var positionAnimator: ValueAnimator? = null
|
|
591
610
|
|
|
592
611
|
fun setupKeyboardObserver() {
|
|
593
612
|
val bottomSheet = bottomSheetView ?: return
|
|
@@ -606,11 +625,51 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
606
625
|
keyboardObserver = null
|
|
607
626
|
}
|
|
608
627
|
|
|
628
|
+
private fun setupTransitionTracker(fromTop: Int, toTop: Int, duration: Long) {
|
|
629
|
+
positionAnimator?.cancel()
|
|
630
|
+
positionAnimator = ValueAnimator.ofInt(fromTop, toTop).apply {
|
|
631
|
+
this.duration = duration
|
|
632
|
+
interpolator = if (fromTop > toTop) {
|
|
633
|
+
android.view.animation.DecelerateInterpolator(2f) // present
|
|
634
|
+
} else {
|
|
635
|
+
android.view.animation.AccelerateInterpolator(2f) // dismiss
|
|
636
|
+
}
|
|
637
|
+
addUpdateListener { animator ->
|
|
638
|
+
emitChangePositionDelegate(animator.animatedValue as Int)
|
|
639
|
+
}
|
|
640
|
+
addListener(object : android.animation.AnimatorListenerAdapter() {
|
|
641
|
+
override fun onAnimationEnd(animation: android.animation.Animator) {
|
|
642
|
+
positionAnimator = null
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
start()
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private fun emitChangePositionDelegate(currentTop: Int, realtime: Boolean = true) {
|
|
650
|
+
if (currentTop == lastEmittedPositionPx) return
|
|
651
|
+
|
|
652
|
+
lastEmittedPositionPx = currentTop
|
|
653
|
+
val visibleHeight = realScreenHeight - currentTop
|
|
654
|
+
val position = getPositionDp(visibleHeight)
|
|
655
|
+
val interpolatedIndex = getInterpolatedIndexForPosition(currentTop)
|
|
656
|
+
val detent = getInterpolatedDetentForPosition(currentTop)
|
|
657
|
+
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
658
|
+
}
|
|
659
|
+
|
|
609
660
|
fun setupBackground() {
|
|
610
661
|
val bottomSheet = bottomSheetView ?: return
|
|
611
662
|
|
|
612
|
-
val
|
|
613
|
-
|
|
663
|
+
val outerRadii = floatArrayOf(
|
|
664
|
+
sheetCornerRadius,
|
|
665
|
+
sheetCornerRadius,
|
|
666
|
+
sheetCornerRadius,
|
|
667
|
+
sheetCornerRadius,
|
|
668
|
+
0f,
|
|
669
|
+
0f,
|
|
670
|
+
0f,
|
|
671
|
+
0f
|
|
672
|
+
)
|
|
614
673
|
val backgroundColor = sheetBackgroundColor ?: getDefaultBackgroundColor()
|
|
615
674
|
|
|
616
675
|
bottomSheet.background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
|
|
@@ -619,24 +678,46 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
619
678
|
bottomSheet.clipToOutline = true
|
|
620
679
|
}
|
|
621
680
|
|
|
622
|
-
/** Configures dim and touch-through behavior based on detent index. */
|
|
623
681
|
fun setupDimmedBackground(detentIndex: Int) {
|
|
624
682
|
val dialog = this.dialog ?: return
|
|
683
|
+
|
|
625
684
|
dialog.window?.apply {
|
|
626
|
-
val
|
|
685
|
+
val touchOutside = findViewById<View>(com.google.android.material.R.id.touch_outside)
|
|
686
|
+
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
687
|
+
|
|
688
|
+
val shouldDimAtDetent = dimmed && detentIndex >= dimmedDetentIndex
|
|
689
|
+
|
|
690
|
+
if (dimmed) {
|
|
691
|
+
val parentDimVisible = (parentSheetView?.viewController?.dimView?.alpha ?: 0f) > 0f
|
|
627
692
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
693
|
+
if (dimView == null) dimView = TrueSheetDimView(reactContext)
|
|
694
|
+
if (!parentDimVisible) dimView?.attach(null)
|
|
695
|
+
|
|
696
|
+
val parentController = parentSheetView?.viewController
|
|
697
|
+
val parentBottomSheet = parentController?.bottomSheetView
|
|
698
|
+
if (parentBottomSheet != null) {
|
|
699
|
+
if (parentDimView == null) parentDimView = TrueSheetDimView(reactContext)
|
|
700
|
+
parentDimView?.attach(parentBottomSheet, parentController.sheetCornerRadius)
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
dimView?.detach()
|
|
704
|
+
dimView = null
|
|
705
|
+
parentDimView?.detach()
|
|
706
|
+
parentDimView = null
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (shouldDimAtDetent) {
|
|
710
|
+
touchOutside.setOnTouchListener(null)
|
|
632
711
|
dialog.setCanceledOnTouchOutside(dismissible)
|
|
633
712
|
} else {
|
|
634
|
-
|
|
713
|
+
touchOutside.setOnTouchListener { v, event ->
|
|
635
714
|
event.setLocation(event.rawX - v.x, event.rawY - v.y)
|
|
636
|
-
|
|
715
|
+
(
|
|
716
|
+
parentSheetView?.viewController?.dialog?.window?.decorView
|
|
717
|
+
?: reactContext.currentActivity?.window?.decorView
|
|
718
|
+
)?.dispatchTouchEvent(event)
|
|
637
719
|
false
|
|
638
720
|
}
|
|
639
|
-
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
640
721
|
dialog.setCanceledOnTouchOutside(false)
|
|
641
722
|
}
|
|
642
723
|
}
|
|
@@ -646,6 +727,20 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
646
727
|
dialog?.window?.setWindowAnimations(windowAnimation)
|
|
647
728
|
}
|
|
648
729
|
|
|
730
|
+
private fun animateDimAlpha(show: Boolean) {
|
|
731
|
+
if (!dimmed) return
|
|
732
|
+
val duration = if (show) PRESENT_ANIMATION_DURATION else DISMISS_ANIMATION_DURATION
|
|
733
|
+
dimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
|
|
734
|
+
parentDimView?.animateAlpha(show, duration, dimmedDetentIndex, currentDetentIndex)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
fun updateDimAmount() {
|
|
738
|
+
if (!dimmed) return
|
|
739
|
+
val sheetTop = bottomSheetView?.top ?: return
|
|
740
|
+
dimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
|
|
741
|
+
parentDimView?.interpolateAlpha(sheetTop, dimmedDetentIndex, ::getSheetTopForDetentIndex)
|
|
742
|
+
}
|
|
743
|
+
|
|
649
744
|
/** Positions footer at bottom of sheet, adjusting during drag via slideOffset. */
|
|
650
745
|
fun positionFooter(slideOffset: Float? = null) {
|
|
651
746
|
val footerView = containerView?.footerView ?: return
|
|
@@ -693,37 +788,16 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
693
788
|
* Calculate the visible sheet height from a sheet view.
|
|
694
789
|
* Uses real screen height for consistency across API levels.
|
|
695
790
|
*/
|
|
696
|
-
private fun getVisibleSheetHeight(sheetView: View): Int
|
|
697
|
-
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
698
|
-
return realHeight - sheetView.top
|
|
699
|
-
}
|
|
791
|
+
private fun getVisibleSheetHeight(sheetView: View): Int = realScreenHeight - sheetView.top
|
|
700
792
|
|
|
701
793
|
private fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
|
|
702
794
|
|
|
703
|
-
private fun emitChangePositionDelegate(sheetView: View, realtime: Boolean) {
|
|
704
|
-
if (sheetView.top == lastEmittedPositionPx) return
|
|
705
|
-
|
|
706
|
-
lastEmittedPositionPx = sheetView.top
|
|
707
|
-
val position = getPositionDp(getVisibleSheetHeight(sheetView))
|
|
708
|
-
val interpolatedIndex = getInterpolatedIndexForPosition(sheetView.top)
|
|
709
|
-
val detent = getInterpolatedDetentForPosition(sheetView.top)
|
|
710
|
-
delegate?.viewControllerDidChangePosition(interpolatedIndex, position, detent, realtime)
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
private fun emitDismissedPosition() {
|
|
714
|
-
val position = screenHeight.pxToDp()
|
|
715
|
-
lastEmittedPositionPx = -1
|
|
716
|
-
delegate?.viewControllerDidChangePosition(-1f, position, 0f, false)
|
|
717
|
-
}
|
|
718
|
-
|
|
719
795
|
/**
|
|
720
796
|
* Get the expected sheetTop position for a detent index.
|
|
721
797
|
*/
|
|
722
798
|
private fun getSheetTopForDetentIndex(index: Int): Int {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
val detentHeight = getDetentHeight(detents[index])
|
|
726
|
-
return realHeight - detentHeight
|
|
799
|
+
if (index < 0 || index >= detents.size) return realScreenHeight
|
|
800
|
+
return realScreenHeight - getDetentHeight(detents[index])
|
|
727
801
|
}
|
|
728
802
|
|
|
729
803
|
/** Returns (fromIndex, toIndex, progress) for interpolation, or null if < 2 detents. */
|
|
@@ -731,12 +805,11 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
731
805
|
val count = detents.size
|
|
732
806
|
if (count == 0) return null
|
|
733
807
|
|
|
734
|
-
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
735
808
|
val firstPos = getSheetTopForDetentIndex(0)
|
|
736
809
|
|
|
737
810
|
// Above first detent - interpolating toward closed
|
|
738
811
|
if (positionPx > firstPos) {
|
|
739
|
-
val range =
|
|
812
|
+
val range = realScreenHeight - firstPos
|
|
740
813
|
val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
|
|
741
814
|
return Triple(-1, 0, progress)
|
|
742
815
|
}
|
|
@@ -847,47 +920,37 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
847
920
|
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
|
|
848
921
|
}
|
|
849
922
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
856
|
-
1 -> BottomSheetBehavior.STATE_EXPANDED
|
|
857
|
-
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
3 -> when (index) {
|
|
861
|
-
0 -> BottomSheetBehavior.STATE_COLLAPSED
|
|
862
|
-
1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
863
|
-
2 -> BottomSheetBehavior.STATE_EXPANDED
|
|
864
|
-
else -> BottomSheetBehavior.STATE_HIDDEN
|
|
865
|
-
}
|
|
923
|
+
/** Maps detent index to BottomSheetBehavior state based on detent count. */
|
|
924
|
+
private fun getStateForDetentIndex(index: Int): Int {
|
|
925
|
+
val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
|
|
926
|
+
return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
|
|
927
|
+
}
|
|
866
928
|
|
|
867
|
-
|
|
868
|
-
|
|
929
|
+
/** Maps BottomSheetBehavior state to DetentInfo based on detent count. */
|
|
930
|
+
fun getDetentInfoForState(state: Int): DetentInfo? {
|
|
931
|
+
val stateMap = getDetentStateMap() ?: return null
|
|
932
|
+
val index = stateMap[state] ?: return null
|
|
933
|
+
return DetentInfo(index, getPositionForDetentIndex(index))
|
|
934
|
+
}
|
|
869
935
|
|
|
870
|
-
|
|
936
|
+
/** Returns state-to-index mapping based on detent count. */
|
|
937
|
+
private fun getDetentStateMap(): Map<Int, Int>? =
|
|
871
938
|
when (detents.size) {
|
|
872
|
-
1 ->
|
|
873
|
-
BottomSheetBehavior.STATE_COLLAPSED,
|
|
874
|
-
BottomSheetBehavior.STATE_EXPANDED
|
|
875
|
-
|
|
876
|
-
else -> null
|
|
877
|
-
}
|
|
939
|
+
1 -> mapOf(
|
|
940
|
+
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
941
|
+
BottomSheetBehavior.STATE_EXPANDED to 0
|
|
942
|
+
)
|
|
878
943
|
|
|
879
|
-
2 ->
|
|
880
|
-
BottomSheetBehavior.STATE_COLLAPSED
|
|
881
|
-
BottomSheetBehavior.STATE_EXPANDED
|
|
882
|
-
|
|
883
|
-
}
|
|
944
|
+
2 -> mapOf(
|
|
945
|
+
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
946
|
+
BottomSheetBehavior.STATE_EXPANDED to 1
|
|
947
|
+
)
|
|
884
948
|
|
|
885
|
-
3 ->
|
|
886
|
-
BottomSheetBehavior.STATE_COLLAPSED
|
|
887
|
-
BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
888
|
-
BottomSheetBehavior.STATE_EXPANDED
|
|
889
|
-
|
|
890
|
-
}
|
|
949
|
+
3 -> mapOf(
|
|
950
|
+
BottomSheetBehavior.STATE_COLLAPSED to 0,
|
|
951
|
+
BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
|
|
952
|
+
BottomSheetBehavior.STATE_EXPANDED to 2
|
|
953
|
+
)
|
|
891
954
|
|
|
892
955
|
else -> null
|
|
893
956
|
}
|
|
@@ -929,7 +992,7 @@ class TrueSheetViewController(private val reactContext: ThemedReactContext) :
|
|
|
929
992
|
this.post {
|
|
930
993
|
setupSheetDetents()
|
|
931
994
|
positionFooter()
|
|
932
|
-
bottomSheetView?.let { emitChangePositionDelegate(it, realtime = false) }
|
|
995
|
+
bottomSheetView?.let { emitChangePositionDelegate(it.top, realtime = false) }
|
|
933
996
|
}
|
|
934
997
|
}
|
|
935
998
|
|
|
@@ -27,8 +27,8 @@ class RNScreensFragmentObserver(
|
|
|
27
27
|
val fragmentManager = activity.supportFragmentManager
|
|
28
28
|
|
|
29
29
|
fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
|
|
30
|
-
override fun
|
|
31
|
-
super.
|
|
30
|
+
override fun onFragmentAttached(fm: FragmentManager, fragment: Fragment, context: android.content.Context) {
|
|
31
|
+
super.onFragmentAttached(fm, fragment, context)
|
|
32
32
|
|
|
33
33
|
if (isModalFragment(fragment) && !activeModalFragments.contains(fragment)) {
|
|
34
34
|
activeModalFragments.add(fragment)
|
|
@@ -20,7 +20,7 @@ object TrueSheetDialogObserver {
|
|
|
20
20
|
val parentSheet = presentedSheetStack.lastOrNull()
|
|
21
21
|
?.takeIf { it.viewController.isPresented && it.viewController.isDialogVisible }
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// Translate parent sheets down to match the new sheet's position
|
|
24
24
|
val newSheetTop = sheetView.viewController.getExpectedSheetTop(detentIndex)
|
|
25
25
|
for (sheet in presentedSheetStack) {
|
|
26
26
|
if (!sheet.viewController.isDialogVisible) continue
|
|
@@ -28,7 +28,8 @@ object TrueSheetDialogObserver {
|
|
|
28
28
|
|
|
29
29
|
val sheetTop = sheet.viewController.currentSheetTop
|
|
30
30
|
if (sheetTop < newSheetTop) {
|
|
31
|
-
|
|
31
|
+
val translationY = newSheetTop - sheetTop
|
|
32
|
+
sheet.viewController.translateDialog(translationY)
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -49,7 +50,7 @@ object TrueSheetDialogObserver {
|
|
|
49
50
|
synchronized(presentedSheetStack) {
|
|
50
51
|
presentedSheetStack.remove(sheetView)
|
|
51
52
|
if (hadParent) {
|
|
52
|
-
presentedSheetStack.lastOrNull()?.viewController?.
|
|
53
|
+
presentedSheetStack.lastOrNull()?.viewController?.translateDialog(0)
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package com.lodev09.truesheet.core
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.Outline
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.view.ViewOutlineProvider
|
|
9
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
10
|
+
import com.lodev09.truesheet.utils.ScreenUtils
|
|
11
|
+
|
|
12
|
+
@SuppressLint("ViewConstructor")
|
|
13
|
+
class TrueSheetDimView(private val reactContext: ThemedReactContext) : View(reactContext) {
|
|
14
|
+
|
|
15
|
+
companion object {
|
|
16
|
+
private const val MAX_ALPHA = 0.4f
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private var targetView: ViewGroup? = null
|
|
20
|
+
|
|
21
|
+
init {
|
|
22
|
+
layoutParams = ViewGroup.LayoutParams(
|
|
23
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
24
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
25
|
+
)
|
|
26
|
+
setBackgroundColor(Color.BLACK)
|
|
27
|
+
alpha = 0f
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
|
|
31
|
+
if (parent != null) return
|
|
32
|
+
targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
|
|
33
|
+
|
|
34
|
+
if (cornerRadius > 0f) {
|
|
35
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
36
|
+
override fun getOutline(v: View, outline: Outline) {
|
|
37
|
+
outline.setRoundRect(0, 0, v.width, v.height, cornerRadius)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
clipToOutline = true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
targetView?.addView(this)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun detach() {
|
|
47
|
+
targetView?.removeView(this)
|
|
48
|
+
targetView = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fun animateAlpha(show: Boolean, duration: Long, dimmedDetentIndex: Int, currentDetentIndex: Int) {
|
|
52
|
+
val targetAlpha = if (show && currentDetentIndex >= dimmedDetentIndex) MAX_ALPHA else 0f
|
|
53
|
+
animate().alpha(targetAlpha).setDuration(duration).start()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
|
|
57
|
+
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
|
|
58
|
+
val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
|
|
59
|
+
val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
|
|
60
|
+
|
|
61
|
+
alpha = when {
|
|
62
|
+
sheetTop <= dimmedDetentTop -> MAX_ALPHA
|
|
63
|
+
|
|
64
|
+
sheetTop >= belowDimmedTop -> 0f
|
|
65
|
+
|
|
66
|
+
else -> {
|
|
67
|
+
val progress = 1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)
|
|
68
|
+
(progress * MAX_ALPHA).coerceIn(0f, MAX_ALPHA)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
<item name="android:windowExitAnimation">@anim/true_sheet_slide_out</item>
|
|
7
7
|
</style>
|
|
8
8
|
|
|
9
|
-
<!--
|
|
10
|
-
<style name="
|
|
11
|
-
<item name="android:windowEnterAnimation">@anim/true_sheet_fade_in</item>
|
|
9
|
+
<!-- Fade out only - used when hiding sheet for RN Screens modal -->
|
|
10
|
+
<style name="TrueSheetFadeOutAnimation" parent="Animation.AppCompat.Dialog">
|
|
12
11
|
<item name="android:windowExitAnimation">@anim/true_sheet_fade_out</item>
|
|
13
12
|
</style>
|
|
14
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lodev09/react-native-true-sheet",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0-beta.0",
|
|
4
4
|
"description": "The true native bottom sheet experience for your React Native Apps.",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"clean": "scripts/clean.sh",
|
|
54
54
|
"prepare": "bob build",
|
|
55
55
|
"release": "release-it",
|
|
56
|
-
"release:beta": "yarn release
|
|
56
|
+
"release:beta": "yarn release --preRelease=beta"
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
59
|
"react-native",
|