본문 바로가기
Android & Kotlin/Android

[Android Kotlin] 안드로이드 네이버 지도SDK 사용자 경험 개선 하기 (카메라 이동, 마커찍기)

by 말린밴댕이_공부 2024. 1. 3.
반응형

네이버 지도에 마커들을 찍는 과정에 대해서 사용자 경험을 개선하기 위해  계속 발전시킨 과정에 대하여 정리하였습니다.

 

 

마커들을 찍는 과정의 발전 순서의 가장 큰 핵심은 zoom Level고정 값 → LatLng Bounds → zoom Level 유동적인 사용순서로 생각을 해보면서 진행하였으며 사용자 경험을 고려하여 발전 시켰습니다.

사용자 경험을 위한 진행 과정

  1. zoom Level 고정적 사용
    1. 마커간의 평균을 계산하여 중간점으로 이동 진행
    2. 현재 카메라의 위치를 기준으로 가장 가까운 곳으로 이동
  2. LatLng Bounds 사용
    1. LatLng Bounds를 측정하여 zoom 대신 camera Bounds 사용
  3. zoom Level 고정적 사용X
    1. 현재 카메라를 위치를 고려하여 밀집도와 거리를 계산하여 이동
  4. 최종 본

1-1. 마커간의 평균을 계산하여 중간 점으로 이동 진행

💡 핵심 로직

음식점들에 대한 위도,경도의 평균을 구하여 
그곳에 따른 위도와 경도로 카메라를 이동과 줌레벨을 고정시킨다.

 

 

zoom Level 낮은 경우
zoom Level 높은 경우

 

💡 피드백

1. zoom Level이 낮은 경우 : 사용자가 확대한 지도가 강제적으로 축소가 되어 정확한 사용자 경험을 감소시킴
2. zoom Level이 높은 경우 : 마커들의 평균 중간점이므로 마커가 아예 안보이는 경우가 발생

 

1-B. 현재 카메라의 위치를 기준으로 가장 가까운 곳으로 이동

💡 핵심 로직

1. Haversine거리 공식을 통해 현재 사용자의 카메라의 위도와 경도를 기준으로 거리를 계산
2. 현재 사용자 카메라의 위치와 가장 가까운 거리로 이동

 

zoom Level이 낮은 경우
zoom Level이 높은 경우

 

💡 피드백

굉장히 부자연스럽다 → 사용자가 필터를 클릭하였을 때 카메라가 확대, 축소가 강제적으로 발생하므로 사용자 경험 감소

결국 Zoom Level을 클라이언트가 강제적으로 고정 시키는 것은 부자연스러움!!

 

2. LatLng Bounds 측정 → Camera Bounds 사용

💡 핵심 로직

1. 각 마커들의 최소, 최대의 위,경도 값을 구한다.
2. 최소,최대의 위,경도 값을 기준으로 Camera Bounds를 설정
3. Bounds에 마커가 찍히는 것을 방지하고자 Padding 설정
4. 카메라의 Bounds 설정

 

💡 피드백

1. 카메라의 강제적 확대,축소로 인한 사용자 경험 감소
    1. 문제 : 결국 강제적으로 모든 음식점들을 보여주는 것이 문제
    - 음식점의 거리가 먼 경우
    모든 범위를 보여 준다는 것은 사용자가 추가한 음식점들의 거리가 서울~제주도인 경우
    - 음식점이 한 개인 경우 
    해당 필터링 된 음식점의 카메라를 매우 확대하여 보여주는 문제 발생

 

 

3. 현재 카메라를 위치를 고려하여 밀집도와 거리를 계산하여 이동

💡 핵심 로직

1. Haversine 거리 공식을 통해 현재 카메라의 위치와 마커들의 거리를 계산
2. 특정 반경 기준 안에 들어온 마커간의 응집도를 계산
3. 마커 이동
    1. 응집도가 가장 높은 곳으로 카메라 위치 업데이트
    2. 특정 반경 기준 안 마커들의 없는 경우 가장 가까운 거리로 이동


👉 부가 효과
1. 애니메이션 : 자연스러운 이동 진행
2. 특정 반경 기준 안 마커들만 계산 : 시간 복잡도 고려

 

 

💡 피드백

1. 기존 문제였던 카메라의 확대,축소 삭제
2. 사용자 경험 개선
    1. 사용자에게 다른 사용자의 맛집들을 가장 밀집도가 높은 곳을 이동
    2. 일정한 거리내의 밀집도가 없는 경우 가장 가까운 거리를 통해 맛집을 바로 제공 가능
3. 애니메이션 추가를 통해 자연스러운 이동

 

 

최종 개선

💡 3번 이후 개선점

1. 사용자들의 맛집 마커와 근처 음식점 마커 개별적 아이콘 선언
2. 근처 음식점 통신을 고려한 거리 계산
3. ‘특정 반경 기준’ 수정
    1. 특정 반경이 아닌 현재 사용자 카메라의 범위 고려
    2. zoom Level → camera 범위 산출 → 밀집도, 거리 계산 후 카메라 이동

 

 

 

최종 코드 로직

1. HaversineDistance 거리공식을 통해 마커들간의 거리 계산을 한다.

2. 사용자의 현재 카메라의 Zoom Level값을 기준으로 화면 거리를 계산하여 응집도를 계산

          이때 거리 밖이면 계산 제외

3. 응집도가 가장 높은곳으로 카메라 이동 및 카메라 범위 내에 없을시에 마커들 중 가장 가까운 곳으로 이동

 

 

 

최종 코드 

 

뷰모델

    private fun calculateDensity(latitude: Double, longitude: Double): Int {
        var density = 0

        for (point in uiState.value.markerList) {
            val distance = haversineDistance(latitude, longitude, point.latitude, point.longitude)
            if (distance <= uiState.value.cameraRadius) {
                density++
            } else {
                break
            }
        }

        return density
    }

    private fun haversineDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
        val radius = 6371000 // 지구 반지름(미터 단위)
        val distanceLatitude = Math.toRadians(lat2 - lat1)
        val distanceLongitude = Math.toRadians(lon2 - lon1)

        val a = (sin(distanceLatitude / 2) * sin(distanceLatitude / 2)) +
                (cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
                        sin(distanceLongitude / 2) * sin(distanceLongitude / 2))

        val c = 2 * atan2(sqrt(a), sqrt(1 - a))
        return radius * c
    }

    private fun moveCamera() {
        if (_uiState.value.markerList.isEmpty()) return
        if (uiState.value.addRestaurantId > 0) {
            val restaurantItem: UiRestaurantData =
                uiState.value.markerList.first { it.id == uiState.value.addRestaurantId }
            _uiState.update { state ->
                state.copy(
                    addRestaurantId = 0,
                    cameraLongitude = restaurantItem.longitude,
                    cameraLatitude = restaurantItem.latitude
                )
            }
            return
        }

        var closestPoint: LatLng? = null
        var maxDensityPoint: LatLng? = null
        var minDistance = Double.MAX_VALUE
        var maxDensity = 0

        for (point in _uiState.value.markerList) {
            val distance = haversineDistance(
                _uiState.value.cameraLatitude,
                _uiState.value.cameraLongitude,
                point.latitude,
                point.longitude
            )
            val density = calculateDensity(
                _uiState.value.cameraLatitude,
                _uiState.value.cameraLatitude,
            )

            if (distance < minDistance) {
                minDistance = distance
                closestPoint = LatLng(point.latitude, point.longitude)
            }

            if (density > maxDensity) {
                maxDensity = density
                maxDensityPoint = LatLng(point.latitude, point.longitude)
            }
        }

        val targetPoint = maxDensityPoint ?: closestPoint
        targetPoint?.let {
            _uiState.update { state ->
                state.copy(
                    cameraLatitude = targetPoint.latitude,
                    cameraLongitude = targetPoint.longitude
                )
            }
        }
    }

 

 

 

 

왜 마커에 대한 카메라 이동로직을 클라이언트에서 처리할까? 👉👉👉

https://bendeng-life.tistory.com/154

 

[Android Kotlin] 네이버 지도 마커 대량 정보에 대한 최적의 카메라 위치 선정 (+클라이언트 처리 이

앞선 글에서 네이버 지도에서 마커를 찍을 때 카메라 이동에 대한 로직을 처리한 글을 보셨을겁니다. https://bendeng-life.tistory.com/153 [Android Kotlin] 안드로이드 네이버 지도SDK 사용자 경험 개선 하기

bendeng-life.tistory.com

 

반응형

댓글