Slide-Unlock 기능이 무엇인가?
요즘 잠금 해제 기능은 다들 지문 인식 또는 페이스 아이디를 통해 잠금 해제를 하는데, 몇 년 전에는 밀어서 잠금 해제를 많이 쓰곤 했다.
대충 이런 느낌인데, 최근에 AnchoredDraggable
에 대해 학습한 김에, 다음 기능을 구현해보고자 한다.
오늘의 완성본
구현
enum class
가장 먼저 상태를 표현할 enum class
를 생각해보자.
잠금 해제의 상태는 처음 - 끝
으로 표현이 가능하다.
enum class Position{
Start,End
}
state 구현
다음은 가장 중요하다고 생각하는 컴포저블의 드래그 상태를 저장하는 state
의 구현이다.
val state = remember {
AnchoredDraggableState(
initialValue = Position.Start,
positionalThreshold = {totalDistance : Float -> totalDistance * 0.5f },
velocityThreshold = { Float.MAX_VALUE },
animationSpec = tween(),
)
}
velocity
의 경우에는 값을 Float.MAX_VALUE
로 주어 사용자가 살짝 드래그 했을 때 슉 이동하지 않도록 하였다.
중요한 부분은 Anchors
의 update
부분이다.
Anchor
의 End
는 컴포저블의 전체 width
에서 padding * 2 + width(미는 대상의 넓이)
를 빼주어야 한다.
컴포저블의 넓이는 부모에 따라 결정이 된다.
이를 측정하기 위해서는 modifier
의 onGloballyPositioned
로 측정이 가능하다.
여기서 주의해야 할 점은 Box
의 크기가 결정이 되어야 해당 기준으로 Anchor
의 End
를 결정해야 한다는 부분이다.
이를 위해, LauncedEffect
를 사용하여, 값이 변경될 때 호출하여 Anchor
를 업데이트 해야 합니다.
var componentSize by remember {
mutableStateOf(IntSize.Zero)
}
val density = LocalDensity.current
LaunchedEffect(componentSize) {
if(componentSize.width > 0) {
val endPosition = with(density){(componentSize.width - 40.dp.toPx() - 16.dp.toPx())}
state.updateAnchors(
DraggableAnchors {
Position.Start at -0f
Position.End at endPosition
}
)
}
}
Start
의 경우에는 시작점이므로 0f
, End
의 경우에는 끝이므로 위에서 설명한 수식으로 계산해주었습니다.
레이아웃 만들기
Box(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
){
Box(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.background(color = Color.Yellow, shape = RoundedCornerShape(40.dp))
.align(Alignment.Center)
.onGloballyPositioned {
componentSize = it.size
}
){
}
val safeOffset = if(state.offset.isNaN()) 0f else state.offset
Box(
modifier = Modifier
.align(Alignment.CenterStart)
.offset {
IntOffset((safeOffset.roundToInt()), 0)
}
.padding(horizontal = 8.dp)
){
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(color = Color.Blue)
.anchoredDraggable(
state = state,
orientation = Orientation.Horizontal
)
){
Text(text = "B", color = Color.White, modifier = Modifier.align(Alignment.Center))
}
}}
가장 먼저 slide
를 둘러싸고 있는 부모 역할을 해주는 Box
를 생성합니다.
그리고 Box의 경우에는 나중에 선언된 코드가 상단에 위치하기 때문에, 가장 먼저 배경을 담당하는 코드를 작성해줍니다.
배경의 경우에는 임시로 노란색과 border
를 설정하였습니다.
이때, componentSize
를 업데이트 해주어야 합니다. onGloballyPositioned
를 이용하여, 해당 컴포넌트의 넓이를 저장해줍니다.
이렇게 되면, 컴포넌트가 생성이 되고 componentSize
에 값이 할당되면, 위의 LaunchEffect
코드가 실행이 됩니다.
다음은 잡아당길 원형의 코드 입니다.
원의 경우에는 잡아 당길 때마다, 이동거리만큼 offset
이 업데이트 되어져야 하므로, state
에 따라 값이 업데이트 되도록 설정하였습니다.
덜 갔으면 돌아오기
물론, 여기까지 구현만 하면 살짝 밀면 끝까지 날라가게 됩니다.
반 이상 밀었을 때 끝까지 이동하도록 코드를 추가하면 다음과 같습니다.
LaunchedEffect(state.currentValue) {
if(state.currentValue == Position.Start && state.offset > componentSize.width.toFloat() / 2) {
state.snapTo(Position.End)
}
}
전체 코드
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SlideandunlockTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.wrapContentSize(),
color = MaterialTheme.colorScheme.background
) {
Track()
}
} } }
}
enum class Position{
Start,End
}
@OptIn(ExperimentalFoundationApi::class)
@Preview
@Composable
private fun Track(modifier: Modifier = Modifier)
{
var componentSize by remember {
mutableStateOf(IntSize.Zero)
}
val density = LocalDensity.current
val state = remember {
AnchoredDraggableState(
initialValue = Position.Start,
positionalThreshold = {totalDistance : Float -> totalDistance * 0.5f },
velocityThreshold = { Float.MAX_VALUE },
animationSpec = tween(),
)
}
LaunchedEffect(componentSize) {
if(componentSize.width > 0) {
val endPosition = with(density){(componentSize.width - 40.dp.toPx() - 16.dp.toPx())}
state.updateAnchors(
DraggableAnchors {
Position.Start at -0f
Position.End at endPosition
}
)
}
}
LaunchedEffect(state.currentValue) {
if(state.currentValue == Position.Start && state.offset > componentSize.width.toFloat() / 2) {
state.snapTo(Position.End)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
){
Box(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.background(color = Color.Yellow, shape = RoundedCornerShape(40.dp))
.align(Alignment.Center)
.onGloballyPositioned {
componentSize = it.size
}
){
}
val safeOffset = if(state.offset.isNaN()) 0f else state.offset
Box(
modifier = Modifier
.align(Alignment.CenterStart)
.offset {
IntOffset((safeOffset.roundToInt()), 0)
}
.padding(horizontal = 8.dp)
){
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(color = Color.Blue)
.anchoredDraggable(
state = state,
orientation = Orientation.Horizontal
)
){
Text(text = "B", color = Color.White, modifier = Modifier.align(Alignment.Center))
}
} }}
'안드로이드 > Compose' 카테고리의 다른 글
[Kotlin/Compose] Scroll Fade-In/Out animation (0) | 2023.12.22 |
---|---|
[Kotlin/Compose] Slide-Unlock(밀어서 잠금해제) 기능 개발하기 (2) (0) | 2023.12.15 |
[Kotlin/Compose] Swipe Action 기능 만들기 with. AnchoredDraggable (0) | 2023.12.14 |
[Android/Kotlin] MotionLayout (0) | 2023.12.08 |
[Compose UI] 텍스트/이미지 반짝임 로딩화면 만들기 (1) | 2023.11.29 |