Android - Google Map API 설치부터 심화 활용까지 (카드뷰 및 최종) (5) (Kotlin)

업데이트:

  • 연구주제 : Android - Google Map 활용 (Kotlin)
  • 연구목적 : 안드로이드에서의 코틀린 활용
  • 연구일시 : 2020년 04월 09일 09:00~17:00
  • 연구자 : 이재환 ljh951103@naver.com
  • 연구장비 : HP EliteDesk 800 G4 TWR, Kotlin, Android studio, IntelliJ
  • 관련연구 : Java, Android, Kotlin, Google, API


서론

이제 추가적으로 구글 맵에 하단에 나오는 카드뷰를 구현해보겠다.
여기서 말하는 카드뷰란 다음과 같은 뷰이다.

image

위 예시의 하단뷰는 뷰페이저로 구현되었지만 우리는 일반 카드뷰로 구현해보겠다.


본론

레이아웃

레이아웃을 작성하기전에 다음 라이브러리를 추가한다.

com.android.support:cardview-v7:28.0.0 는 카드뷰 라이브러리다.
com.makeramen:roundedimageview:2.3.0는 둥근 모서리로 이미지를 사용하게 해주는 라이브러리다.

dependencies {
    ...
    implementation 'com.android.support:cardview-v7:28.0.0'
    implementation 'com.makeramen:roundedimageview:2.3.0'
    ...
}


이제 레이아웃을 작성해보자.
카드뷰 안의 자식뷰들은 각자의 프로젝트에 맞게 수정하면 된다.

 <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="310dp"
        android:layout_gravity="bottom"
        android:layout_marginLeft="12dp"
        android:layout_marginTop="12dp"
        android:layout_marginRight="12dp"
        android:layout_marginBottom="0dp"
        android:visibility="gone"
        android:gravity="top"

        app:cardCornerRadius="20dp"
        app:cardUseCompatPadding="true">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <com.makeramen.roundedimageview.RoundedImageView
                android:id="@+id/map_image"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="0.8"
                android:scaleType="centerCrop"
                app:riv_border_color="#333333"
                app:riv_corner_radius_top_left="20dp"
                app:riv_corner_radius_top_right="20dp"
                app:riv_mutate_background="true"
                app:riv_oval="false" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="12dp"
                    android:layout_marginTop="3dp"
                    android:layout_marginRight="3dp"
                    android:layout_marginBottom="3dp"
                    android:orientation="vertical">

                    <androidx.appcompat.widget.AppCompatTextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="이름: "
                        android:textStyle="bold" />

                    <androidx.appcompat.widget.AppCompatTextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="날짜: "
                        android:textStyle="bold" />

                    <androidx.appcompat.widget.AppCompatTextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="장소: "
                        android:textStyle="bold" />

                    <androidx.appcompat.widget.AppCompatTextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="태그: "
                        android:textStyle="bold" />
                </LinearLayout>

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="3dp"
                    android:orientation="vertical">

                    <androidx.appcompat.widget.AppCompatTextView
                        android:id="@+id/map_name"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />

                    <androidx.appcompat.widget.AppCompatTextView
                        android:id="@+id/map_date"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />

                    <androidx.appcompat.widget.AppCompatTextView
                        android:id="@+id/map_location"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />

                    <androidx.appcompat.widget.AppCompatTextView
                        android:id="@+id/map_tag"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content" />
                </LinearLayout>
            </LinearLayout>
        </LinearLayout>

        <ImageView
            android:id="@+id/map_favorite"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:layout_gravity="bottom|right"
            android:layout_margin="10dp"
            android:src="@drawable/ic_favorite" />
    </androidx.cardview.widget.CardView>


맵에 카드뷰 삽입

이제 맵에 작성한 카드뷰 레이아웃을 삽입해야 한다.
다음과 같이 맵을 클릭했을 때, 카드뷰를 감추도록 한다.

mMap.setOnMapClickListener {
            card_view.visibility = View.GONE
            if(selectedMarker != null) {
                selectedMarker!!.setIcon(BitmapDescriptorFactory.fromBitmap(createDrawableFromView(this, marker_view)))
                selectedMarker = null
            }
        }


클러스터 마커를 클릭했을 때, 나타나도록 카드뷰를 보이게 한다.

그 안에 카드뷰를 클릭했을 때, 프로젝트마다 정의한 부로 넘어가도록 한다.

 private fun clusterItemClick(mMap: GoogleMap) {
        mClusterManager.setOnClusterItemClickListener { p0 ->
            // // 클러스터 아이템 클릭 리스너
            card_view.visibility = View.VISIBLE
            val center: CameraUpdate = CameraUpdateFactory.newLatLng(p0?.position)
            mMap!!.animateCamera(center)
            ImageLoder.execute(
                ImageLoad(
                    map_image,
                    p0.id
                )
            )
  
            Log.d("qweqwe","wqe")
            changeRenderer(p0)

            card_view.setOnClickListener {
                if (SystemClock.elapsedRealtime() - mLastClickTime > 1000) {
                    val intent = Intent(this@Main_Map, PhotoViewPager::class.java)
                    intent.putExtra("index", p0.index)
                    startActivityForResult(intent, 900)
                }
                mLastClickTime = SystemClock.elapsedRealtime()
            }
            true
        }
    }


맵 실행

image

image

모든 것이 정상적으로 동작함을 확인할 수 있다.


전체 코드

본 코드에서는 내가 진행중인 프로젝트를 기준으로 작성되었기 때문에 참고용으로 적합하다고 생각한다.

DB 입/출력이나 각각의 데이터, 일부 연산들은 현재 맵 클래스 뿐만 아니라 다른 외부 클래스에도 포함되어있기 떄문에 그대로 사용하면 절대 돌아가지 않는다.

개인 프로젝트에 맞춰서 참고하여 잘 사용하길 바라는 바이다.

package com.example.wimmy.Activity

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.DisplayMetrics
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProviders
import com.example.wimmy.Activity.Main_Map.Companion.selectedMarker
import com.example.wimmy.Activity.MarkerClusterRenderer.Companion.createDrawableFromView
import com.example.wimmy.ImageLoad
import com.example.wimmy.ImageLoder
import com.example.wimmy.R
import com.example.wimmy.db.LatLngData
import com.example.wimmy.db.MediaStore_Dao
import com.example.wimmy.db.PhotoViewModel
import com.google.android.gms.maps.*
import com.google.android.gms.maps.model.*
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer
import kotlinx.android.synthetic.main.main_map.*


class Main_Map: AppCompatActivity(), OnMapReadyCallback {
    private var index = 0
    private lateinit var vm : PhotoViewModel
    private lateinit var mClusterManager: ClusterManager<LatLngData>
    private lateinit var clusterRenderer: DefaultClusterRenderer<LatLngData>
    private val builder: LatLngBounds.Builder = LatLngBounds.builder()
    private val ZoomLevel: Int = 12
    private var size_check: Int = 0
    private var mLastClickTime: Long = 0

    private lateinit var marker_view: View
    private lateinit var tag_marker: TextView


    companion object {
        var selectedMarker: Marker? = null
    }
    private lateinit var mMap: GoogleMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_map)

        val mapFragment = supportFragmentManager.findFragmentById(R.id.mapview) as SupportMapFragment
        marker_view = LayoutInflater.from(this).inflate(R.layout.marker_layout, null)
        tag_marker = marker_view.findViewById(R.id.tag_marker) as TextView

        vm = ViewModelProviders.of(this).get(PhotoViewModel::class.java)
        mapFragment.getMapAsync(this)
    }

    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap
        mMap.uiSettings.isMyLocationButtonEnabled = true;
        mMap.uiSettings.isZoomControlsEnabled = true;

        mClusterManager = ClusterManager<LatLngData>(this, mMap)
        clusterRenderer = MarkerClusterRenderer(
            this,
            mMap,
            mClusterManager,
            marker_view
        )
        getExtra()
        mClusterManager.setRenderer(clusterRenderer)

        mMap.setOnCameraChangeListener (mClusterManager)
        mMap.setOnMarkerClickListener(mClusterManager)

        mMap.setOnInfoWindowClickListener { }
        mMap.setOnMapClickListener {
            card_view.visibility = View.GONE
            if(selectedMarker != null) {
                selectedMarker!!.setIcon(BitmapDescriptorFactory.fromBitmap(createDrawableFromView(this, marker_view)))
                selectedMarker = null
            }
        }

        clusterItemClick(mMap)
        clusterClick(mMap)

        refresh_loaction.setOnClickListener() {
            boundmap()
        }
    }

    fun cameraInit() {
        if(size_check < 100) {
            boundmap()
        }
        loading_location_name.visibility = View.GONE
    }

    private fun clusterClick(mMap: GoogleMap) {
        mClusterManager.setOnClusterClickListener { cluster ->
            // 클러스터 클릭 리스너
            val builder_c: LatLngBounds.Builder = LatLngBounds.builder()
            for (item in cluster.items) {
                if (item != null) {
                    builder_c.include(item.position)
                }
            }
            val bounds_c: LatLngBounds = builder_c.build()
            mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds_c, ZoomLevel))
            val zoom: Float = mMap.cameraPosition.zoom - 0.5f
            mMap.animateCamera(CameraUpdateFactory.zoomTo(zoom))
            true
        }
    }

    private fun clusterItemClick(mMap: GoogleMap) {
        mClusterManager.setOnClusterItemClickListener { p0 ->
            // // 클러스터 아이템 클릭 리스너
            card_view.visibility = View.VISIBLE
            val center: CameraUpdate = CameraUpdateFactory.newLatLng(p0?.position)
            mMap!!.animateCamera(center)
            ImageLoder.execute(
                ImageLoad(
                    map_image,
                    p0.id
                )
            )
            vm.setName(map_name, p0.id )
            vm.setDate(map_date, p0.id)
            vm.setLocation(map_location, p0.id)
            vm.checkFavorite(map_favorite, p0.id)

            Log.d("qweqwe","wqe")
            changeRenderer(p0)

            card_view.setOnClickListener {
                if (SystemClock.elapsedRealtime() - mLastClickTime > 1000) {
                    val intent = Intent(this@Main_Map, PhotoViewPager::class.java)
                    intent.putExtra("index", p0.index)
                    startActivityForResult(intent, 900)
                }
                mLastClickTime = SystemClock.elapsedRealtime()
            }

            true
        }
    }

    fun boundmap() {
        val bounds: LatLngBounds = builder.build()
        mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, ZoomLevel))
        val zoom: Float = mMap.getCameraPosition().zoom - 0.5f
        mMap.animateCamera(CameraUpdateFactory.zoomTo(zoom))
    }

    private fun changeRenderer(item: LatLngData) {
        if(selectedMarker != clusterRenderer.getMarker(item)) {
            if (selectedMarker != null) {
                tag_marker.setTextColor(Color.BLACK)
                tag_marker.setBackgroundResource(R.drawable.ic_marker_phone)
                selectedMarker!!.setIcon(
                    BitmapDescriptorFactory.fromBitmap(
                        createDrawableFromView(this, marker_view)
                    )
                )
            }

            if (clusterRenderer.getMarker(item) != null) {
                tag_marker.setTextColor(Color.WHITE)
                tag_marker.setBackgroundResource(R.drawable.ic_marker_phone_blue)
                tag_marker.text = MediaStore_Dao.getNameById(this, item.id)
                clusterRenderer.getMarker(item).setIcon(
                    BitmapDescriptorFactory.fromBitmap(
                        createDrawableFromView(
                            this,
                            marker_view
                        )
                    )
                )
                selectedMarker = clusterRenderer.getMarker(item)
                tag_marker.setTextColor(Color.BLACK)
                tag_marker.setBackgroundResource(R.drawable.ic_marker_phone)
            }
        }
    }
    fun getExtra(){
        size_check = 0
        if (intent.hasExtra("location_name")) {
            val getname = intent.getStringExtra("location_name")
            val title: TextView = findViewById(R.id.title_location_name)
            title.text = getname
            vm.setOpenLocationDir(this, getname, this@Main_Map)
        }
    }

    fun addLatLNgData(id : Long, latlng : LatLng) {
        val data = LatLngData(index++, id, latlng)
        if(size_check == 0) {
            Handler(Looper.getMainLooper()).post { mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(data.latlng, 12F)) }
        }
        mClusterManager.addItem(data)
        builder.include(data.latlng)
        size_check++
        if(size_check == 100) {
            Handler(Looper.getMainLooper()).post {boundmap()}
        }
    }

    override fun onBackPressed() {
        super.onBackPressed()
        finish()
    }

}

class MarkerClusterRenderer(context: Context?, map: GoogleMap?, clusterManager: ClusterManager<LatLngData>?,
                            marker_view: View) : DefaultClusterRenderer<LatLngData>(context, map, clusterManager) {
    private val context = context
    private var marker_view = marker_view
    private lateinit var tag_marker: TextView

    override fun onBeforeClusterItemRendered(item: LatLngData, markerOptions: MarkerOptions) { // 5
        tag_marker = marker_view.findViewById(R.id.tag_marker) as TextView
        tag_marker.text = MediaStore_Dao.getNameById(context!!, item.id)
        markerOptions.icon( BitmapDescriptorFactory.fromBitmap(createDrawableFromView(context, marker_view)) )
    }

    override fun shouldRenderAsCluster(cluster: Cluster<LatLngData>?): Boolean {
        super.shouldRenderAsCluster(cluster)
        return cluster != null && cluster.size >= 3
    }

    override fun onClustersChanged(clusters: MutableSet<out Cluster<LatLngData>>?) {
        super.onClustersChanged(clusters)

        if(selectedMarker != null) {
            selectedMarker!!.setIcon(BitmapDescriptorFactory.fromBitmap(createDrawableFromView(context, marker_view)))
            selectedMarker = null
        }
    }

    companion object {
        fun createDrawableFromView(context: Context?, view: View): Bitmap {
            val displayMetrics = DisplayMetrics()
            (context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics)
            view.layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            view.measure(displayMetrics.widthPixels, displayMetrics.heightPixels)
            view.layout(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
            view.buildDrawingCache()
            val bitmap: Bitmap = Bitmap.createBitmap(
                view.measuredWidth,
                view.measuredHeight,
                Bitmap.Config.ARGB_8888
            )
            val canvas = Canvas(bitmap)
            view.draw(canvas)
            return bitmap
        }
    }
}


결론

이렇게 구글 맵 API를 사용자에 맞게 수정하여 다양하게 활용하여 사용할 수 있다.
클러스터링에 대한 정보는 국내 자료 뿐만아니라 국외의 자료에서도 찾아보기 힘들어서 고생을 좀 했는데 고생한 만큼 조금이나마 다른 개발자 분들과 나누었으면 한다.

향후과제


참고자료


Writer: Jae-Hwan Lee

댓글남기기