티스토리 뷰

안드로이드/코드

Compose Naver Map

알렌보이스 2022. 10. 29. 21:06
728x90

1) API 키 발급

이번에 역시 키 발급 방법에 관련해서는 생략하겠습니다.

Naver Colud Platform 사이트에 서비스 > Application Services > Maps에서 활용 신청을 하시면 됩니다.

 

 

NAVER CLOUD PLATFORM

cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification

www.ncloud.com

활용 신청 후 아래 링크의 인증 정보를 확인해서 Client Id와 Client Secret의 값들을 저장해 둡니다.

2) 라이브러리 설치

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {
            url 'https://naver.jfrog.io/artifactory/maven/'
        }
    }
}
implementation 'com.naver.maps:map-sdk:3.15.0'
implementation 'io.github.fornewid:naver-map-compose:1.2.1'

네이버에서 공식으로 지원하는 것은 com.naver.maps:map-sdk:3.15.0입니다.

Github에 Sangyoung An 님께서 올려주신 라이브러리가 있어서 사용해 보려고 합니다.

 

 

GitHub - fornewid/naver-map-compose: NAVER Map Android SDK for Jetpack Compose 🗺

NAVER Map Android SDK for Jetpack Compose 🗺. Contribute to fornewid/naver-map-compose development by creating an account on GitHub.

github.com

2-1) 오류

네이버 맵 라이브러리를 추가 시 저의 경우 Duplicate class found 오류가 발생하였습니다.

해당 문제가 발생 시 해결 방안에 관해서 참고용으로 링크 첨부합니다.

 

 

Android Duplicate class found 오류 해결

안드로이드 개발하면서 라이브러리 추가하던 중 발생한 오류입니다. Duplicate class found는 라이브러리 간에 충돌이 발생한 에러입니다. 가장 간단한 해결 방법은 충돌하는 라이브러리를 사용하지

alanboyce.tistory.com

3) API 설정 세팅

Api 키를 발급받아서 저장해 둔 Client Id와 Client Secret가 있을 것입니다.

client_id=35r...
client_secret=okhgI...

local.properties에 위와 같은 형식으로 저장해 줍니다.

Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def naver_map_client_id = properties.getProperty('client_id')
def naver_map_client_secret = properties.getProperty('client_secret')

android {
    ...

    defaultConfig {
        ... 
        manifestPlaceholders= [
                naverClientId: naver_map_client_id,
                naverClientSecret: naver_map_client_secret
        ]

        ...
    }
}

1

2줄은 local.properties 안에 있는 가져오기 위한 코드이고, 3

4줄은 그 안에 방금 전에 넣은 값을 불러오기 위한 코드입니다.

android → defaultConfing 안에 manifestPlaceholders를 이용해서 변수처럼 사용할 수 있도록 키 밸류 형식으로 넣어줍니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.composestudy">

    <application
        ...>
        <meta-data
            android:name="com.naver.maps.map.CLIENT_ID"
            android:value="${naverClientId}" />
    </application>

</manifest>

application 태그 안에 meta-data를 만들고 name은 동일하게 작성하고 value 값에 위에서 만든 키값을 넣어주시면 됩니다.

이렇게 네이버 지도를 사용하기 위한 준비는 완료되었습니다.

4) 지도 표시

NaverMap()

가장 기본적인 지도 표시는 NaverMap을 사용하면 끝입니다.

4-1) CameraPositionState

val cameraPositionState = rememberCameraPositionState {
    position = CameraPosition(latLng, 15.0)
}

NaverMap(
    cameraPositionState = cameraPositionState
)

CameraPositionState는 지도에 사용되는 카메라의 상태를 제어 및 관찰을 위한 객체로 rememberCameraPositionState를 이용하여 생성할 수 있습니다.

생성 시 position의 값을 주면 맵 뷰 실행 시 초기 위치를 설정할 수 있습니다.

CameraPosition은 LatLng(위도, 경도)와 줌의 크기를 받습니다.

CameraPositionState를 코드로 변경을 하고자 할 때에는

cameraPositionState.move()cameraPositionState.animate()가 있습니다.

둘의 차이는 애니메이션이 들어간다는 것과 애니메이션 적용을 위해 CoroutineScope가 필요하다는 차이점이 있습니다.

val scope = rememberCoroutineScope()
Button(onClick = {
    scope.launch {
      cameraPositionState.animate(
          update = CameraUpdate.scrollTo(LatLng(37.740111,127.0475821)),
          animation = CameraAnimation.Fly,
      )
    }
}

예시로 특정 위치로 좌표를 이동하는 코드입니다.

좌표 이동 외에도 카메라 포지션을 통한 이동, 줌 크기 변경, 줌과 좌표 동시에 변경 등이 있습니다.

animation에는 Linear, Easing, Fly, None이 있고 기본 값은 Easing이 있습니다.

4-2) UiSettings

NaverMap(
    uiSettings = mapUiSettings
)

UiSettings는 위와 같이 등록할 수 있습니다.

var mapUiSettings by remember {
    mutableStateOf(
        MapUiSettings(
            isLocationButtonEnabled = false,
            isZoomControlEnabled = false,
            logoGravity = Gravity.TOP or Gravity.END,
        )
    )
}

변수 생성은 위와 같이 할 수 있습니다.

이번 예시에서 사용한 내용은

isLocationButtionEnableds는 위치 아이콘 버튼 표시 여부입니다. (클릭 시 동작은 추가 구현 필요)

isZoomControlEnabled는 버튼을 통한 줌 컨트롤러 표시 여부입니다.

logoGravity는 네이버 로고가 들어갈 위치를 조정합니다.

이 외에도 여러 가지 속성들이 있는데 양이 많고 코드로 확인하면 한글로 주석이 잘 되어있으니 확인해 보시기 바랍니다.

public data class MapUiSettings(
    public val pickTolerance: Dp = NaverMapConstants.DefaultPickTolerance,
    public val isScrollGesturesEnabled: Boolean = true,
    public val isZoomGesturesEnabled: Boolean = true,
    public val isTiltGesturesEnabled: Boolean = true,
    public val isRotateGesturesEnabled: Boolean = true,
    public val isStopGesturesEnabled: Boolean = true,
    public val scrollGesturesFriction: Float = NaverMapConstants.DefaultScrollGesturesFriction,
    public val zoomGesturesFriction: Float = NaverMapConstants.DefaultZoomGesturesFriction,
    public val rotateGesturesFriction: Float = NaverMapConstants.DefaultRotateGesturesFriction,
    public val isCompassEnabled: Boolean = true,
    public val isScaleBarEnabled: Boolean = true,
    public val isZoomControlEnabled: Boolean = true,
    public val isIndoorLevelPickerEnabled: Boolean = false,
    public val isLocationButtonEnabled: Boolean = false,
    public val isLogoClickEnabled: Boolean = true,
    public val logoGravity: Int = Gravity.BOTTOM or Gravity.START,
    public val logoMargin: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 16.dp),
)

4-3) Properties

NaverMap(
    properties = mapProperties
)

사용은 역시 위와 동일합니다.

var mapProperties by remember {
    mutableStateOf(
        MapProperties(
            maxZoom = 20.0,
            minZoom = 5.0,
            isBicycleLayerGroupEnabled = true,
            isCadastralLayerGroupEnabled = true,
            isIndoorEnabled = true
        )
    )
}

역시 많은 종류 중 일부만 사용해 보았습니다.

maxZoom/minZoom 최소/최대 줌 크기 설정

isBicycleLayerGroupEnabled는 자전거 도로 표시 여부입니다.

isCadastralLayerGroupEnabled는 지적편집도 표시 여부입니다.

isIndoorEnabled는 실내지도 활성화 여부입니다.

Group 1.png

public data class MapProperties(
    public val mapType: MapType = MapType.Basic,
    public val extent: LatLngBounds? = null,
    public val minZoom: Double = NaverMapConstants.MinZoom,
    public val maxZoom: Double = NaverMapConstants.MaxZoom,
    public val maxTilt: Double = NaverMapConstants.DefaultMaxTilt,
    public val defaultCameraAnimationDuration: Int = NaverMapConstants.DefaultCameraAnimationDuration,
    public val fpsLimit: Int = 0,
    public val isBuildingLayerGroupEnabled: Boolean = true,
    public val isTransitLayerGroupEnabled: Boolean = false,
    public val isBicycleLayerGroupEnabled: Boolean = false,
    public val isTrafficLayerGroupEnabled: Boolean = false,
    public val isCadastralLayerGroupEnabled: Boolean = false,
    public val isMountainLayerGroupEnabled: Boolean = false,
    public val isLiteModeEnabled: Boolean = false,
    public val isNightModeEnabled: Boolean = false,
    public val isIndoorEnabled: Boolean = false,
    public val indoorFocusRadius: Dp = NaverMapConstants.DefaultIndoorFocusRadius,
    public val buildingHeight: Float = 1f,
    public val lightness: Float = 0f,
    public val symbolScale: Float = 1f,
    public val symbolPerspectiveRatio: Float = 1f,
    public val backgroundColor: Color = NaverMapConstants.DefaultBackgroundColorLight,
    @DrawableRes
    public val backgroundResource: Int = NaverMapConstants.DefaultBackgroundDrawableLight,
    public val locationTrackingMode: LocationTrackingMode = LocationTrackingMode.None,
)

역시 주석이 잘 나와있으니 직접 참고해 보시기 바랍니다.

5) Overay

제공되는 Overay의 종류는 다음과 같습니다.

ArrowheadPathOverlay
CircleOverlay
GroundOverlay
Marker
MultipartPathOverlay
PathOverlay
PolygonOverlay
PolylineOverlay

5-1) Marker

Marker(
    state = MarkerState(LatLng(37.740111, 127.0475821)),
    captionText = "스타벅스 의정부공원점",
    subCaptionText = "카페",
    captionOffset = 10.dp,
    captionColor = Color.Magenta,
    captionRequestedWidth = 10.dp
)
Marker(
    state = MarkerState(LatLng(37.7378009,127.0461568)),
    captionText = "스타벅스 신세계의정부",
    subCaptionText = "카페",
    captionOffset = 10.dp,
    captionColor = Color.White,
    captionHaloColor = Color.Black,
    captionRequestedWidth = 10.dp,
    icon = OverlayImage.fromResource(R.drawable.ic_round_star_24),
    height = 40.dp,
    width = 40.dp
)

captionRequestedWidth는 캡션의 넓이를 지정해 주는데 넓이 보다 텍스트가 길어지면 어절 단위로 다음 라인으로 넘어가게 됩니다.

captionOffset는 마커의 아이콘과 캡션과의 거리입니다.

Screenshot_20221029_194219.png

오른쪽 상단에 있는 것이 기본 아이콘을 사용한 마커이고, 왼쪽 하단의 노란색 별이 아이콘을 변경한 마커입니다.

5-2) ArrowheadPathOverlay

ArrowheadPathOverlay(
    coords = listOf(
        LatLng(37.740111,127.0475821),
        LatLng(37.7378009,127.0461568),
        LatLng(37.733071,127.0388841),
    ),
    width = 5.dp
)

ArrowheadPathOverlay는 지도에 화살표를 그려줍니다.

경로 탐색 기능이 있는 건 아니고 지정한 좌표와 좌표 사이를 연결한 화살표입니다.

Screenshot_20221029_195433 1.png

5-3) CircleOverlay

CircleOverlay(
    center = LatLng(37.7378009,127.0461568),
    color = Color(0x4DFFEA00),
    outlineColor = Color.White,
    outlineWidth = 10.dp
)

CircleOverlay는 center의 좌표 기준으로 radius(기본 값 1000) 만큼 원을 그려줍니다.

Screenshot_20221029_195946 1.png

5-4) GroundOverlay

val builder = LatLngBounds.Builder()
builder.include(LatLng(37.7378009,127.0461568))
builder.include(LatLng(37.733071,127.0388841))

GroundOverlay(
    bounds = builder.build(),
    image = OverlayImage.fromResource(R.drawable.ic_launcher_background),
    alpha = .5f
)

GroundOverlay는 특정 LatLngBounds에 이미지를 표시해 주는 Overlay입니다.

Screenshot_20221029_201043 1.png

5-5) PathOverlay

PathOverlay(
  coords = listOf(
      LatLng(37.740111, 127.0475821),
      LatLng(37.7378009, 127.0461568),
      LatLng(37.733071, 127.0388841),
  ),
  progress = 0.4,
  color = Color(0xFFFF1744),
  passedColor = Color(0xFFFFEA00),
  outlineWidth = 10.dp
)

PathOverlay는 지나온 경로를 표시해 주는 Overlay입니다.

Screenshot_20221029_202320 1.png

5-6) MultipartPathOverlay

MultipartPathOverlay(
    coordParts = listOf(
        listOf(
            LatLng(37.740111, 127.0475821),
            LatLng(37.7378009, 127.0461568),
            LatLng(37.733071, 127.0388841),
        ),
        listOf(
            LatLng(37.7378009, 127.0461568),
            LatLng(37.733071, 127.0388841),
        )
    ),
    colorParts = listOf(
        ColorPart(
            color = Color(0xFFFF1744),
            passedColor = Color(0xFFFFEA00)
        ),
    ),
    progress = 0.8,
)

MultipartPathOverlay는 PathOverlay를 리스트를 통해 여러 개 등록할 수 있는 Overlay입니다.

6) 활용

우선 데이터입니다.

data class NaverMapItem(
    val latLng: LatLng,
    val title: String,
    val image: String
)

fun getNaverMapItemList() = listOf(
    NaverMapItem(
        latLng = LatLng(37.740111, 127.0475821),
        title = "스타벅스 의정부공원점",
        image = "https://ldb-phinf.pstatic.net/20200825_200/1598301949474sBbkY_JPEG/3509_20171121080426_cnkfi.jpg"
    ),
    NaverMapItem(
        latLng = LatLng(37.7378009, 127.0461568),
        title = "스타벅스 신세계의정부",
        image = "https://ldb-phinf.pstatic.net/20200825_242/1598302003990GfJlO_JPEG/9698_20180615072632_2qg34.jpg"
    ),
    NaverMapItem(
        latLng = LatLng(37.733071, 127.0388841),
        title = "스타벅스 의정부예술의전당DT점",
        image = "https://ldb-phinf.pstatic.net/20210804_47/1628023521693AOigS_JPEG/3571_20210803091352_qzsc3.jpg"
    ),
)

다음은 뷰페이저입니다.

@OptIn(ExperimentalPagerApi::class)
@Composable
fun NaverMapViewPager(
    list: List<NaverMapItem>,
    state: PagerState,
    modifier: Modifier = Modifier
) {
    HorizontalPager(
        count = list.size,
        state = state,
        contentPadding = PaddingValues(start = 25.dp, end = 50.dp),
        modifier = modifier
    ) { index ->
        NaverMapCard(
            item = list[index],
            modifier = Modifier
                .graphicsLayer {
                    val pageOffset = calculateCurrentOffsetForPage(index).absoluteValue
                    lerp(
                        start = 0.85f,
                        stop = 1f,
                        fraction = 1f - pageOffset.coerceIn(0f, 1f)
                    ).also { scale ->
                        scaleX = scale
                        scaleY = scale
                    }
                }
        )
    }
}

@Composable
fun NaverMapCard(
    item: NaverMapItem,
    modifier: Modifier = Modifier
) {
    Card(
        shape = RoundedCornerShape(10.dp),
        modifier = modifier
            .fillMaxWidth()
            .padding(end = 10.dp)
    ) {
        Column(
            modifier = Modifier.padding(10.dp)
        ) {
            AsyncImage(
                model = item.image,
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .clip(RoundedCornerShape(10.dp))
            )

            Spacer(modifier = Modifier.height(10.dp))

            Text(text = item.title, fontSize = 16.sp)
        }
    }
}

뷰페이저 관련해서는 아래 링크에서 다루었습니다.

 

Compose 기초 3 : ViewPager

이번 포스팅에서는 기존 XML 에서 ViewPager 의 기능을 Compose 로 유사하게 구현하는 방법에 대하여 알아보겠습니다. 시작하기 전에 다음의 내용을 build.gradle(:app) 에 추가해 주세요 implementation "com.go..

alanboyce.tistory.com

@OptIn(ExperimentalNaverMapApi::class, ExperimentalPagerApi::class)
@Composable
fun NaverMapTest() {
    val list = getNaverMapItemList()

    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition(list[1].latLng, 14.0)
    }
    val pagerState = rememberPagerState()
    val scope = rememberCoroutineScope()

    Box(modifier = Modifier.fillMaxSize()) {
        NaverMap(
            cameraPositionState = cameraPositionState
        ) {
            list.forEachIndexed { index, item ->
                Marker(
                    state = MarkerState(position = item.latLng),
                    onClick = {
                        scope.launch {
                            cameraPositionState.animate(update = CameraUpdate.scrollTo(list[index].latLng))
                            pagerState.animateScrollToPage(index)
                        }
                        true
                    }
                )
            }
        }

        NaverMapViewPager(
            list = list,
            state = pagerState,
            modifier = Modifier
                .fillMaxWidth()
                .heightIn(min = 10.dp, max = 200.dp)
                .align(Alignment.BottomCenter)
        )

        LaunchedEffect(pagerState) {
            snapshotFlow { pagerState.currentPage }.collect { page ->
                cameraPositionState.animate(update = CameraUpdate.scrollTo(list[page].latLng))
            }
        }
    }
}

코드는 단순합니다.

NaverMap을 통해 네이버 지도를 그리고

위에서 만든 데이터를 통해 Marker를 그립니다.

Marker를 클릭 시 socpe.lauch 블록을 실행하여 뷰페이저의 페이지를 이동시킵니다.

cameraPositionState.animate는 안 넣어도 화면이 움직이지만 뷰페이저의 이동이 끝난 후에 지도 이동하는 게 어색해서 넣었습니다.

LaunchedEffect를 통해 뷰페이저의 변화를 감지하고 카메라를 이동시킵니다.

Screen_Recording_20221029_204913_ComposeStudy_1.gif

결과 화면입니다.

728x90

'안드로이드 > 코드' 카테고리의 다른 글

Compose MotionLayout  (0) 2022.11.02
Compose ConstraintLayout  (0) 2022.11.02
Compose GoogleMap  (0) 2022.10.27
Compose LaunchedEffect  (0) 2022.10.27
Compose GraphicsLayer  (0) 2022.09.02
댓글