@momo-kits/native-kits 0.152.4-beta.5 → 0.152.4-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CODE_OF_CONDUCT.md +133 -0
  2. package/CONTRIBUTING.md +114 -0
  3. package/LICENSE +20 -0
  4. package/README.md +7 -0
  5. package/build.gradle.kts +32 -0
  6. package/compose/MoMoComposeKits.podspec +54 -0
  7. package/compose/build.gradle.kts +149 -0
  8. package/compose/src/androidMain/AndroidManifest.xml +2 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +105 -0
  10. package/compose/src/commonMain/composeResources/files/lottie_circle_loader.json +1 -0
  11. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  12. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  21. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  22. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  23. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  24. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +57 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +201 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +222 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +48 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +306 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +715 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +214 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +392 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +77 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +27 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +334 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +345 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +90 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +131 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +58 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +143 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +179 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +111 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +384 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +160 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +234 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +223 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +232 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +236 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +228 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +364 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +50 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +34 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +85 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +33 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +338 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +95 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +64 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +89 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +84 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +208 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +172 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +199 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +237 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +191 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +13 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +191 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +258 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +59 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +68 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +49 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +232 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +111 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +94 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +159 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +302 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScaleSizeScope.kt +17 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +484 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +169 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +216 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +180 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +251 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +80 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +306 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +31 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +385 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +123 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +38 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +62 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +88 -0
  109. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +144 -0
  110. package/gradle.properties +19 -0
  111. package/gradlew +240 -0
  112. package/gradlew.bat +91 -0
  113. package/ios/Application/ApplicationEnvironment.swift +50 -0
  114. package/ios/Application/Components.swift +263 -0
  115. package/ios/Application/ComposeApi.swift +22 -0
  116. package/ios/Application/FloatingButton.swift +172 -0
  117. package/ios/Application/HeaderRight.swift +271 -0
  118. package/ios/Application/Screen.swift +249 -0
  119. package/ios/Badge/BadgeDot.swift +31 -0
  120. package/ios/Button/Button.swift +211 -0
  121. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +126 -0
  122. package/ios/Checkbox/Checkbox.swift +81 -0
  123. package/ios/Chip/Chip.swift +96 -0
  124. package/ios/Colors+Radius+Spacing/Colors.swift +172 -0
  125. package/ios/Colors+Radius+Spacing/Radius.swift +22 -0
  126. package/ios/Colors+Radius+Spacing/Spacing.swift +12 -0
  127. package/ios/Extensions/Color++.swift +25 -0
  128. package/ios/Icon/Icon.swift +51 -0
  129. package/ios/Image/Image.swift +70 -0
  130. package/ios/Input/Input.swift +207 -0
  131. package/ios/Input/InputPhoneNumber.swift +176 -0
  132. package/ios/Input/InputSearch.swift +238 -0
  133. package/ios/Input/InputTextArea.swift +242 -0
  134. package/ios/Lottie/LottieView.swift +86 -0
  135. package/ios/OTPKeyboard/KeyboardButton.swift +41 -0
  136. package/ios/OTPKeyboard/OTPKeyboard.swift +145 -0
  137. package/ios/Popup/PopupDisplay.swift +284 -0
  138. package/ios/Popup/PopupInput.swift +96 -0
  139. package/ios/Popup/PopupPromotion.swift +73 -0
  140. package/ios/PopupView/FullscreenPopup.swift +251 -0
  141. package/ios/PopupView/Modifiers.swift +158 -0
  142. package/ios/PopupView/PopupView.swift +289 -0
  143. package/ios/PopupView/Utils++.swift +281 -0
  144. package/ios/ScrollIndicator/ScrollIndicator.swift +110 -0
  145. package/ios/Swipeable/SwipeCell.swift +278 -0
  146. package/ios/Swipeable/SwipeCellModel.swift +86 -0
  147. package/ios/Switch/Switch.swift +44 -0
  148. package/ios/Template/Logo/Logo.swift +75 -0
  149. package/ios/Template/TrustBanner/TrustBanner.swift +120 -0
  150. package/ios/Theme.md +18 -0
  151. package/ios/Typography/Text.swift +140 -0
  152. package/ios/Typography/Typography.swift +95 -0
  153. package/ios/native-kits.podspec +18 -0
  154. package/package.json +6 -7
  155. package/settings.gradle.kts +25 -0
  156. package/shared/build.gradle.kts +0 -74
@@ -0,0 +1,543 @@
1
+ package vn.momo.kits.components
2
+
3
+ import androidx.compose.animation.core.AnimationState
4
+ import androidx.compose.animation.core.VectorConverter
5
+ import androidx.compose.animation.core.animateTo
6
+ import androidx.compose.animation.core.spring
7
+ import androidx.compose.foundation.OverscrollEffect
8
+ import androidx.compose.material.ExperimentalMaterialApi
9
+ import androidx.compose.runtime.Stable
10
+ import androidx.compose.runtime.State
11
+ import androidx.compose.runtime.getValue
12
+ import androidx.compose.runtime.mutableStateOf
13
+ import androidx.compose.runtime.setValue
14
+ import androidx.compose.ui.Modifier
15
+ import androidx.compose.ui.geometry.Offset
16
+ import androidx.compose.ui.geometry.Rect
17
+ import androidx.compose.ui.geometry.Size
18
+ import androidx.compose.ui.geometry.toRect
19
+ import androidx.compose.ui.graphics.drawscope.ContentDrawScope
20
+ import androidx.compose.ui.graphics.drawscope.clipRect
21
+ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
22
+ import androidx.compose.ui.input.pointer.PointerEvent
23
+ import androidx.compose.ui.input.pointer.PointerEventPass
24
+ import androidx.compose.ui.input.pointer.PointerEventType
25
+ import androidx.compose.ui.layout.Measurable
26
+ import androidx.compose.ui.layout.MeasureResult
27
+ import androidx.compose.ui.layout.MeasureScope
28
+ import androidx.compose.ui.node.DelegatableNode
29
+ import androidx.compose.ui.node.DrawModifierNode
30
+ import androidx.compose.ui.node.LayoutAwareModifierNode
31
+ import androidx.compose.ui.node.LayoutModifierNode
32
+ import androidx.compose.ui.node.PointerInputModifierNode
33
+ import androidx.compose.ui.unit.Constraints
34
+ import androidx.compose.ui.unit.Density
35
+ import androidx.compose.ui.unit.Dp
36
+ import androidx.compose.ui.unit.IntOffset
37
+ import androidx.compose.ui.unit.IntSize
38
+ import androidx.compose.ui.unit.Velocity
39
+ import androidx.compose.ui.unit.dp
40
+ import androidx.compose.ui.unit.round
41
+ import androidx.compose.ui.unit.toOffset
42
+ import androidx.compose.ui.unit.toSize
43
+ import kotlinx.coroutines.CoroutineScope
44
+ import kotlinx.coroutines.cancel
45
+ import kotlinx.coroutines.isActive
46
+ import kotlin.coroutines.coroutineContext
47
+ import kotlin.math.abs
48
+ import kotlin.math.sign
49
+
50
+ private enum class CupertinoScrollSource {
51
+ DRAG, FLING
52
+ }
53
+
54
+ private enum class CupertinoOverscrollDirection {
55
+ UNKNOWN, VERTICAL, HORIZONTAL
56
+ }
57
+
58
+ private enum class CupertinoSpringAnimationReason {
59
+ FLING_FROM_OVERSCROLL, POSSIBLE_SPRING_IN_THE_END
60
+ }
61
+
62
+
63
+ private data class CupertinoOverscrollAvailableDelta(
64
+
65
+ val availableDelta: Float,
66
+
67
+
68
+ val newOverscrollValue: Float
69
+ )
70
+
71
+ data class PullToRefreshCustomState(
72
+ val refreshingState: State<Boolean>,
73
+ val onRefresh: () -> Unit,
74
+ val threadHold: Dp = 140.dp,
75
+ val refreshOffset: Dp = 100.dp,
76
+ val sendHaptic: (() -> Unit)? = null,
77
+ ) {
78
+ val position = mutableStateOf(0f)
79
+
80
+ fun updatePercentage(percentage: Float) {
81
+ position.value = percentage.coerceIn(0f, 1f)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * CupertinoOverscrollEffect
87
+ *
88
+ * @param density to be taken into consideration during computations;
89
+ * Cupertino formulas use DPs, and scroll machinery uses raw values.
90
+ *
91
+ * @param applyClip Some consumers of overscroll effect apply clip by themselves and some don't,
92
+ * thus this flag is needed to update our modifier chain and make the clipping correct in every case while avoiding redundancy
93
+ */
94
+ internal class CupertinoOverscrollEffect(
95
+ val pullRefreshState: PullToRefreshCustomState? = null,
96
+ private val density: Float,
97
+ val applyClip: Boolean
98
+ ) : OverscrollEffect {
99
+ /*
100
+ * Direction of scrolling for this overscroll effect, derived from arguments during
101
+ * [applyToScroll] calls. Technically this effect supports both dimensions, but current API requires
102
+ * that different stages of animations spawned by this effect for both dimensions
103
+ * end at the same time, which is not the case:
104
+ * Spring->Fling->Spring, Fling->Spring, Spring->Fling effects can have different timing per dimension
105
+ * (see Notes of https://github.com/JetBrains/compose-multiplatform-core/pull/609),
106
+ * which is not possible to express without changing API. Hence this effect will be fixed to latest
107
+ * received delta.
108
+ */
109
+ private var direction: CupertinoOverscrollDirection = CupertinoOverscrollDirection.UNKNOWN
110
+
111
+ /*
112
+ * Size of container is taking into consideration when computing rubber banding
113
+ */
114
+ private var scrollSize: Size = Size.Zero
115
+
116
+ /*
117
+ * Current offset in overscroll area
118
+ * Negative for bottom-right
119
+ * Positive for top-left
120
+ * Zero if within the scrollable range
121
+ * It will be mapped to the actual visible offset using the rubber banding rule inside
122
+ * [Modifier.offset] within [effectModifier]
123
+ */
124
+ private var overscrollOffsetState = mutableStateOf(Offset.Zero)
125
+ private var overscrollOffset: Offset
126
+ get() = overscrollOffsetState.value
127
+ set(value) {
128
+ overscrollOffsetState.value = value
129
+ drawCallScheduledByOffsetChange = true
130
+ }
131
+ private var drawCallScheduledByOffsetChange = true
132
+
133
+ private var lastFlingUnconsumedDelta: Offset = Offset.Zero
134
+ private val visibleOverscrollOffset: IntOffset
135
+ get() = overscrollOffsetState.value.rubberBanded().round()
136
+
137
+ override val isInProgress: Boolean
138
+ get() =
139
+ // If visible overscroll offset has at least one pixel
140
+ // this effect is considered to be in progress
141
+ visibleOverscrollOffset.toOffset().getDistance() > 0.5f
142
+
143
+ @OptIn(ExperimentalMaterialApi::class)
144
+ private val overscrollNode = CupertinoOverscrollNode(
145
+ pullRefreshState = pullRefreshState,
146
+ offset = { visibleOverscrollOffset },
147
+ onNodeRemeasured = { scrollSize = it.toSize() },
148
+ onDraw = ::onDraw,
149
+ applyClip = applyClip
150
+ )
151
+ override val node: DelegatableNode get() = overscrollNode
152
+
153
+ private fun onDraw() {
154
+ // Fix an issue where scrolling was cancelled but the overscroll effect was not completed.
155
+ // Reset the overscroll effect when no ongoing animation or interaction is applied.
156
+ if (!drawCallScheduledByOffsetChange && isInProgress && overscrollNode.pointersDown == 0) {
157
+ overscrollOffsetState.value = Offset.Zero
158
+ }
159
+
160
+ drawCallScheduledByOffsetChange = false
161
+ }
162
+
163
+ private fun NestedScrollSource.toCupertinoScrollSource(): CupertinoScrollSource? =
164
+ when (this) {
165
+ NestedScrollSource.UserInput -> CupertinoScrollSource.DRAG
166
+ NestedScrollSource.SideEffect -> CupertinoScrollSource.FLING
167
+ else -> null
168
+ }
169
+
170
+ /*
171
+ * Takes input scroll delta, current overscroll value, and scroll source, return [CupertinoOverscrollAvailableDelta]
172
+ */
173
+ @Stable
174
+ private fun availableDelta(
175
+ delta: Float,
176
+ overscroll: Float,
177
+ source: CupertinoScrollSource
178
+ ): CupertinoOverscrollAvailableDelta {
179
+ // if source is fling:
180
+ // 1. no delta will be consumed
181
+ // 2. overscroll will stay the same
182
+ if (source == CupertinoScrollSource.FLING) {
183
+ return CupertinoOverscrollAvailableDelta(delta, overscroll)
184
+ }
185
+
186
+ val newOverscroll = overscroll + delta
187
+
188
+ return if (delta >= 0f && overscroll <= 0f) {
189
+ if (newOverscroll > 0f) {
190
+ CupertinoOverscrollAvailableDelta(newOverscroll, 0f)
191
+ } else {
192
+ CupertinoOverscrollAvailableDelta(0f, newOverscroll)
193
+ }
194
+ } else if (delta <= 0f && overscroll >= 0f) {
195
+ if (newOverscroll < 0f) {
196
+ CupertinoOverscrollAvailableDelta(newOverscroll, 0f)
197
+ } else {
198
+ CupertinoOverscrollAvailableDelta(0f, newOverscroll)
199
+ }
200
+ } else {
201
+ CupertinoOverscrollAvailableDelta(0f, newOverscroll)
202
+ }
203
+ }
204
+
205
+ /*
206
+ * Returns the amount of scroll delta available after user performed scroll inside overscroll area
207
+ * It will update [overscroll] resulting in visual change because of [Modifier.offset] depending on it
208
+ */
209
+ private fun availableDelta(delta: Offset, source: CupertinoScrollSource): Offset {
210
+ val (x, overscrollX) = availableDelta(delta.x, overscrollOffset.x, source)
211
+ val (y, overscrollY) = availableDelta(delta.y, overscrollOffset.y, source)
212
+ println("availableDelta delta=${delta.y} consumed y=$y overscrollY=$overscrollY source=${source}")
213
+ overscrollOffset = Offset(overscrollX, overscrollY)
214
+
215
+ return Offset(x, y)
216
+ }
217
+
218
+ /*
219
+ * Semantics of this method match the [OverscrollEffect.applyToScroll] one,
220
+ * The only difference is NestedScrollSource being remapped to CupertinoScrollSource to narrow
221
+ * processed states invariant
222
+ */
223
+ private fun applyToScroll(
224
+ delta: Offset,
225
+ source: CupertinoScrollSource,
226
+ performScroll: (Offset) -> Offset
227
+ ): Offset {
228
+ // Calculate how much delta is available after being consumed by scrolling inside overscroll area
229
+ val deltaLeftForPerformScroll = availableDelta(delta, source)
230
+
231
+ // Then pass remaining delta to scroll closure
232
+ val deltaConsumedByPerformScroll = performScroll(deltaLeftForPerformScroll)
233
+
234
+ // Delta which is left after `performScroll` was invoked with availableDelta
235
+ val unconsumedDelta = deltaLeftForPerformScroll - deltaConsumedByPerformScroll
236
+
237
+ return when (source) {
238
+ CupertinoScrollSource.DRAG -> {
239
+ // [unconsumedDelta] is going into overscroll again in case a user drags and hits the
240
+ // overscroll->content->overscroll or content->overscroll scenario within single frame
241
+ overscrollOffset += unconsumedDelta
242
+ lastFlingUnconsumedDelta = Offset.Zero
243
+ delta - unconsumedDelta
244
+ }
245
+
246
+ CupertinoScrollSource.FLING -> {
247
+ // If unconsumedDelta is not Zero, [CupertinoOverscrollEffect] will cancel fling and
248
+ // start spring animation instead
249
+ lastFlingUnconsumedDelta = unconsumedDelta
250
+ delta - unconsumedDelta
251
+ }
252
+ }
253
+ }
254
+
255
+ override fun applyToScroll(
256
+ delta: Offset,
257
+ source: NestedScrollSource,
258
+ performScroll: (Offset) -> Offset
259
+ ): Offset {
260
+ springAnimationScope?.cancel()
261
+ springAnimationScope = null
262
+
263
+ direction = direction.combinedWith(delta.toCupertinoOverscrollDirection())
264
+
265
+ return source.toCupertinoScrollSource()?.let {
266
+ applyToScroll(delta, it, performScroll)
267
+ } ?: performScroll(delta)
268
+ }
269
+
270
+ override suspend fun applyToFling(
271
+ velocity: Velocity,
272
+ performFling: suspend (Velocity) -> Velocity
273
+ ) {
274
+ val availableFlingVelocity = playInitialSpringAnimationIfNeeded(velocity)
275
+ val velocityConsumedByFling = performFling(availableFlingVelocity)
276
+ val postFlingVelocity = availableFlingVelocity - velocityConsumedByFling
277
+
278
+ val unconsumedDelta = lastFlingUnconsumedDelta.toFloat()
279
+ if (unconsumedDelta == 0f && overscrollOffset == Offset.Zero) {
280
+ return
281
+ }
282
+ applyPullToRefresh()
283
+
284
+ playSpringAnimation(
285
+ unconsumedDelta,
286
+ postFlingVelocity.toFloat(),
287
+ CupertinoSpringAnimationReason.POSSIBLE_SPRING_IN_THE_END
288
+ )
289
+ }
290
+
291
+ private fun applyPullToRefresh() {
292
+ val (isRefresh, refresh, threshHold, _, sendHaptic) = pullRefreshState ?: return
293
+ val overscrollY = overscrollOffset.y
294
+ if (isRefresh.value || overscrollY < threshHold.value * density) return
295
+ refresh.invoke()
296
+ }
297
+
298
+ private fun Offset.toCupertinoOverscrollDirection(): CupertinoOverscrollDirection {
299
+ val hasXPart = abs(x) > 0f
300
+ val hasYPart = abs(y) > 0f
301
+
302
+ return if (hasXPart xor hasYPart) {
303
+ if (hasXPart) {
304
+ CupertinoOverscrollDirection.HORIZONTAL
305
+ } else {
306
+ // hasYPart != hasXPart and hasXPart is false
307
+ CupertinoOverscrollDirection.VERTICAL
308
+ }
309
+ } else {
310
+ // hasXPart and hasYPart are equal
311
+ CupertinoOverscrollDirection.UNKNOWN
312
+ }
313
+ }
314
+
315
+ private fun CupertinoOverscrollDirection.combinedWith(other: CupertinoOverscrollDirection): CupertinoOverscrollDirection =
316
+ when (this) {
317
+ CupertinoOverscrollDirection.UNKNOWN -> when (other) {
318
+ CupertinoOverscrollDirection.UNKNOWN -> CupertinoOverscrollDirection.UNKNOWN
319
+ CupertinoOverscrollDirection.VERTICAL -> CupertinoOverscrollDirection.VERTICAL
320
+ CupertinoOverscrollDirection.HORIZONTAL -> CupertinoOverscrollDirection.HORIZONTAL
321
+ }
322
+
323
+ CupertinoOverscrollDirection.VERTICAL -> when (other) {
324
+ CupertinoOverscrollDirection.UNKNOWN, CupertinoOverscrollDirection.VERTICAL -> CupertinoOverscrollDirection.VERTICAL
325
+ CupertinoOverscrollDirection.HORIZONTAL -> CupertinoOverscrollDirection.HORIZONTAL
326
+ }
327
+
328
+ CupertinoOverscrollDirection.HORIZONTAL -> when (other) {
329
+ CupertinoOverscrollDirection.UNKNOWN, CupertinoOverscrollDirection.HORIZONTAL -> CupertinoOverscrollDirection.HORIZONTAL
330
+ CupertinoOverscrollDirection.VERTICAL -> CupertinoOverscrollDirection.VERTICAL
331
+ }
332
+ }
333
+
334
+ private fun Velocity.toFloat(): Float =
335
+ toOffset().toFloat()
336
+
337
+ private fun Float.toVelocity(): Velocity =
338
+ toOffset().toVelocity()
339
+
340
+ private fun Offset.toFloat(): Float =
341
+ when (direction) {
342
+ CupertinoOverscrollDirection.UNKNOWN -> 0f
343
+ CupertinoOverscrollDirection.VERTICAL -> y
344
+ CupertinoOverscrollDirection.HORIZONTAL -> x
345
+ }
346
+
347
+ private fun Float.toOffset(): Offset =
348
+ when (direction) {
349
+ CupertinoOverscrollDirection.UNKNOWN -> Offset.Zero
350
+ CupertinoOverscrollDirection.VERTICAL -> Offset(0f, this)
351
+ CupertinoOverscrollDirection.HORIZONTAL -> Offset(this, 0f)
352
+ }
353
+
354
+ private suspend fun playInitialSpringAnimationIfNeeded(initialVelocity: Velocity): Velocity {
355
+ val velocity = initialVelocity.toFloat()
356
+ val overscroll = overscrollOffset.toFloat()
357
+
358
+ return if ((velocity <= 0f && overscroll > 0f) || (velocity >= 0f && overscroll < 0f)) {
359
+ playSpringAnimation(
360
+ unconsumedDelta = 0f,
361
+ velocity,
362
+ CupertinoSpringAnimationReason.FLING_FROM_OVERSCROLL
363
+ ).toVelocity()
364
+ } else {
365
+ initialVelocity
366
+ }
367
+ }
368
+
369
+ private var springAnimationScope: CoroutineScope? = null
370
+
371
+ private suspend fun playSpringAnimation(
372
+ unconsumedDelta: Float,
373
+ initialVelocity: Float,
374
+ reason: CupertinoSpringAnimationReason
375
+ ): Float {
376
+ val initialValue = overscrollOffset.toFloat() + unconsumedDelta
377
+ val initialSign = sign(initialValue)
378
+ var currentVelocity = initialVelocity
379
+
380
+ // All input values are divided by density so all internal calculations are performed as if
381
+ // they operated on DPs. Callback value is then scaled back to raw pixels.
382
+ val visibilityThreshold = 0.5f / density
383
+
384
+ val spec = when (reason) {
385
+ CupertinoSpringAnimationReason.FLING_FROM_OVERSCROLL -> {
386
+ spring(
387
+ stiffness = 300f,
388
+ visibilityThreshold = visibilityThreshold
389
+ )
390
+ }
391
+
392
+ CupertinoSpringAnimationReason.POSSIBLE_SPRING_IN_THE_END -> {
393
+ spring(
394
+ stiffness = 120f,
395
+ visibilityThreshold = visibilityThreshold
396
+ )
397
+ }
398
+ }
399
+
400
+ springAnimationScope?.cancel()
401
+ springAnimationScope = CoroutineScope(coroutineContext)
402
+ springAnimationScope?.run {
403
+ AnimationState(
404
+ Float.VectorConverter,
405
+ initialValue / density,
406
+ initialVelocity / density
407
+ ).animateTo(
408
+ targetValue = 0f,
409
+ animationSpec = spec
410
+ ) {
411
+ overscrollOffset = pullRefreshState?.let { (refreshing, _, _, refreshOffset) ->
412
+ if (!refreshing.value) return@let null
413
+ value.coerceAtLeast(refreshOffset.value * density).toOffset()
414
+ } ?: (value * density).toOffset()
415
+ currentVelocity = velocity * density
416
+
417
+ // If it was fling from overscroll, cancel animation and return velocity
418
+ if (reason == CupertinoSpringAnimationReason.FLING_FROM_OVERSCROLL &&
419
+ initialSign != 0f &&
420
+ sign(value) != initialSign
421
+ ) {
422
+ this.cancelAnimation()
423
+ }
424
+ }
425
+ springAnimationScope = null
426
+ }
427
+
428
+ if (coroutineContext.isActive) {
429
+ // The spring is critically damped, so in case spring-fling-spring sequence is slightly
430
+ // offset and velocity is of the opposite sign, it will end up with no animation
431
+ overscrollOffset = Offset.Zero
432
+ }
433
+
434
+ if (reason == CupertinoSpringAnimationReason.POSSIBLE_SPRING_IN_THE_END) {
435
+ currentVelocity = 0f
436
+ }
437
+
438
+ return currentVelocity
439
+ }
440
+
441
+ private fun Offset.rubberBanded(): Offset {
442
+ if (scrollSize.width == 0f || scrollSize.height == 0f) {
443
+ return Offset.Zero
444
+ }
445
+
446
+ val dpOffset = this / density
447
+ val dpSize = scrollSize / density
448
+
449
+ return Offset(
450
+ rubberBandedValue(dpOffset.x, dpSize.width, RUBBER_BAND_COEFFICIENT),
451
+ rubberBandedValue(dpOffset.y, dpSize.height, RUBBER_BAND_COEFFICIENT)
452
+ ) * density
453
+ }
454
+
455
+ /*
456
+ * Maps raw delta offset [value] on an axis within scroll container with [dimension]
457
+ * to actual visible offset
458
+ */
459
+ private fun rubberBandedValue(value: Float, dimension: Float, coefficient: Float) =
460
+ sign(value) * (1f - (1f / (abs(value) * coefficient / dimension + 1f))) * dimension
461
+
462
+ companion object Companion {
463
+ private const val RUBBER_BAND_COEFFICIENT = 0.55f
464
+ }
465
+ }
466
+
467
+ private class CupertinoOverscrollNode @OptIn(ExperimentalMaterialApi::class) constructor(
468
+ val pullRefreshState: PullToRefreshCustomState?,
469
+ val offset: Density.() -> IntOffset,
470
+ val onNodeRemeasured: (IntSize) -> Unit,
471
+ val onDraw: () -> Unit,
472
+ val applyClip: Boolean
473
+ ) : Modifier.Node(),
474
+ LayoutModifierNode,
475
+ LayoutAwareModifierNode,
476
+ DrawModifierNode,
477
+ PointerInputModifierNode {
478
+ override fun onRemeasured(size: IntSize) = onNodeRemeasured(size)
479
+
480
+ var pointersDown by mutableStateOf(0)
481
+
482
+ override fun onPointerEvent(
483
+ pointerEvent: PointerEvent,
484
+ pass: PointerEventPass,
485
+ bounds: IntSize
486
+ ) {
487
+ if (pass == PointerEventPass.Final) {
488
+ if (pointerEvent.type == PointerEventType.Press) {
489
+ pointersDown++
490
+ } else if (pointerEvent.type == PointerEventType.Release) {
491
+ pointersDown--
492
+ // assert(pointersDown >= 0) { "pointersDown cannot be negative" }
493
+ }
494
+ }
495
+ }
496
+
497
+ override fun onCancelPointerInput() {
498
+ pointersDown = 0
499
+ }
500
+
501
+ override fun ContentDrawScope.draw() {
502
+ onDraw()
503
+ if (applyClip) {
504
+ val bounds = Rect(-offset().toOffset(), size)
505
+ val rect = size.toRect().intersect(bounds)
506
+ clipRect(
507
+ left = rect.left,
508
+ top = rect.top,
509
+ right = rect.right,
510
+ bottom = rect.bottom,
511
+ ) { this@draw.drawContent() }
512
+ } else {
513
+ this@draw.drawContent()
514
+ }
515
+ }
516
+
517
+ override fun MeasureScope.measure(
518
+ measurable: Measurable,
519
+ constraints: Constraints
520
+ ): MeasureResult {
521
+ val placeable = measurable.measure(constraints)
522
+ return layout(placeable.width, placeable.height) {
523
+ var position = offset()
524
+
525
+ with(pullRefreshState) {
526
+ this ?: return@with
527
+ if (refreshingState.value) {
528
+ position = position.copy(
529
+ y = position.y.coerceAtLeast(refreshOffset.roundToPx())
530
+ )
531
+ }
532
+ updatePercentage(position.y / refreshOffset.toPx())
533
+ }
534
+ placeable.placeWithLayer(position)
535
+ }
536
+ }
537
+ }
538
+
539
+ private fun Velocity.toOffset(): Offset =
540
+ Offset(x, y)
541
+
542
+ private fun Offset.toVelocity(): Velocity =
543
+ Velocity(x, y)
@@ -0,0 +1,23 @@
1
+ package vn.momo.kits.components
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Box
5
+ import androidx.compose.foundation.layout.fillMaxWidth
6
+ import androidx.compose.foundation.layout.height
7
+ import androidx.compose.foundation.layout.padding
8
+ import androidx.compose.runtime.Composable
9
+ import androidx.compose.ui.Modifier
10
+ import androidx.compose.ui.unit.dp
11
+ import vn.momo.kits.const.AppTheme
12
+ import vn.momo.kits.const.Spacing
13
+
14
+ @Composable
15
+ fun Divider() {
16
+ Box(
17
+ modifier = Modifier
18
+ .fillMaxWidth()
19
+ .height(1.dp)
20
+ .background(color = AppTheme.current.colors.border.default)
21
+ .padding(vertical = Spacing.XS)
22
+ )
23
+ }
@@ -0,0 +1,58 @@
1
+ package vn.momo.kits.components
2
+
3
+ import androidx.compose.foundation.layout.Box
4
+ import androidx.compose.foundation.layout.size
5
+ import androidx.compose.runtime.Composable
6
+ import androidx.compose.runtime.remember
7
+ import androidx.compose.ui.Modifier
8
+ import androidx.compose.ui.graphics.Color
9
+ import androidx.compose.ui.graphics.ColorFilter
10
+ import androidx.compose.ui.layout.ContentScale
11
+ import androidx.compose.ui.semantics.contentDescription
12
+ import androidx.compose.ui.semantics.semantics
13
+ import androidx.compose.ui.unit.Dp
14
+ import androidx.compose.ui.unit.dp
15
+ import coil3.compose.AsyncImage
16
+ import vn.momo.kits.const.AppTheme
17
+ import vn.momo.kits.utils.Icons
18
+ import vn.momo.kits.utils.noThemeIcons
19
+
20
+ @Composable
21
+ fun Icon(
22
+ source: String,
23
+ size: Dp = 24.dp,
24
+ color: Color? = AppTheme.current.colors.text.default,
25
+ modifier: Modifier = Modifier,
26
+ ) {
27
+ val iconUrl = remember(source) {
28
+ if (source.contains("https")) {
29
+ source
30
+ } else {
31
+ Icons[source] ?: ""
32
+ }
33
+ }
34
+
35
+ val iconColor = remember(color) {
36
+ if (noThemeIcons.contains(source)) null else color
37
+ }
38
+
39
+ val colorFilter = remember(iconColor) {
40
+ iconColor?.let { ColorFilter.tint(it) }
41
+ }
42
+
43
+ val contentDesc = remember(iconUrl) { "img|$iconUrl" }
44
+
45
+ Box(
46
+ modifier = modifier
47
+ .size(size)
48
+ .semantics { contentDescription = contentDesc }
49
+ ) {
50
+ AsyncImage(
51
+ modifier = Modifier.matchParentSize(),
52
+ model = iconUrl,
53
+ contentDescription = null,
54
+ contentScale = ContentScale.Fit,
55
+ colorFilter = colorFilter,
56
+ )
57
+ }
58
+ }