How to do Android Drag and Drop in Jetpack Compose?

Bahadireray
6 min readMar 10, 2024

--

Merhaba, sizlere bu yazımda android cihazlarımızda kullanıcı deneyimi açısından önemli olabileceğini düşündüğüm “sürükle bırak” nasıl yapılır onu anlatacağım.

Tabi ki Jetpack Compose ile :)

Adım adım hangi fonksiyonlara ihtiyacımızın olduğundan bahsedeceğim.

data class SkillsItem(
val name:String,
val id:String,
val backgroundColor: Color
)
  1. SkillsItem: Bu, tanımlanan veri sınıfının adıdır. Veri sınıfı, name, id ve backgroundColor adında üç özelliğe sahiptir.
  2. val name: String, val backgroundColor: Color: Bu, name,backgroundColor adında bir özellik tanımlar. Özellik bir metin (String) türünde ve değiştirilemez (val)dir, yani bir kez atandıktan sonra değeri değiştirilemez.
internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() }

@Composable
fun DragableScreen(
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 = Modifier,
dataToDrop: T,
viewModel: MainViewModel,
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 = {
viewModel.startDragging()
currentState.dataToDrop = dataToDrop
currentState.isDragging = true
currentState.dragPosition = currentPosition + it
currentState.draggableComposable = content
}, onDrag = { change, dragAmount ->
change.consumeAllChanges()
currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
}, onDragEnd = {
viewModel.stopDragging()
currentState.isDragging = false
currentState.dragOffset = Offset.Zero
}, onDragCancel = {
viewModel.stopDragging()
currentState.dragOffset = Offset.Zero
currentState.isDragging = false
})
}) {
content()
}
}

@Composable
fun <T> DropItem(
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)
}
  1. LocalDragTargetInfo: Bu, bir Composition Local'i temsil eder. Composition Local'ler, Compose ağacındaki bileşenler arasında değerlerin paylaşılmasını sağlar. LocalDragTargetInfo, sürükleme hedefi ile ilgili bilgileri paylaşır.
  2. DragableScreen: Bu, sürükleme işlevselliğini sağlayan bir bileşen işlevidir. Bu işlev, ekranın tamamında sürükleme işlemlerini etkinleştirir. İçindeki Box bileşeni, içerikten önce bir boş kutu oluşturur ve içeriği çizer. Eğer sürüklenen bir öğe varsa, ekranın üstünde bu öğeyi temsil eden bir kutu çizilir.
  3. DragTarget: Bu, bir sürükleme hedefi oluşturan bir bileşen işlevidir. Bu bileşen, sürüklenebilir bir öğenin bırakılacağı alanı temsil eder. Kullanıcı, bir öğeyi bu alana sürüklediğinde, belirli işlevler tetiklenir. Bu işlev, sürüklenen öğenin özelliklerini (örneğin, veri ve görünüm) alır ve hedef alınan alanda kullanıcıya gösterir.
  4. DropItem: Bu, bir bırakılabilir öğe oluşturan bir bileşen işlevidir. Bu işlev, sürüklenen bir öğenin bırakılabileceği bir alanı temsil eder. Kullanıcı, sürüklenen öğeyi bu alana bıraktığında, belirli işlevler tetiklenir. Bu işlev, kullanıcıya sürüklenen öğenin bırakıldığı alanı gösterir.
  5. DragTargetInfo: Bu, sürükleme işlemi hakkında bilgileri saklayan bir sınıftır. Bu sınıf, sürüklenen öğenin durumu, konumu ve diğer özellikleri gibi bilgileri içerir. Bu bilgiler, sürükleme ve bırakma işlemlerini yönetmek için kullanılır ve bileşenler arasında paylaşılır.

Bu kod bloğu, kullanıcı arayüzünde sürükleme ve bırakma işlemlerini etkinleştirmek için kullanılır. DragableScreen bileşeni, sürükleyiciyi temsil ederken, DragTarget ve DropItem bileşenleri, sürüklenen öğelerin bırakılabileceği alanları temsil eder. Bu sayede, kullanıcılar belirli öğeleri sürükleyip bırakarak etkileşime girebilirler.

class MainViewModel :ViewModel() {

var isCurrentlyDragging by mutableStateOf(false)
private set

var items by mutableStateOf(emptyList<SkillsItem>())
private set

var addedSkills = mutableStateListOf<SkillsItem>()
private set

init {
items = listOf(
SkillsItem("Android","1", Color.Gray),
SkillsItem("Jetpack","2", Color.Blue),
SkillsItem("Compose","3", Color.Green),
)
}

fun startDragging(){
isCurrentlyDragging = true
}
fun stopDragging(){
isCurrentlyDragging = false
}

fun addSkills(skillsItem: SkillsItem){
println("Added Item")
addedSkills.add(skillsItem)
}
}
  1. var isCurrentlyDragging by mutableStateOf(false) private set: Bu, isCurrentlyDragging adında bir değişken tanımlar. Bu değişken, bir öğenin sürüklenip sürüklenmediğini izler. mutableStateOf işleviyle birlikte kullanılarak, bu değişkenin değeri değiştiğinde UI'nin otomatik olarak güncellenmesi sağlanır. private set, bu değişkenin sadece MainViewModel sınıfı içinde değiştirilebileceğini belirtir.
  2. var items by mutableStateOf(emptyList<SkillsItem>()) private set: Bu, items adında bir değişken tanımlar. Bu değişken, SkillsItem türünden öğelerin listesini içerir. Başlangıçta boş bir liste ile başlatılır. Bu değişken değiştirilebilir bir durumda (mutableStateOf) ve sadece sınıf içinde değiştirilebilir (private set) olarak tanımlanmıştır.
  3. var addedPersons = mutableStateListOf<SkillsItem>() private set: Bu, addedPersons adında bir değişken tanımlar. Bu değişken, sürükleme ve bırakma işlemi sırasında eklenen öğelerin listesini tutar. Başlangıçta boş bir liste ile başlatılır. Bu değişken değiştirilebilir bir durumda (mutableStateListOf) ve sadece sınıf içinde değiştirilebilir (private set) olarak tanımlanmıştır.
  4. init { ... }: Bu, MainViewModel sınıfının başlatıcı bloğudur. Başlangıçta, items listesi, üç örnekle (Android, Jetpack, Compose) doldurulur. Her örnek, bir SkillsItem nesnesini temsil eder.
  5. fun startDragging() { ... }: Bu, bir sürükleme işleminin başladığını işaretlemek için bir işlev tanımlar. isCurrentlyDragging değişkeninin değeri true olarak ayarlanır.
  6. fun stopDragging() { ... }: Bu, bir sürükleme işleminin sona erdiğini işaretlemek için bir işlev tanımlar. isCurrentlyDragging değişkeninin değeri false olarak ayarlanır.
  7. fun addPerson(skillsItem: SkillsItem) { ... }: Bu, bir kişi öğesini addedPersons listesine eklemek için bir işlev tanımlar. Bu işlev, bir SkillsItem nesnesi alır ve bu nesneyi addedPersons listesine ekler. Ayrıca, konsola "Added Item" metnini yazdırır.
@Composable
fun MainScreen(
mainViewModel: MainViewModel
) {

val screenWidth = LocalConfiguration.current.screenWidthDp

Column(
modifier = Modifier
.background(Color.White)
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(50.dp),
horizontalAlignment = Alignment.CenterHorizontally
){
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
){
mainViewModel.items.forEach { skill ->
DragTarget(
dataToDrop = skill,
viewModel = mainViewModel
) {
Box(
modifier = Modifier
.size(Dp(screenWidth / 5f))
.clip(RoundedCornerShape(15.dp))
.shadow(5.dp, RoundedCornerShape(15.dp))
.background(skill.backgroundColor, RoundedCornerShape(15.dp)),
contentAlignment = Alignment.Center,
){
Text(
text = skill.name,
style = MaterialTheme.typography.bodyLarge,
color = Color.Black,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
AnimatedVisibility(
mainViewModel.isCurrentlyDragging,
enter = slideInHorizontally (initialOffsetX = {it})
) {
DropItem<SkillsItem>(
modifier = Modifier
.size(Dp(screenWidth / 3.5f))
) { isInBound, skillItem ->
if(skillItem != null){
LaunchedEffect(key1 = skillItem){
mainViewModel.addSkills(skillItem)
}
}
if(isInBound){
Box(
modifier = Modifier
.fillMaxSize()
.border(
1.dp,
color = Color.Red,
shape = RoundedCornerShape(15.dp)
)
.background(Color.Gray.copy(0.5f), RoundedCornerShape(15.dp))
,
contentAlignment = Alignment.Center
){
Text(
text = "Add skills",
style = MaterialTheme.typography.bodyLarge,
color = Color.Black
)
}
}else{
Box(
modifier = Modifier
.fillMaxSize()
.border(
1.dp,
color = Color.Black,
shape = RoundedCornerShape(15.dp)
)
.background(
Color.Black.copy(0.5f),
RoundedCornerShape(15.dp)
),
contentAlignment = Alignment.Center
){
Text(
text = "Add skill",
style = MaterialTheme.typography.bodyLarge,
color = Color.Black
)
}
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 30.dp)
,
contentAlignment = Alignment.Center
){
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
.padding(bottom = 100.dp)
,
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp)
){
Text(
text = "Added skills",
color = Color.Black,
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Start
)
mainViewModel.addedSkills.forEach { skill ->
Text(
text = skill.name,
color = Color.Black,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
  1. @Composable fun MainScreen(mainViewModel: MainViewModel) { ... }: Bu, ana ekranı temsil eden bir bileşen işlevidir. Bu işlev, MainViewModel türünden bir nesne alır ve bu nesneyi kullanarak ekranı oluşturur.
  2. val screenWidth = LocalConfiguration.current.screenWidthDp: Bu satır, ekran genişliğini piksel cinsinden alır. Bu, ekranı düzgün bir şekilde yerleştirmek için kullanılır.
  3. Column { ... }: Bu, dikey olarak hizalı bir sütun bileşenini temsil eder. İçindeki diğer bileşenlerin düzenini sağlar.
  4. İlk Row bileşeni, ekranda sürüklenmeye hazır olan beceri öğelerini gösterir. Her öğe, DragTarget bileşenine sarılıdır ve sürüklenebilir bir öğe olarak işlev görür.
  5. DragTarget { ... }: Bu, bir sürüklenen öğenin bırakılabileceği bir alanı temsil eder. Her bir sürükleme hedefi, beceri öğesinin adını gösteren bir kutu içerir.
  6. İkinci AnimatedVisibility bileşeni, bir öğenin sürüklendiğinde görünen bir bırakma bölgesi oluşturur. Bu bölge, DropItem bileşenine sarılıdır ve bırakılmış bir öğenin eklenmesini sağlar.
  7. DropItem { ... }: Bu, bir öğenin bırakılabileceği bir alanı temsil eder. Bu bileşen, ekranın alt kısmında bir bırakma bölgesi oluşturur.
  8. Son Box bileşeni, ekranın altında eklenen beceri öğelerini gösterir. Bu bileşen, MainViewModel içindeki addedPersons listesinde bulunan beceri öğelerini gösterir.

Bu kod, kullanıcıların beceri öğelerini sürükleyip bırakarak eklemelerini sağlar. Ana ekranda, mevcut beceri öğeleri listelenir ve eklenen beceri öğeleri ayrı bir bölümde görüntülenir.

Bu yazdıklarımızı ekranda görebilmek için geriye ilgili screen’ı çağırmak kalıyor.

class MainActivity : ComponentActivity() {

private val viewModel = MainViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
DragAndDropJetpackComposeTheme {
DragableScreen(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(0.8f))
) {
MainScreen(viewModel)
}
}
}
}
}

Bir başka Android ile ilgili yazımda görüşmek üzere, güzel günler dilerim. :)

Bütün kodlarını aşağıdaki github adresinde bulabilirsiniz.

https://github.com/Bahadireray/DragAndDropJetpackCompose

--

--