Skip to content

Drag and drop #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: #1_composable_function_info
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# JetPackCompose_Basic

In this project, we implement Drag-n-Drop functionality in Jetpack compose.

# Video
https://github.com/KaushalVasava/JetPackCompose_Basic/assets/49050597/10df632c-ebc6-44cf-9bca-b31df623ca6d

# Important topics are:
👉 GraphicsLayer :

Create a Modifier.Node called 'graphicsLayer' that allows content to be drawn into a separate draw layer. This layer can be invalidated independently from its parent, reducing unnecessary updates. Use a 'graphicsLayer' when content updates independently to minimize invalidations.
You can apply various effects like scaling, rotation, opacity, shadow, and clipping to the content within this layer. It's best suited for situations where layer properties are driven by 'androidx.compose.runtime.State' or animated values. Avoid reading state inside the block, as it only updates the layer properties without triggering recomposition and re-layout.

Parameters:
'block': A block within the `GraphicsLayerScope` where you can define layer properties.


👉 PointerInput:

Create a modifier for processing pointer input within the modified element's region. This modifier allows you to handle pointer input events using the 'pointerInputs' function. You can use the 'PointerInputScope.awaitPointerEventScope' function to set up a pointer input handler that can await and consume pointer events via 'AwaitPointerEventScope.awaitPointerEvent'.

Here's a simplified version of your description:
"When creating a pointerInput modifier through composition, if the 'block' function captures any local variables for processing, you can specify these variables as key parameters. This allows the 'block' to cancel and restart from the beginning if any of these key variables change. This behavior is useful for handling changes to variables while working with pointer input."

This simplification retains the essential information while removing some of the detailed technical terminology.

👉 onGloballyPositioned:

Call `onGloballyPositioned` with the `LayoutCoordinates` of the element when the global position of the content might have changed. This callback will be triggered at least once when the coordinates are finalized and whenever the element's position changes within the window. However, it's not guaranteed to trigger every time the element's position relative to the screen changes. For instance, if the system moves content within a window without triggering a callback, you might not receive updates when calculating the position on the entire screen, as opposed to just within the window.

Basic of drag are here:
[https://lnkd.in/dqjSypsM](https://developer.android.com/jetpack/compose/touch-input/pointer-input/drag-swipe-fling)

Follow me for more if you find it helpful and share it.
24 changes: 12 additions & 12 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ plugins {

android {
namespace 'com.lahsuak.apps.jetpackcomposebasic'
compileSdk 33
compileSdk 34

defaultConfig {
applicationId "com.lahsuak.apps.jetpackcomposebasic"
minSdk 23
targetSdk 33
targetSdk 34
versionCode 1
versionName "1.0"

Expand All @@ -27,17 +27,17 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '17'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.1'
kotlinCompilerExtensionVersion '1.4.3'
}
packagingOptions {
resources {
Expand All @@ -48,15 +48,15 @@ android {

dependencies {

implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.compose.material3:material3:1.1.0-alpha03'
implementation 'androidx.compose.material3:material3:1.2.0-alpha03'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
Expand Down
118 changes: 118 additions & 0 deletions app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/DragAndDrop.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.lahsuak.apps.jetpackcomposebasic

import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize

internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() }

@Composable
fun LongPressDraggable(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
val state = remember { DragTargetInfo() }
CompositionLocalProvider(LocalDragTargetInfo provides state) {
Box(modifier = modifier.fillMaxSize()) {
content()
if (state.isDragging) {
var targetSize by remember {
mutableStateOf(IntSize.Zero)
}
Box(modifier = Modifier
.graphicsLayer {
val offset = (state.dragPosition + state.dragOffset)
scaleX = 1.3f
scaleY = 1.3f
alpha = if (targetSize == IntSize.Zero) 0f else .9f
translationX = offset.x.minus(targetSize.width / 2)
translationY = offset.y.minus(targetSize.height / 2)
}
.onGloballyPositioned {
targetSize = it.size
}
) {
state.draggableComposable?.invoke()
}
}
}
}
}

@Composable
fun <T> DragTarget(
modifier: Modifier,
dataToDrop: T,
content: @Composable (() -> Unit),
) {

var currentPosition by remember { mutableStateOf(Offset.Zero) }
val currentState = LocalDragTargetInfo.current

Box(modifier = modifier
.onGloballyPositioned {
currentPosition = it.localToWindow(Offset.Zero)
}
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = {
currentState.dataToDrop = dataToDrop
currentState.isDragging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
}, onDrag = { change, dragAmount ->
change.consume()
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
}, onDragEnd = {
currentState.isDragging = false
currentState.dragOffset = Offset.Zero
}, onDragCancel = {
currentState.isDragging = false
currentState.dragOffset = Offset.Zero
})
}) {
content()
}
}

@Composable
fun <T> DropTarget(
modifier: Modifier,
content: @Composable (BoxScope.(isInBound: Boolean, data: T?) -> Unit),
) {

val dragInfo = LocalDragTargetInfo.current
val dragPosition = dragInfo.dragPosition
val dragOffset = dragInfo.dragOffset
var isCurrentDropTarget by remember {
mutableStateOf(false)
}

Box(modifier = modifier.onGloballyPositioned {
it.boundsInWindow().let { rect ->
isCurrentDropTarget = rect.contains(dragPosition + dragOffset)
}
}) {
val data =
if (isCurrentDropTarget && !dragInfo.isDragging) dragInfo.dataToDrop as T? else null
content(isCurrentDropTarget, data)
}
}

internal class DragTargetInfo {
var isDragging: Boolean by mutableStateOf(false)
var dragPosition by mutableStateOf(Offset.Zero)
var dragOffset by mutableStateOf(Offset.Zero)
var draggableComposable by mutableStateOf<(@Composable () -> Unit)?>(null)
var dataToDrop by mutableStateOf<Any?>(null)
}
28 changes: 28 additions & 0 deletions app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/FoodItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.lahsuak.apps.jetpackcomposebasic

import androidx.annotation.DrawableRes

data class FoodItem(
val id: Int,
val name: String,
val price: Double,
@DrawableRes val image: Int,
)

val foodList = listOf(
FoodItem(1, "Pizza", 20.0, R.drawable.food1),
FoodItem(2, "French toast", 10.05, R.drawable.foo2),
FoodItem(3, "Chocolate cake", 12.99, R.drawable.food3),
)

data class Person(
val id: Int,
val name: String,
@DrawableRes val profile: Int,
)

val persons = listOf(
Person(1, "Kaushal", R.drawable.person1),
Person(2, "Jigar", R.drawable.person2),
Person(3, "John", R.drawable.person3),
)
Loading