📌  구글 플레이 서비스의 Google Maps API를 사용하면 구글 지도 데이터를 기반으로 앱에 지도를 추가할 수 있음

      -  구글 지도는 Google Maps Platform 서비스 중의 하나이며, 교통정보 기반의 경로 찾기와 장소 정보, 검색 등의 기능을 제공.
      -  국내에서는 구글 지도의 경로찾기 메뉴중 버스만 사용할 수 있음.

 

1.  구글 지도 시작하기

 

 

안드로이드 스튜디오는 구글 지도를 쉽게 사용할 수 있도록 프로젝트 생성 시 프로젝트의 종류를 선택하는 메뉴에서 Google Maps Activity를 제공

 

  ➡️  MainActivity 대신 MapsActivity가 생성됨

 

 

 

 

 

 

 

Google Maps API 키 설정


구글 지도를 포함한 구글 플레이 서비스에 엑세스하려면 구글 플레이 서비스의 API 키가 필요
이전에는 google_maps_api.xml 파일이 자동 생성되고, 해당 파일에 API키를 입력했으나, 범블비와 칩멍크 버전 부터는 AndroidManifest.xml에 API키를 입력하도록 변경이 됨

<!--
    TODO: Before you run your application, you need a Google Maps API key.

    To get one, follow the directions here:

    https://developers.google.com/maps/documentation/android-sdk/get-api-key

    Once you have your API key (it starts with "AIza"), define a new property in your
    project's local.properties file (e.g. MAPS_API_KEY=Aiza...), and replace the
    "YOUR_API_KEY" string in this file with "${MAPS_API_KEY}".
-->
    <meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="YOUR_API_KEY" />

 

 

키를 입력 후에 안드로이드 스튜디오에서 앱을 빌드하고 시작하면, 시드니에 마커가 표시된 지도를 표시

 

  📍 지도가 안 뜨면 구글 cloud 접속 하여 API key 수정에서 패키지를 추가해주면 됨 !  ▶️ 패키지명은 무조건 소문자 !!

 

 


2.  구글 지도 코드 살펴보기

activity_maps.xml의 SupportMapFragment

 

프로젝트를 생성하면 activity_maps.xml 파일이 자동 생성됨. 코드를 보면 android:name에 "com.google.android.gms.maps.SupportMapFragment"가 설정되어 있음.

Google Maps API는 SupportMapFragment에 구글 지도를 표시.

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MapsActivity" />

 

 

MapsActivity.kt의 SupportMapFragment.getMapAsync
 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMapsBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Obtain the SupportMapFragment and get notified when the map is ready to be used.
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

 

MapsActivity.kt 파일을 열면 onCreate() 메서드 블록 안에서는 supportFragmentManager의 findFragmentById() 메서드로 id가 map인 SupportMapFragment를 찾은 후 getMapAsync()를 호출해서 안드로이드에 구글 지도를 그려달라는 요청을 함.

 

MapsActivity.kt의 OnMapReadyCallback

 

class MapsActivity : AppCompatActivity(), OnMapReadyCallback { }

 

안드로이드는 구글 지도가 준비되면 OnMapReadyCallback 인터페이스의 onMapReady() 메서드를 호출하면서 파라미터로 준비된 GoogleMap을 전달해 줌.

override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap

        // Add a marker in Sydney and move the camera
        val sydney = LatLng(-34.0, 151.0)
        mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
        mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
    }

 

메서드 안에서 미리 선언된 mMap 프로퍼티에 GoogleMap을 저장해두면 액티비티 전체에서 맵을 사용할 수 있음.
구글에서 기본으로 제공하는 시드니의 좌표 코드가 있음.

 

* API Level 12 이하 버전에서의 호환성이 필요없다면 SupportMapFragment 대신 MapFragment를 사용할 수 있음.
* 스마트폰에 구글 플레이 서비스가 설치되어 있지 않으면 사용자가 구글 플레이 서비스를 설치할 때까지 onMapReady() 메서드가 호출되지 않음.

 


3.  카메라와 지도 뷰

구글 지도에서는 카메라를 통해 현재 화면의 지도 뷰를 변경할 수 있음.
  ✓  지도 뷰는 평면에서 아래를 내려다 보면서 모델링 되며 카메라의 포지션은 위도/경도, 방위, 기울기 및 확대/축소 속성으로 지정
  ✓  카메라의 위치는 CameraPosition 클래스에 각종 옵션을 사용해서 조절할 수 있음

CameraPosition.Builder().옵션1.옵션2.build()

Target


    카메라의 목표 지점 Target은 지도 중심의 위치이며 위도 및 경도 좌표로 지정

CameraPosition.Builder().target(LatLng(-34.0, 151.0))

 


Zoom

 

  ✓  카메라의 줌 Zoom (확대/축소) 레벨에 따라 지도의 배율이 결정. 줌 레벨이 높을 수록 더 자세한 지도를 볼 수 있는 반면, 줌 레벨이 작을수록 더 넓은 지도를 볼 수 있음.

CameraPosition.Builder().zoom(15.5f)

 

 

📍  줌 레벨이 0인 지도의 배율은 전 세계의 너비가 약 256dp가 되며 레벨의 범위는 다음과 같음

레벨 설명
1.0 세계
5.0 대륙
10.0 도시
15.0 거리
20.0 건물

 


Bearing


  ✓  카메라의 베어링 Bearing은 지도의 수직선이 북쪽을 기준으로 시계 방향 단위로 측정되는 방향.

CameraPosition.Builder().bearing(300f)

Tilt


  ✓  카메라의 기울기 Tilt는 지도의 중앙 위치와 지구 표면 사이의 원호에서 카메라 위치를 지정
  ✓  기울기로 시야각을 변경하면 멀리 떨어진 지형이 더 작게 나타나고 주변 지형이 더 켜져 맵이 원근으로 나타남

CameraPosition.Builder().tilt(50f)

 


4.  소스 코드에서 카메라 이동하기

옵션을 이용해서 CameraPosition 객체를 생성하고 moveCamera() 메서드로 카메라의 위치를 이동시켜 지도을 변경할 수 있음.
  ➡️  MapsActivity.kt 파일의 onMapReady() 메서드 안에 작성.

CameraPosition.Builder 객체로 카메라 포지션을 설정


  📍  build() 메서드를 호출해서 CameraPosition 객체를 생성

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

 

CameraUpdateFactory.newCameraPosition() 메서드에 CameraPosition 객체를 전달하면 카메라 포지션에 지도에서 사용할 수 있는 카메라 정보가 생성
val cameraUpdate =
    CameraUpdateFactory.newCameraPosition(cameraPosition)

 

변경된 카메라 정보를 GoogleMap의 () 메서드에 전달하면 카메라 포지션을기준으로 지도의 위치, 배율, 기울기 등이 변경되어 표시
mMap.moveCamera(cameraUpdate)

 


5.  마커

마커 Marker는 지도에 위치를 표시. 아이콘의 색상, 이미지, 위치를 변경할 수 있으며 대화식으로 설계되었기 때문에 마커를 클릭하면 정보창을 띄우거나 클릭리스너처럼 클릭에 대한 코드 처리를 할 수 있음.

 

마커 표시하기

 

  1. mMap = googleMap 코드 아래에 특정 장소의 위도와 경도 좌표값으로 LatLng 객체를 생성

val LATLNG = LatLng(35.8715, 128.6017)

 

  2.  마커를 추가하려면 마커의 옵션을 정의한 MarkerOptions 객체가 필요.

        MarkerOptions 객체를 생성하고 머커의 좌표와 제목을 설정.

val markerOptions = MarkerOptions()
    .position(LATLNG)
    .title("Marker")

 

  3. GoogleMap 객체의 addMarker() 메서드에 MarkerOptions를 전달하면 구글 지도에 마커가 추가.

mMap.addMarker(markerOptions)

 

  4. 카메라를 마커의 좌표로 이동하고 줌을 거리 레벨로 확대

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

val cameraUpdate =
    CameraUpdateFactory.newCameraPosition(cameraPosition)
    mMap.moveCamera(cameraUpdate)

 

  5. MarkerOptions 객체의 title(), snippet() 메서드로 정보 창을 수정할 수 있으며 마커를 클릭하면 정보창이 표시

val markerOptions = MarkerOptions()
    .position(LATLNG)
    .title("Daegu City Hall")
    .snippet("35.8715, 128.6017")

 

 

마커 아이콘 변경하기

 

마커 아이콘은 기본적으로 제공되는 아이콘뿐만 아니라 비트맵 이미지로 변경할수 있음.
PNG 이미지 파일을 프로젝트에 추가하고 비트맵으로 변환해서 아이콘으로 변경.

 

  1. drawable 디렉토리에 마커 아이콘으로 적용할 PNG 이미지 파일을 추가

  2. onMapReady() 안에 아래의 코드를 추가

val bitmapDrawable: BitmapDrawable

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    bitmapDrawable = getDrawable(R.drawable.marker) as BitmapDrawable
} else {
    bitmapDrawable = resources.getDrawable(R.drawable.marker) as BitmapDrawable
}

 

  3. BitmapDescriptorFactory.fromBitmap() 메서드에 BitmapDrawable의 비트맵 객체를 전달하는 마커 아이콘을 위한
BitmapDescriptor 객체를 생성하고 import

var discriptor =
    BitmapDescriptorFactory.fromBitmap(bitmapDrawable.bitmap)

 

  4. MarkerOptions 객체의 icon() 메서드를 호출해서 BitmapDescriptor 객체의 아이콘을 마커에 적용하도록 수정

val markerOptions = MarkerOptions()
    .position(LATLNG)
    .icon(discriptor)
mMap.addMarker(markerOptions)

 

  * 아이콘의 크기가 클 경우 Bitmap.createScaledBitmap() 메서드를 호출해서 크기를 줄인 비트맵 객체를 반환받아야 함

 


6.  현재 위치 검색하기

스마트폰처럼 모바일 환경에서는 사용자가 위치를 이동하고 그 위치를 기반으로하는 서비스를 제공할 수 있음.
  ✓  앱에서 스마트폰의 현재 위치를 검색하려면 위치 권한이 필요
  ✓  안드로이드 플랫폼은 현재 위치를 검색하는 FusedLocationProviderClinet API를 제공
        ➡️  FusedLocationProviderClinet API는 GPS Global Positioning System 신호 및 와이파이와 통신사 네트워크 위치를 결합해서 최소한의 배터리 사용량으로 빠르고 정확하게 위치를 검색

 

Google Play Service 의존성 추가하기


  FusedLocationProviderClinet API를 사용하기 위해서 build.gradle 파일에 구글 플레이 서비스의 Loacation 라이브러리 의존성을 추가. Location 라이브러리는 Maps 라이브러리와 버전이 같아야 함. Location 라이브러리와 같아지도록 Maps의 라이브러리 버전을 맞춰줌.

implementation 'com.google.android.gms:play-services-location:18.2.0'
implementation 'com.google.android.gms:play-services-maps:18.2.0'

 

권한을 명세하고 요청/ 처리하기

 

  스마트폰의 위치 기능에 접근하기 위해 AndroidManifest.xml 파일에 위치 권한을 선언. 위치 권한은 두 가지가 있으며 기능은 다음과 같음.

<!-- 도시 블록 내에서 정확한 위치 (네트워크 위치) -->
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- 정확한 위치 확보 (네트워크 위치 + GPS 위치) -->
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"/>

 

  MapsActivity에 OnMapReadyCallback 인터페이스를 상속 받음.

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {}

 

  권한 처리를 위해서 onCreate() 메서드 위에 런처를 선언. 여기서는 한 번에 2개의 권한에 대한 승인을 요청하기 때문에 Contract로 RequestMultiplePermissions()를 사용해야 함. 따라서 런처의 제네릭은 문자열 배열인 <Array<String>>이 됨

lateinit var locationPermission: ActivityResultLauncher<Array<String>>

 

  onCreate() 메서드 아래에 빈 startProcess() 메서드를 미리 만들어 둠. 

fun startProcess() {
    // 승인 후 실행할 코드를 입력
}

 

  onCreate() 메서드 안에 런처를 생성하는 코드를 작성하고 앞에서 선언해 둔 변수에 저장.

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

 

  바로 아래줄에 런처를 실행해서 권한 승인을 요청. 2개의 권한을 파라미터에 전달해야되기 때문에 arrayOf()를 사용해서 권한 2개를 같이 launch()의 파라미터로 입력.

locationPermission.launch(arrayOf(
    Manifest.permission.ACCESS_COARSE_LOCATION,
    Manifest.permission.ACCESS_FINE_LOCATION
))

 

  위치 권한이 승인되면 startProcess() 메서드에서 구글 지도를 준비하는 작업을 진행하도록 코드를 수정. onCreate()에 작성되어 있는 val mapFragment 로 시작하는 세 줄을 잘라내기 한 후 startProcess() 메서드 안에 붙여넣기 하면 됨.

fun startProcess() {
    // 승인 후 실행할 코드를 입력
    // Obtain the SupportMapFragment and get notified when the map is ready to be used.
    val mapFragment = supportFragmentManager
        .findFragmentById(R.id.map) as SupportMapFragment
         mapFragment.getMapAsync(this)
}

 

  이제 권한이 모두 승인되고 맵이 준비되면 onMapReady() 메서드가 정상적으로 호출.

 


현재 위치 검색하기

 

 1. onCreate() 위에 onMapReady() 위치를 처리하기 위한 변수 2개를 선언
     ✓  fusedLocationClient는 위치값을 사용하기 위해서 필요하고, locationCallback은 위칫값 요청에 대한 갱신 정보를 받는 데 필요

private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationCallback: LocationCallback

 

 2. onMapReady() 안의 시드니 좌표 코드를 삭제한 다음, 위치 검색 클라이언트를 생성하는 코드를 추가하고 updateLocation() 메서드를 호출

override fun onMapReady(googleMap: GoogleMap) {
    mMap = googleMap
    fusedLocationClient =
        LocationServices.getFusedLocationProviderClient(this)
    updateLocation()
}

 

 3. updateLocation() 메서드를 작성.

  • 위치 정보를 요청할 정확도와 주기를 설정할 locationRequest를 먼저 생성하고, 해당 주기마다 반환받을 locationCallback을 생성
  • onMapReady에서 생성한 위치 검색 클라이언트의 requestLocationUpdates()에 앞에서 생성한 2개와 루퍼 정보를 넘겨줌
  • 이제 1초(1,000밀리초)에 한 번씩 변화된 위치정보가 LocationCallback의 onLocationResult()로 전달이 됨.
  • onLocationResult()는 반환받은 정보에서 위치 정보를 setLastLoation()으로 전달.
  • fusedLocationClient.requestLocationUpdates 코드는 권한 처리가 필요한데 현재 코드에서는 확인할 수 없음.
  • 따라서 메서드 상단에 해당 코드를 체크하지 않아도 된다는 의미로 @SuppressLint("MissingPermission") 애너테이션을 달아줌.
@SuppressLint("MissingPermission")
fun updateLocation() {
    val locationRequest = LocationRequest.create()
    locationRequest.run {
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        interval = 1000
    }

locationCallback = object: LocationCallback() {
    override fun onLocationResult(locationResult: LocationResult?) {
        locationResult?.let {
            for ((i, location) in it.locations.withIndex()) {
                Log.d("Location", "$i ${location.latitude}, ${location.longitude}")
                setLastLoation(location)
            }
        }
    }
}

fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())

 

 

4. updateLocation() 메서드 아래에 위치 정보를 받아서 마커를 그리고 화면을 이동하는 setLastLoation()을 작성

fun setLastLoation(lastLocatin: Location) { }

 

5. 전달받은 위치 정보로 좌표를 생성하고 해당 좌표로 마커를 생성

fun setLastLoation(lastLocatin: Location) {
    val LATLNG = LatLng(lastLocatin.latitude, lastLocatin.longitude)
    val markerOptions =
        MarkerOptions().position(LATLNG).title("Here!")
}

 

6. 카메라 위치를 현재 위치로 세팅하고 마커에 함께 지도에 반영.

    ✓  마커를 지도에 반영하기 전에 mMap.clear()를 호출해서 이전에 그려진 마커가있으면 지움

fun setLastLoation(lastLocatin: Location) {
    val LATLNG = LatLng(lastLocatin.latitude, lastLocatin.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)
}

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

 


1.  양방향 액티비티

메인 액티비티에서 세컨드 액티비티로 데이터를 넘긴 후에 세컨드 액티비티에서 메인 액티비티로 데이터를 돌려주는 경우

 

1)  메인 액티비티에서 putExtra()로 인텐트에 데이터를 넣는 것은 동일하지만, 세컨드 액티비티에서 데이터를 돌려 받으려면 액티비티를 호출할 때startActivityForResult() 메서드를 사용해야 함
2) 그리고 세컨드 액티비티에서 finish()로 끝내기 전에 메인 액티비티에 돌려줄 인텐트를 생성하여 putExtra()로 데이터를 넣은 다음 setResult()로 돌려줌.
3) 메인 액티비티에서는 onActivityResult() 메서드를 오버라이딩하고 오버라인딩된 메서드 안에서 getExtra()로 돌려 받은 데이터를 사용.

 

 

 

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <EditText
        android:id="@+id/editNum1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/editNum2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btnNewActivity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="더하기" />
    
</LinearLayout>

 

activity_second.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SecondActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnReturn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="돌아가기" />

</LinearLayout>

 

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

        title = "메인 액티비티"

        val editNum1 = findViewById<EditText>(R.id.editNum1)
        val editNum2 = findViewById<EditText>(R.id.editNum2)
        val btnNewActivity = findViewById<Button>(R.id.btnNewActivity)
        btnNewActivity.setOnClickListener {
            val intent = Intent(applicationContext, SecondActivity::class.java)
            intent.putExtra("num1", editNum1.text.toString().toInt())
            intent.putExtra("num2", editNum2.text.toString().toInt())
            startActivityForResult(intent, 0)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            val sum = data!!.getIntExtra("sum", 0)
            Toast.makeText(applicationContext, "합계 :  ${sum}", Toast.LENGTH_SHORT).show()
        }
    }
}

 

  📍  startActivityForResult(intent, 0);

       값을 돌려받기 위해 startActivityForResult()를 사용. 두 번째 파라미터에는 돌려받을 값이 있는 경우에 0이상을 사용.
       여러 액티비티를 쓰는 경우, 어떤 Activity인지 식별하는 값.

  📍  onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
        세컨드 액티비티에서 setResult()로 값을 돌려주면 오버라이딩한 onActivityResult() 메서드가 실행

   📍  if (resultCode == RESULT_OK) 
         setResult()에서 보낸 값이 RESULT_OK이면 인텐트에서 돌려받은 값을 토스트 메시지로 화면에 출력

 

SecondActivity.kt
class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)

        title = "Second 액티비티"

        // 인텐트 관련 처리
        val intent = intent
        val sum = intent.getIntExtra("num1", 0) + intent.getIntExtra("num2", 0)

        val btnReturn = findViewById<Button>(R.id.btnReturn)
        btnReturn.setOnClickListener {
            val intentResult = Intent(applicationContext, MainActivity::class.java)
            intentResult.putExtra("sum", sum)
            setResult(Activity.RESULT_OK, intentResult)
            finish()
        }
    }
}

 

  📍 val intent = intent
        val sum = intent.getIntExtra("num1", 0) + intent.getIntExtra("num2", 0)

 

      메인 액티비티로부터 받은 두 값을 더함

  📍 val intentResult = Intent(applicationContext, MainActivity::class.java)
       intentResult.putExtra("sum", sum)
       setResult(Activity.RESULT_OK, intentResult)

      setResult()로 메인 액티비티에 돌려줌. 메인 액티비티의 onActivityResult() 메서드가 실행

 

 

 


2.  암시적 인텐트

명시적 인텐트의 개념이 두 액티비티를 사용자가 직접 생성하고 코딩하는 것이라면, 암시적 인텐트 implicit intent는 약속된 액션을 지정하여 '안드로이드에서 제공하는 기존 응용 프로그램을 실행하는 것'
  예를 들어 전화번호를 인텐트로 넘긴 후에 전화 걸기 응용 프로그램이 실행되는 것.

 

  ⚡️  메인 액티비티에서 인텐트를 생성할 때 실행하고자 하는 액션을 지정하고 액션의 데이터 값을 설정하면 기존의 안드로이드 응용 프로그램이 실행됨

   

119에 응급 전화를 거는 형식의 예
Intent intent = new Intent(Intent.ACTION_DIAL, Url.parse("tel:/119"));
startActivity(intent);

 

전화 걸기와 구글 맵을 사용하려면 AndroidManifest.xml의 <application 위에 다음과 같이 권한을 추가
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"/>

 

암시적 인텐트의 XML 파일
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnDial"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="전화 걸기" />

    <Button
        android:id="@+id/btnWeb"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="홈 페이지 열기" />

    <Button
        android:id="@+id/btnGoogle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="구글 맵 열기" />

    <Button
        android:id="@+id/btnSearch"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="구글 검색하기" />

    <Button
        android:id="@+id/btnSms"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="문자 보내기" />

    <Button
        android:id="@+id/btnPhoto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="사진 찍기" />

</LinearLayout>

 

암시적 인텐트의 Kotlin 코드
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        title = "암시적 인텐트 예제"
        val btnDial = findViewById<Button>(R.id.btnDial)
        val btnWeb = findViewById<Button>(R.id.btnWeb)
        val btnGoogle = findViewById<Button>(R.id.btnGoogle)
        val btnSearch = findViewById<Button>(R.id.btnSearch)
        val btnSms = findViewById<Button>(R.id.btnSms)
        val btnPhoto = findViewById<Button>(R.id.btnPhoto)

        btnDial.setOnClickListener {
            val tel = Uri.parse("tel:010-1234-5678")
            startActivity(Intent(Intent.ACTION_DIAL, tel))
        }

        btnWeb.setOnClickListener {
            val uri = Uri.parse("http://daum.net")
            startActivity(Intent(Intent.ACTION_VIEW, uri))
        }

        btnGoogle.setOnClickListener {
            val uri = Uri.parse("https://maps.google.com/maps?q="
                + 35.886606 + "," + 128.5938 + "&z=15" )
            startActivity(Intent(Intent.ACTION_VIEW, uri))
        }

        btnSearch.setOnClickListener {
            val intent = Intent(Intent.ACTION_WEB_SEARCH)
            intent.putExtra(SearchManager.QUERY, "안드로이드")
            startActivity(intent)
        }

        btnSms.setOnClickListener {
            val intent = Intent(Intent.ACTION_SENDTO)
            intent.putExtra("sms_body", "안녕하세요?")
            intent.setData(Uri.parse("smsto:010-1234-5678"))
            startActivity(intent)
        }

        btnPhoto.setOnClickListener {
            startActivity(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
        }

    }
}

 

  📍  val uri = Uri.parse("tel:010-1234-5678")
        startActivity(Intent(Intent.ACTION_DIAL, uri))

      전화를 걸기 위해 URI 문자열을 'tel:전화번호'형식으로 사용. 액션으로 ACTION_DIAL을 사용하면 전화 걸기 창이 열림.

  📍  val uri = Uri.parse("http://daum.net")
        startActivity(Intent(Intent.ACTION_VIEW, uri))

       웹브라우저를 열기 위해 URI 문자열을 'http://웹 주소'형식으로 사용. 액션은 ACTION_VIEW를 사용.

  📍  val uri = Uri.parse("https://maps.google.com/maps?q=" + 35.86606 + "," + 128.5938 + "&z=15" )
        startActivity(Intent(Intent.ACTION_VIEW, uri))

      구글 맵을 열기 위해 URI 문자열을 구글 맵 주소과 경위도 형식으로 사용. 액션은 ACTION_VIEW를 사용

  📍  val intent = Intent(Intent.ACTION_WEB_SEARCH)
        intent.putExtra(SearchManager.QUERY, "안드로이드")

       구글 검색을 열기 위해 액션은 ACTION_WEB_SEARCH를 사용. 검색을 위해 putExtra()로 넘기는데, 첫 번째 파라미터로 SearchManager.QUERY를 사용하고 두 번째 파라미터에는 검색할 단어를 넘김.

  📍  val intent = Intent(Intent.ACTION_SENDTO)
         intent.putExtra("sms_body", "안녕하세요?")

       문자 메시지를 보내기 위해 액션은 ACTION_SENDTO를 사용. 보낼 문자는 putExtra()로 넘기는데, 첫 번째 파라미터에는 'sms_body'를 넣고 두 번째 파라미터에 보낼 문자를 넣음. setData()도 설정해야 함.

  📍  startActivity(Intent(MediaStore.ACTION_IMAGE_CAPTURE))

      카메라를 열기 위해 액션은 MediaStore.ACTION_IMAGE_CAPTURE를 사용.

 

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  안드로이드의 4대 컴포넌트

안드로이드의 4대 컴포넌트는 액티비티 · 서비스 · 브로드캐스트 리시버 · 콘텐트 프로바이더


1)  액티비티 Activity

 화면을 구성하는 가장 기본적인 컴포넌트

 

2)  서비스 service

  눈에 보이는 화면(액티비티)과 상관없이 백그라운드에서 동작하는 컴퍼넌트. 백신 프로그램처럼 눈에 보이지는 않지만 계속 동작
  -  로컬에서 동작하는 서비스는 다음과 같이 세 단계를 거침
      서비스 생성 ▶️ 서비스 시작 ▶️ 서비스 종료

 

3)  브로드캐스트 리시버

  -  안드로이드는 여러 응용 프로그램이나 장치에 메시지를 전달하기 위해 방송 broadcasting 메시지를 사용
  -  안드로이드는 문자 메시지 도착, 배터리 방전, SD 카드 탈부착, 네트워크 환경 변화가 발생하면 전체 응용 프로그램이 알 수 있도록 방송 신호를 보냄 그리고 브로드캐스트 리시버 Broadcast Receiver는 이러한 방송 신호가 발생하면 반응함

 

4)  콘텐트 프로바이더

 콘텐트 프로바이더 Content Provider는 응용 프로그램 사이에 데이터를 공유하기 위한 컴포넌트
  -  안드로이드 응용 프로그램은 데이터에 자신만 접근할 수 있으므로 자신의 데이터를 외부에 공유하려면 콘텐트 프로바이더를 만들어야 함. 콘텐트 프로바이더에서 처리된 데이터는 일반적으로 데이터베이스 또는 파일로 저장

 


2.  액티비티의 개요

액티비티는 안드로이드폰에 나타나는 화면 하나하나를 말함. 액티비티는 사용자에게 보여주는 화면을 만들기 때문에 안드로이드의 4개 컴포넌트 중 가장 핵심적인 요소.

 

  • 안드로이드 프로젝트를 생성할 때 activity_main.xml과 MainActivity.kt 파일이 생성
  • activity_main.xml은 화면을 구성하는 코드로 되어 있지만 activity_main.xml이 아니라 MainActivity.kt가 액티비티에 해당
  • 일반적으로 액티비티 하나당 XML 파일 하나를 만들어서 사용
activity_main.xml 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnNewActivity"
        android:text="새 화면 열기" />


</LinearLayout>

 

second.xml 

 

    [res] - [layout] 에 second.xml 생성

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#CDE4F6">

    <Button
        android:id="@+id/btnReturn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="돌아가기" />

</LinearLayout>

 

SecondActivity.kt
class SecondActivity : AppCompatActivity() {

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

        val btnReturn: Button = findViewById(R.id.btnReturn)
        btnReturn.setOnClickListener {
            finish()
        }

    }
}

 

  📍  finish()가 호출되면 현재 액티비티를 종료. 세컨드 액티비티는 메인 액티비티에서 호출할 것이므로 세컨드 액티비티를 종료하면 다시 메인 액티비티가 나타남

 

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

        val btnNewActivity = findViewById<Button>(R.id.btnNewActivity)
        btnNewActivity.setOnClickListener {
            val intent = Intent(applicationContext, SecondActivity::class.java)
            startActivity(intent)
        }
    }
}

 

val intent = Intent(applicationContext, SecondActivity::class.java)

 

  -  인텐트를 생성. Intent의 첫 번째 파라미터 applicationContext는 메인 클래스의 컨텍스트를 반환
  -  applicationContext 대신 this@MainActivity 도 가능
  -  두 번째 파라미터로 이 인텐트에 포함될 액티비티 SecondActivity를 넘겨줌. ::class.java를 붙여야 한다는 것을 주의.

 

startActivity(intent)


  -  startActivity() : 새로운 액티비티를 화면에 출력. 파라미터로 인텐트를 받음.

 

AndroidManifest.xml
<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
         category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".SecondActivity" />

 

  📍  안드로이드는 사용될 액티비티를 AndroidManifest.xml에 반드시 등록. 프로젝트를 생성할 때 메인 액티비티는 자동으로 등록되지만, 추가한 세컨드 액티비티는 별도로 등록해야 함.

 


3.  명시적 인텐트

인텐트 Intent는 안드로이드의 4대 컴포넌트가 서로 데이터를 주고받기 위한 메시지 객체.

명시적 인텐트와 암시적 인텐트로 구분


1)  명시적 인텐트와 데이터 전달

  🐰  명시적 인텐트 explicit intent는 다른 액티비티의 이름을 명확히 지정할 때 사용

val intent = Intent(applicationContext, SecondActivity::class.java)
startActivity(intent)


  ✓  Intent() 생성자의 두 번째 파라미터에서는 액티비티 클래스를 넘길 수 있는데, 위의 코드에서는 그 전에 생성한 SecondActivity로 정확히 지정. 그리고 생성한 인텐트를 넘겨서 세컨드 액티비티를 실행

  ✓  이처럼 명확하게 액티비티의 이름을 지정했기 때문에 명시적 인텐트가 됨. 일반적으로 명시적 인텐트는 사용자가 새로운 액티비티를 직접 생성하고 호출할 때 사용

  🐰  인텐트에 데이터를 담아서 넘기는 것도 가능. 메인 액티비티에서 인텐트에 데이터를 실어서 넘긴 다음 세컨드 액티비티에게 받은 데이터를 처리

 

 

1)  putExtra()를 이용해서 필요한 만큼 데이터를 인텐트에 넣음
2)  startActivity()로 인텐트를 다른 액티비티에 넘김
3)  인텐트를 받은 액티비티에서 getStringExtra(), getIntExtra(), getBooleanExtra() 등의 메서드로 넘어온 데이터에 접근

 

 

 


2)  레이팅바

xml 코드
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <RatingBar
        android:id="@+id/ratingBar1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <RatingBar
        android:id="@+id/ratingBar2"
        style="?android:attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="10"
        android:stepSize="1" />

    <RatingBar
        android:id="@+id/ratingBar3"
        style="?android:attr/ratingBarStyleIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:rating="1.5" />

    <Button
        android:id="@+id/btnIncrease"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="증가시키기" />

    <Button
        android:id="@+id/btnDecrease"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="감소시키기" />

</LinearLayout>

 

style="?android:attr/ratingBarStyleSmall"
style="?android:attr/ratingBarStyleIndicator"

 

  -  레이팅바의 스타일을 지정. 작은 모양과 중간 모양을 표시.

android:stepSize="1"


  -  한 번에 증가될 크기를 별 1개로 지정. 소수점으로 설정할 수 있으며 디폴트는 0.5.

android:numStars="10"


  -  별의 개수를 지정. 디폴트는 5개.

 

android:rating="1.5"


  -  초깃값을 설정. 1.5개가 채워진 상태로 표현.

 

kotlin 코드
val ratingBar1 = findViewById<RatingBar>(R.id.raingBar1)
val ratingBar2 = findViewById<RatingBar>(R.id.raingBar1)
val ratingBar3 = findViewById<RatingBar>(R.id.raingBar1)
val btnIncrease = findViewById<RatingBar>(R.id.btnIncrease)
val btnDecrease = findViewById<RatingBar>(R.id.btnDecrease)

btnDecrease.setOnClickListener {
    ratingBar1.rating = ratingBar1.rating + ratingBar1.stepSize
    ratingBar2.rating = ratingBar2.rating + ratingBar2.stepSize
    ratingBar3.rating = ratingBar3.rating + ratingBar3.stepSize
}    

btnIncrease.setOnClickListener {
    ratingBar1.rating = ratingBar1.rating + ratingBar1.stepSize
    ratingBar2.rating = ratingBar2.rating + ratingBar2.stepSize
    ratingBar3.rating = ratingBar3.rating + ratingBar3.stepSize
}

 

  -  rating으로 레이팅바에 채워진 개수와 stepSize로 레이팅바에 설정된 stepSize 속성의 증가 크기 (디폴트는 0.5)를 더하거나 뺌

 

 

 

 

[ 내용참고 : IT 학원 강의 ]

 


1.  Thymeleaf

스프링과 마찬가지로 스프링 부트도 다양한 뷰 view 관련 기술을 적용할 수 있음. 스프링은 대부분 JSP를 위주로 개발하는 경우가 많지만 스프링 부트는 Thymeleaf라는 템플릿 엔진을 주로 이용.

Thymeleaf는 '템플릿'이기 때문에 JSP 처럼 직접 데이터를 생성하지 않고, 만들어진 결과에 데이터를 맞춰서 보여주는 방식으로 구현.
JSP와 마찬가지로 서버에서 동작하기는 하지만 Thymleaf는 HTML을 기반으로 화면을 구성하기 때문에 HTML에 조금 더 가까운 방식으로 작성


2.   Thymeleaf 기초 문법

Thymeleaf는 JSP를 대신하는 목적으로 작성된 라이브러리이므로, JSP에서 필요한 기능들을 Thymeleaf로 구성


1) 인텔리제이 설정

앞에서 작성한 hello.html을 열기. Thymeleaf를 이용하기 위해서 가장 중요한 설정은 네임스페이스 xmlns에 Thymeleaf를 지정. 네임스페이스를 지정하면 'th:'와 같은 Thymeleaf의 모든 기능을 사용할 수 있음.

작성된 hello.html은 다음과 같이 'th:'로 시작하는 기능을 사용할 수 있지만 Model에 담긴 데이터를 사용할 때는 '해당 변수를 찾을 수 없다'는 방식으로 에러가 날 수 있음.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="${msg}"></h1>
</body>
</html>


  📍  만일 에러가 발생하는 경우에는 인텔리제이의 설정을 조금 변경해서 에러 없는 화면을 보는 것이 더 좋기 때문에 Setting 메뉴에서 Thymeleaf를 검색하고 Unresolved references .. 를 체크 해제 해 주도록 함

    ➡️  설정을 변경하고 기존에 열려있는 hello.html 에디터를 종료한 후에 다시 에디터로 보면 변수에 대한 검사를 하지 않는 것을 확인


2)  Thymleaf 출력

Model로 전달된 데이터를 출력하기 위해서 HTML 태그 내에 'th:'로 시작하는 속성을 이용하거나 inlining을 이용
SampleController에 ex1()을 추가해서 '/ex/ex1'이라는 경로를 호출할 때 동작하도록 구성

 @GetMapping("/ex/ex1")
    public void ex1(Model model) {
        List<String> list = Arrays.asList("AAA", "BBB", "CCC", "DDD");

        model.addAttribute("list", list);
    }

 

ex1()의 결과 화면은 templates 내에 ex 디렉토리를 생성하고 ex1.html을 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- inlining 사용 -->
<h4>[[${list}]]</h4>
<hr>
<!-- 'th:' 속성 사용 -->
<h4 th:text="${list}"></h4>
</body>
</html>


 

3)  Thymeleaf 주석 처리

Thymeleaf가 작성하는 단계에서는 단순해 보이지만 디버깅할 때는 상황이 다름. 에러가 발생하게 되면 에러의 원인을 찾아내기 힘들다.
에러가 난 부분을 찾기 위해서는 주석 처리를 해야할 때는 '<!--/* ... */-->'를 이용하는 것이 좋음. 주석은 Thymeleaf가 파싱 처리할 때 삭제되어 처리되기 때문에 1) 잘못된 문법 에 대한 체크도 건너 뛸 수 있고, 삭제된 상태에서 처리되므로 2)브라우저에서는 아예 해당 부분은 결과 자체가 없음

 

hello.html 수정

 


 

4)  th:with를 이용한 변수 선언

Thymeleaf를 이용하는 과정에서 임시로 변수를 선언해야 하는 상황에서는 'th:with'를 이용해서 간단히 처리 가능
'th:with'로 만드는 변수를 '변수명 = 값'의 형태로 ', '를 이용해서 여러 개를 선언 할 수도 있음

    <div th:with="num1 = ${10}, num2 = ${20}">
        <h4 th:text="${num1 + num2}"></h4>
    </div>

 


3.  반복문과 제어문 처리

화면 구성에서 가장 많이 사용되는 반복문과 제어문 처리.
SampleController의 ex1()에서는 Model을 이용해 'List<String>'을 담고 ex1.html을 이용해서 출력하도록 구성.

 @GetMapping("/ex/ex1")
    public void ex1(Model model) {
        List<String> list = Arrays.asList("AAA", "BBB", "CCC", "DDD");

        model.addAttribute("list", list);
    }

 

반복문 처리는 크게 2가지 방법을 이용


    -  반복이 필요한 태그에 'th:each'를 적용하는 방법
    -  <th:block>이라는 별도의 태그를 이용하는 방법


    💡  'th:each' 속성을 이용할 때는 기존의 HTML을 그대로 둔 상태에서 반복 처리를 할 수 있다는 장점이 있지만 JSTL 과는 조금 이질적인 형태이고, <th:block>을 이용할 때는 추가로 태그가 들어가는 단점이 있음.

 

ex1.html의 내용을 변경
<ul>
    <li th:each="str: ${list}" th:text="${str}"></li>
</ul>

<ul>
    <th:block th:each="str: ${list}">
        <li>[[${str}]]</li>
    </th:block>
</ul>


 

1)  반복문의 status 변수

Thymeleaf는 th:each를 처리할 때 현재 반복문의 내부 상태에 변수를 추가해서 사용할 수 있음.
일명 status 변수라고 하는데 index / count / size / first / last / odd / even 등을 이용해서 자주 사용하는 값들을 출력할 수 있음

<ul>
    <li th:each="str, status: ${list}">
        [[${status.index}]] -- [[${str}]]
    </li>
</ul>

 

  ✓  status 변수명은 사용자가 지정할 수 있고, index는 0부터 시작하는 번호를 의미. count는 1부터 시작


 

2)  th:if / th:unless / th:switch

Thymeleaf는 제어문의 형태로 th:if / th:unless / th:switch를 이용할 수 있음

th:if / th:unless는 별도의 속성으로 사용할 수 있으므로 if ~ else 와는 다르게 사용
예를 들어 반복문의 홀수 / 짝수를 구분해서 처리하고 싶다면 다음과 작성

<ul>
    <li th:each="str, status: ${list}">
        <span th:if="${status.odd}">ODD -- [[${str}]]</span>
        <span th:unless="${status.odd}">EVEN -- [[${str}]]</span>
    </li>
</ul>


 

📍  ?를 이용하면 앞선 방식보다는 좀 더 편하게 이항 혹은 삼항 처리가 가능
      예를 들어 반복 중에 홀수 번째만 무언가를 보여주고 싶다면 다음과 같이 ? 뒤에 하나만 표현식을 사용할 수 있음

<ul>
    <li th:each="str, status: ${list}">
        <span th:text="${status.odd} ? 'ODD ---' + ${str}"></span>
    </li>
</ul>


 

📍  ?를 삼항연산자 그대로 사용할 수도 있음

<ul>
    <li th:each="str, status: ${list}">
        <span th:text="${status.odd} ? 'ODD ---' + ${str} : 'EVEN ---' + ${str}"></span>
    </li>
</ul>


 

📍 th:switch는 th:case와 같이 사용해서 Switch 문을 처리할 때 사용할 수 있음

<ul>
    <li th:each="str, status: ${list}">
        <th:block th:switch="${status.index % 3}">
            <span th:case="0">0</span>
            <span th:case="1">1</span>
            <span th:case="2">2</span>
        </th:block>
    </li>
</ul>


4.  Thymeleaf 링크 처리

  -  Thymeleaf는 '@'로 링크를 작성하기만 하면 됨

<a th:href="@{/hello}">Go to /hello </a>

 

 

1) 링크의 쿼리 스트링 처리

 

링크를 'key=value'의 형태로 필요한 파라미터를 처리해야 할 때 상당히 편리.

쿼리 스트링은 '()'를 이용해서 파라미터의 이름과 값을 지정.

<a th:href="@{/hello(name="AAA", age=16)}">Go to /hello </a>


 

📍  GET 방식으로 처리되는 링크에서 한글이나 공백 문자는 항상 주의해야 하는데 Thymeleaf를 이용하면 이에 대한 URL 인코딩 처리가 자동으로 이루어짐

<a th:href="@{/hello(name="한글처리", age=16)}">Go to /hello</a>


 

📍  만일 링크를 만드는 값이 배열과 같이 여러 개일 때는 자동으로 같은 이름의 파라미터를 처리

<a th:href="@{/hello(types=${{'AAA', 'BBB', 'CCC'}}, age=16}">Go to /hello</a>


5.  Thymeleaf의 특별한 기능들

1)  인라인 처리

Thymeleaf는 여러 편리한 점이 있지만 상황에 따라 동일한 데이터를 다르게 출력해 주는 인라인 기능은 자바 스크립트를 사용할 때 편리한 기능.

 

SampleController에 다양한 종류의 데이터를 Model에 담아서 전달하는 메서드를 추가

 

  • 추가되는 코드는 내부 클래스인 SampleDTO와 ex2()
  • SampleDTO를 정의할 때는 반드시 getter들을 만들어 줌
class SampleDTO {
        private String p1, p2, p3;

        public String getP1() {
            return p1;
        }

        public String getP2() {
            return p2;
        }

        public String getP3() {
            return p3;
        }

    }

 

@GetMapping("ex/ex2")
    public void ex2(Model model) {

        log.info("ex/ex2..........");

        List<String> strList = IntStream.range(1,10)
                .mapToObj(i -> "Data"+i)
                .collect(Collectors.toList());

        model.addAttribute("list", strList);

        Map<String, String> map = new HashMap<>();
        map.put("A", "AAAA");
        map.put("B", "BBBB");

        model.addAttribute("map", map);

        SampleDTO sampleDTO = new SampleDTO();
        sampleDTO.p1 = "Value -- p1";
        sampleDTO.p2 = "Value -- p2";
        sampleDTO.p3 = "Value -- p3";

        model.addAttribute("dto", sampleDTO);

    }

 

화면 구성을 위해 ex2.html을 추가
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div th:text="${list}"></div>
    <div th:text="${map}"></div>
    <div th:text="${dto}"></div>

    <script th:inline="javascript">

        const list = [[${list}]]

        const map = [[${map}]]

        const dto = [[${dto}]]

        console.log(list)
        console.log(map)
        console.log(dto)

    </script>
</body>
</html>

 

📍 HTML 코드를 이용하거나 자바 스크립트 코드를 이용할 때 같은 객체를 사용. 다만 차이점은 <script th:inline="javascript">가 지정된 점

📍 프로젝트를 실행해서 만들어진 결과를 보면 HTML은 기존처럼 출력되고, <script> 부분은 자바 스크립트에 맞는 문법으로 만들어진 것을 확인

 


2)  Thymeleaf의 레이아웃 기능

Thymeleaf의 <th:block>을 이용하면 레이아웃을 만들고 특정한 페이지에서는 필요한 부분만을 작성하는 방식으로 개발이 가능

 

레이아웃 기능을 위해서 별도의 라이브러리가 필요하므로 build.gradle에 추가
// 레이아웃 기능
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '3.1.0'

 

templates에 layout 폴더를 생성하고 레이아웃을 위한 layout1.html을 작성
<!DOCTYPE html>
<html lang="en"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Layout page</title>
</head>
<body>

    <div>
        <h3>Sample Layout Header</h3>
    </div>

    <div layout:fragment="content">
        <p>Page content goes here</p>
    </div>

    <div>
        <h3>Sample Layout Footer</h3>
    </div>

    <th:block layout:fragment="script">

    </th:block>

</body>
</html>

 

📍 코드 위쪽에는 http://www.ultraq.net.nz/thymeleaf/layout을 이용해서 Thymeleaf의 Layout을 적용하기 위한 네임스페이스를 지정
📍 코드 중간에는 layout:fragment 속성을 이용해서 해당 영역은 나중에 다른 파일에서 해당 부분만을 개발할 수 있음
     ➡️  layout1.html에는 content와 script 부분을 fragment로 지정

 

SampleController에 레이아웃 예제를 위한 ex3()을 추가
  @GetMapping("/ex/ex3")
    public void ex3(Model model) {
        model.addAttribute("arr", new String[] {"AAA", "BBB", "CCC"});
    }

 

templates의 ex폴더에 ex3.html을 생성. 가장 중요한 부분은 <html>에서 사용된 레이아웃 관련 설정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout1.html}">

<div layout:fragment="content">

    <h1>ex3.html</h1>

</div>

 

fragment에 content 부분만 작성한 것을 확인

 

 

layout1.html에는 content와 script 영역을 따로 구성했으므로 이를 이용해서 자바스크립트를 처리하고 싶다면 별도의 영역을 지정하고 fragment를 지정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout1.html}">

<div layout:fragment="content">

    <h1>ex3.html</h1>

</div>

<script layout:fragment="script" th:inline="javascript">

    const arr = [[${arr}]]

</script>

 


1.  대화상자

대화상자 dialog는 화면에 메시지를 나타낸 후 확인이나 취소 같은 사용자의 선택을 받아들이지는 경우에 사용
토스트보다 좀 더 강력한 메시지를 전할 때 적당


1)   기본 대화상자

대화상자는 사용자에게 중요한 사항을 알려준 후 사용자가 어떤 선택을 하게 하는 것이 주요 목적
그래서 사용자에게 좀 더 확실히 주지시켜야 할 때 혹은 계속 진행할지 여부를 선택하게 할 때 사용

대화상자의 가장 일반적인 사용 형식의 순서


   A.  대화상자 생성
      -  AlertDialog.Builer 클래스로 생성
 

   B.  용도에 따른 설정
      -  setTitle()  :  제목 설정
      -  setMessage()  :  내용 입력
      -  setIcon()  :  아이콘 설정
      -  setPositiveButton()  :  OK 버튼
      -  setNegativeButton()  :  Cancel 버튼
      -  setItems()  :  목록 출력

      - setSingleChoiceItems :  라디오 목록 출력
      - setMultiChoiceItems  :  체크박스 목록 출력

   C.  대화상자 화면 출력
      -  show()

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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:layout_width="match_parent"

    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center_horizontal">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="대화상자"/>

</LinearLayout>
class MainActivity : AppCompatActivity() {

    lateinit var button: Button

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

        button = findViewById(R.id.button)

        button.setOnClickListener {

            val dlg = AlertDialog.Builder(this@MainActivity)
            dlg.setTitle("제목입니다.")
            dlg.setMessage("이곳이 내용입니다.")
            dlg.setIcon(R.mipmap.ic_launcher)
            dlg.setPositiveButton("확인", null)
            dlg.show()
        }
    }
}

 

dlg.setPositiveButton("확인", null)
원형은 dlg.setPositiveButton("문자열", 리스너);


  확인이나 닫기 1개만 사용할 때는 setPositiveButton() 메서드를 사용
  리스너 부분이 null이라 대화상자만 닫힐 뿐 아무 동작이 일어나지 않음

 


 

2)  목록 대화상자

대화상자에 리스트 형태의 목록을 출력하고 그중 하나를 선택할 수 있음.가장 기본적인 목록을 만드는 형식

class MainActivity : AppCompatActivity() {

    lateinit var button: Button

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

        button = findViewById(R.id.button)

        button.setOnClickListener {
            val versionArray = arrayOf("오레오", "파이", "큐(10)")
            val checkArray = booleanArrayOf(true, false, false)
            val dlg = AlertDialog.Builder(this@MainActivity)
            dlg.setTitle("좋아하는 버전은? ")
            dlg.setIcon(R.mipmap.ic_launcher)
            dlg.setMultiChoiceItems(versionArray, checkArray) {dialog, which, isCheked ->
                button.text = versionArray[which]
            }
            dlg.setPositiveButton("닫기", null)
            dlg.show()
        }
    }
}

 

val versionArray = arrayOf("오레오", "파이", "큐(10)")

 

    ✓  출력할 항목의 문자열 배열 생성

 

dlg.setItems(versionArray) {dialog, which ->
    button.text = versionArray[which]
}


    ✓  문자열에 클릭 이벤트 할당. which는 몇번째 항목인지 알려줌.
    ✓  선택과 동시에 창이 닫힘
    ✓ 선택해도 대화상자가 닫히지 않도록 하려면 setItems 대신 setSingleChoiceItems를 사용하면 되는데, 라디오버튼 같은 형태로 출력.  이 메서드는 setSingleChoiceItems(문자열 배열, 초기 선택 인덱스, 리스너)로 파라미터가 3개.

    ✓  여러 개를 동시에 선택하게 하려면 setMultiChoiceItems() 사용하며, 이는 체크박스 형태로 표시

 

val checkArray = booleanArrayOf(true, false, false)


    ✓  각 항목이 체크되었는지 boolean 배열로 만듦

 

dlg.setMultiChoiceItems(versionArray, checkArray) { dialog, which, isChecked ->


    ✓  setMultiChoiceItems()의 두 번째 파라미터는 첫 번째 파라미터인 문자열 배열과 개수가 같은 boolean 배열이어야 함

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  메뉴

안드로이드의 메뉴는 옵션 메뉴 option menu와 컨텍스트 메뉴 context menu로구분

  ⚡️  옵션 메뉴를 사용하는 방법에는 메뉴 XML 파일을 생성한 후 Kotlin에서 호출하는 방법과 XML 파일 없이 Kotlin 코드만으로 메뉴를 생성하는 방법이 있음
  ⚡️  메뉴는 항목이 대부분 고정되어 있으므로 메뉴 XML 파일을 이용
        ➡️  메뉴 XML 파일을 이용하면 앱을 다른 나라 언어로 변경할 때 Kotlin코드를 건드리지 않아도 되므로 효율적

1)  XML을 이용한 옵션 메뉴

메뉴 XML 파일을 이용하는 방식은 세 가지만 설정하면 됨

  • 메뉴 코딩 : 메뉴 폴더 생성 및 메뉴 XML 파일 생성, 편집
  • 메뉴 파일 등록 : Kotlin 코딩. onCreateOptionsMenu() 메서드 오버라이딩
  • 메뉴 선택 시 동작할 내용 코딩 : Kotlin 코딩. onOptionsItemSelected() 메서드 오버라이딩
메뉴 XML 파일
<menu>
    <item
        android:id="@+id:항목1아이디"
        android:title="항목1 제목" />
    <item
        android:id="@+id:항목2아이디"
        android:title="항목2 제목" />
</menu>

 

  ✓ 위의 예에서 항목 item 이 2개 ➡️  메뉴에는 '항목1 제목', '항목2 제목'이 출력됨. 또한 메뉴 안에 서브 메뉴도 생성할 수 있음

 

Activity 클래스에서 오버라이딩하는 onCreateOptionsMenu() 메서드

 

    ✓  앱이 실행되면 메뉴의 내용을 XML 파일에서 자동으로 읽어옴. 메서드에 코딩할 내용은 거의 고정화 되어 있음

  override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        super.onCreateOptionsMenu(menu)
        menuInflater.inflate(R.menu.메뉴XML아이디, menu)
        return true
    }

 

Activity 클래스에서 오버라이딩하는 onOptionsItemSelected() 메서드 

 

    ✓  메뉴를 선택했을 때 어떤 동작을 할 것인지 담고 있음. 이 메서드에 실제 동작할 내용을 코딩하면 됨
    ✓  메뉴는 항목이 여러 개 나오기 때문에 보통 메서드 내부에서 when 을 사용

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when(item.itemId) {
        R.id.항목1아이디 -> {
            항목 1을 선택했을 때 실행되는 코드
        }
        R.id.항목2아이디 -> {
            항목 2을 선택했을 때 실행되는 코드
        }
    }    
    return super.onOptionsItemSelected(item)
}

 

 

 

인플레이터 Inflater


정적으로 존재하는 XML 파일을 Kotlin 코드에서 접근해서 실제 객체로 만들어 사용하는 것
메뉴 인플레이터 MenuInflater 객체는 메뉴 XML 파일을 Kotlin 코드에서 가져와 사용

레이아웃 인플레이터 LayoutInflater 객체는 레이아웃 XML 파일을 Kotlin 코드에서 가져와 사용하는 것

 


 

2)  Kotlin 코드만 이용한 옵션 메뉴

onCreateOptionsMenu() 메서드 안에 메뉴 XML 파일에 접근하는 대신에 직접 munu.add() 메서드로 메뉴항목을 추가
onOptionsItemSelected() 메소드의 case문을 코드에서 지정한 항목의 아이디 순번으로 변경하면 됨

class MainActivity : AppCompatActivity() {

    lateinit var baseLayout: LinearLayout
    lateinit var button: Button
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        title = "배경색 바꾸기"

        baseLayout = findViewById(R.id.baseLayout)
        button = findViewById(R.id.button1)

    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        super.onCreateOptionsMenu(menu)

        menu?.add(0, 1, 0, "배경색 (빨강)")
        menu?.add(0, 2, 0, "배경색 (초록)")
        menu?.add(0, 3, 0, "배경색 (파랑)")

        val subMenu = menu?.addSubMenu("버튼 변경 >> ")
        subMenu?.add(0, 4, 0, "버튼 45도 회전")
        subMenu?.add(0, 5, 0, "버튼 2배 확대")
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId) {
            1 -> {
                baseLayout.setBackgroundColor(Color.RED)
            }
            2 -> {
                baseLayout.setBackgroundColor(Color.GREEN)
            }
            3 -> {
                baseLayout.setBackgroundColor(Color.BLUE)
            }
            4 -> {
                button.rotation = 45f
            }
            5 -> {
                button.scaleX = 2f
            }
        }
        return super.onOptionsItemSelected(item)
    }
}

 

    ✓  add() 메서드의 파라미터는 그룹 아이디, 항목아이디, 순번, 제목순으로 지정
          ➡️  두 번째 지정한 아이디가 onOptionsItemSelected()의 when 과 같으면 됨

 

 

 


3)  XML을 이용한 컨텍스트 메뉴

🚀  옵션 메뉴는 키패드의 메뉴 버튼을 클릭할 때 나타나는 것과 달리 컨텍스트 메뉴는 레이아웃 또는 버튼, 에디트텍스트 등의 위젯을 롱클릭했을 때 나타남
🚀  컨텍스트 메뉴에서 메뉴 XML파일을 이용하는 방식은 옵션 메뉴와 비슷. 단, 여러 개의 위젯에 메뉴를 설정할 수 있으므로 onCreate 메서드에서 컨텍스트 메뉴을 나타낼 위젯을 registerForContextMenu()로 등록해야 함. 또, 옵션 메뉴에서 사용한 메서드와 메서드 이름이 약간 다름

  • 메뉴 코딩 : 메뉴 폴더 생성 및 위젯의 메뉴 XML 파일 생성. 편집
  • 메뉴를 사용할 위젯 등록 : Kotlin 코딩. onCreate() 안에 registerForContextMenu()로 등록
  • 메뉴 파일 등록 : Kotlin 코딩. onCreateContextMenu() 메서드 오버라이딩
  • 메뉴 선택시 동작할 내용 코딩 : Kotlin 코딩. onContextItemSelected() 메서드 오버라이딩
@Override 
public void onCreateContextMenu(ContextMenu menu, 
    View v, 
    ContextMenu.ContextMenuInfo menuInfo) { 
    
        super.onCreateContextMenu(menu, v, menuInfo);  
        
        MenuInflater menuInflater = getMenuInflater(); 
         
            if (v == 위젯1) { 
                menuInflater.inflate(R.menu.첫번째메뉴XML파일, menu); 
            } 
            if (v == 위젯2) { 
                menuInflater.inflate(R.menu.두번째메뉴XML파일, menu); 
            }
}

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts