본문 바로가기

프로그래밍/안드로이드

[Android/KakaoAPI] 커스텀 마커 & 말풍선, 마커 클릭 시 이벤트

이 글에서는 안드로이드 카카오 지도 API에서 커스텀 마커커스텀 말풍선의 사용법, 그리고 POIItemEventListener를 이용해 마커와 말풍선 클릭 시 이벤트를 추가하는 방법을 적는다.


커스텀 마커

커스텀 마커는 굉장히 쉽다. 지도에 마커 등록 시 마커 이미지를 CustomImage로 설정하면 된다.

MainActivity.kt
 
class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding  // 뷰 바인딩
    private lateinit var mapView : MapView              // 카카오 지도 뷰

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        mapView = binding.mapView   // 카카오 지도 뷰

        // 서울시청에 마커 추가
        val marker = MapPOIItem()
        marker.apply {
            itemName = "서울시청"   // 마커 이름
            mapPoint = MapPoint.mapPointWithGeoCoord(37.5666805, 126.9784147)   // 좌표
            markerType = MapPOIItem.MarkerType.CustomImage          // 마커 모양 (커스텀)
            customImageResourceId = R.drawable.이미지               // 커스텀 마커 이미지
            selectedMarkerType = MapPOIItem.MarkerType.CustomImage  // 클릭 시 마커 모양 (커스텀)
            customSelectedImageResourceId = R.drawable.이미지       // 클릭 시 커스텀 마커 이미지
            isCustomImageAutoscale = false      // 커스텀 마커 이미지 크기 자동 조정
            setCustomImageAnchor(0.5f, 1.0f)    // 마커 이미지 기준점
        }
        mapView.addPOIItem(marker)

    }
}

★ 참고로 마커 이미지는 비트맵만 가능하다. 벡터 이미지는 지원하지 않는다.

 

isCustomImageAutoscale은 마커 이미지 크기를 기기 해상도에 따라 조정할 것인지를 정하는 것인데

true로 할 경우 이미지 크기를 해상도에 맞게 조절한다. 기기에 따라 마커가 너무 크거나 작게 나올 수 있다.

false로 할 경우 기기 해상도에 상관없이 동일한 크기로 표시한다. 고해상도 기기에서 마커가 흐리게 나올 수 있다.

 

다음은 isCustomImageAutoscale 설정에 따른 HD, FHD, QHD 해상도에서 마커의 모습이다. 마커의 크기는 64x64px 이다.

 

좌측부터 HD, FHD, QHD / isCustomImageAutoscale = true
좌측부터 HD, FHD, QHD / isCustomImageAutoscale = false

커스텀 말풍선

CalloutBalloonAdapter를 상속받는 클래스를 만들어 레이아웃을 커스텀 말풍선으로 이용할 수 있다.

다음은 커스텀 말풍선으로 사용할 레이아웃이다.

balloon_layout.xml
 
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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:id="@+id/ball_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="4px"
    app:cardCornerRadius="8px">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="200dp"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/ball_tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:text="장소명"
            android:textColor="?android:attr/textColorPrimary"
            android:textSize="12sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/ball_tv_address"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="주소"
            android:textSize="10sp"
            app:layout_constraintStart_toStartOf="@+id/ball_tv_name"
            app:layout_constraintTop_toBottomOf="@+id/ball_tv_name" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:text="클릭해서 추가 정보"
            android:textSize="10sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/ball_tv_address" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

balloon_layout.xml 레이아웃

참고로 레이아웃의 모습을 이미지의 형태로 변경해 말풍선을 표시하므로, 레이아웃에 버튼 등을 추가해도 상호작용이 불가능하다.

 

다음은 메인 액티비티 코드다.

MainActivity.kt
 
class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding  // 뷰 바인딩
    private lateinit var mapView : MapView              // 카카오 지도 뷰

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        mapView = binding.mapView   // 카카오 지도 뷰

        mapView.setCalloutBalloonAdapter(CustomBalloonAdapter(layoutInflater))  // 커스텀 말풍선 등록

        // 서울시청 마커 추가
        val marker = MapPOIItem()
        marker.apply {
            itemName = "서울시청"   // 마커 이름
            mapPoint = MapPoint.mapPointWithGeoCoord(37.5666805, 126.9784147)
            markerType = MapPOIItem.MarkerType.CustomImage
            customImageResourceId = R.drawable.marker_blue
            selectedMarkerType = MapPOIItem.MarkerType.CustomImage
            customSelectedImageResourceId = R.drawable.marker_red
            isCustomImageAutoscale = false
            setCustomImageAnchor(0.5f, 1.0f)
        }
        mapView.addPOIItem(marker)

    }

    // 커스텀 말풍선 클래스
    class CustomBalloonAdapter(inflater: LayoutInflater): CalloutBalloonAdapter {
        val mCalloutBalloon: View = inflater.inflate(R.layout.balloon_layout, null)
        val name: TextView = mCalloutBalloon.findViewById(R.id.ball_tv_name)
        val address: TextView = mCalloutBalloon.findViewById(R.id.ball_tv_address)

        override fun getCalloutBalloon(poiItem: MapPOIItem?): View {
            // 마커 클릭 시 나오는 말풍선
            name.text = poiItem?.itemName   // 해당 마커의 정보 이용 가능
            address.text = "getCalloutBalloon"
            return mCalloutBalloon
        }

        override fun getPressedCalloutBalloon(poiItem: MapPOIItem?): View {
            // 말풍선 클릭 시
            address.text = "getPressedCalloutBalloon"
            return mCalloutBalloon
        }
    }
}

getCalloutBalloon: 마커 클릭 시 표시할 뷰 (말풍선)

getPressedCalloutBalloon: 말풍선 클릭 시 표시할 뷰

 

추가 정보로 setCalloutBalloonAdapter가 마커를 추가하는 부분보다 앞에 있어야 커스텀 말풍선이 표시된다.

(처음에 만들 때 계속 기본 말풍선이 표시되서 당황했다.)

 

다음은 실행결과 화면이다.

마커 클릭 시 커스텀 말풍선
말풍선을 누르고 있을 경우

마커 & 말풍선 클릭 이벤트

POIItemEventListener를 상속받는 클래스를 만들어서 마커나 말풍선을 클릭했을 때 이벤트가 실행되도록 만들 수 있다.

이 글에서는 말풍선 클릭 시 대화상자가 표시되게 만들었다.

MainActivity.kt
 
class MainActivity : AppCompatActivity() {
    private lateinit var binding : ActivityMainBinding  // 뷰 바인딩
    private lateinit var mapView : MapView              // 카카오 지도 뷰
    private val eventListener = MarkerEventListener(this)   // 마커 클릭 이벤트 리스너

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        mapView = binding.mapView   // 카카오 지도 뷰

        mapView.setCalloutBalloonAdapter(CustomBalloonAdapter(layoutInflater))  // 커스텀 말풍선 등록
        mapView.setPOIItemEventListener(eventListener)  // 마커 클릭 이벤트 리스너 등록

        // 서울시청 마커 추가
        val marker = MapPOIItem()
        marker.apply {
            itemName = "서울시청"
            mapPoint = MapPoint.mapPointWithGeoCoord(37.5666805, 126.9784147)
            markerType = MapPOIItem.MarkerType.CustomImage
            customImageResourceId = R.drawable.marker_blue
            selectedMarkerType = MapPOIItem.MarkerType.CustomImage
            customSelectedImageResourceId = R.drawable.marker_red
            isCustomImageAutoscale = false
            setCustomImageAnchor(0.5f, 1.0f)
        }
        mapView.addPOIItem(marker)

    }

    // 커스텀 말풍선 클래스
    class CustomBalloonAdapter(inflater: LayoutInflater): CalloutBalloonAdapter {
        val mCalloutBalloon: View = inflater.inflate(R.layout.balloon_layout, null)
        val name: TextView = mCalloutBalloon.findViewById(R.id.ball_tv_name)
        val address: TextView = mCalloutBalloon.findViewById(R.id.ball_tv_address)

        override fun getCalloutBalloon(poiItem: MapPOIItem?): View {
            // 마커 클릭 시 나오는 말풍선
            name.text = poiItem?.itemName
            address.text = "getCalloutBalloon"
            return mCalloutBalloon
        }

        override fun getPressedCalloutBalloon(poiItem: MapPOIItem?): View {
            // 말풍선 클릭 시
            address.text = "getPressedCalloutBalloon"
            return mCalloutBalloon
        }
    }

    // 마커 클릭 이벤트 리스너
    class MarkerEventListener(val context: Context): MapView.POIItemEventListener {
        override fun onPOIItemSelected(mapView: MapView?, poiItem: MapPOIItem?) {
            // 마커 클릭 시
        }

        override fun onCalloutBalloonOfPOIItemTouched(mapView: MapView?, poiItem: MapPOIItem?) {
            // 말풍선 클릭 시 (Deprecated)
            // 이 함수도 작동하지만 그냥 아래 있는 함수에 작성하자
        }

        override fun onCalloutBalloonOfPOIItemTouched(mapView: MapView?, poiItem: MapPOIItem?, buttonType: MapPOIItem.CalloutBalloonButtonType?) {
            // 말풍선 클릭 시
            val builder = AlertDialog.Builder(context)
            val itemList = arrayOf("토스트", "마커 삭제", "취소")
            builder.setTitle("${poiItem?.itemName}")
            builder.setItems(itemList) { dialog, which ->
                when(which) {
                    0 -> Toast.makeText(context, "토스트", Toast.LENGTH_SHORT).show()  // 토스트
                    1 -> mapView?.removePOIItem(poiItem)    // 마커 삭제
                    2 -> dialog.dismiss()   // 대화상자 닫기
                }
            }
            builder.show()
        }

        override fun onDraggablePOIItemMoved(mapView: MapView?, poiItem: MapPOIItem?, mapPoint: MapPoint?) {
            // 마커의 속성 중 isDraggable = true 일 때 마커를 이동시켰을 경우
        }
    }
}

context는 대화상자 표시 때문에 추가했다. 없어도 상관없다.

 

이벤트 리스너 클래스의 객체는 반드시 MainActivity의 가장 바깥쪽에서 생성되어야 한다.

onCreate에서 생성되거나 setPOIItemEventListener에 클래스를 바로 넣을 경우 작동하지 않는다.

 

다음은 실행 결과다.

실행 결과