[Compose] Naver Map 현재 위치로 이동하기

Android/Android · 2023. 4. 8. 16:12
반응형

Naver Map을 사용하여 현 위치로 이동하는 Compose 코드를 작성하겠습니다.

 

 

먼저 아래 소스를 작성합니다.

 

import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.naver.maps.map.LocationSource
import com.naver.maps.map.compose.ExperimentalNaverMapApi

@ExperimentalNaverMapApi
@OptIn(ExperimentalPermissionsApi::class)
@Composable
public fun rememberFusedLocationSource(): LocationSource {
    val permissionsState = rememberMultiplePermissionsState(
        listOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION,
        )
    )
    val context = LocalContext.current
    val locationSource = remember {
        object : FusedLocationSource(context) {
            override fun hasPermissions(): Boolean {
                return permissionsState.allPermissionsGranted
            }

            override fun onPermissionRequest() {
                permissionsState.launchMultiplePermissionRequest()
            }
        }
    }

    val allGranted = permissionsState.allPermissionsGranted
    LaunchedEffect(allGranted) {
        if (allGranted) {
            locationSource.onPermissionGranted()
        }
    }
    return locationSource
}

private abstract class FusedLocationSource(context: Context) : LocationSource {

    private val callback = object : FusedLocationCallback(context.applicationContext) {
        override fun onLocationChanged(location: Location?) {
            lastLocation = location
        }
    }

    private var listener: LocationSource.OnLocationChangedListener? = null
    private var isListening: Boolean = false
    private var lastLocation: Location? = null
        set(value) {
            field = value
            if (listener != null && value != null) {
                listener?.onLocationChanged(value)
            }
        }

    abstract fun hasPermissions(): Boolean
    abstract fun onPermissionRequest()

    fun onPermissionGranted() {
        setListening(true)
    }

    override fun activate(listener: LocationSource.OnLocationChangedListener) {
        this.listener = listener
        if (isListening.not()) {
            if (hasPermissions()) {
                setListening(true)
            } else {
                onPermissionRequest()
            }
        }
    }

    override fun deactivate() {
        if (isListening) {
            setListening(false)
        }
        this.listener = null
    }

    private fun setListening(listening: Boolean) {
        if (listening) {
            callback.startListening()
        } else {
            callback.stopListening()
        }
        isListening = listening
    }

    private abstract class FusedLocationCallback(private val context: Context) {

        private val locationCallback: LocationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                onLocationChanged(locationResult.lastLocation)
            }
        }

        fun startListening() {
            GoogleApiClient.Builder(context)
                .addConnectionCallbacks(object : GoogleApiClient.ConnectionCallbacks {
                    @SuppressLint("MissingPermission")
                    override fun onConnected(bundle: Bundle?) {
                        val request = LocationRequest()
                        request.priority = 100
                        request.interval = 1000L
                        request.fastestInterval = 1000L
                        LocationServices.getFusedLocationProviderClient(context)
                            .requestLocationUpdates(request, locationCallback, null)
                    }

                    override fun onConnectionSuspended(i: Int) {}
                })
                .addApi(LocationServices.API)
                .build()
                .connect()
        }

        fun stopListening() {
            LocationServices
                .getFusedLocationProviderClient(context)
                .removeLocationUpdates(locationCallback)
        }

        abstract fun onLocationChanged(location: Location?)
    }
}

 

네이버가 제공하는 FusedLocationSource 코드를 사용했습니다. 

 

* 먼저 rememberMultiplePermissionsState()로 위치 권한을 받습니다.

* FusedLocationSource 콜백을 호출합니다.

* activate() / deactivate() (위치추적기능 활성화 / 비활성화)가 호출되었을 때 FusedLocationSourceCallback의 onLocationChanged() 콜백 메서드를 호출하여 위치 정보를 갱신합니다.

 

FusedLocationSourceCallback의 코드 중 GoogleApiClient 빌더가 deprecated 되었습니다. 이는 추후에 새로운 코드로 작성해 보겠습니다.

 

 

다음으로 Naver Map을 호출합니다.

 

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.common.location.rememberFusedLocationSource
import com.naver.maps.map.compose.*

@ExperimentalNaverMapApi
@Composable
fun MainScreen(modifier: Modifier = Modifier) {
    Box(
        modifier = modifier
    ) {
        val cameraPositionState = rememberCameraPositionState()
        NaverMap(
            modifier = Modifier.fillMaxSize(),
            cameraPositionState = cameraPositionState,
            locationSource = rememberFusedLocationSource(),
            properties = MapProperties(
                locationTrackingMode = LocationTrackingMode.Follow
            ),
            uiSettings = MapUiSettings(
                isLocationButtonEnabled = true,
            ),
        ) {

        }
    }
}

 

* 먼저 cametaPositionState를 remember 합니다. 이는 현재 카메라 position을 기억하고, recomposition 되면 저장한 position을 restore 하는 구문입니다. 아래는 해당 라이브러리의 rememberCameraPositionState 코드 중 일부입니다.

 

@Composable
public inline fun rememberCameraPositionState(
    key: String? = null,
    crossinline init: CameraPositionState.() -> Unit = {},
): CameraPositionState = rememberSaveable(key = key, saver = CameraPositionState.Saver) {
    CameraPositionState().apply(init)
}

/* 중략 */

public val Saver: Saver<CameraPositionState, CameraPosition> = Saver(
    save = { it.position },
    restore = { CameraPositionState(it) }
)

 

* 다음으로 locationSource 파라미터에 아까 작성한 rememberFusedLocationSource() 메서드를 넣어줍니다.

* properties 파라미터에 locationTrackingMode를 LocationTrackingMode.Follow로 설정해서 Naver Map이 로드될 때 현재 위치로 바로 이동할 수 있도록 지정합니다.

* uiSettings 파라미터로 locationButton을 활성화합니다.

 

다음과 같이 현재 위치가 정상적으로 출력됩니다. (현위치는 에뮬레이터 임의로 조정했습니다.)

 

 

 

감사합니다.

반응형