본문 바로가기
안드로이드 자바

[JAVA][Android] 카카오 지도에 현재 위치 표시하기

by teamnova 2024. 4. 22.

오늘은 카카오 지도 API를 사용해서 현재 위치를 지도 위에 표시해 보겠습니다.

 

카카오 지도 API를 사용하기 위한 설정 방법은 아래 글을 참고해 주세요.

2024.04.13 - [안드로이드 자바] - [JAVA][Android] 카카오 지도 API Android v2 사용하기

 

시연 영상 먼저 보여드리겠습니다.

 

 

우선은 기기의 현재 위치 정보를 받아와야 합니다.

안드로이드에서는 위치 정보를 가져오기 위해서 LocationManager나 FusedLocationProviderClient를 사용할 수 있습니다.

이 글에서는 FusedLocationProviderClient를 사용해 보겠습니다.

FusedLocationProviderClient(통합위치정보제공자) API는 Google Play 서비스의 Location API 중 하나로, 해당 API를 사용하면 간편하게 기기의 위치정보를 얻을 수 있습니다.

1. 의존성 추가

우선 build.gradle 에 Google Play 서비스 location에 대한 의존성을 추가합니다.

dependencies {
	implementation 'com.google.android.gms:play-services-location:21.1.0'
}

2. 매니페스트 설정

앱 매니페스트에 위치정보 사용 권한을 선언합니다.

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

3.  위치정보 액세스 권한 요청하기

다음으로 액티비티에서 사용자에게 위치 권한을 승인할 것을 요청합니다. 만약 사용자가 권한을 거부할 경우 FusedLocationProviderClient를 사용할 수 없습니다.

따라서 이 예제에서는 우선 해당 앱의 위치권한 승인 여부를 확인한 뒤, 만약 권한이 허용되지 않았다면 alert 다이얼로그를 띄워서 사용자가 위치권한을 허용하거나 앱을 종료하기를 선택할 수 있도록 하였습니다.

//onRequestPermissionsResult에서 권한 요청 결과를 받기 위한 request code입니다.
private final int PERMISSION_REQUEST_CODE = 1001;
//요청할 위치 권한 목록입니다.
private String[] locationPermissions = {
    Manifest.permission.ACCESS_COARSE_LOCATION, 
    Manifest.permission.ACCESS_FINE_LOCATION};
    
    // ...
    
@Override
protected void onCreate(Bundle savedInstanceState) {
    // ...
    // 이미 앱에 위치 권한이 있는지 확인합니다.
    if (ContextCompat.checkSelfPermission(this, locationPermissions[0]) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, locationPermissions[1]) == PackageManager.PERMISSION_GRANTED) {
        // 위치 권한이 이미 허용되어 있는 경우입니다. 여기서 원하는 작업을 진행하면 됩니다.
        getCurLocation(); // 현재 위치 가져오는 메서드 - 4번 항목 참고
    } else {
        // 위치 권한이 없는 경우, 권한 요청 다이얼로그를 띄웁니다.
         ActivityCompat.requestPermissions(this, locationPermissions, LOCATION_PERMISSION_REQUEST_CODE);
    }
    
}

// 사용자가 권한 요청 다이얼로그에 응답하면 이 메소드가 실행됩니다.
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    // 위치 권한 요청에 대한 응답일 경우
    if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 사용자가 위치 권한을 허가했을 경우입니다. 여기서 원하는 작업을 진행하면 됩니다.
            getCurLocation();// 현재 위치 가져오는 메서드 - 4번 항목 참고
        } else {
            // 위치 권한이 거부되었을 경우, 다이얼로그를 띄워서 사용자에게 앱을 종료할지, 권한 설정 화면으로 이동할지 선택하게 합니다.
            showPermissionDeniedDialog();
        }
    }
}

private void showPermissionDeniedDialog() {
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage("위치 권한 거부시 앱을 사용할 수 없습니다.")
            .setPositiveButton("권한 설정하러 가기", new DialogInterface.OnClickListener() {
                // 권한 설정하러 가기 버튼 클릭시 해당 앱의 상세 설정 화면으로 이동합니다.
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    try {
                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:" + getPackageName()));
                        startActivity(intent);
                    } catch (ActivityNotFoundException e) {
                        e.printStackTrace();
                        Intent intent = new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
                        startActivity(intent);
                    } finally {
                        finish();
                    }
                }
            })
            .setNegativeButton("앱 종료하기", new DialogInterface.OnClickListener() {
                // 앱 종료하기 버튼 클릭시 앱을 종료합니다.
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    finish();
                }
            })
            .setCancelable(false)
            .show();
}

 

 

4. FusedLocationProviderClient로 현재 위치 가져와 카카오 지도에 시작하기

이제 FusedLocationProviderClient의 인스턴스를 만든 후, getCurrentLocation() 메서드를 사용하여 현재 기기 위치를 가져옵니다.

private FusedLocationProviderClient fusedLocationClient;
private LatLng startPosition = null;
private ProgressBar progressBar;
private MapView mapView;
private Label centerLabel;

// 카카오지도가 정상적으로 시작되었을때 실행되는 콜백 메서드 입니다.
private KakaoMapReadyCallback readyCallback = new KakaoMapReadyCallback() {
        @Override
        public void onMapReady(@NonNull KakaoMap kakaoMap) {
            progressBar.setVisibility(View.GONE);
            
            // 현재 위치를 나타낼 label를 그리기 위해 kakaomap 인스턴스에서 LabelLayer를 가져옵니다.
            LabelLayer layer = kakaoMap.getLabelManager().getLayer();
            
            // LabelLayer에 라벨을 추가합니다. 카카오 지도 API 공식 문서에 지도에서 사용하는 이미지는 drawable-nodpi/ 에 넣는 것을 권장합니다.
            centerLabel = layer.addLabel(LabelOptions.from("centerLabel", startPosition)
                    .setStyles(LabelStyle.from(R.drawable.red_dot_marker).setAnchorPoint(0.5f, 0.5f))
                    .setRank(1));
                    
            //라벨의 위치가 변하더라도 항상 화면 중앙에 위치할 수 있도록 trackingManager를 통해 tracking을 시작합니다.
            TrackingManager trackingManager = kakaoMap.getTrackingManager();
            trackingManager.startTracking(centerLabel);
            
            // 위치 업데이트 메서드 입니다. 5번 항목 참조
            startLocationUpdates();
        }

	// 카카오 지도의 초기 위치를 반환하는 메서드입니다.
        @NonNull
        @Override
        public LatLng getPosition() {
            return startPosition;
        }
		
        // 카카오 지도의 초기 줌레벨을 설정하는 메서드 입니다.
        @NonNull
        @Override
        public int getZoomLevel() {
            return 17;
        }
    };

//...

@Override
protected void onCreate(Bundle savedInstanceState) {
    //..
    //FusedLocationProviderClient 인스턴스를 가져옵니다.
    fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
    
}

//...
// 해당 메서드를 위치 권한 획득한 이후에 실행되도록 합니다.
@SuppressLint("MissingPermission")
private void getCurLocation() {
    fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY,null).addOnSuccessListener(this, location -> {
        if (location != null) {
        	// 구해진 현재 위치를 카카오 지도의 시작 위치를 설정할 때 사용하기 위해 LatLng 객체로 변환합니다.
            startPosition = LatLng.from(location.getLatitude(), location.getLongitude());
            // 카카오 지도를 시작합니다.
            mapView.start(readyCallback);
        }
    });
}

 

 

 

5. 위치 업데이트 요청하여 기기 위치가 지도에 계속 반영되도록 하기

// 위치 업데이트가 요청된 적 있는지 여부를 저장합니다.
private boolean requestingLocationUpdates = false;
private LocationRequest locationRequest;
private LocationCallback locationCallback;

// ...

@Override
protected void onCreate(Bundle savedInstanceState) {
	//...
	
    // 2초마다 최고 정확도로 업데이트를 요청하는 LocationRequest 객체를 생성합니다.
    locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000L).build();
    // 위치가 업데이트 될때마다 실행될 LocationCallback 메서드를 생성합니다.
    locationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(@NonNull LocationResult locationResult) {
            for (Location location : locationResult.getLocations()) {
            // 업데이트된 위치로 라벨을 이동시킵니다.
                centerLabel.moveTo(LatLng.from(location.getLatitude(), location.getLongitude()));
            }
        }
    };
}

// 위치 업데이트 요청을 시작하는 메서드입니다.
@SuppressLint("MissingPermission")
private void startLocationUpdates() {
    requestingLocationUpdates = true;
    fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper());
}

// 액티비티가 onPause 되었다가 onResume될때, requestingLocationUpdates의 값이 true면 위치 업데이트가 다시 시작되도록 합니다.
@Override
protected void onResume() {
    super.onResume();
    if (requestingLocationUpdates) {
        startLocationUpdates();
    }
}

// 액티비티가 onPause 상태가 될때, 위치업데이트가 중단시켜서 불필요한 배터리 및 리소스 낭비를 막습니다.
@Override
protected void onPause() {
    super.onPause();
    fusedLocationClient.removeLocationUpdates(locationCallback);
}

 

 

6. 전체 코드

1. activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.kakao.vectormap.MapView
        android:id="@+id/map_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" >

    </com.kakao.vectormap.MapView>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

2. MainActivity

package com.example.kakaomaptest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.provider.Settings;
import android.view.View;
import android.widget.ProgressBar;

import com.google.android.gms.location.FusedLocationProviderClient;
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.google.android.gms.location.Priority;
import com.kakao.vectormap.KakaoMap;
import com.kakao.vectormap.KakaoMapReadyCallback;
import com.kakao.vectormap.LatLng;
import com.kakao.vectormap.MapView;
import com.kakao.vectormap.label.Label;
import com.kakao.vectormap.label.LabelLayer;
import com.kakao.vectormap.label.LabelOptions;
import com.kakao.vectormap.label.LabelStyle;
import com.kakao.vectormap.label.TrackingManager;

public class MainActivity extends AppCompatActivity {
    private final int LOCATION_PERMISSION_REQUEST_CODE = 1001;
    private final String[] locationPermissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
    private FusedLocationProviderClient fusedLocationClient;
    private LatLng startPosition = null;
    private ProgressBar progressBar;
    private MapView mapView;
    private Label centerLabel;
    private boolean requestingLocationUpdates = false;
    private LocationRequest locationRequest;
    private LocationCallback locationCallback;
    private KakaoMapReadyCallback readyCallback = new KakaoMapReadyCallback() {
        @Override
        public void onMapReady(@NonNull KakaoMap kakaoMap) {
            progressBar.setVisibility(View.GONE);
            LabelLayer layer = kakaoMap.getLabelManager().getLayer();
            centerLabel = layer.addLabel(LabelOptions.from("centerLabel", startPosition)
                    .setStyles(LabelStyle.from(R.drawable.red_dot_marker).setAnchorPoint(0.5f, 0.5f))
                    .setRank(1));
            TrackingManager trackingManager = kakaoMap.getTrackingManager();
            trackingManager.startTracking(centerLabel);
            startLocationUpdates();
        }

        @NonNull
        @Override
        public LatLng getPosition() {
            return startPosition;
        }

        @NonNull
        @Override
        public int getZoomLevel() {
            return 17;
        }
    };

    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mapView = findViewById(R.id.map_view);
        progressBar = findViewById(R.id.progressBar);

        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
        locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000L).build();
        locationCallback = new LocationCallback() {
            @Override
            public void onLocationResult(@NonNull LocationResult locationResult) {
                for (Location location : locationResult.getLocations()) {
                    centerLabel.moveTo(LatLng.from(location.getLatitude(), location.getLongitude()));
                }
            }
        };

        if (ContextCompat.checkSelfPermission(this, locationPermissions[0]) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this, locationPermissions[1]) == PackageManager.PERMISSION_GRANTED) {
            getStartLocation();
        } else {
            ActivityCompat.requestPermissions(this, locationPermissions, LOCATION_PERMISSION_REQUEST_CODE);
        }

    }

    @Override
    protected void onResume() {
        super.onResume();
        if (requestingLocationUpdates) {
            startLocationUpdates();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        fusedLocationClient.removeLocationUpdates(locationCallback);
    }

    @SuppressLint("MissingPermission")
    private void getStartLocation() {
        fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY,null)
                .addOnSuccessListener(this, location -> {
                    if (location != null) {
                        startPosition = LatLng.from(location.getLatitude(), location.getLongitude());
                        mapView.start(readyCallback);
                    }
                });
    }

    @SuppressLint("MissingPermission")
    private void startLocationUpdates() {
        requestingLocationUpdates = true;
        fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper());
    }


    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                getStartLocation();
            } else {
                showPermissionDeniedDialog();
            }
        }
    }

    private void showPermissionDeniedDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setMessage("위치 권한 거부시 앱을 사용할 수 없습니다.")
                .setPositiveButton("권한 설정하러 가기", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        try {
                            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse("package:" + getPackageName()));
                            startActivity(intent);
                        } catch (ActivityNotFoundException e) {
                            e.printStackTrace();
                            Intent intent = new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
                            startActivity(intent);
                        } finally {
                            finish();
                        }
                    }
                })
                .setNegativeButton("앱 종료하기", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        finish();
                    }
                })
                .setCancelable(false)
                .show();
    }
}