오늘공부한것

[TIL_Kotlin] 구글맵과 카메라 기능을 사용한 앱 만들기 (실습 작성코드)

Cune 2022. 2. 19. 22:39

 

수업시간에 배웠던 코드를 사용하여 실습시간에 만들었던 앱 코드를 정리해서 올립니다.

 


 

 

1. Data 클래스 (Parcelable 사용)

import android.os.Parcel
import android.os.Parcelable
import java.sql.Timestamp

data class Data(var seq:Int?, var content:String?, var wdate:String?, var imgpath:String?, var latitude:Double?, var longitude:Double? ):
    Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readValue(Int::class.java.classLoader) as? Int,
        parcel.readString(),
        parcel.readString(),
        parcel.readString(),
        parcel.readValue(Double::class.java.classLoader) as? Double,
        parcel.readValue(Double::class.java.classLoader) as? Double
    ) {
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeValue(seq)
        parcel.writeString(content)
        parcel.writeString(wdate)
        parcel.writeString(imgpath)
        parcel.writeValue(latitude)
        parcel.writeValue(longitude)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<Data> {
        override fun createFromParcel(parcel: Parcel): Data {
            return Data(parcel)
        }

        override fun newArray(size: Int): Array<Data?> {
            return arrayOfNulls(size)
        }
    }

}

 

2. DBHelper (SQLiteOpenHelper 사용)

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class DBHelper (context:Context, filename:String)
    : SQLiteOpenHelper(context, filename, null, 1) {

    // 싱글톤 설정
    companion object{
        private var dbhelper:DBHelper? = null
        fun getInstance(context:Context, filename: String) : DBHelper {
            if(dbhelper == null){
                dbhelper = DBHelper(context, filename)
            }
            return dbhelper!!
        }
    }

    //테이블 생성
    override fun onCreate(db: SQLiteDatabase?) {
        var sql : String =  " CREATE TABLE IF NOT EXISTS DATA( " +
                            "   SEQ INTEGER PRIMARY KEY AUTOINCREMENT, " +
                            "   CONTENT TEXT,  " +
                            "   WDATE TEXT,  " +    //DEFAULT CURRENT_TIMESTAMP 사용해도 좋았을 듯
                            "   IMGPATH TEXT,  " +
                            "   LATITUDE DOUBLE, " +
                "               LONGITUDE DOUBLE ) "

        db?.execSQL(sql)

    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        val sql: String = "DROP TABLE if exists DATA"
        db?.execSQL(sql)
        onCreate(db)
    }

    //데이터 추가 
    fun insert(vo: Data){
        var sql = " INSERT INTO DATA(content,wdate,imgpath,latitude,longitude) " +
                "   VALUES('${vo.content}','${vo.wdate}',${vo.imgpath},'${vo.latitude}','${vo.longitude}') "

        writableDatabase.execSQL(sql)
    }

    //데이터 조회
    fun select() : MutableList<Data>{
        val list = mutableListOf<Data>()

        var sql = "SELECT * FROM DATA "

        val cursor = readableDatabase.rawQuery(sql, null)

        while (cursor.moveToNext()){
            val noIdx = cursor.getColumnIndex("SEQ")
            val contentIdx = cursor.getColumnIndex("CONTENT")
            val wdateIdx = cursor.getColumnIndex("WDATE")
            val imgPathIdx = cursor.getColumnIndex("IMGPATH")
            val latiIdx = cursor.getColumnIndex("LATITUDE")
            val longIdx = cursor.getColumnIndex("LONGITUDE")

            val no = cursor.getInt(noIdx)
            val content = cursor.getString(contentIdx)
            val wdate = cursor.getString(wdateIdx)
            val imgpath = cursor.getString(imgPathIdx)
            val latitude = cursor.getDouble(latiIdx)
            val longitude = cursor.getDouble(longIdx)


            list.add(Data(no,content,wdate,imgpath,latitude,longitude))
        }

        return list
    }

}

 

3. MainActivity

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val regiBtn = findViewById<Button>(R.id.regiBtn)

        regiBtn.setOnClickListener {
            var i = Intent(this, WriteActivity::class.java)
            startActivity(i)
        }

       val list:MutableList<Data> = DBHelper.getInstance(this, "account.db").select()

        var recycleView = findViewById<RecyclerView>(R.id.recyclerView)

        val mAdapter = CustomAdapter(this, list)
        recycleView.adapter = mAdapter

        val layout = LinearLayoutManager(this)
        recycleView.layoutManager = layout

        recycleView.setHasFixedSize(true)
    }
}

 

4. WriteActivity

import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentValues
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.graphics.Bitmap
import android.location.Address
import android.location.Geocoder
import android.location.Location
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.*
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import java.io.FileOutputStream
import java.io.IOException
import java.sql.Timestamp
import java.text.SimpleDateFormat

class WriteActivity : AppCompatActivity() {


    // storage 권한 처리에 필요한 변수
    val CAMERA = arrayOf(Manifest.permission.CAMERA)
    val STORAGE = arrayOf(
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE)
    val CAMERA_CODE = 98
    val STORAGE_CODE = 99

    // 권한처리
    lateinit var locationPermission: ActivityResultLauncher<Array<String>>
    lateinit var fusedLocationClient: FusedLocationProviderClient
    lateinit var locationCallback: LocationCallback

    // 위도, 경도, 주소
    var latitude:Double = 0.0
    var longitude:Double = 0.0

    var timestamp = Timestamp(System.currentTimeMillis()).toString()

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

        // 권한
        locationPermission = registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions() ){ results->
            if(!results.all { it.value }){
                Toast.makeText(this, "권한 승인이 필요합니다", Toast.LENGTH_LONG).show()
            }
        }

        // 권한 요청
        locationPermission.launch(
            arrayOf(
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION
            )
        )

        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

        // 위치 정보를 실시간으로 받기 위한 함수를 호출
        updateLocation()

        // 카메라
        val camera = findViewById<Button>(R.id.camera)
        camera.setOnClickListener {
            CallCamera()
        }

        // 이미지 경로
        val imagePath = findViewById<TextView>(R.id.imgPath)
        // 내용
        val content = findViewById<EditText>(R.id.contentEdit)
        // 저장 버튼
        val save = findViewById<Button>(R.id.save)

        save.setOnClickListener {
            // 이미지 경로, 현재의 위치 주소, 내용

            var message = "위도:$latitude 경도:$longitude \n" +
                    "이미지 경로:${imagePath.text} \n" +
                    "내용:${content.text} \n " +
                    "날짜시간:$timestamp"

            // 우선은 출력해보자!
            AlertDialog.Builder(this)
                .setTitle("대화 상자")
                .setMessage(message)
                .setCancelable(false)
                .setNeutralButton("닫기", DialogInterface.OnClickListener { dialog, which ->
                }).show()


             var imgPath = "\'"+imagePath.text+"\'"  //슬래시 오류 해결

             val data = Data(
                null,
                 content.text.toString(),
                 timestamp,
                 imgPath,
                 latitude,
                 longitude
             )

            DBHelper.getInstance(this, "data.db").insert(data)

            Toast.makeText(this, "글저장 완료!", Toast.LENGTH_LONG).show()
            var i = Intent(this, MainActivity::class.java)
            startActivity(i)
         }
    }

    // 카메라 권한, 저장소 권한
    // 요청 권한
    override fun onRequestPermissionsResult(requestCode: Int,
                                            permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when(requestCode){
            CAMERA_CODE -> {
                for (grant in grantResults){
                    if(grant != PackageManager.PERMISSION_GRANTED){
                        Toast.makeText(this, "카메라 권한을 승인해 주세요", Toast.LENGTH_LONG).show()
                    }
                }
            }
            STORAGE_CODE -> {
                for(grant in grantResults){
                    if(grant != PackageManager.PERMISSION_GRANTED){
                        Toast.makeText(this, "저장소 권한을 승인해 주세요", Toast.LENGTH_LONG).show()
                    }
                }
            }
        }
    }

    // 다른 권한등도 확인이 가능하도록
    fun checkPermission(permissions: Array<out String>, type:Int):Boolean{
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
            for (permission in permissions){
                if(ContextCompat.checkSelfPermission(this, permission)
                    != PackageManager.PERMISSION_GRANTED){
                    ActivityCompat.requestPermissions(this, permissions, type)
                    return false
                }
            }
        }
        return true
    }

    // 카메라 촬영 - 권한 처리
    fun CallCamera(){
        if(checkPermission(CAMERA, CAMERA_CODE) && checkPermission(STORAGE, STORAGE_CODE)){
            val itt = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
            startActivityForResult(itt, CAMERA_CODE)
        }
    }

    // 사진 저장
    fun saveFile(fileName:String, mimeType:String, bitmap: Bitmap):Uri?{

        var CV = ContentValues()

        // MediaStore 에 파일명, mimeType 을 지정
        CV.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
        CV.put(MediaStore.Images.Media.MIME_TYPE, mimeType)

        // 안정성 검사
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
            CV.put(MediaStore.Images.Media.IS_PENDING, 1)
        }

        // MediaStore 에 파일을 저장
        val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, CV)
        if(uri != null){
            var scriptor = contentResolver.openFileDescriptor(uri, "w")

            val fos = FileOutputStream(scriptor?.fileDescriptor)

            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
            fos.close()

            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                CV.clear()
                // IS_PENDING 을 초기화
                CV.put(MediaStore.Images.Media.IS_PENDING, 0)
                contentResolver.update(uri, CV, null, null)
            }
        }
        return uri
    }

    // 결과 (찍은 사진 보여주기)
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        val imageView = findViewById<ImageView>(R.id.avatars)

        if(resultCode == Activity.RESULT_OK){
            when(requestCode){
                CAMERA_CODE -> {
                    if(data?.extras?.get("data") != null){
                        val img = data?.extras?.get("data") as Bitmap
                        val uri = saveFile(RandomFileName(), "image/jpeg", img)
                        imageView.setImageURI(uri)

                        Toast.makeText(this, "$uri", Toast.LENGTH_LONG).show()
                        println("이미지 경로: $uri")
                        println("실제 이미지 경로:" + getPath(uri))

                        // 이미지 경로
                        val imagePath = findViewById<TextView>(R.id.imgPath)

                        imagePath.text = getPath(uri)
                    }
                }
                STORAGE_CODE -> {
                    val uri = data?.data
                    imageView.setImageURI(uri)
                }
            }
        }
    }

    // 파일명을 날짜 저장
    fun RandomFileName() : String{
        val fileName = SimpleDateFormat("yyyyMMddHHmmss").format(System.currentTimeMillis())
        return fileName
    }

    // 실제 경로로 변경해 주는 함수
    // Uri -> String
    fun getPath(uri: Uri?): String {
        val projection = arrayOf<String>(MediaStore.Images.Media.DATA)
        val cursor: Cursor = managedQuery(uri, projection, null, null, null)
        startManagingCursor(cursor)
        val columnIndex: Int = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
        cursor.moveToFirst()
        return cursor.getString(columnIndex)
    }

    // 위치 취득 함수
    @SuppressLint("MissingPermission")
    fun updateLocation(){
        val locationRequest = LocationRequest.create()
        locationRequest.run {
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
            interval = 1000
        }

        // 실시간으로 자신의 위치를 취득
        locationCallback = object :LocationCallback(){
            // 1초에 한번씩 변경된 위치 정보가 onLocationResult 로 전달된다
            override fun onLocationResult(locationResult: LocationResult) {
                locationResult?.let {
                    for (location in it.locations){
                        Log.d( "위치정보", " - 위도:${location.latitude} 경도:${location.longitude}")
                        // 변수에 저장한다
                        latitude = location.latitude
                        longitude = location.longitude
                    }
                }
            }
        }

     /*  // 변환 : 위도, 경도 -> 주소
        addrBtn.setOnClickListener {
            var list:List<Address>? = null

            try {
                val d1: Double = latEdit.text.toString().toDouble()
                val d2: Double = lonEdit.text.toString().toDouble()

                list = geocoder.getFromLocation(d1, d2, 10)

            }catch (e:IOException){
                Log.d("위도/경도", "입출력 오류")
            }

            if(list != null){
                if(list.isEmpty()){
                    textView.text = "해당되는 주소는 없습니다"
                }else{ // 정상적으로 산출됨
                    textView.text = list[0].toString()
                }
            }
        }*/

        // 권한 처리
        fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper()!!)
    }

    // 갤러리 취득
    fun GetAlbum(){
        if(checkPermission(STORAGE, STORAGE_CODE)){
            val itt = Intent(Intent.ACTION_PICK)
            itt.type = MediaStore.Images.Media.CONTENT_TYPE
            startActivityForResult(itt, STORAGE_CODE)
        }
    }
}

 

5. DetailActivity

import android.graphics.BitmapFactory
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap

class DetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)

        val mapBtn = findViewById<Button>(R.id.mapBtn)
        val updateBtn = findViewById<Button>(R.id.updateBtn)
        val deleteBtn = findViewById<Button>(R.id.deleteBtn)

        //구글맵 사용
        val fm = supportFragmentManager
        val fragmentTransaction = fm.beginTransaction()
        val mapsFragment = MapsFragment(this)
        fragmentTransaction.add(R.id.mapFrame, mapsFragment)
        fragmentTransaction.commit()

        //어댑터에서 싼 짐 푸르기
        val data = intent.getParcelableExtra<Data>("data")

        //사진,내용,날짜 넣어주기
        val imageView = findViewById<ImageView>(R.id.imageView)
        val content = findViewById<TextView>(R.id.contentDetail)
        val date = findViewById<TextView>(R.id.dateDetail)

        //경로로 이미지 불러오기
        var imgFile = data?.imgpath
        var myBitmap = BitmapFactory.decodeFile(imgFile)
        imageView.setImageBitmap(myBitmap)

        content.text = data?.content
        date.text = data?.wdate.toString()

	//지도보기 버튼 클릭 시 지도위치 변경
        mapBtn.setOnClickListener { 
        mapsFragment.setLocation(data?.latitude!!, data?.longitude!!) 
        }


    }
}

 

 

6. MapsFragment


import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import androidx.fragment.app.Fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationServices

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.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions

class MapsFragment(val activity: Activity) : Fragment(), OnMapReadyCallback {

    lateinit var locationPermission: ActivityResultLauncher<Array<String>>

    private var mMap: GoogleMap? = null

    lateinit var fusedLocationClient: FusedLocationProviderClient

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        locationPermission = registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions() ){ results->
            if(!results.all { it.value }){
                Toast.makeText(activity, "권한 승인이 필요합니다", Toast.LENGTH_LONG).show()
            }
        }

        // 권한 요청
        locationPermission.launch(
            arrayOf(
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION
            )
        )
        return inflater.inflate(R.layout.fragment_maps, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment?
        mapFragment?.getMapAsync(this)
    }

	//맵 사용
    override fun onMapReady(googleMap: GoogleMap) {
        println("~~~~~~onMapReady~~~~")
        mMap = googleMap
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(activity)
    }

	//지도 위치 설정
    fun setLocation(latitude:Double, longitude:Double){
        val LATLNG = LatLng(latitude, longitude)

        println("~~~~~위도:$latitude 경도:$longitude~~~~~~~")
        val markerOptions = MarkerOptions()
            .position(LATLNG)
            .title("Here!")

       val cameraPosition = CameraPosition.Builder()
            .target(LATLNG)
            .zoom(15.0f)
            .build()

        mMap?.clear()
        mMap?.addMarker(markerOptions)
        mMap?.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
    }
}

 

7. CustomAdapter.kt (리사이클러뷰 어댑터)


import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.io.File


class CustomAdapter(private val context:Context, private val dataList: MutableList<Data>) : RecyclerView.Adapter<ItemViewHolder>(){

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.item_recycler, parent, false)
        return  ItemViewHolder(view)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(dataList[position], context)
    }

    override fun getItemCount(): Int {
        return  dataList.size
    }
}

class ItemViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView){
    private val photo = itemView.findViewById<ImageView>(R.id.avatars)
    private val date = itemView.findViewById<TextView>(R.id.wdateText)
    private val content = itemView.findViewById<TextView>(R.id.contentText)


    // data -> resource (binding)
    fun bind(data: Data, context: Context){

        //사진 처리
        if (data.imgpath != "") {
            val resourceId = context.resources.getIdentifier(data.imgpath, "drawable", context.packageName)

            if (resourceId > 0) {
                photo.setImageResource(resourceId) // 찍은사진 가져오기
            } else {

                // userPhoto.setImageResource(R.mipmap.ic_launcher_round)
                // Glide.with(itemView).load(dataVo.photo).into(userPhoto)
                Log.d("","~~~~~~~~~~~~~~~~~~~~~ 들어옴")

                val file: File = File(data.imgpath)
                val bExist = file.exists()
                if (bExist) {
                    Log.d("","이미지 파일 있음")
                    val myBitmap = BitmapFactory.decodeFile(data.imgpath)
                    photo.setImageBitmap(myBitmap)
                }else{
                    Log.d("","${data.imgpath} 이미지 파일 없음")
                }
            }
        } else {
            photo.setImageResource(R.mipmap.ic_launcher_round)
        }

        // TextView 데이터를 세팅
        date.text = data.wdate.toString()
        content.text = data.content

        // itemView 를 클릭시
        itemView.setOnClickListener{
            println(data.content + " " + data.wdate)

            // ProfileDetailActivity 로 이동
            Intent(context, DetailActivity::class.java).apply {

                // 짐싸!
                putExtra("data", data)

                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }.run { context.startActivity(this) }
        }
    }
}

 

+

fragment_maps.xml (Google Maps Fragment) - 지도를 보여주기 위한 프래그먼트 

item_recycler.xml - 리사이클러 레이아웃

activity_main.xml

activity_write.xml

activity_detail.xml

 

 

 

 


<MainActivity>

기록추가하기 아래에는 리사이클러 뷰로 나중에 글 추가시 보임

 

 

 

 

<WriteActivity>

기본화면

 

 

 

 

 

<WriteActivity>

사진찍기 버튼을 누르고 사진을 찍으면 찍은 사진이 불러와진다

사진아래에는 원하는 글을 써주면 된다

맨아래 찍은 사진의 경로가 표시된다

글 저장하기 버튼을 누르면 메인화면으로 돌아간다

 

 

 

 

 

<MainActivity>

앞에서 작성한 글이 추가되어 메인화면 아래 리사이클러뷰에 표시됩니다

 

 

 

 

 

<DetailActivity>

리사이클러뷰에 있는 글을 터치하면 디테일 화면으로 이동되고

기록했던 사진과 글, 시간들이 나오고

지도보기 버튼을 누르면 사진을 찍은 장소를 지도로 확인할 수 있습니다