[Must Have Joyce의 안드로이드 앱프로그래밍] 12장 미세먼지 앱 V 2.0: 구글 맵

알린·2024년 2월 1일
0

Google Map API 사용

구글 맵 라이브러리

  1. 모듈 수준 buile.gradle에 다음 의존성 주입
dependencies {
	// Google Map
    implementation("com.google.android.gms:play-services-maps:17.0.0")
    implementation("com.google.android.gms:play-services-location:17.0.0")
}

구글 맵 API 키 발급받기

  1. 다음 페이지 들어가서 AirQuality 프로젝트 생성
    구글 맵 프로젝트 선택 페이지
  2. [API 및 서비스] ➡️ [ENABLE APIS AND SERVICE] 클릭
  3. Map SDK for Android ➡️ [사용]
  4. 메뉴에서 [API 및 서비스] ➡️ [사용자 인증 정보]
  5. [사용자 인증 정보 만들기] ➡️ [API 키]
  6. API 키 생성 완료 화면에서 [API 키를 수정] 클릭
  7. [키 제한사항]에서 Android 앱 선택 후 [Android 제한사항]에서 패키지 이름과 SHA-1 입력후 [완료]

    SHA-1 얻는 방법
    맥 기준 => 터미널에서 다음 명령어 입력
    keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

  8. API 키 생성 완료
  9. 매니패스트 파일의 application 태그 내에 다음 코드 추가
<meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="발급받은 API 키 넣기" />

Google Play 서비스 SDK 설치

  1. [Tools] ➡️ [SDK Manager]
  2. [Android SDK] ➡️ [SDK Tools] ➡️ [Google Play services] 체크 ➡️ [OK]
  3. [Finish]

floatingactionbutton

  • 화면이 움직이더라도 화면의 최상위에 고정할 수 있는 버튼
  • 아래 화면에서 오른쪽 하단에 있는 버튼

레이아웃 코드 작성

  1. 해당 버튼을 띄울 액티비티의 xml 파일에 다음 코드 추가
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="23dp"
        android:layout_marginBottom="30dp"
        android:backgroundTint="@color/skyBlue"
        android:src="@drawable/icon_map"
        app:borderWidth="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:shapeAppearanceOverlay="?attr/shapeAppearanceCornerExtraLarge"
        app:tint="@color/white" />

🚨 모양 설정을 하지 않은 기본이 버튼이 다음 사진과 같이 네모난 모양의 버튼으로 뜨는 경우

  • app:shapeAppearanceOverlay를 추가해 ?attr/shapeAppearanceCornerExtraLarge 속성을 넣으면 동그란 버튼이 됨
  1. [drawable] ➡️ [New] ➡️ [Drawable Resource File]

  2. Root element를 shape로 설정하고 OK

  3. 다음 코드 작성

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners
        android:radius="10dp"/>
    <solid
        android:color="@color/skyBlue_transparent" />
</shape>
  1. [layout]에서 activity_map.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_current_location"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="20dp"
        android:layout_marginBottom="12dp"
        android:backgroundTint="@color/white"
        android:src="@drawable/icon_location"
        android:tint="@color/skyBlue"
        app:borderWidth="0dp"
        app:layout_constraintBottom_toTopOf="@+id/btn_check_here"

        app:layout_constraintEnd_toEndOf="parent"
        app:shapeAppearanceOverlay="?attr/shapeAppearanceCornerExtraLarge" />

    <LinearLayout
        android:id="@+id/btn_check_here"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginStart="20dp"
        android:layout_marginEnd="20dp"
        android:layout_marginBottom="20dp"
        android:background="@drawable/bg_button_blue"
        android:gravity="center"
        android:orientation="horizontal"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageView
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:src="@drawable/icon_search"
            app:tint="@color/white" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="5dp"
            android:letterSpacing="-0.035"
            android:text="여기 미세먼지 농도 측정"
            android:textColor="@color/white"
            android:textSize="12sp"
            android:textStyle="bold" />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

기능 구현

  1. MainActivity.kt에서 위도, 경도 저장
class MainActivity : AppCompatActivity() {
	// 위도, 경도 저장
    var latitude: Double = 0.0
    var longitude: Double = 0.0
    
    private fun updateUI() {
    	if (latitude == 0.0 || longitude == 0.0) {
            // 위도와 경도 정보를 가져옴
            latitude = locationProvider.getLocationLatitude()
            longitude = locationProvider.getLocationLongitude()
        }
    }

}
  1. MapActivity.kt에 다음 코드 추가
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.airquality_app.databinding.ActivityMapBinding
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions

class MapActivity : AppCompatActivity(), OnMapReadyCallback {
    lateinit var binding: ActivityMapBinding

    private var mMap: GoogleMap? = null
    var currentLat: Double = 0.0
    var currentLng: Double = 0.0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMapBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // MainActivity에서 전달된 값 가져옴
        currentLat = intent.getDoubleExtra("currentLat", 0.0)
        currentLng = intent.getDoubleExtra("currentLng", 0.0)
        // SupportMapFragment 객체를 mapFragment에 저장
        // SupportMapFragment: 구글 맵 객체의 생명주기를 관리하는 객체
        val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
        // getMapAsync(): mapFragment에 OnMapReadyCallback 인터페이스를 등록해줌 => 지도가 준비되면 onMapReady()함수 자동 실행
        mapFragment?.getMapAsync(this)

        binding.btnCheckHere.setOnClickListener {
            mMap?.let {
                val intent = Intent()
                // 버튼이 눌린 시점의 카메라 포지션 가져옴(보이는 지도의 중앙지점 좌푯값 가져옴)
                intent.putExtra("latitude", it.cameraPosition.target.latitude)
                intent.putExtra("longitude", it.cameraPosition.target.longitude)
                // setResult() 함수: MainActivity.kt에서 정의해두었던 onActivityResult() 함수 실행
                setResult(Activity.RESULT_OK, intent)
                finish()  // 지도 액티비티 종료
            }
        }
    }

    // 지도가 준비되었을 때 실행되는 콜백
    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap
        mMap?.let {
            val currentLocation = LatLng(currentLat, currentLng)
            it.setMaxZoomPreference(20.0f)  // 줌 최댓값 설정
            it.setMinZoomPreference(12.0f)  // 줌 최솟값 설정
            it.moveCamera(CameraUpdateFactory.newLatLngZoom(currentLocation, 16f))
        }

        setMarker()

        // 플로팅 버튼이 눌렸을 때 현재 위도, 경도 정보를 가져와 지도의 위치를 움직임
        binding.fabCurrentLocation.setOnClickListener {
            val locationProvider = LocationProvider(this@MapActivity)
            // 위도와 경도 정보 가져옴
            val latitude = locationProvider.getLocationLatitude()
            val lontitude = locationProvider.getLocationLongitude()
            mMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(latitude, lontitude), 16f))
            setMarker()
        }
    }

    // 마커 설정 함수
    private fun setMarker() {
        mMap?.let {
            it.clear()  // 지도에 있는 마커 먼저 삭제
            val markerOptions = MarkerOptions()
            markerOptions.position(it.cameraPosition.target)  // 마커의 위치 설정
            markerOptions.title("마커 위치") // 마커의 이름 설정
            val marker = it.addMarker(markerOptions) // 지도에 마커를 추가하고, 마커 객체를 반환

            // setPosition() 함수: 마커를 지도에 추가
            // setOnCameraMoveListener() 함수: 지도가 움직일 때 마커도 함께 움직임
            it.setOnCameraMoveListener {
                marker?.let {
                    marker -> marker.setPosition(it.cameraPosition.target)
                }
            }
        }
    }
}
  1. MainActivity.kt에 다음 코드 추가
class MainActivity : AppCompatActivity() {
    // 결과를 받아와야 하는 액티비티를 실행할 때 사용하는 변수 선언
    // registerForActivityResult(: 다른 액티비티의 실행 결과를 콜백에 등록) 객체 생성
    // 콜백은 해당 액티비티가 결과를 반환할 때 실행
    val startMapActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult(), object : ActivityResultCallback<ActivityResult> {
        override fun onActivityResult(result: ActivityResult) {
            if (result?.resultCode ?: 0 == Activity.RESULT_OK) {
                // 지도 페이지에서 위도와 경도 반환
                latitude = result?.data?.getDoubleExtra("latitude", 0.0) ?: 0.0
                longitude = result?.data?.getDoubleExtra("longitude", 0.0) ?: 0.0
                updateUI()
            }
        }
    })
    
    override fun onCreate(savedInstanceState: Bundle?) 
        // 플로팅 액션 버튼 클릭 시
        setFab()
    }

    private fun setFab() {
        binding.fab.setOnClickListener {
            val intent = Intent(this@MainActivity, MapActivity::class.java)
            intent.putExtra("currentLat", latitude)
            intent.putExtra("currntLng", longitude)
            // startMapActivityResult.launch() : 지도 페이지로 이동하고, 등록해둔 onActivityResult 콜백에 보낸 값이 전달
            startMapActivityResult.launch(intent)
        }
    }
}

구현 결과

profile
Android 짱이 되고싶은 개발 기록 (+ ios도 조금씩,,👩🏻‍💻)

0개의 댓글