스와이프하여 닫거나 업데이트

SwipeToDismissBox 구성요소를 사용하면 사용자가 항목을 왼쪽이나 오른쪽으로 스와이프하여 닫거나 업데이트할 수 있습니다.

API 노출 영역

SwipeToDismissBox 컴포저블을 사용하여 스와이프 동작으로 트리거되는 작업을 구현합니다. 주요 매개변수는 다음과 같습니다.

  • state: 스와이프 항목에서 계산에 의해 생성된 값을 저장하기 위해 생성된 SwipeToDismissBoxState 상태로, 생성 시 이벤트를 트리거합니다.
  • backgroundContent: 항목 콘텐츠 뒤에 표시되고 콘텐츠를 스와이프할 때 표시되는 맞춤 구성 가능한 컴포저블입니다.

기본 예: 스와이프 시 업데이트 또는 닫기

이 예시의 스니펫은 시작에서 끝으로 스와이프하면 항목을 업데이트하거나 끝에서 시작으로 스와이프하면 항목을 닫는 스와이프 구현을 보여줍니다.

data class TodoItem(
    val itemDescription: String,
    var isItemDone: Boolean = false
)

@Composable
fun TodoListItem(
    todoItem: TodoItem,
    onToggleDone: (TodoItem) -> Unit,
    onRemove: (TodoItem) -> Unit,
    modifier: Modifier = Modifier,
) {
    val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
        confirmValueChange = {
            if (it == StartToEnd) onToggleDone(todoItem)
            else if (it == EndToStart) onRemove(todoItem)
            // Reset item when toggling done status
            it != StartToEnd
        }
    )

    SwipeToDismissBox(
        state = swipeToDismissBoxState,
        modifier = modifier.fillMaxSize(),
        backgroundContent = {
            when (swipeToDismissBoxState.dismissDirection) {
                StartToEnd -> {
                    Icon(
                        if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank,
                        contentDescription = if (todoItem.isItemDone) "Done" else "Not done",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(Color.Blue)
                            .wrapContentSize(Alignment.CenterStart)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                EndToStart -> {
                    Icon(
                        imageVector = Icons.Default.Delete,
                        contentDescription = "Remove item",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(Color.Red)
                            .wrapContentSize(Alignment.CenterEnd)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                Settled -> {}
            }
        }
    ) {
        ListItem(
            headlineContent = { Text(todoItem.itemDescription) },
            supportingContent = { Text("swipe me to update or remove.") }
        )
    }
}

코드 관련 핵심 사항

  • swipeToDismissBoxState는 구성요소 상태를 관리합니다. 항목과의 상호작용이 완료되면 confirmValueChange 콜백을 트리거합니다. 콜백 본문은 가능한 다양한 작업을 처리합니다. 콜백은 구성요소에 닫기 애니메이션을 표시해야 하는지 여부를 나타내는 불리언을 반환합니다. 이 경우:
    • 항목을 처음부터 끝까지 스와이프하면 onToggleDone 람다를 호출하여 현재 todoItem를 전달합니다. 이는 할 일 항목 업데이트에 해당합니다.
    • 항목을 끝에서 시작으로 스와이프하면 onRemove 람다를 호출하여 현재 todoItem를 전달합니다. 이는 할 일 항목을 삭제하는 것과 같습니다.
    • it != StartToEnd: 이 줄은 스와이프 방향이 StartToEnd이 아니면 true을 반환하고, 그렇지 않으면 false를 반환합니다. false를 반환하면 '전환 완료' 스와이프 후 SwipeToDismissBox가 즉시 사라지지 않아 시각적 확인이나 애니메이션을 사용할 수 있습니다.
  • SwipeToDismissBox는 각 항목에서 가로 스와이프 상호작용을 사용 설정합니다. 쉬는 상태에서는 구성요소의 내부 콘텐츠가 표시되지만 사용자가 스와이프를 시작하면 콘텐츠가 사라지고 backgroundContent이 표시됩니다. 일반 콘텐츠와 backgroundContent 모두 상위 컨테이너의 전체 제약 조건을 가져와 렌더링합니다. contentbackgroundContent 위에 그려집니다. 이 경우:
    • backgroundContentSwipeToDismissBoxValue를 기반으로 배경 색상이 있는 Icon로 구현됩니다.
    • Blue StartToEnd 스와이프 시: 할 일 항목 전환
    • Red EndToStart 스와이프 시 — 할 일 항목 삭제
    • Settled의 배경에는 아무것도 표시되지 않습니다. 항목을 스와이프하지 않으면 배경에 아무것도 표시되지 않습니다.
    • 마찬가지로 표시되는 Icon는 스와이프 방향에 맞게 조정됩니다.
    • StartToEnd: 할 일 항목이 완료되면 CheckBox 아이콘이, 완료되지 않으면 CheckBoxOutlineBlank 아이콘이 표시됩니다.
    • EndToStartDelete 아이콘을 표시합니다.

@Composable
private fun SwipeItemExample() {
    val todoItems = remember {
        mutableStateListOf(
            TodoItem("Pay bills"), TodoItem("Buy groceries"),
            TodoItem("Go to gym"), TodoItem("Get dinner")
        )
    }

    LazyColumn {
        items(
            items = todoItems,
            key = { it.itemDescription }
        ) { todoItem ->
            TodoListItem(
                todoItem = todoItem,
                onToggleDone = { todoItem ->
                    todoItem.isItemDone = !todoItem.isItemDone
                },
                onRemove = { todoItem ->
                    todoItems -= todoItem
                },
                modifier = Modifier.animateItem()
            )
        }
    }
}

코드 관련 핵심 사항

  • mutableStateListOf(...)TodoItem 객체를 포함할 수 있는 관찰 가능한 목록을 만듭니다. 이 목록에 항목이 추가되거나 삭제되면 Compose는 이 항목에 종속된 UI 부분을 재구성합니다.
    • mutableStateListOf() 내에서 4개의 TodoItem 객체가 각각의 설명('Pay bills', 'Buy groceries', 'Go to gym', 'Get dinner')으로 초기화됩니다.
  • LazyColumn는 세로로 스크롤되는 todoItems 목록을 표시합니다.
  • onToggleDone = { todoItem -> ... }는 사용자가 객체를 완료로 표시할 때 TodoListItem 내에서 호출되는 콜백 함수입니다. todoItemisItemDone 속성을 업데이트합니다. todoItemsmutableStateListOf이므로 이 변경사항은 리컴포지션을 트리거하여 UI를 업데이트합니다.
  • onRemove = { todoItem -> ... }는 사용자가 항목을 삭제할 때 트리거되는 콜백 함수입니다. todoItems 목록에서 특정 todoItem를 삭제합니다. 이렇게 하면 재구성이 발생하고 항목이 표시된 목록에서 삭제됩니다.
  • 항목이 닫힐 때 수정자의 placementSpec가 호출되도록 각 TodoListItemanimateItem 수정자가 적용됩니다. 이렇게 하면 항목 삭제와 목록의 다른 항목 재정렬이 애니메이션으로 표시됩니다.

결과

다음 동영상에서는 앞의 스니펫에 나온 기본 스와이프하여 닫기 기능을 보여줍니다.

그림 1. 항목을 완료로 표시하고 목록의 항목에 닫기 애니메이션을 표시할 수 있는 스와이프하여 닫기의 기본 구현입니다.

전체 샘플 코드는 GitHub 소스 파일을 참고하세요.

고급 예: 스와이프 시 배경 색상 애니메이션

다음 스니펫은 위치 기준점을 통합하여 스와이프 시 항목의 배경 색상을 애니메이션 처리하는 방법을 보여줍니다.

data class TodoItem(
    val itemDescription: String,
    var isItemDone: Boolean = false
)

@Composable
fun TodoListItemWithAnimation(
    todoItem: TodoItem,
    onToggleDone: (TodoItem) -> Unit,
    onRemove: (TodoItem) -> Unit,
    modifier: Modifier = Modifier,
) {
    val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
        confirmValueChange = {
            if (it == StartToEnd) onToggleDone(todoItem)
            else if (it == EndToStart) onRemove(todoItem)
            // Reset item when toggling done status
            it != StartToEnd
        }
    )

    SwipeToDismissBox(
        state = swipeToDismissBoxState,
        modifier = modifier.fillMaxSize(),
        backgroundContent = {
            when (swipeToDismissBoxState.dismissDirection) {
                StartToEnd -> {
                    Icon(
                        if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank,
                        contentDescription = if (todoItem.isItemDone) "Done" else "Not done",
                        modifier = Modifier
                            .fillMaxSize()
                            .drawBehind {
                                drawRect(lerp(Color.LightGray, Color.Blue, swipeToDismissBoxState.progress))
                            }
                            .wrapContentSize(Alignment.CenterStart)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                EndToStart -> {
                    Icon(
                        imageVector = Icons.Default.Delete,
                        contentDescription = "Remove item",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(lerp(Color.LightGray, Color.Red, swipeToDismissBoxState.progress))
                            .wrapContentSize(Alignment.CenterEnd)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                Settled -> {}
            }
        }
    ) {
        OutlinedCard(shape = RectangleShape) {
            ListItem(
                headlineContent = { Text(todoItem.itemDescription) },
                supportingContent = { Text("swipe me to update or remove.") }
            )
        }
    }
}

코드 관련 핵심 사항

  • drawBehindIcon 컴포저블의 콘텐츠 뒤에 있는 캔버스에 직접 그립니다.
    • drawRect()는 캔버스에 직사각형을 그리고 그리기 범위의 전체 경계를 지정된 Color로 채웁니다.
  • 스와이프하면 lerp를 사용하여 항목의 배경 색상이 원활하게 전환됩니다.
    • StartToEnd에서 스와이프하면 배경 색상이 밝은 회색에서 파란색으로 점진적으로 변경됩니다.
    • EndToStart에서 스와이프하면 배경 색상이 밝은 회색에서 빨간색으로 점진적으로 변경됩니다.
    • 한 색상에서 다른 색상으로의 전환 정도는 swipeToDismissBoxState.progress에 따라 결정됩니다.
  • OutlinedCard: 목록 항목 사이에 미묘한 시각적 구분을 추가합니다.

@Composable
private fun SwipeItemWithAnimationExample() {
    val todoItems = remember {
        mutableStateListOf(
            TodoItem("Pay bills"), TodoItem("Buy groceries"),
            TodoItem("Go to gym"), TodoItem("Get dinner")
        )
    }

    LazyColumn {
        items(
            items = todoItems,
            key = { it.itemDescription }
        ) { todoItem ->
            TodoListItemWithAnimation(
                todoItem = todoItem,
                onToggleDone = { todoItem ->
                    todoItem.isItemDone = !todoItem.isItemDone
                },
                onRemove = { todoItem ->
                    todoItems -= todoItem
                },
                modifier = Modifier.animateItem()
            )
        }
    }
}

코드 관련 핵심 사항

  • 이 코드에 관한 주요 사항은 동일한 코드 스니펫을 설명하는 이전 섹션의 주요 사항을 참고하세요.

결과

다음 동영상에서는 애니메이션 배경 색상이 있는 고급 기능을 보여줍니다.

그림 2. 애니메이션 배경 색상과 작업이 등록되기 전의 더 긴 기준점을 사용하여 표시 또는 삭제하기 위한 스와이프 구현입니다.

전체 샘플 코드는 GitHub 소스 파일을 참고하세요.

추가 리소스