diff --git a/README.md b/README.md new file mode 100644 index 0000000..254c11f --- /dev/null +++ b/README.md @@ -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. diff --git a/app/build.gradle b/app/build.gradle index 75677b5..c35e5f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" @@ -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 { @@ -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" diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/DragAndDrop.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/DragAndDrop.kt new file mode 100644 index 0000000..1b424c0 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/DragAndDrop.kt @@ -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 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 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(null) +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/FoodItem.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/FoodItem.kt new file mode 100644 index 0000000..d6c2145 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/FoodItem.kt @@ -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), +) \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/HomeScreen.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/HomeScreen.kt new file mode 100644 index 0000000..c900c93 --- /dev/null +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/HomeScreen.kt @@ -0,0 +1,174 @@ +package com.lahsuak.apps.jetpackcomposebasic + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardColors +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun BoxScope.PersonListContainer() { + LazyRow( + modifier = Modifier + .fillMaxHeight(0.3f) + .fillMaxWidth() + .background( + Color.LightGray, + shape = RoundedCornerShape(topEnd = 10.dp, topStart = 10.dp) + ) + .padding(vertical = 10.dp) + .align(Alignment.BottomCenter), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + items(items = persons) { person -> + PersonCard(person) + } + } + +} + +@Composable +fun PersonCard(person: Person) { + val foodItems = remember { + mutableStateMapOf() + } + + DropTarget( + modifier = Modifier + .padding(6.dp) + .width(width = 120.dp) + .fillMaxHeight(0.8f) + ) { isInBound, foodItem -> + val bgColor = if (isInBound) { + Color.Green + } else { + Color.White + } + + foodItem?.let { + if (isInBound) + foodItems[foodItem.id] = foodItem + } + + Column( + modifier = Modifier + .fillMaxSize() + .shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp)) + .background( + bgColor, + RoundedCornerShape(16.dp) + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = person.profile), contentDescription = null, + modifier = Modifier + .size(70.dp) + .clip(CircleShape), + contentScale = ContentScale.Fit + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = person.name, + fontSize = 18.sp, + color = Color.Black, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(6.dp)) + + if (foodItems.isNotEmpty()) { + Text( + text = "$${foodItems.values.sumOf { it.price }}", + fontSize = 16.sp, + color = Color.Black, + fontWeight = FontWeight.ExtraBold + ) + Text( + text = "${foodItems.size} Items", + fontSize = 14.sp, + color = Color.Black + ) + } + } + } +} + +@Composable +fun FoodItemCard(foodItem: FoodItem) { + Card( + shape = RoundedCornerShape(24.dp), + modifier = Modifier.padding(8.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp), + colors = CardColors( + containerColor = Color.White, contentColor = MaterialTheme.colorScheme.primary, + disabledContentColor = Color.Gray, disabledContainerColor = Color.LightGray + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(10.dp) + ) { + DragTarget(modifier = Modifier.size(130.dp), dataToDrop = foodItem) { + Image( + painter = painterResource(id = foodItem.image), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(130.dp) + .clip(RoundedCornerShape(16.dp)) + ) + } + Spacer(modifier = Modifier.width(20.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = foodItem.name, + fontSize = 22.sp, + color = Color.DarkGray + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "$${foodItem.price}", + fontSize = 18.sp, + color = Color.Black, + fontWeight = FontWeight.ExtraBold + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/MainActivity.kt b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/MainActivity.kt index 187ee32..b0d913a 100644 --- a/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/MainActivity.kt +++ b/app/src/main/java/com/lahsuak/apps/jetpackcomposebasic/MainActivity.kt @@ -3,13 +3,17 @@ package com.lahsuak.apps.jetpackcomposebasic import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.lahsuak.apps.jetpackcomposebasic.ui.theme.JetPackComposeBasicTheme class MainActivity : ComponentActivity() { @@ -22,20 +26,23 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - Greeting("Android") + LongPressDraggable(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 10.dp) + ) { + items(items = foodList) { food -> + FoodItemCard(foodItem = food) + } + } + PersonListContainer() + } } } } } } -/*** -Composable functions : -A composable function is a regular function annotated with @Composable. -This enables your function to call other @Composable functions within it. -You can see how the Greeting function is marked as @Composable. -This function will produce a piece of UI hierarchy displaying the given input, -String. Text is a composable function provided by the library. -***/ + @Composable fun Greeting(name: String) { Text(text = "Hello $name!") diff --git a/app/src/main/res/drawable/foo2.jpg b/app/src/main/res/drawable/foo2.jpg new file mode 100644 index 0000000..437efbf Binary files /dev/null and b/app/src/main/res/drawable/foo2.jpg differ diff --git a/app/src/main/res/drawable/food1.jpg b/app/src/main/res/drawable/food1.jpg new file mode 100644 index 0000000..31f3b2e Binary files /dev/null and b/app/src/main/res/drawable/food1.jpg differ diff --git a/app/src/main/res/drawable/food3.jpg b/app/src/main/res/drawable/food3.jpg new file mode 100644 index 0000000..30fbc4f Binary files /dev/null and b/app/src/main/res/drawable/food3.jpg differ diff --git a/app/src/main/res/drawable/person1.jpg b/app/src/main/res/drawable/person1.jpg new file mode 100644 index 0000000..cd94151 Binary files /dev/null and b/app/src/main/res/drawable/person1.jpg differ diff --git a/app/src/main/res/drawable/person2.png b/app/src/main/res/drawable/person2.png new file mode 100644 index 0000000..4c82fdf Binary files /dev/null and b/app/src/main/res/drawable/person2.png differ diff --git a/app/src/main/res/drawable/person3.jpg b/app/src/main/res/drawable/person3.jpg new file mode 100644 index 0000000..54cb545 Binary files /dev/null and b/app/src/main/res/drawable/person3.jpg differ diff --git a/build.gradle b/build.gradle index e868fc8..b0f2f02 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ buildscript { ext { - compose_version = '1.3.2' + compose_version = '1.4.3' } }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.3.0' apply false - id 'com.android.library' version '7.3.0' apply false - id 'org.jetbrains.kotlin.android' version '1.6.10' apply false + id 'com.android.application' version '8.0.2' apply false + id 'com.android.library' version '8.0.2' apply false + id 'org.jetbrains.kotlin.android' version '1.8.10' apply false } \ No newline at end of file