레이블이 지정된 범위 슬라이더를 대화형으로 만들기

우리는 , 그러나 지금은 꽤 쓸모가 없습니다. 우리는 여전히 그것과 상호 작용할 방법이 없습니다. 문제를 해결해 보겠습니다.

터치 핸들은 막대를 가로질러 드래그할 수 있어야 하고, 탭할 때 즉시 위치를 지정하고, 상호 작용이 완료되면 가장 가까운 값에 맞춰야 합니다.

이동



제스처 감지기를 사용하는 방법을 살펴보았습니다. 이를 달성하기 위해 detectTapGestures 및 detectDragGestures를 사용할 수 있습니다. 그러나 탭하거나 드래그할 때 핸들을 터치 포인트에 배치할 때 거의 동일한 작업을 수행하기를 원하므로 간략하게 언급한 awaitPointerEventScope를 사용하여 더 유연하고 더 적합한 터치 핸들러를 구현할 수 있습니다.

관심 있는 터치 상태를 봉인된 클래스로 정의할 수 있습니다.

sealed class TouchInteraction {
    object NoInteraction : TouchInteraction()
    object Up : TouchInteraction()
    data class Move(val position: Offset) : TouchInteraction()
}


현재 상호 작용이 없는지, 핸들이 특정 위치로 이동되고 사용자가 손가락을 뗐는지 알면 충분합니다.

우리의 터치 핸들러는 pointerInput Modifier를 사용하여 구현됩니다.

fun Modifier.touchInteraction(key: Any, block: (TouchInteraction) -> Unit): Modifier =
    pointerInput(key) {
        forEachGesture {
            awaitPointerEventScope {
                do {
                    val event: PointerEvent = awaitPointerEvent()

                    event.changes
                        .forEach { pointerInputChange: PointerInputChange ->
                            if (pointerInputChange.positionChange() != Offset.Zero) pointerInputChange.consume()
                        }

                    block(TouchInteraction.Move(event.changes.first().position))
                } while (event.changes.any { it.pressed })

                block(TouchInteraction.Up)
            }
        }
    }


우리는 awaitPointerEventScope로 사용자의 터치 입력을 기다립니다. 입력을 받으면 사용자가 레이블이 지정된 범위 슬라이더와 상호 작용하고 있음을 알 수 있습니다. 사용자의 손가락이 Composable에 있는 한 이벤트를 반복하고 이벤트의 절대 위치를 가져와 TouchInteraction.Move 이벤트로 직접 전달합니다. 사용자가 손가락을 떼는 즉시 TouchInteraction.Up으로 응답하여 UI가 핸들을 가장 가까운 단계로 스냅하여 반응할 수 있는 기회를 제공합니다.

컴포저블에서 Modifier를 캔버스에 추가하고, 현재 상호작용 상태를 추적하기 위한 세 가지 상태 변수를 추가하고, 핸들 위치를 업데이트하는 로직을 추가합니다.

var touchInteractionState by remember { mutableStateOf<TouchInteraction>(TouchInteraction.NoInteraction) }
var moveLeft by remember { mutableStateOf(false) }
var moveRight by remember { mutableStateOf(false) }
...

Canvas(
    modifier = modifier
        .touchInteraction(remember { MutableInteractionSource() }) {
            touchInteractionState = it
        }
) {
    ...
}

when (val touchInteraction = touchInteractionState) {
    is TouchInteraction.Move -> {
        val touchPositionX = touchInteraction.position.x
        if (abs(touchPositionX - leftCirclePosition.x) < abs(touchPositionX - rightCirclePosition.x)) {
            leftCirclePosition = calculateNewLeftCirclePosition(touchPositionX, leftCirclePosition, rightCirclePosition, stepSpacing, stepXCoordinates.first())
            moveLeft = true
        } else {
            rightCirclePosition = calculateNewRightCirclePosition(touchPositionX, leftCirclePosition, rightCirclePosition, stepSpacing, stepXCoordinates.last())
            moveRight = true
        }
    }
    is TouchInteraction.Up   -> {
        moveLeft = false
        moveRight = false
        touchInteractionState = TouchInteraction.NoInteraction
    }
    else                     -> {
        // nothing to do
    }
}



이동할 핸들을 알아야 합니다. 이를 위해 터치 상호 작용의 x 위치를 보고 왼쪽 핸들과 오른쪽 핸들 사이의 거리를 계산하고 상호 작용이 가장 가까운 핸들을 이동합니다. 핸들의 새 위치를 계산할 때 고려해야 할 사항은 핸들이 바를 벗어나지 않아야 하며 이동하는 동안 두 핸들이 겹치지 않아야 한다는 것입니다. 더 명확하게 하기 위해 왼쪽 핸들의 업데이트된 위치 계산을 간단히 살펴보겠습니다.

private fun calculateNewLeftCirclePosition(
    touchPositionX: Float,
    leftCirclePosition: Offset,
    rightCirclePosition: Offset,
    stepSpacing: Float,
    firstStepXPosition: Float
): Offset = when {
    touchPositionX < firstStepXPosition                    -> leftCirclePosition.copy(x = firstStepXPosition)
    touchPositionX > (rightCirclePosition.x - stepSpacing) -> leftCirclePosition
    else                                                   -> leftCirclePosition.copy(x = touchPositionX)
}


터치 위치, 다른 핸들의 위치, 계단의 간격, 이 경우 첫 번째 계단의 위치에 따라 볼 수 있듯이 왼쪽 핸들이 가질 수 있는 새 위치를 계산합니다.



슬라이더를 터치하는 동안 핸들이 움직이고, 핸들이 즉시 점프하는 위치를 탭할 수 있으며, 손가락을 떼지 않고도 두 핸들 사이를 이동할 수도 있습니다.

그것을 빨리 확인



핸들은 아직 원하는 대로 작동하지 않습니다. 사용자가 손가락을 뗀 후와 제어 핸들이 변경된 후 가장 가까운 단계로 스냅되어야 합니다. 이를 실현하기 위해 우리는 터치 상호 작용 논리를 업데이트하여 가장 가까운 단계와 해당 x 좌표를 찾고 이에 따라 핸들 위치를 업데이트합니다.

is TouchInteraction.Move -> {
    val touchPositionX = touchInteraction.position.x
    if (abs(touchPositionX - leftCirclePosition.x) < abs(touchPositionX - rightCirclePosition.x)) {
        leftCirclePosition = calculateNewLeftCirclePosition(touchPositionX, leftCirclePosition, rightCirclePosition, stepSpacing, stepXCoordinates.first())
        moveLeft = true

        if (moveRight) {
            val (closestRightValue, _) = stepXCoordinates.getClosestNumber(rightCirclePosition.x)
            rightCirclePosition = rightCirclePosition.copy(x = closestRightValue)
            moveRight = false
        }
    } else {
        rightCirclePosition = calculateNewRightCirclePosition(touchPositionX, leftCirclePosition, rightCirclePosition, stepSpacing, stepXCoordinates.last())
        moveRight = true

        if (moveLeft) {
            val (closestRightValue, _) = stepXCoordinates.getClosestNumber(leftCirclePosition.x)
            leftCirclePosition = leftCirclePosition.copy(x = closestRightValue)
            moveLeft = false
        }
    }
}
is TouchInteraction.Up   -> {
    val (closestLeftValue, closestLeftIndex) = stepXCoordinates.getClosestNumber(leftCirclePosition.x)
    val (closestRightValue, closestRightIndex) = stepXCoordinates.getClosestNumber(rightCirclePosition.x)
    if (moveLeft) {
        leftCirclePosition = leftCirclePosition.copy(x = closestLeftValue)
        moveLeft = false
    } else if (moveRight) {
        rightCirclePosition = rightCirclePosition.copy(x = closestRightValue)
        moveRight = false
    }
    touchInteractionState = TouchInteraction.NoInteraction
}




이제 이미 달성하려는 최종 결과처럼 보입니다 :-). 그러나 한 가지 사소한 세부 사항이 누락되었습니다. 업데이트된 범위를 호출자에게 다시 전달해야 호출자가 응답할 수 있습니다 ;-).

이 마지막 단계는 이제 매우 쉽습니다. Composable에 콜백 onRangeChanged를 매개변수로 추가합니다.

@Composable
fun <T : Number> LabeledRangeSlider(
    selectedLowerBound: T,
    selectedUpperBound: T,
    steps: List<T>,
    onRangeChanged: (lower: T, upper: T) -> Unit,
    modifier: Modifier = Modifier,
    sliderConfig: SliderConfig = SliderConfig()
)


그리고 사용자가 선택한 단계의 값으로 손가락을 뗄 때마다 간단히 호출합니다.

is TouchInteraction.Up   -> {
    val (closestLeftValue, closestLeftIndex) = stepXCoordinates.getClosestNumber(leftCirclePosition.x)
    val (closestRightValue, closestRightIndex) = stepXCoordinates.getClosestNumber(rightCirclePosition.x)
    if (moveLeft) {
        leftCirclePosition = leftCirclePosition.copy(x = closestLeftValue)
        onRangeChanged(steps[closestLeftIndex], steps[closestRightIndex])
        moveLeft = false
    } else if (moveRight) {
        rightCirclePosition = rightCirclePosition.copy(x = closestRightValue)
        onRangeChanged(steps[closestLeftIndex], steps[closestRightIndex])
        moveRight = false
    }
    touchInteractionState = TouchInteraction.NoInteraction
}


결론





해냈습니다 🎉. 우리는 레이블이 지정된 범위 슬라이더를 처음부터 새로 만들어 컴포저블에 필요한 모든 것을 직접 그리고 해당 모디파이어 🥳와 상호작용하도록 만들었습니다.

Labeled Range Slider의 전체 소스 코드는 GitHub에서 찾을 수 있습니다.

이 시리즈를 따라하면서 도움이 되는 영감을 얻었기를 바랍니다 :-).

좋은 웹페이지 즐겨찾기