[Android] 카카오 맵 SDK 지도 구현하기 1부 - 기본 기능 구현하기

H.Zoon·2024년 8월 31일
0
post-thumbnail

최근 사이드 프로젝트로 차량 관리 어플리케이션을 만들고 있다.
해당 어플리케이션 기능 중 경로찾기 기능이 필요하여 카카오맵 SDk를 이용하여 기능을 구현하였다.

이번 길찾기 기능은 카카오맵 SDK와 컴포즈로 구현하였다.
1부에서는 기본 카카오 SDK 구성하기를 중심으로 작성해보려고 한다.

1. 카카오맵 SDK 뷰 구성

Jetpack Compose에서 XML 뷰(MapView) 통합 및 라이프사이클 관리 방법

카카오맵 SDK는 XML로 제공되고 있기 때문에 Compose에서 XML 뷰를 통합하고, 라이프사이클을 관리하는 방법으로 적용하여야 한다.

MapScreen Composable 함수

@Composable
fun MapScreen() {
    val mapView = rememberMapViewWithLifecycle()
    AndroidView({ mapView }) { view ->
        // 추가적인 뷰 설정이 필요하다면 이곳에 작성합니다.
    }
}

MapScreen 함수는 Compose에서 MapView를 표시하기 위한 Composable 함수이다.
여기서 핵심은 AndroidView를 사용하여 기존의 XML 뷰를 Compose에 통합하는 것이다.

•	rememberMapViewWithLifecycle(): 이 함수는 MapView를 생성하고, Compose의 라이프사이클과 연결하여 관리.
•	AndroidView: Compose에서 기존의 Android View를 표시하기 위해 사용.

rememberMapViewWithLifecycle 함수

@Composable
fun rememberMapViewWithLifecycle(): View {
    val context = LocalContext.current
    val mapView = remember { MapView(context) }
    val lifecycle = LocalLifecycleOwner.current.lifecycle

    DisposableEffect(lifecycle) {
        val observer = object : DefaultLifecycleObserver {
            override fun onCreate(owner: LifecycleOwner) {
                mapView.start(
                    object : MapLifeCycleCallback() {
                        override fun onMapDestroy() {
                            // 지도 API가 정상적으로 종료될 때 호출됩니다.
                        }

                        override fun onMapError(error: Exception) {
                            // 인증 실패 및 지도 사용 중 에러가 발생할 때 호출됩니다.
                        }
                    },
                    object : KakaoMapReadyCallback() {
                        override fun getPosition(): LatLng {
                            return userLocation
                        }

                        override fun onMapReady(kakaoMap: KakaoMap) {
                            // 지도 초기화 및 설정 작업
                        }
                    }
                )
            }

            override fun onResume(owner: LifecycleOwner) {
                mapView.resume()
            }

            override fun onPause(owner: LifecycleOwner) {
                mapView.pause()
            }

            // 다른 라이프사이클 메서드들도 필요에 따라 오버라이드합니다.
        }

        lifecycle.addObserver(observer)
        onDispose {
            lifecycle.removeObserver(observer)
        }
    }
    return mapView
}

이 함수는 MapView를 생성하고, Compose의 라이프사이클에 맞게 관리하기 위한 로직을 담고 있다.

주요 구성 요소

•	remember: Compose에서 상태를 유지하기 위해 사용됩니다. 여기서는 MapView 인스턴스를 기억.
•	DisposableEffect: 사이드 이펙트를 관리하고, Composable이 처분될 때 정리 작업을 수행.
•	DefaultLifecycleObserver: 라이프사이클 이벤트를 감지하여 MapView의 상태를 관리.

라이프사이클 관리

DefaultLifecycleObserver를 구현하여 MapView의 라이프사이클 메서드를 호출하여 처리한다.

•	onCreate: mapView.start()를 호출하여 지도를 초기화.
•	onResume: mapView.resume()을 호출하여 지도를 다시 표시.
•	onPause: mapView.pause()를 호출하여 지도의 업데이트를 중지.
•	onDestroy: 필요한 경우 mapView를 정리합니다.

지도 초기화 및 콜백 처리

지도 초기화 시 MapLifeCycleCallback과 KakaoMapReadyCallback을 사용하여 지도 상태 및 준비 완료 이벤트를 처리한다.

MapLifeCycleCallback

object : MapLifeCycleCallback() {
    override fun onMapDestroy() {
        // 지도 API가 정상적으로 종료될 때 호출됩니다.
    }

    override fun onMapError(error: Exception) {
        // 인증 실패 및 지도 사용 중 에러가 발생할 때 호출됩니다.
    }
}
•	onMapDestroy: 지도 리소스가 정상적으로 해제될 때 호출된다.
•	onMapError: 지도 로드 중 에러가 발생하면 호출된다.

KakaoMapReadyCallback

object : KakaoMapReadyCallback() {
    override fun getPosition(): LatLng {
        return userLocation
    }

    override fun onMapReady(kakaoMap: KakaoMap) {
        // 지도 준비가 완료되면 호출됩니다.
        // 여기서 지도 설정 및 마커 추가 등의 작업을 수행합니다.
    }
}
•	getPosition: 지도의 초기 위치를 설정합니다.
•	onMapReady: 지도가 준비되면 호출되며, 지도에 대한 설정을 이곳에서 진행합니다.

2. 기본 레이아웃 / 위치 갱신 구성하기

메인 화면에 지도 노출이 성공했으면 지도 기본기능이라 할 수 있는 현재 위치표시와 카메라 이동 기능을 추가해보겠다

지도에 요소 추가 및 애니메이션 처리

onMapReady 메서드 내에서 지도에 라벨, 폴리곤 등을 추가하고 애니메이션을 설정한다.

// 중심 라벨 생성
centerLabel = kakaoMap.labelManager?.layer?.addLabel(
    LabelOptions.from("dotLabel", userLocation)
        .setStyles(
            LabelStyle.from(R.drawable.c).setAnchorPoint(0.5f, 0.5f)
        )
        .setRank(1)
)

// 애니메이션 폴리곤 생성
animationPolygon = kakaoMap.shapeManager?.layer?.addPolygon(
    PolygonOptions.from("circlePolygon")
        .setDotPoints(DotPoints.fromCircle(userLocation, 1.0f))
        .setStylesSet(
            PolygonStylesSet.from(
                PolygonStyles.from(Color(0xFFff722b).toArgb())
            )
        )
)
•	라벨 추가: 사용자 위치에 중심 라벨을 추가.
•	폴리곤 추가: 원형 폴리곤을 추가하여 지도에 표시.

위 코드에서 사용한 중심라벨 요소 (R.drawable.c) 나 색상은 사용할 요소를 넣어주면 된다. 다만 나의 경우에는 백터요소 사용시 지도에 표시되지 않는 이슈가 있어 png 파일을 이용했다.

애니메이션 설정

val circleWaves: CircleWaves = CircleWaves.from(
    "circleWaveAnim",
    CircleWave.from(1F, 0F, 0F, 100F)
)
    .setHideShapeAtStop(false)
    .setInterpolation(Interpolation.CubicInOut)
    .setDuration(1500)
    .setRepeatCount(500)

val shapeAnimator: ShapeAnimator? = kakaoMap.shapeManager?.addAnimator(circleWaves)
shapeAnimator?.addPolygons(animationPolygon)
shapeAnimator?.start()
•	CircleWaves: 원형 웨이브 애니메이션을 생성합니다.
•	ShapeAnimator: 생성된 애니메이션을 폴리곤에 적용하고 시작합니다.

이후 생성된 라벨과 trackingManager 의 사용자 위치를 갱신해서 넣어주면 해당 위치로 이동된다.

centerLabel?.moveTo(userLocation)
trackingManager?.startTracking(centerLabel)

다만 이렇게 할 경우 위치가 갱신될 때 마다 moveTo 처리를 하면 카메라가 이동하여 사용자 인터랙션 처리가 필요하다.

kakaoMap.setOnCameraMoveStartListener { kakaoMap, gestureType ->
    viewModel.setCameraTracking(false)
}

setOnCameraMoveStartListener를 이용하여 뷰모델에 카메라 트래킹 상태를 관찰하였다.

사용자 위치 이동 토글버튼 구현

// FloatingActionButton으로 트래킹 버튼 구현
FloatingActionButton(
    onClick = {
        // 트래킹 상태 토글
        viewModel.setCameraTracking(!cameraState)
    },
    modifier = Modifier
        .align(Alignment.BottomEnd)
        .padding(16.dp),
    shape = CircleShape,
    containerColor = if(cameraState) Color.Blue else Color.Gray // 상태에 따른 색상 변경
) {
    Icon(
        imageVector = Icons.Default.LocationOn,
        contentDescription = "내 위치",
        tint = Color.White
    )
}

FloatingActionButton을 이용하여 현제 사용자 트레킹 유/무를 설정하고 상태를 확인할 수 있는 버튼을 구현하였다.

3. 최종 메인UI 시현

최종 코드를 보면 라벨, 에니메이션, 사용자 인터렉션이 잘 적용되였다.

2부에서는 지도기능의 핵심 서비스 구현코드를 작성해보겠다.

0개의 댓글