Compose로 Pattern 잠금화면 구현하기

SSY·2024년 1월 12일
0

Compose

목록 보기
2/10
post-thumbnail

☘️ Pattern 잠금화면을 사용하고자 한다면?

제가 작성한 오픈소스 라이브러리를 사용해보시면 좋을것 같습니다. 깃허브로 이동하시면 자세한 연동 방법 및 가이드 문서가 있으니 도움이 되실거라 생각합니다. 만약 사용하시는 중이라면 star하나 박아주시면 아주 기분이 좋을것 같습니다 ^^

[Compose로 구현한 Pattern 잠금화면 깃허브 이동하기]

👋 시작하며

처음 잠금화면을 구현하려던 당시, 단순 VerticalGrid와 같은 API를 사용하여 구현할 수 있을 줄 알았다. 하지만 이건 큰 오산이었는데, Pattern 잠금화면의 경우, 손으로 드래그할 때 선이 쭉 이이지는 제스쳐 필요했는데, 이를 VerticalGrid로는 구현에 무리가 있기 때문이었다.

VerticalGridColumn을 사용하여 3 * 3형식의 dot를 그리는데까진 성공하였다. 또한 dot가 클릭되었을 때 색칠이 되는것까진 성공하였다. 하지만 선으로 쭉 잇는 것이 문제였다. 어떻게 구현해야 할까?

결론부터 말하자면, draw관련 API를 사용하고 Offset을 계산하여 구현하는 방법이 최선이란 생각이 들었다.

이번 포스팅에선 Offset을 사용 및 x, y좌표를 계산해가며 Pattern 잠금 화면을 구현하기까지의 과정을 간략히 설명한다.

[읽기 전]
아래의 구현코드엔 생략된 부분이 매우 많습니다. 이해를 돕기 위해 단순 의사코드로만 작성되어 있으며, 해당 의사코드 구현을 위해 사전 정의가 필요한 메서드와 변수 또한 많은 생략이 들어갔습니다. 따라서 자세한 분석을 원하시면 아래 글을 가볍게 읽으신 후, 오픈소스를 직접 읽어보시길 권장드립니다 :)

1. Composition 및 3 x 3 dot을 그린다.

draw API를 사용하기 위해선, Canvas를 사용해야 한다. 이 API에는 onDraw라는 고차함수가 존재하는데, 이 고차함수를 통해 3 * 3 dot를 그려준다. 의사코드를 작성해보겠다.

onDraw = {
    repeat(BasePatternDrawingUiState.DOT_SIZE) { rowIndex ->
        repeat(BasePatternDrawingUiState.DOT_SIZE) { colIndex ->
            
            // 위로부터 colPosition를 계산 및 drawCircle API를 사용하여 dot를 그린다.
            drawCircle(
                center = colPosition,
                radius = drawingSetting.dotSize.toPx()
            )
        }
    }

위 코드는 생략된게 많지만, 핵심은 위 코드가 전부이다. 우선, device의 물리적 크기를 구한다. 그리고 이를 등분 및 Offset를 계산한다. 그 후, 3 x 3 반복문을 돌림으로써 drawCiecle API를 호출해준다. 그러면 위와 같은 3 x 3형식의 dot가 그려진다.

2. MotionEvent.ACTION_DOWN 사용 및 색깔 변경 처리

CanvasModifier를 사용하여 pointerInteropFilter를 사용한다. 그러면 MotionEvent.ACTION_DOWNMotionEvent.ACTION_UPMotionEvent.ACTION_MOVE에 각각 코드를 작성해줄 수 있다.

Canvas(
    modifier = modifier
        .size(264.dp)
        .align(Alignment.CenterHo
        .pointerInteropFilter(
            onTouchEvent = { motionEvent ->
                when (motionEvent.action) {
                    MotionEvent.ACTION_DOWN -> { ... }
                    MotionEvent.ACTION_MOVE -> { ... }
                    MotionEvent.ACTION_UP -> { ... }
        )
    )

위를 아래와 같이 작성한다.

Canvas(
    modifier = modifier
        .size(264.dp)
        .align(Alignment.CenterHo
        .pointerInteropFilter(
            onTouchEvent = { motionEvent ->
                when (motionEvent.action) {
                    MotionEvent.ACTION_DOWN -> { 
                        repeat(BasePatternDrawingUiState.DOT_SIZE) { rowIndex ->
  						    repeat(BasePatternDrawingUiState.DOT_SIZE) { colIndex ->
                                
                                // 선택된 Offset식별 및 색깔 변경을 위해 selectedOffsets에 현재 선택된 좌표를 저장한다. 
                                // 이 부분의 변경으로 selectedOffsets에 해당하는 좌표만 리컴포지션 될것이다.
                                Offset(
                                    x = dotOffset.x,
                                    y = dotOffset.y
                                ).let { firstSelectedOffset ->
                                    uiState =
                                        uiState.copy(
                                            selectedOffsets = uiState.selectedOffsets.apply {
                                                add(firstSelectedOffset)
                                             }
                                         )
                                 }
                            }
                        }
                    }
        )
    )

위와 같은 작업이 처리되면 onDraw메서드가 Recomposition되면서 변경된 색깔을 반영하게 된다. 아래와 같이 말이다.

onDraw = {
    repeat(BasePatternDrawingUiState.DOT_SIZE) { rowIndex ->
        repeat(BasePatternDrawingUiState.DOT_SIZE) { colIndex ->
            
            // drawCircle내에 있는 color파라미터에 변형을 준다.
            drawCircle(
                color = uiState.getDotColor(
                    rowIndex = rowIndex,
                    colIndex = colIndex,
                    selectedDotColor = drawingSetting.selectedDotColor,
                    unselectedDotColor = drawingSetting.unselectedDotColor),
                center = colPosition,
                radius = drawingSetting.dotSize.toPx()
            )
        }
    }

위의 코드가 작성되면 손으로 터치를 했을 때, dot의 색깔이 변하게 된다.

3. MotionEvent.ACTION_MOVE 사용 및 drawLine()을 사용하여 직선 그리기

해당 부분이 가장 애를 먹은 부분이기도 하다. 우선, 이는 MotionEvent.ACTION_MOVE 쪽에 코드를 지속적으로 Recomposition을 시킴으로써 line이 그려지도록 만들었다.

Canvas(
        modifier = modifier
            .pointerInteropFilter(
                onTouchEvent = { motionEvent ->
                    when (motionEvent.action) {
                        MotionEvent.ACTION_MOVE -> {
                            uiState =
                                uiState.copy(
                                
                                    // 현재 움직여지는 좌표를 트래킹한다.
                                    draggingOffset = Offset(
                                        x = motionEvent.x,
                                        y = motionEvent.y
                                    )
                                )

                            repeat(BasePatternDrawingUiState.DOT_SIZE) { rowDotIndex ->
                                repeat(BasePatternDrawingUiState.DOT_SIZE) { colDotIndex ->
                                    
                                    // 그 후, 해당 좌표의 line값을 지속적으로 계산한다. 
                                    // 이때의 line이란건 직선을 의미하며, 직선은 당연히 '시작점'과 '끝점'이 존재한다. 따라서 2개의 Offset을 저장해야 한다. 
                                    // 그리고 이를 저장하기 위한 자료형은 Pair를 선택했다.
                                    drawDotAndLine(
                                        currentUiState = uiState,
                                        destinationOffset = middleOffset,
                                        onDrawSuccess = { newUiState ->
                                            // 성공적으로 그려지면 이를 uiState프로퍼티에 저장하고 Recomposition트리거 준비를 한다.
                                            uiState = newUiState
                                        }
                                    )
                                }
                            }
                        }
                    }
                }
                true
            }
            )
            .drawWithContent {
                drawContent()
                
                // API를 사용하여 직선을 그린다. 
                // 이때 직선은 'drawDotAndLine'을 통해 저장된 Offset을 기준으로 그려지게 된다.
                drawLine(
                    color = drawingSetting.lineColor,
                    start = uiState.latestSelectedDotOffset,
                    end = uiState.draggingOffset,
                    strokeWidth = convertDpToPx(context = context, dp = drawingSetting.lineWidth)
                )
            }
        })

👋 마치며

지금까지 Composable함수로 만든 Pattern 잠금 화면 라이브러리가 어떤 방식으로 동작하는지를 설명드렸습니다. 궁금하신 부분이나 이해가 안가는 부분 있으면 댓글 남겨주시면 성의껏 답변드리겠습니다. ^^

profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글