diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64377c36d..b11f66945 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,6 +48,7 @@ timber = "5.0.1" mockk = "1.13.8" tosspayments = "0.1.15" immutable = "0.3.7" +reorderable = "0.9.6" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-ktx" } @@ -115,6 +116,7 @@ retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "immutable" } +reorderable = { group = "org.burnoutcrew.composereorderable", name = "reorderable", version.ref = "reorderable" } [plugins] android-application = { id = "com.android.application", version.ref = "android" } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 4bfabf9eb..5ad2851a3 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -93,6 +93,7 @@ dependencies { implementation(libs.timber) implementation(libs.zxing.android.embedded) + implementation(libs.reorderable) androidTestImplementation(libs.bundles.android.test) androidTestImplementation(platform(libs.andoridx.compose.compose.bom)) diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditScreen.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditScreen.kt index 03936dc00..8dd0acec8 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditScreen.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditScreen.kt @@ -17,8 +17,11 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions @@ -72,6 +75,11 @@ import com.nexters.boolti.presentation.util.ObserveAsEvents import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow +import org.burnoutcrew.reorderable.ReorderableItem +import org.burnoutcrew.reorderable.ReorderableState +import org.burnoutcrew.reorderable.detectReorder +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -147,6 +155,8 @@ fun ProfileEditScreen( onClickAddLink = { navigateToLinkEdit(null) }, onClickEditSns = { sns -> navigateToSnsEdit(sns) }, onClickEditLink = { link -> navigateToLinkEdit(link) }, + onReorderSns = viewModel::reorderSns, + onReorderLink = viewModel::reorderLink, ) } @@ -170,6 +180,8 @@ fun ProfileEditScreen( onClickAddLink: () -> Unit, onClickEditSns: (Sns) -> Unit, onClickEditLink: (Link) -> Unit, + onReorderSns: (from: Int, to: Int) -> Unit, + onReorderLink: (from: Int, to: Int) -> Unit, ) { val scrollState = rememberScrollState() val snackbarHostState = LocalSnackbarController.current @@ -334,43 +346,91 @@ fun ProfileEditScreen( ) } + val snsReorderState = rememberReorderableLazyListState( + onMove = { from, to -> + onReorderSns(from.index - 1, to.index - 1) + }, + ) Section( modifier = Modifier.padding(top = 12.dp), title = stringResource(R.string.profile_edit_sns_title), ) { - Column { - LinkAddButton( - modifier = Modifier.padding(top = 4.dp), - label = stringResource(R.string.sns_add), - onClick = onClickAddSns, - enabled = !saving, - ) - snsList.forEach { sns -> - SnsItem( - modifier = Modifier.padding(top = 12.dp), - sns = sns, - ) { if (!saving) onClickEditSns(sns) } + LazyColumn( + state = snsReorderState.listState, + modifier = Modifier + .heightIn(max = 100.dp * (snsList.size + 1)) // 대충 넉넉하게 잡은 높이 + .reorderable(snsReorderState), + ) { + item( + contentType = "SnsAddButton", + ) { + LinkAddButton( + modifier = Modifier.padding(top = 4.dp), + label = stringResource(R.string.sns_add), + onClick = onClickAddSns, + enabled = !saving, + ) + } + items( + items = snsList, + key = { it.id }, + contentType = { "SnsItem" }, + ) { sns -> + ReorderableItem( + state = snsReorderState, + key = sns.id, + ) { + SnsItem( + modifier = Modifier.padding(top = 12.dp), + sns = sns, + reorderableState = snsReorderState, + ) { if (!saving) onClickEditSns(sns) } + } } } } + val linkReorderState = rememberReorderableLazyListState( + onMove = { from, to -> + onReorderLink(from.index - 1, to.index - 1) + }, + ) Section( modifier = Modifier.padding(top = 12.dp), title = stringResource(R.string.label_links), ) { - Column { - LinkAddButton( - modifier = Modifier.padding(top = 4.dp), - label = stringResource(R.string.link_add_btn), - onClick = onClickAddLink, - enabled = !saving, - ) - links.forEach { link -> - LinkItem( - modifier = Modifier.padding(top = 12.dp), - title = link.name, - url = link.url, - ) { if (!saving) onClickEditLink(link) } + LazyColumn( + state = linkReorderState.listState, + modifier = Modifier + .heightIn(max = 100.dp * (links.size + 1)) // 대충 넉넉하게 잡은 높이 + .reorderable(linkReorderState), + ) { + item( + contentType = "LinkAddButton", + ) { + LinkAddButton( + modifier = Modifier.padding(top = 4.dp), + label = stringResource(R.string.link_add_btn), + onClick = onClickAddLink, + enabled = !saving, + ) + } + items( + items = links, + key = { it.id }, + contentType = { "LinkItem" }, + ) { link -> + ReorderableItem( + state = linkReorderState, + key = link.id, + ) { + LinkItem( + modifier = Modifier.padding(top = 12.dp), + title = link.name, + url = link.url, + reorderableState = linkReorderState, + ) { if (!saving) onClickEditLink(link) } + } } } } @@ -454,6 +514,7 @@ private fun LinkAddButton( private fun SnsItem( sns: Sns, modifier: Modifier = Modifier, + reorderableState: ReorderableState<*>, onClickEdit: () -> Unit, ) { Row( @@ -488,10 +549,13 @@ private fun SnsItem( color = Grey15, ) Icon( - modifier = Modifier.size(20.dp), - imageVector = ImageVector.vectorResource(R.drawable.ic_edit_pen), + modifier = Modifier + .padding(start = 20.dp) + .size(20.dp) + .detectReorder(reorderableState), + imageVector = ImageVector.vectorResource(R.drawable.ic_reordable_handle), tint = Grey50, - contentDescription = stringResource(R.string.link_edit), + contentDescription = stringResource(R.string.sns_reorder_description), ) } } @@ -500,6 +564,7 @@ private fun SnsItem( private fun LinkItem( title: String, url: String, + reorderableState: ReorderableState<*>, modifier: Modifier = Modifier, onClickEdit: () -> Unit, ) { @@ -533,10 +598,11 @@ private fun LinkItem( Icon( modifier = Modifier .padding(start = 20.dp) - .size(20.dp), - imageVector = ImageVector.vectorResource(R.drawable.ic_edit_pen), + .size(20.dp) + .detectReorder(reorderableState), + imageVector = ImageVector.vectorResource(R.drawable.ic_reordable_handle), tint = Grey50, - contentDescription = stringResource(R.string.link_edit), + contentDescription = stringResource(R.string.link_reorder_description), ) } } diff --git a/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditViewModel.kt b/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditViewModel.kt index d7bd915b3..bf625c583 100644 --- a/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditViewModel.kt +++ b/presentation/src/main/java/com/nexters/boolti/presentation/screen/profileedit/profile/ProfileEditViewModel.kt @@ -108,6 +108,28 @@ class ProfileEditViewModel @Inject constructor( event(ProfileEditEvent.OnSnsRemoved) } + fun reorderSns(from: Int, to: Int) { + val snsList = uiState.value.snsList.toMutableList() + if (from !in snsList.indices || to !in snsList.indices) return + + _uiState.update { + it.copy( + snsList = snsList.apply { add(to, removeAt(from)) }, + ) + } + } + + fun reorderLink(from: Int, to: Int) { + val links = uiState.value.links.toMutableList() + if (from !in links.indices || to !in links.indices) return + + _uiState.update { + it.copy( + links = links.apply { add(to, removeAt(from)) }, + ) + } + } + fun completeEdits(thumbnailFile: File?) { viewModelScope.launch(recordExceptionHandler) { _uiState.update { it.copy(saving = true) } diff --git a/presentation/src/main/res/drawable/ic_reordable_handle.xml b/presentation/src/main/res/drawable/ic_reordable_handle.xml new file mode 100644 index 000000000..1ea6eac18 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_reordable_handle.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 626bf778b..17bdc172a 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -298,6 +298,7 @@ SNS를 추가했어요 SNS를 편집했어요 SNS를 삭제했어요 + SNS 순서 변경 ex) boolti_official \@을 제외한 Username을 입력해 주세요 지원하지 않는 특수문자가 포함되어 있습니다 @@ -310,6 +311,7 @@ 링크 추가 링크 추가 링크 편집 + 링크 순서 변경 링크 삭제 링크 이름 URL