Android 공부 (7)

백상휘·2025년 11월 3일
0

Android

목록 보기
4/5

안드로이드는 특이하게 앱이 앱 권환을 OS 에 요청하는 방식을 취한다. 예제로는 구글맵스 API 를 사용해 지도를 추가하고 상호작용하는 법을 통해 아래의 내용을 학습한다.

  • 사용자 권한 요청
  • 사용자 위치 지도 표시
  • 지도 클릭과 커스텀 마커

앱이 요청할 수 있는 권한에는 기기 위치 획득, 연락처 접근, 카메라 실행, 블루투스 연결 등이 있다.

앱이 가진 권한에 따라 작업을 정의하는 법을 배워보자.

사용자 권한 요청

안드로이드 6 마시멜로 이상에서 실행 중이고 타겟 API 가 23 이상일 경우 일부 기능에 대해서는 런타임에 권한에 대한 경고를 표시한 뒤 사용하도록 할 수 있다.

이런 권한 요청에는 앱을 개발할 때 AndroidManifest.xml 수정이 필수이다. 예를 들어 SEND_SMS 권한을 포함하려면 아래와 같이 한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
  <uses-feature
        android:name="android.hardware.telephony"
        android:required="false" />
  <uses-permission android:name="android.permission.SEND_SMS"/>
</manifest>

일반적으로 안전한 권한(구글에서 정한)은 자동으로 허용된다. 그러나 위험한 권한은 사용자로부터 승인을 받도록 되어 있다. 사용자 승인 없이 기능을 수행하려고 하면 앱이 크래시 될 수도 있다.

그러므로 권한 요청 전 이미 승인했는지 여부를 확인하고 Dialog 창을 호출한다. shouldShowRequestPermissionRationale(Activity, String) 이라는 함수로 이런 작업이 가능하다.

권한 요청을 위해서는 build.gradle.kts , libs.versions.xml 에 의존성 주입을 해야 한다.

// build.gradle.kts
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)

// libs.versions.toml
android-acitivy-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
android-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }

다음은 권한 요청을 하는 코드이다.

class MainActivity: AppCompatActivity() {
  private lateinit var requestPermissionLauncher: ActivityResultLauncheer<String>
  override fun onCreate() {
  	// ...
    requestPermissionLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
      if (isGranted) {
        // ...
      } else {
        // ...
      }
    }
  }
}

권한 요청을 위한 런처를 등록하고 있다. 참조를 유지하는 이유는 결과값을 사용하기 위해서이다. 액티비티가 다시 시작되면 권한 상태값을 이용해 다른 작업을 수행할 수 있다.

override fun onResume() {
  when {
    hasLocationPermission() -> getLastLocation()
    shouldShowRequestPermissionRationale(this, ACESS_FINE_LOCATION) -> {
      showPermissionRationale {
        requestPermissionLauncher.launch(ACCESS_FINE_LOCATION)
      } // 권한 설명 필요한 경우
    }
    else -> requestPermissionLauncher.launch(ACCESS_FINE_LOCATION) // 권한 설명 필요없는 경우
  }
}

private fun hasLocationPermission() = checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PERMISSION_GRANTED

private fun showPermissionRationale(
  positiveAction: () -> Unit) {
    AlertDialog.Builder(this)
      .setTitle("Location Permission")
      .setMessage("We need your permission to find your current position")
      .setPositiveButton(android.R.string.ok) { _, _ -> positiveAction() }
      .setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
      .create().show()
  }
}

hasLocationPermission() 을 이용해 위치 권한을 확인하고 있다. checkSelfPermission(Context, String) 을 사용하는 로컬 함수이다. showPermissionRationale 함수는 사용자에게 권한이 필요한 이유를 설명하는 대화상자를 표시한다.

requestPermissionLauncher 의 결과값인 isGranted 에 따라 위의 코드에서 결과값을 처리할 수 있다.

registerForActivityResult(RequestPermission()) { isGranted ->
  if (isGranted) { getLastLocation() }
  else {
    showPermissionRationale {
      requestPermissionLauncher.launch(ACCESS_FINE_LOCATION)
    }
  }
}

사용자 위치 지도 표시

사용자가 위치 접근 권한을 허용하면 사용자의 기기에 가장 마지막으로 기록된 위치를 얻을 수 있다.

구글 플레이 로케이션 서비스(Google Play Location service) 를 통해 FusedLocationProviderClient 클래스를 제공한다. Fused Location Provider API 를 제공하는 이 클래스는 구글 플레이 로케이션 서비스 라이브러리 추가가 필요하다.

// build.gradle.kts
implementation(libs.gms.play.services.location)

// libs.versions.toml
playServicesLocation = "21.0.1"
gms-play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }

FusedLocationProviderClient 인스턴스는 LocationServices.getFusedLocationProviderClient(this@MainActivity) 를 호출하면 된다.

이 때의 작업들은 이미 사용자로부터 위치 권한을 획득했다고 가정한다.

마지막 위치는 인스턴스의 lastLocation 이라는 Task<Location> 이다. 즉, 비동기로 마지막 위치를 가져오며 실패 상황의 리스너도 추가 가능하다. Location 인스턴스는 위/경도 데이터를 담고 있다.

.addOnSuccessListener { location: Location? -> }

가끔 null 인 경우가 있는데 사용자가 위치 호출 중 앱의 위치 서비스를 비활성화하는 경우가 이에 해당한다.

지도 자체는 프래그먼트이며 SupportMapFragment 클래스를 사용한다.

지도 배치는 GoogleMap 인스턴스의 moveCamera 에다가 CameraUpdateFactory.newLatLng(LatLng) 를 반영하고, 지도에서 줌을 수행하려면 newLatLngZoom(LatLng, Float) 을 반영하면 된다. 마커는 MarkerOptions 를 사용한다.

지도 클릭과 커스텀 마커

마커를 더 다양한 방법으로 다루어보자.

지도 클릭을 감지하기 위한 리스너를 추가하면 된다.

override fun onMapReady(googleMap: GoogleMap) {
  mMap = googleMap.apply {
    setOnMapClickListener { latlng ->
      addMarkerAtLocation(latlng, "Deploy here")
    }
  }
}

기존에 추가된 마커를 다루기 위해서는 우선 GoogleMap.addMarker(MarkerOptions) 의 반환값을 참조 유지하도록 로컬 변수를 만들어야 할 것이다. 마커의 위치를 변환하는 데엔 Marker 의 position 세터가 필요하다.

MarkerOptions() 인스턴스의 BitmapDescriptor 를 이용해 마커를 커스텀 아이콘으로 대체 가능하다. BitmapDescriptor 는 BitmapDescriptorFactory 를 사용한다.

마커를 만들기 위해 벡터 드로어블을 만든다. New -> Vector Asset 을 선택하면 만들 수 있다. 만들어진 애셋은 ContextCompat.getDrawable(Context, Int) 를 호출하여 참조를 받아올 수 있다. Drawable 인스턴스는 그려져야 할 영역도 정의해야 하는데 Drawable.setBound(0,0,drawable.intrinsicWidth,drawable.intrinsicHeight) 를 호출한다. 이외에 틴트 설정, 비트맵 생성, 캔버스 생성은 아래와 같이 정리할 수 있다.

private fun getBitmapDescriptorFromVector(@DrawableRes vectorDrawableResourceId: Int): BitmapDescriptor? {
  var bitmap = ContextCompat.getDrawable(this, vectorDrawableResourceId)?.let { vectorDrawable ->
    vectorDrawable.setBounds(0, 0, vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight)
    // DrawableCompat 으로 감싸기
    val drawableWithTint = DrawableCompat.wrap(vectorDrawable)
    // 틴트 색상 설정
    DrawableCompat.setTint(drawableWithTint, Color.RED)
    // 비트맵 생성
    val bitmap = Bitmap.createBitmap(
      vectorDrawable.intrinsicWidth,
      vectorDrawable.intrinsicHeight,
      Bitmap.Config.ARGB_8888
    )
    // Canvas 생성
    val canvas = Canvas(bitmap)
    // 드로어블 그리기
    drawableWithTint.draw(canvas)
  }
  
  return BitmapDescriptorFactory.fromBitmap(bitmap)
    .also { bitmap?.recycle() }
}

이렇게 BitMapDescriptorFactory 를 생성하여 인스턴스를 얻을 수 있다. 비트맵을 리사이클하는 것을 잊으면 안되는데 메모리 누수를 방지하기 위함이다.

profile
plug-compatible programming unit

0개의 댓글