@imgly/editor-react-native 1.81.0-rc.0 → 1.81.0-rc.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.
Files changed (74) hide show
  1. package/android/build.gradle +4 -1
  2. package/package.json +1 -1
  3. package/src/NativeIMGLYEditor.ts +1 -1
  4. package/android/src/main/assets/ly.img.memories.style/content.json +0 -50
  5. package/android/src/main/assets/ly.img.memories.style/thumbnails/bubblegum.png +0 -0
  6. package/android/src/main/assets/ly.img.memories.style/thumbnails/hologram.png +0 -0
  7. package/android/src/main/assets/ly.img.memories.style/thumbnails/noir.png +0 -0
  8. package/android/src/main/assets/ly.img.memories.style/videos/bubblegum.mp4 +0 -0
  9. package/android/src/main/assets/ly.img.memories.style/videos/hologram.mp4 +0 -0
  10. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesApp.kt +0 -77
  11. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesConfiguration.kt +0 -38
  12. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesEditor.kt +0 -79
  13. package/android/src/main/kotlin/ly/img/editor/configuration/memories/MemoriesViewModel.kt +0 -469
  14. package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnCreate.kt +0 -69
  15. package/android/src/main/kotlin/ly/img/editor/configuration/memories/callback/OnExport.kt +0 -46
  16. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/BottomPanel.kt +0 -151
  17. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Dock.kt +0 -145
  18. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/ExportOverlay.kt +0 -229
  19. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/NavigationBar.kt +0 -80
  20. package/android/src/main/kotlin/ly/img/editor/configuration/memories/component/Overlay.kt +0 -138
  21. package/android/src/main/kotlin/ly/img/editor/configuration/memories/extension/EditorScopeExt.kt +0 -111
  22. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/AddPhotoAlternate.kt +0 -79
  23. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/ArrowForward.kt +0 -45
  24. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Audiotrack.kt +0 -43
  25. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Check.kt +0 -39
  26. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/CheckCircle.kt +0 -44
  27. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Close.kt +0 -43
  28. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Delete.kt +0 -54
  29. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/DeleteOutline.kt +0 -53
  30. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Fullscreen.kt +0 -74
  31. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Image.kt +0 -51
  32. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Info.kt +0 -48
  33. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Pause.kt +0 -46
  34. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/PlayArrow.kt +0 -38
  35. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOff.kt +0 -80
  36. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/RepeatOn.kt +0 -59
  37. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/SkipPrevious.kt +0 -46
  38. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Sort.kt +0 -55
  39. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Style.kt +0 -59
  40. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/Title.kt +0 -43
  41. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/TouchTriple.kt +0 -134
  42. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeDown.kt +0 -48
  43. package/android/src/main/kotlin/ly/img/editor/configuration/memories/iconPack/VolumeUp.kt +0 -60
  44. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ExportStatus.kt +0 -20
  45. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/ImageItem.kt +0 -8
  46. package/android/src/main/kotlin/ly/img/editor/configuration/memories/model/TimelineImage.kt +0 -18
  47. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Events.kt +0 -59
  48. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Playback.kt +0 -29
  49. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/SceneSetup.kt +0 -79
  50. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Timeline.kt +0 -27
  51. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/Title.kt +0 -162
  52. package/android/src/main/kotlin/ly/img/editor/configuration/memories/scene/TrackEditor.kt +0 -740
  53. package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/ImageSelectionScreen.kt +0 -387
  54. package/android/src/main/kotlin/ly/img/editor/configuration/memories/screen/LoadingScreen.kt +0 -258
  55. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheet.kt +0 -167
  56. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/FilterStylesSheetType.kt +0 -21
  57. package/android/src/main/kotlin/ly/img/editor/configuration/memories/sheet/VolumeSheet.kt +0 -104
  58. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleApplier.kt +0 -220
  59. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/StyleAssetSource.kt +0 -43
  60. package/android/src/main/kotlin/ly/img/editor/configuration/memories/style/VideoStyle.kt +0 -118
  61. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Animations.kt +0 -58
  62. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Constants.kt +0 -20
  63. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/ImageDate.kt +0 -81
  64. package/android/src/main/kotlin/ly/img/editor/configuration/memories/util/Utils.kt +0 -27
  65. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/CenteredAddImageButton.kt +0 -82
  66. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/DraggableImageItem.kt +0 -145
  67. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/FullscreenVideoControls.kt +0 -136
  68. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGrid.kt +0 -122
  69. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageGridOverlay.kt +0 -149
  70. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageTimelineStrip.kt +0 -323
  71. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ImageToolbar.kt +0 -401
  72. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/ReorderableGridState.kt +0 -183
  73. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoControlPanel.kt +0 -180
  74. package/android/src/main/kotlin/ly/img/editor/configuration/memories/widget/VideoThumbnail.kt +0 -110
@@ -1,401 +0,0 @@
1
- @file:OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
2
-
3
- package ly.img.editor.configuration.memories.widget
4
-
5
- import androidx.compose.animation.AnimatedVisibility
6
- import androidx.compose.animation.scaleIn
7
- import androidx.compose.animation.scaleOut
8
- import androidx.compose.foundation.background
9
- import androidx.compose.foundation.clickable
10
- import androidx.compose.foundation.interaction.MutableInteractionSource
11
- import androidx.compose.foundation.layout.Arrangement
12
- import androidx.compose.foundation.layout.Box
13
- import androidx.compose.foundation.layout.Row
14
- import androidx.compose.foundation.layout.fillMaxHeight
15
- import androidx.compose.foundation.layout.fillMaxSize
16
- import androidx.compose.foundation.layout.fillMaxWidth
17
- import androidx.compose.foundation.layout.height
18
- import androidx.compose.foundation.layout.padding
19
- import androidx.compose.foundation.layout.size
20
- import androidx.compose.foundation.shape.CircleShape
21
- import androidx.compose.foundation.shape.RoundedCornerShape
22
- import androidx.compose.material.ripple.rememberRipple
23
- import androidx.compose.material3.AlertDialog
24
- import androidx.compose.material3.Badge
25
- import androidx.compose.material3.BadgedBox
26
- import androidx.compose.material3.Card
27
- import androidx.compose.material3.CardDefaults
28
- import androidx.compose.material3.Icon
29
- import androidx.compose.material3.IconButton
30
- import androidx.compose.material3.LocalContentColor
31
- import androidx.compose.material3.MaterialTheme
32
- import androidx.compose.material3.Text
33
- import androidx.compose.material3.TextButton
34
- import androidx.compose.material3.minimumInteractiveComponentSize
35
- import androidx.compose.runtime.Composable
36
- import androidx.compose.runtime.CompositionLocalProvider
37
- import androidx.compose.runtime.getValue
38
- import androidx.compose.runtime.mutableStateOf
39
- import androidx.compose.runtime.remember
40
- import androidx.compose.runtime.setValue
41
- import androidx.compose.ui.Alignment
42
- import androidx.compose.ui.Modifier
43
- import androidx.compose.ui.graphics.Color
44
- import androidx.compose.ui.graphics.graphicsLayer
45
- import androidx.compose.ui.semantics.Role
46
- import androidx.compose.ui.tooling.preview.Preview
47
- import androidx.compose.ui.unit.dp
48
- import ly.img.editor.configuration.memories.iconPack.AddPhotoAlternate
49
- import ly.img.editor.configuration.memories.iconPack.ArrowForward
50
- import ly.img.editor.configuration.memories.iconPack.Check
51
- import ly.img.editor.configuration.memories.iconPack.Delete
52
- import ly.img.editor.configuration.memories.iconPack.DeleteOutline
53
- import ly.img.editor.configuration.memories.iconPack.Sort
54
- import ly.img.editor.configuration.memories.iconPack.TouchTriple
55
- import ly.img.editor.core.iconpack.IconPack
56
-
57
- @Composable
58
- fun ImageToolbar(
59
- modifier: Modifier = Modifier,
60
- onAddImage: () -> Unit = {},
61
- onToggleSort: () -> Unit = {},
62
- onDeleteAll: (() -> Unit)? = null,
63
- onDeleteSelected: () -> Unit = {},
64
- onProceed: () -> Unit = {},
65
- isSortEnabled: Boolean = false,
66
- isSortAscending: Boolean = true,
67
- hasImages: Boolean = false,
68
- showDeleteButton: Boolean = true,
69
- selectedForDeletionCount: Int = 0,
70
- totalCount: Int = 0,
71
- confirmMode: Boolean = false,
72
- multiSelectMode: Boolean = false,
73
- onToggleMultiSelect: () -> Unit = {},
74
- ) {
75
- var showDeleteDialog by remember { mutableStateOf(false) }
76
-
77
- Row(
78
- modifier = modifier
79
- .fillMaxWidth(),
80
- horizontalArrangement = Arrangement.spacedBy(
81
- 12.dp,
82
- alignment = Alignment.CenterHorizontally,
83
- ),
84
- verticalAlignment = Alignment.CenterVertically,
85
- ) {
86
- // Left pill with multiple actions
87
- Card(
88
- modifier = Modifier
89
- .weight(1f, false)
90
- .height(64.dp),
91
- shape = CircleShape,
92
- elevation = CardDefaults.elevatedCardElevation(
93
- defaultElevation = 3.dp,
94
- ),
95
- colors = CardDefaults.cardColors(
96
- containerColor = MaterialTheme.colorScheme.surfaceVariant,
97
- ),
98
- ) {
99
- Row(
100
- modifier = Modifier
101
- .fillMaxHeight()
102
- .padding(horizontal = 8.dp),
103
- horizontalArrangement = Arrangement.Center,
104
- verticalAlignment = Alignment.CenterVertically,
105
- ) {
106
- // Add photos button — hidden while selecting images to delete. Its trailing
107
- // gap lives inside the AnimatedVisibility so it collapses with the button.
108
- AnimatedVisibility(visible = !multiSelectMode) {
109
- IconButton(
110
- onClick = onAddImage,
111
- modifier = Modifier.padding(end = 4.dp),
112
- ) {
113
- Icon(
114
- imageVector = IconPack.AddPhotoAlternate,
115
- contentDescription = "Add photos",
116
- )
117
- }
118
- }
119
-
120
- // Multi-select toggle
121
- IconButton(
122
- onClick = onToggleMultiSelect,
123
- modifier = Modifier
124
- .background(
125
- color = if (multiSelectMode) {
126
- MaterialTheme.colorScheme.primaryContainer
127
- } else {
128
- Color.Transparent
129
- },
130
- shape = CircleShape,
131
- ),
132
- ) {
133
- Icon(
134
- imageVector = IconPack.TouchTriple,
135
- contentDescription = if (multiSelectMode) {
136
- "Exit selection mode"
137
- } else {
138
- "Select media"
139
- },
140
- tint = if (multiSelectMode) {
141
- MaterialTheme.colorScheme.onPrimaryContainer
142
- } else {
143
- LocalContentColor.current
144
- },
145
- )
146
- }
147
-
148
- // Sort button
149
- IconButton(
150
- onClick = onToggleSort,
151
- modifier = Modifier
152
- .padding(start = 4.dp)
153
- .background(
154
- color = if (isSortEnabled) {
155
- MaterialTheme.colorScheme.primaryContainer
156
- } else {
157
- Color.Transparent
158
- },
159
- shape = CircleShape,
160
- ),
161
- ) {
162
- Icon(
163
- imageVector = IconPack.Sort,
164
- contentDescription = "Sort by date",
165
- modifier = Modifier
166
- .graphicsLayer(
167
- scaleY = if (isSortEnabled && !isSortAscending) -1f else 1f,
168
- ),
169
- tint = if (isSortEnabled) {
170
- MaterialTheme.colorScheme.onPrimaryContainer
171
- } else {
172
- LocalContentColor.current
173
- },
174
- )
175
- }
176
-
177
- // Delete button. Off: filled trash → delete all. On: outlined trash with a
178
- // count badge (from 0) → delete the current selection.
179
- if (showDeleteButton) {
180
- val deleteEnabled =
181
- if (multiSelectMode) selectedForDeletionCount > 0 else hasImages
182
- // A plain IconButton clips its content to a circular state layer, which cuts off
183
- // the BadgedBox count badge at the icon's corner. Use an unclipped, unbounded button.
184
- UnboundedIconButton(
185
- onClick = { showDeleteDialog = true },
186
- enabled = deleteEnabled,
187
- modifier = Modifier.padding(start = 4.dp),
188
- ) {
189
- BadgedBox(
190
- badge = {
191
- SelectionCountBadge(
192
- visible = multiSelectMode,
193
- count = selectedForDeletionCount,
194
- )
195
- },
196
- ) {
197
- Icon(
198
- imageVector = if (multiSelectMode) {
199
- IconPack.DeleteOutline
200
- } else {
201
- IconPack.Delete
202
- },
203
- contentDescription = if (multiSelectMode) {
204
- "Delete selected media"
205
- } else {
206
- "Delete all media"
207
- },
208
- )
209
- }
210
- }
211
- }
212
- }
213
- }
214
-
215
- // Right pill - Proceed button
216
- Card(
217
- modifier = Modifier
218
- .size(56.dp),
219
- shape = RoundedCornerShape(14.dp),
220
- elevation = CardDefaults.elevatedCardElevation(
221
- defaultElevation = 3.dp,
222
- ),
223
- colors = CardDefaults.cardColors(
224
- containerColor = if (hasImages) {
225
- MaterialTheme.colorScheme.primary
226
- } else {
227
- MaterialTheme.colorScheme.surfaceVariant
228
- },
229
- ),
230
- ) {
231
- IconButton(
232
- onClick = onProceed,
233
- enabled = hasImages,
234
- modifier = Modifier.fillMaxSize(),
235
- ) {
236
- Icon(
237
- imageVector = if (confirmMode) {
238
- IconPack.Check
239
- } else {
240
- IconPack.ArrowForward
241
- },
242
- contentDescription = if (confirmMode) "Apply changes" else "Proceed",
243
- tint = if (hasImages) {
244
- MaterialTheme.colorScheme.onPrimary
245
- } else {
246
- MaterialTheme.colorScheme.onSurfaceVariant
247
- },
248
- )
249
- }
250
- }
251
- }
252
-
253
- if (showDeleteDialog) {
254
- val count = if (multiSelectMode) selectedForDeletionCount else totalCount
255
- val noun = if (count == 1) "item" else "items"
256
- DeleteConfirmationDialog(
257
- title = if (multiSelectMode) "Delete selected media?" else "Delete all media?",
258
- message = if (multiSelectMode) {
259
- "Remove $count selected $noun? This can't be undone."
260
- } else {
261
- "Remove all $count $noun? This can't be undone."
262
- },
263
- onConfirm = {
264
- showDeleteDialog = false
265
- if (multiSelectMode) onDeleteSelected() else onDeleteAll?.invoke()
266
- },
267
- onDismiss = { showDeleteDialog = false },
268
- )
269
- }
270
- }
271
-
272
- /**
273
- * An icon button that does not clip its content to a circle (so a [BadgedBox] badge can overflow
274
- * past the icon's corner) and ripples without a bounded circular mask.
275
- */
276
- @Composable
277
- private fun UnboundedIconButton(
278
- onClick: () -> Unit,
279
- modifier: Modifier = Modifier,
280
- enabled: Boolean = true,
281
- content: @Composable () -> Unit,
282
- ) {
283
- Box(
284
- modifier = modifier
285
- .minimumInteractiveComponentSize()
286
- .size(48.dp)
287
- .clickable(
288
- interactionSource = remember { MutableInteractionSource() },
289
- indication = rememberRipple(bounded = false, radius = 22.dp),
290
- enabled = enabled,
291
- role = Role.Button,
292
- onClick = onClick,
293
- ),
294
- contentAlignment = Alignment.Center,
295
- ) {
296
- val baseColor = LocalContentColor.current
297
- CompositionLocalProvider(
298
- LocalContentColor provides if (enabled) baseColor else baseColor.copy(alpha = 0.38f),
299
- content = content,
300
- )
301
- }
302
- }
303
-
304
- @Composable
305
- private fun SelectionCountBadge(
306
- visible: Boolean,
307
- count: Int,
308
- ) {
309
- AnimatedVisibility(
310
- visible,
311
- enter = scaleIn(initialScale = 0f),
312
- exit = scaleOut(targetScale = 0f),
313
- ) {
314
- Badge {
315
- Text("$count")
316
- }
317
- }
318
- }
319
-
320
- @Composable
321
- private fun DeleteConfirmationDialog(
322
- title: String,
323
- message: String,
324
- onConfirm: () -> Unit,
325
- onDismiss: () -> Unit,
326
- ) {
327
- AlertDialog(
328
- onDismissRequest = onDismiss,
329
- icon = {
330
- Icon(imageVector = IconPack.Delete, contentDescription = null)
331
- },
332
- title = { Text(title) },
333
- text = { Text(message) },
334
- confirmButton = {
335
- TextButton(onClick = onConfirm) {
336
- Text("Delete", color = MaterialTheme.colorScheme.error)
337
- }
338
- },
339
- dismissButton = {
340
- TextButton(onClick = onDismiss) {
341
- Text("Cancel")
342
- }
343
- },
344
- )
345
- }
346
-
347
- @Preview(name = "Empty State", showBackground = true)
348
- @Composable
349
- fun ImageToolbar_EmptyState_Preview() {
350
- MaterialTheme {
351
- ImageToolbar(
352
- modifier = Modifier.padding(16.dp),
353
- hasImages = false,
354
- showDeleteButton = true,
355
- onDeleteAll = {},
356
- )
357
- }
358
- }
359
-
360
- @Preview(name = "With Images - Clear all", showBackground = true)
361
- @Composable
362
- fun ImageToolbar_WithImages_Preview() {
363
- MaterialTheme {
364
- ImageToolbar(
365
- modifier = Modifier.padding(16.dp),
366
- hasImages = true,
367
- totalCount = 8,
368
- showDeleteButton = true,
369
- onDeleteAll = {},
370
- )
371
- }
372
- }
373
-
374
- @Preview(name = "Multi-select mode (badge)", showBackground = true)
375
- @Composable
376
- fun ImageToolbar_SelectionMode_Preview() {
377
- MaterialTheme {
378
- ImageToolbar(
379
- modifier = Modifier.padding(16.dp),
380
- hasImages = true,
381
- totalCount = 8,
382
- selectedForDeletionCount = 3,
383
- showDeleteButton = true,
384
- onDeleteAll = {},
385
- multiSelectMode = true,
386
- )
387
- }
388
- }
389
-
390
- @Preview(name = "Overlay Mode (No Delete)", showBackground = true)
391
- @Composable
392
- fun ImageToolbar_OverlayMode_Preview() {
393
- MaterialTheme {
394
- ImageToolbar(
395
- modifier = Modifier.padding(16.dp),
396
- hasImages = true,
397
- isSortEnabled = true,
398
- showDeleteButton = false,
399
- )
400
- }
401
- }
@@ -1,183 +0,0 @@
1
- package ly.img.editor.configuration.memories.widget
2
-
3
- import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
4
- import androidx.compose.foundation.lazy.grid.LazyGridState
5
- import androidx.compose.runtime.Composable
6
- import androidx.compose.runtime.State
7
- import androidx.compose.runtime.getValue
8
- import androidx.compose.runtime.mutableFloatStateOf
9
- import androidx.compose.runtime.mutableStateOf
10
- import androidx.compose.runtime.remember
11
- import androidx.compose.runtime.rememberUpdatedState
12
- import androidx.compose.runtime.setValue
13
- import androidx.compose.ui.geometry.Offset
14
- import androidx.compose.ui.graphics.graphicsLayer
15
- import androidx.compose.ui.platform.LocalDensity
16
- import androidx.compose.ui.unit.IntOffset
17
- import androidx.compose.ui.unit.dp
18
-
19
- /**
20
- * Holds the live drag state. Kept outside the composable so the gesture callbacks can mutate it
21
- * without re-running composition until something observable actually changes.
22
- *
23
- * Consumed by [ImageGrid] via [rememberReorderableGridState]: it drives the drag-and-drop
24
- * reordering of the media grid built on Compose Foundation / AndroidX APIs (no third-party
25
- * reorder libraries).
26
- */
27
- class ReorderableGridState internal constructor(
28
- private val gridState: LazyGridState,
29
- private val scrollThresholdPx: Float,
30
- private val maxScrollPerFramePx: Float,
31
- private val scrollActivationSlopPx: Float,
32
- private val onMove: (Int, Int) -> Unit,
33
- ) {
34
- // Current index of the dragged item. Changes as it swaps past neighbours.
35
- var draggingItemIndex by mutableStateOf<Int?>(null)
36
- private set
37
-
38
- val isDragging: Boolean get() = draggingItemIndex != null
39
-
40
- // Auto-scroll speed in px/frame, set during onDrag and consumed by the LaunchedEffect.
41
- var autoScrollSpeed by mutableFloatStateOf(0f)
42
- private set
43
-
44
- // Layout offset of the item at pick-up (viewport coords) plus the finger's accumulated travel.
45
- private var initialOffset by mutableStateOf(Offset.Zero)
46
- private var draggedDelta by mutableStateOf(Offset.Zero)
47
-
48
- private val draggingItemLayoutInfo: LazyGridItemInfo?
49
- get() = gridState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
50
-
51
- /**
52
- * The translation to apply to the dragged cell's [graphicsLayer]. Drawing the cell at
53
- * `currentLayoutOffset + draggingItemOffset` always lands it at `initialOffset + draggedDelta`
54
- * — i.e. exactly under the finger — no matter where its layout slot currently sits. That is the
55
- * re-anchor: when a swap changes the item's slot (or auto-scroll shifts the layout), this value
56
- * compensates automatically instead of snapping back.
57
- */
58
- val draggingItemOffset: Offset
59
- get() = draggingItemLayoutInfo?.let { item ->
60
- initialOffset + draggedDelta - item.offset.toOffset()
61
- } ?: Offset.Zero
62
-
63
- fun onDragStart(offset: Offset) {
64
- // detectDragGesturesAfterLongPress reports the touch relative to the grid node's top-left
65
- // (the viewport, which includes the content-padding region). visibleItemsInfo offsets live in
66
- // the item coordinate space whose origin sits at viewportStartOffset (= -beforeContentPadding
67
- // on the main axis). Shift the pointer onto that space before hit-testing, or a large top
68
- // content padding makes us pick a cell below the one actually touched. Vertical grid → only
69
- // the main (y) axis is affected.
70
- val pointer = offset.copy(y = offset.y + gridState.layoutInfo.viewportStartOffset)
71
- gridState.layoutInfo.visibleItemsInfo
72
- .firstOrNull { item -> pointer.inBounds(item) }
73
- ?.also {
74
- draggingItemIndex = it.index
75
- initialOffset = it.offset.toOffset()
76
- draggedDelta = Offset.Zero
77
- }
78
- }
79
-
80
- fun onDrag(dragAmount: Offset) {
81
- draggedDelta += dragAmount
82
- val dragged = draggingItemLayoutInfo ?: return
83
- val from = draggingItemIndex ?: return
84
-
85
- // Visual top-left then centre of the dragged cell, in viewport coords.
86
- val topLeft = dragged.offset.toOffset() + draggingItemOffset
87
- val center = topLeft + Offset(dragged.size.width / 2f, dragged.size.height / 2f)
88
-
89
- // Hit-test the centre against every other visible cell; swap into the first that contains it.
90
- val target = gridState.layoutInfo.visibleItemsInfo.firstOrNull { item ->
91
- item.index != from && center.inBounds(item)
92
- }
93
- if (target != null) {
94
- onMove(from, target.index)
95
- // Re-point to the new index; draggingItemOffset re-anchors so the cell stays put.
96
- draggingItemIndex = target.index
97
- }
98
-
99
- autoScrollSpeed = computeAutoScroll(topLeft.y, dragged.size.height.toFloat())
100
- }
101
-
102
- fun onDragInterrupted() {
103
- draggingItemIndex = null
104
- initialOffset = Offset.Zero
105
- draggedDelta = Offset.Zero
106
- autoScrollSpeed = 0f
107
- }
108
-
109
- /**
110
- * Positive = scroll down, negative = scroll up, 0 = no auto-scroll.
111
- *
112
- * Being inside an edge zone is not enough: a cell picked up while already sitting in the top (or
113
- * bottom) zone must not yank the list the instant the gesture moves. So we also require the user
114
- * to have dragged *toward* that edge past [scrollActivationSlopPx]. [draggedDelta] is cumulative
115
- * finger travel since pick-up (auto-scroll does not change it), so a horizontal-only drag keeps
116
- * `draggedDelta.y ≈ 0` and never triggers a scroll.
117
- */
118
- private fun computeAutoScroll(
119
- top: Float,
120
- height: Float,
121
- ): Float {
122
- val bottom = top + height
123
- val viewStart = gridState.layoutInfo.viewportStartOffset.toFloat()
124
- val viewEnd = gridState.layoutInfo.viewportEndOffset.toFloat()
125
- return when {
126
- bottom > viewEnd - scrollThresholdPx && draggedDelta.y > scrollActivationSlopPx ->
127
- (bottom - (viewEnd - scrollThresholdPx)).coerceAtMost(maxScrollPerFramePx)
128
- top < viewStart + scrollThresholdPx && draggedDelta.y < -scrollActivationSlopPx ->
129
- (top - (viewStart + scrollThresholdPx)).coerceAtLeast(-maxScrollPerFramePx)
130
- else -> 0f
131
- }
132
- }
133
-
134
- private fun Offset.inBounds(item: LazyGridItemInfo): Boolean = x >= item.offset.x &&
135
- x <= item.offset.x + item.size.width &&
136
- y >= item.offset.y &&
137
- y <= item.offset.y + item.size.height
138
-
139
- private fun IntOffset.toOffset() = Offset(x.toFloat(), y.toFloat())
140
- }
141
-
142
- @Composable
143
- fun rememberReorderableGridState(
144
- gridState: LazyGridState,
145
- onMove: (from: Int, to: Int) -> Unit,
146
- ): ReorderableGridState {
147
- val density = LocalDensity.current
148
- val scrollThresholdPx = with(density) { 72.dp.toPx() }
149
- val maxScrollPerFramePx = with(density) { 16.dp.toPx() }
150
- // How far the user must drag toward an edge before auto-scroll kicks in (prevents an instant
151
- // jump when a cell is picked up already sitting in an edge zone).
152
- val scrollActivationSlopPx = with(density) { 24.dp.toPx() }
153
- // Always route to the latest onMove even though the state object itself is remembered.
154
- val onMoveState: State<(Int, Int) -> Unit> = rememberUpdatedState(onMove)
155
- return remember(gridState, scrollThresholdPx, maxScrollPerFramePx, scrollActivationSlopPx) {
156
- ReorderableGridState(
157
- gridState = gridState,
158
- scrollThresholdPx = scrollThresholdPx,
159
- maxScrollPerFramePx = maxScrollPerFramePx,
160
- scrollActivationSlopPx = scrollActivationSlopPx,
161
- onMove = { from, to -> onMoveState.value(from, to) },
162
- )
163
- }
164
- }
165
-
166
- /*
167
- * Edge cases this minimal version intentionally does not cover:
168
- *
169
- * - Auto-scroll alone does not re-run hit-testing. Swaps happen in onDrag, which only fires on
170
- * finger movement. If you hold the finger still in an edge zone the grid scrolls but no new
171
- * swaps occur until you nudge the finger. To fix, also call the swap logic from the scroll loop.
172
- *
173
- * - GridCells.Adaptive / variable-span items are untested. Hit-testing assumes uniform cells laid
174
- * out left-to-right, top-to-bottom; spanned items can produce surprising targets.
175
- *
176
- * - No drop animation. When released, the cell jumps from its dragged position to its final slot.
177
- * Animate draggingItemOffset toward zero on release for a settle effect.
178
- *
179
- * - No haptics on pick-up — add a LocalHapticFeedback.performHapticFeedback in onDragStart.
180
- *
181
- * - A null key falls back to position-based identity, which breaks animateItem() move animations
182
- * and can mis-track the dragged item across data changes. Provide a stable key in real use.
183
- */