How to do Android Drag and Drop in Jetpack Compose?
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
)
SkillsItem
: Bu, tanımlanan veri sınıfının adıdır. Veri sınıfı,name
,id
vebackgroundColor
adında üç özelliğe sahiptir.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)
}
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.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. İçindekiBox
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.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.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.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)
}
}
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 sadeceMainViewModel
sınıfı içinde değiştirilebileceğini belirtir.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.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.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, birSkillsItem
nesnesini temsil eder.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ğeritrue
olarak ayarlanır.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ğerifalse
olarak ayarlanır.fun addPerson(skillsItem: SkillsItem) { ... }
: Bu, bir kişi öğesiniaddedPersons
listesine eklemek için bir işlev tanımlar. Bu işlev, birSkillsItem
nesnesi alır ve bu nesneyiaddedPersons
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
)
}
}
}
}
}
@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.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.Column { ... }
: Bu, dikey olarak hizalı bir sütun bileşenini temsil eder. İçindeki diğer bileşenlerin düzenini sağlar.- İ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. 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.- İ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. 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.- Son
Box
bileşeni, ekranın altında eklenen beceri öğelerini gösterir. Bu bileşen,MainViewModel
içindekiaddedPersons
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