안드로이드 자바

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

teamnova 2024. 4. 22. 12:00
728x90

오늘은 카카오 지도 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();
    }
}