A. namespace 키워드

 

namespace란?

프로그램은 메모리 상에 수 많은 저장 장소('변수')를 생성해서 사용하고 각 변수에 이름을 붙여 구별합니다.

프로그램 기법이 발전하면서 단순한 변수 외에도 같은 소(小)작업에 동원되는 여러 변수들을 하나로 묶어 변수들의 배열 같은 모습의 대형 변수를 만들 필요가 있었습니다. 그리고 이것을 구조체라고 불렀습니다. 그 이후 프로그램 기법은 구조체 내에 그 구조체 안에서만의 특별한 동작 기능을 추가(함수인데 특별히 '메소드'라 부름) 하고 이를 '클래스'라고 부르게 되었습니다. 이 클래스는 하나의 작은 프로그램 모듈의 붕어빵 틀같은 모양을 하고 있으며 그 자체로는 실행될 수 없고 실제 런타임 때, new 라는 키워드에 의해서 메모리에 업로드 되어 비로소 실행되는데, 이렇게 메모리에 업로드되어 하나의 독립 모듈이 된 상태를 '객체'라고 부릅니다. 즉, 프로그램 기법이 구조체를 도입하면서 급발전하여 곧바로 클래스를 만들어 객체화(캡슐화)시키자는 방향으로 발전한 것 입니다. 여기서 객체의 설계도라고 할 수 있는 것이 클래스 입니다. 객체화 프로그래밍의 장점은 코드를 편리하게 재사용 할 수 있고 수 많은 변수들을 체계적으로 분류하여 프로그래밍의 효율성을 높인다는 데에 있습니다.

 

다시 한 번 요약하자면, 어떤 소(小)작업을 위한 여러 변수들과 함수를 한 데 묶어 클래스라는 붕어빵 틀을 만들었습니다.

붕어빵 틀이 있으니 '이 틀로 객체를 만들어라!' 하는 명령만으로  무한대의 똑같이 생긴 붕어빵 객체들을 만들 수 있게 된 거죠. 때문에 현대의 프로그램에서 클래스는 소스 코드 작성의 핵심입니다. 그리고 그 클래스라는 붕어빵틀은 복사, 기능추가, 다형성(후에 설명함), 은폐성, 개선된 가독성등에서 강력한 잇점들을 프로그래머들에게 안겨 주었습니다.

 

C# 프로그램 소스에는 많은 클래스들이 포함되어 있습니다.

네임스페이스란, 대략적으로 말하자면 클래스가 나열된 공간입니다. (= 클래스들의 덩어리)

하나의 네임스페이스 안에서는 동일한 이름을 가진 클래스가 존재하면 안됩니다. 구별할 수 없으니까요.

클래스명의 중복을 불허 한다는 것은 하나의 네임스페이스 안에서의 얘기일 뿐, 다른 네임스페이스에 같은 이름의 클래스명이 있냐 없냐는 문제되지 않습니다. 네임스페이스란 이렇게 클래스들을 무리지어 놓는 하나의 구획입니다.

 

미리 정의되어 있는 대표적인 C#의 편리한 네임스페이스에는 System 이 있습니다.

   

 

 

 

 

B. using 키워드의 용도

 

using 키워드로 다른 네임스페이스에 정의된 클래스를 가져오거나

네임스페이스의 별칭을 정의해서 사용할 수 있습니다.

 

1. 다른 네임스페이스에 있는 클래스 가져오기

형식)

using 다른네임스페이스명;

using 다른네임스페이스명.클래스명;      // 하위 클래스들은 .으로 구분

 

예문)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

 

 

 

2. 별칭 정의하기

 

C#의 네임스페이스 이름이나 클래스 이름들이 정말 긴 경우가 많습니다.

실생활 용어도 약어를 많이 사용하는데, 프로그램 코드에서도 약자를 쓰고 싶어 근질근질 합니다.

 

사용하는 전처리 지시자는 using (소문자임!) 입니다.

using으로 간략화 할 수 있는 대상은 네임스페이스 이름과 클래스 이름 뿐 입니다.

 

형식)

using 별칭 = 네임스페이스명;

using 별칭 = 네임스페이스명.클래스명;

 

예문)

using con = System.Console;      // 별칭을 정의하고 있는 using 키워드

class Program
{
    static void Main(string[] args)
    {
        con.WriteLine("으하하");
        con.ReadLine();
    }
}

 

별칭 사용시 고려할 점)

여러 사람들과 코드를 공유하면서 개발 하는 경우라면, 별칭 사용을 자제 하는 것이 좋을 수도 있습니다.

자신은 알아보기 쉽겠지만, 다른 사람들에겐 불편할 수 있으니까요!

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

직렬화 란? 그리고 마샬링 이란?

 

직렬화

 

 

1. 직렬화(Serialization)

  객체 데이터를 일련의 byte stream으로 변환하는 작업을 직렬화라고 합니다.

  반대로 일련의 byte stream을 본래의 객체 모양으로 복원하는 작업은 Deserialization 이라고 합니다.

 

2. 마샬링(Marshalling)

  메모리 상에 형상화된 객체 데이터를 적당한 다른 데이터 형태로 변환하는 과정을 말합니다.

  컴퓨터간 데이터 전달 또는 프로그램 간 데이터 전달을 할 때 사용되죠.

  전송된 데이터를 다시 원래의 객체 모양으로 복원하는 작업은 언마샬링(Unmarshalling)이라고 합니다.

 

3. 직렬화와 마샬링의 차이

  직렬화와 마샬링은 사실 상 거의 같은 개념입니다.

  직렬화는 오래 전부터 데이터를 주고 받는 모든 전자 기기에 폭 넓게 사용해오던 개념입니다.

  예를 들면, 위 그림 처럼 레고 블럭으로 만든 빌딩을 먼 거리로 전송한다고 상상을 해 보죠.

  전송 채널은 일반적으로 단일 채널 즉, 1차원적인 일렬로 보낼 수 밖에 없으므로 빌딩 블럭을

  순서대로 해체해서 블럭 하나 하나를 채널을 통해 발송하고, 수신하는 측에서는 이렇게 받은

  하나 하나의 블럭을 약속된 방법에 따라 다시 빌딩의 모습으로 복원하는 거죠.

  이런 직렬화 작업을 컴퓨터 프로그래밍 데이터 처리에 적용한 개념이 마샬링입니다.

  음... 이름만 특이할 뿐이지 똑같은 개념입니다~.

 

  단지 이런 차이점이 있습니다. 컴퓨터의 데이터 처리에는 여러 가지 매개 값들이 오고 갑니다.

  때문에 직렬화된 데이터에 여러 가지 매개 값들도 추가하고 그 리턴 값들도 한꺼번에 집어넣게 되었는데,

  이렇게 좀 더 세분화된 정보 처리기에서의 직렬화 작업을 약간 더 특별하게 마샬링이라고 부르고 있습니다.

  직렬화 작업이 프로그래밍적으로 조금 더 전문화 된 것이지요.

  (그래서 프로그래밍에서 함수나 클래스를 직렬화 할 때 함수 마샬링, 클래스 마샬링 이라고 부릅니다)

 

  즉, 직렬화라는 작업은 마샬링이라는 것이 포함된 폭 넓은 개념입니다. (직렬화 >> 마샬링)

  직렬화 작업들 중에 프로그래밍 작업에서 좀 더 전문화된 세부 개념이 마샬링인 것이지요.

 

 

 

  

 

 

 

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

 === TodoList (일정표) ===

 

- 기능 소개

할 일 목록을 표시

할 일을 데이터베이스에 추가, 수정, 삭제

 

 

- 주된 도구

ListView   (목록을 표현하는 리스트 형태의 뷰)

Realm   (모바일용 데이터베이스)

 

 

1. 프로젝트 생성

 

두 개의 화면을 사용할 것임 (할 일 목록 표시 화면 + 편집 화면)

데이터 베이스로는 모바일에서 SQLite를 대체할 정도로 인기가 높은 Realm을 사용할 것임

  (Realm SQL 문법을 몰라도 사용 가능함)

 

 

프로젝트명 : TodoList

minSdkVersion : 19

기본 액티비티 : Basic Activity

Anko 라이브러리 설정 할 것.

VertorDrawable 하위 호환성 설정 할 것.

 

* 참고)

  Basic Activity는 지금까지의 액티비티와 조금 다른 면이 있습니다.

  플로팅 액션 버튼(FAB)과 메뉴가 미리 작성되어 있고 activity_main.xml content_main.xml 두 개의 레이아웃 파일이 생성됩니다.

    activity_main.xml의 컴포넌트 트리 창을 보면, 'include...'를 통해 content_main.xml 파일이 포함되어 있음을 알 수 있습니다.

    (include 속성을 이용해 여러 개의 화면을 구성할 수 있음)

 

 

2. 할 일 목록을 표시하는 화면의 레이아웃 추가

content_main.xml의 기본 텍스트 뷰 삭제

Autoconnect 모드 on

팔레트 창에서 Legacy 카테고리의 ListView를 끌어다 화면 정중앙에 배치

  (id: listView, layout_width: match_constraint, layout_height: match_constraint, 상하좌우 여백: 모두 0)

 

 

 

3. 편집 화면의 레이아웃 추가

File > New > Activity > 'Empty Activity'

   (이름: EditActivity)

 

activity_edit.xml에 팔레트 창 Widgets 카테고리에서 CalendarView를 끌어다 화면 상단에 배치

   id: calendarView

   layout_width, layout_height: wrap_content

   , 좌우 여백: 0

 

팔레트 창 Text 카테고리에서 Plain Text를 끌어다 달력 아래에 적당히 배치 (할 일 편집용으로 쓸 것임)

Autoconnect 모드는 부모 레이아웃과의 제약을 자동으로 생성하지만, 자식 뷰들간의 제약은 생성되지 않습니다.

아마도 좌우 여백 제약은 추가되어 있을 것입니다. EditText CalendarView와의 제약을 추가합시다.

   id: todoEditText

   layout_width: match_constraint

   layout_height: wrap_content

   , 좌우 여백: 8

   inputType: text

   hint: 할 일

   text: (공백)

 

 

완료버튼용, 삭제버튼용 벡터이미지를 추가

 

팔레트 창 Buttons 카테고리에서 FloatingActionButton을 끌어다 우측 하단에 배치 (편집 완료 버튼으로 사용할 것임)

   이미지는 프로젝트에 이미 추가한 완료버튼 이미지를 지정해서 사용하고 다음 속성을 지정.

   id: doneFab

   하단, 우측 여백: 16

   backgroundTint: @android:color/holo_orange_light

   tint: @android:color/white

 

팔레트 창 Buttons 카테고리에서 FloatingActionButton을 끌어다 좌측 하단에 배치 (삭제 버튼으로 사용할 것임)

   이미지는 프로젝트에 이미 추가한 삭제버튼 이미지를 지정해서 사용하고 다음 속성을 지정.

   id: deleteFab

   하단, 좌측 여백: 16

   tint: @android:color/white

 

Activity_main.xml에서 일정 추가버튼의 벡터 이미지를 아래 이미지로 변경

  srcCompat: @drawable/ic_add_black_24dp

  tint: @android:color/white

 

 

4. Realm 데이터 베이스 사용 준비

비만도 계산기를 구현할 때에는 데이터 베이스를 사용하지 않고 SharedPreferences를 이용해 데이터를 간단히 저장했었습니다.

그러나 데이터가 많고 복잡하다면 데이터 베이스를 활용하는 것이 효율적입니다.

안드로이드는 SQLite를 지원하지만 다루기가 어렵고 코드량도 많아 Realm 데이터 베이스를 활용해 보기로 합시다.

안드로이드에서는 앱 별로 격리된 데이터 베이스를 가질 수 있습니다.

앱 간에 자신의 데이터 베이스를 공개하려면 프로바이더를 이용하면 됩니다.

 

참고) 데이터 베이스에서의 테이블은 엑셀의 시트와 같습니다.

 

 

1) 사용 준비

  Build.gradle(project)에 플러그인 정보 설정

 

     dependencies {

        ...

        classpath "io.realm:realm-gradle-plugin:6.0.0"

     }

 

  Build.gradle(module)에 플러그인 추가

 

     apply plugin: 'kotlin-kapt'

     apply plugin: 'realm-android'

   

 

 

2) 할 일 정보 데이터 베이스의 모양

 

  데이터 베이스 명: todolist

  테이블 명 : todo

id (Long)

title (String)

date (Long)

1

청소

2020. 05. 25

2

빨래

2020. 05. 25

3

공부

2020. 05. 27

 

3) 모델 클래스 작성

  File > New > Kotlin > 'File/Class'   (클래스명: Todo)

 

  ② 위 데이터 베이스를 클래스화 한 모양 --> "모델 클래스" 라고 부름

     class Todo(

        @PrimaryKey var id: Long = 0,    // PrimaryKey: 중복되지 않는 유일키

        var title: String = "",

        var date: Long = 0

     ) {

 

     }

 

     이 모델 클래스를 Realm에서 테이블로 사용하려면,

       클래스 명 앞에 open 키워드를 사용하고

       RealmObject 클래스를 상속받으면 됩니다.

 

 

     -- Todo.kt 코딩

     import io.realm.RealmObject

     import io.realm.annotations.PrimaryKey

 

     open class Todo(

        @PrimaryKey var id: Long = 0,

        var title: String = "",

        var date: Long = 0

     ) : RealmObject() {

 

     }

 

 

4) Realm 초기화

  앱 실행시 가장 먼저 Realm이 초기화 되도록해야 액티비티들이 공동으로 데이터 베이스를 사용할 수 있습니다.

  이 역할을 하는 클래스를 하나 새로 만듭시다.

 

  File > New > 'Kotlin File/Class'     (클래스명: MyApplication, 종류: Class)

 

  -- MyApplication.kt

    import android.app.Application

    import io.realm.Realm

 

    class MyApplication : Application() {      // Application 클래스를 상속

 

       override fun onCreate() {              // 이 메서드는 액티비티가 생성되기 전에 호출됨

          super.onCreate()

 

          Realm.init(this)                    // Realm 초기화

       }

    }

 

  -- AndroidManifest.xml  (application 엘리먼트에 name 속성을 추가 --> 앱에서 사용하는 전체 액티비티에

    공동으로 사용하는 객체를 초기화할 때 이런 방법을 사용합니다)

 

    <application

       android:name = ".MyApplication"               // <-- 이 행을 추가

       android:allowBackup="true"

       android:icon="@mipmap/ic_launcher"

       android:label="@string/app_name"

       android:roundIcon="@mipmap/ic_launcher_round"

       android:supportsRtl="true"

       android:theme="@style/AppTheme">

 

       <activity android:name=".EditActivity"></activity>

       <activity

          android:name=".MainActivity"

          android:label="@string/app_name"

          android:theme="@style/AppTheme.NoActionBar">

          <intent-filter>

             <action android:name="android.intent.action.MAIN" />

 

             <category android:name="android.intent.category.LAUNCHER" />

          </intent-filter>

       </activity>

    </application>

 

   

 5. EditActivity.kt 코딩

 

-- EditActivity.kt



package com.tistory.todolist

import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import kotlinx.android.synthetic.main.activity_edit.*
import org.jetbrains.anko.alert
import org.jetbrains.anko.yesButton
import java.util.*

class EditActivity : AppCompatActivity() {
    val realm = Realm.getDefaultInstance()  // Realm 인스턴스 얻기
    val calendar: Calendar = Calendar.getInstance()   // 캘린더 객체 생성(오늘 날짜로 초기화됨)

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

        // 인텐트로 id를 전달해서 데이터 베이스의 삽입/변경/삭제를 분기
        // id=-1 (추가모드)
        val id = intent.getLongExtra("id", -1L)
        if (id == -1L) {
            insertMode()    // 추가 모드
        } else {
            updateMode(id)  // 삭제 모드
        }

        // 캘린더 뷰의 날짜를 선택했을 때 캘린더 객체에 설정
        calendarView.setOnDateChangeListener { view, year, month, dayOfMonth ->
            calendar.set(Calendar.YEAR, year)
            calendar.set(Calendar.MONTH, month)
            calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
        }
    }

    // 추가 모드
    private fun insertMode() {
        deleteFab.visibility = View.GONE     // 삭제 버튼 숨기기
              // visiblity 프로퍼티: setVisibility()
              //    VISIBLE : 보임, INVISIBLE : 영역은 차지하지만 보이지는 않음, GONE : 숨김
        doneFab.setOnClickListener { insertTodo() }   // 완료 버튼 클릭시 insertTodo() 호출
    }

    // 삭제 모드
    private  fun updateMode(id: Long) {
        // id에 해당하는 객체를 화면에 표시
        val todo = realm.where<Todo>().equalTo("id", id).findFirst()!!
        todoEditText.setText(todo.title)
        calendarView.date = todo.date

        // 완료 버튼 클릭시 updateTodo() 호출
        doneFab.setOnClickListener { updateTodo(id) }

        // 삭제 버튼 클릭시 deleteTodo() 호출
        deleteFab.setOnClickListener { deleteTodo(id) }
    }

 
    override fun onDestroy() {
        super.onDestroy()

        realm.close()          // Realm 인스턴스 해제
    }


    // 데이터 베이스 할 일 삽입
    private fun insertTodo() {
        realm.beginTransaction()     // *** 트랜잭션 시작 ***
        // 트랜잭션 시작 (트랜젝션: 데이터베이스의 작업단위)
        // beginTransaction ~ commitTransaction 사이의 코드들은
        // 전체가 하나의 작업(트랜잭션)이며 도중에 에러가 나면 일괄 취소됨
        // 데이터베이스의 추가/삭제/업데이트는 항상 이 사이에 작성해야 함

        val newItem = realm.createObject<Todo>(nextId())
        newItem.title = todoEditText.text.toString()
        newItem.date = calendar.timeInMillis

        realm.commitTransaction()   // *** 트랜잭션 종료 ***

        alert("일정이 추가 되었습니다") {
            yesButton { finish() }
        }.show()
    }

    // 데이터 베이스 할 일 변경
    private fun updateTodo(id: Long) {
        realm.beginTransaction()

        val updateItem = realm.where<Todo>().equalTo("id", id).findFirst()!!
             // where<Todo>() : 테이블의 모든 값을 얻어옴
             // .equalTo(필드명, Long) : 해당 '필드명'의 Long형 id값의 데이터를 가져옴
             // findFirst() : 첫 번째 데이터

        updateItem.title = todoEditText.text.toString()
        updateItem.date = calendar.timeInMillis
            // timeInMillis 프로퍼티 : 날짜를 가져오는 getTimeInMilles()

        realm.commitTransaction()

        alert("일정이 변경 되었습니다") {
            yesButton { finish() }
        }.show()
    }

    // 데이터 베이스 할 일 삭제
    private fun deleteTodo(id: Long) {
        realm.beginTransaction()

        val deleteItem = realm.where<Todo>().equalTo("id", id).findFirst()!!
        deleteItem.deleteFromRealm()

        realm.commitTransaction()

        alert("일정이 삭제 되었습니다") {
            yesButton { finish() }
        }.show()
    }

    // Realm은 자동 키 증가를 지원하지 않으므로 아래 메서드를 만들었음
    private fun nextId() : Int {
        val maxId = realm.where<Todo>().max("id")
             // where<Todo>() : 테이블의 모든 값을 얻어옴
             // .max(필드명) : 현재 '필드명'중 가장 큰 값을 얻음 (Number형)

        if (maxId != null) {
            return maxId.toInt() + 1
        }
        return 0
    }

}

 

6. MainActivity.kt 코딩

메인 액티비티에 할 일 추가 버튼 클릭 시, EditActivity를 시작하도록 리스너 등록

 

-- MainActivity.kt

class MainActivity : AppCompatActivity() {

 

   override fun onCreate(savedInstanceState: Bundle?) {

      ...

      fab.setOnClickListener {      // 할 일 추가 버튼 클릭 리스너

            startActivity<EditActivity>()

      }

   }

 

 

7. 메인 액티비티에 할 일 목록 표시

타이머 예제의 랩 기록과 같이 적은 목록을 표시할 때는 스크롤 뷰로 충분했지만,

표시해야 할 목록의 양이 많아질 경우엔 리스트 뷰를 사용합니다.

 

스크롤 뷰

모든 아이템을 메모리에 로드하므로 데이터가 많아지면 많은 메모리를 차지

리스트 뷰

뷰를 재사용하고 화면에 보이는 것만 동적으로 로딩 하므로 적은 메모리를 사용

 

리스트 뷰에 데이터를 출력하려면 읽어올 데이터 소스와 리스트 뷰의 입력부간에 데이터 형식을 맞춰주기 위한

어댑터가 필요하다. 이 어댑팅 작업은 출력 속도에 큰 영향을 주므로 아주 중요합니다.

 

리스트 뷰에 목록을 출력할 때에는 '뷰홀더' 방식을 사용합니다.

뷰홀더 방식이란 딱 한 번 생성한 레이아웃에 내용만 수정해서 재사용하는 방식을 말합니다.

사용하는 메모리나 성능 면에서 효율적이죠.

 

 

1) 어댑터 사용 준비

 

  -- build.gradle (Module)

  dependencies {

     implementation 'io.realm:android-adapters:2.1.1'

     ...

  }

 

 

2) 목록을 표시할 레이아웃 리소스 파일 작성

  res/layout 폴더에서 마우스 컨텍스트 메뉴 > File > New > Layout resource file

     (파일명: item_todo  , 레이아웃: ConstraintLayout)

 

  (item_todo.xml 파일 작업)

  날짜 표시용 TextView를 좌측 상단에 배치

     id: text1

     layout_width: match_constraint

     위 여백:0, 좌우 여백: 8

     text: (공백)

     ()text: 2020/05/28 (디자인 시 보일 텍스트 아무거나)

     textAppearance: AppCompat.Body1

 

  할 일 표시용 TextView를 좌측 상단에 배치

     id: text2

     layout_width: match_constraint

     , 좌우 여백: 8

     text: (공백)

     ()text: 청소하기 (디자인 시 보일 텍스트 아무거나)

     textAppearance: AppCompat.Body2

 

  전체 레이아웃의 layout_height 속성: wrap_content

 

 

3) 어댑터 클래스 작성

  File > New > 'Kotlin File/Class'    (이름: TodoListAdapter, 종류: Class)

 

  -- TodoListAdapter.kt

package com.tistory.todolist

  import android.text.format.DateFormat    // 자동으로 임포트가 잘 안되면 직접 코딩할 것
  import android.view.LayoutInflater
  import android.view.View
  import android.view.ViewGroup
  import android.widget.TextView
  import io.realm.OrderedRealmCollection
  import io.realm.RealmBaseAdapter
 
  class TodoListAdapter (realmResult: OrderedRealmCollection<Todo>)
       : RealmBaseAdapter<Todo>(realmResult){     // RealmBaseAdapter 를 상속

     // RealmBaseAdapter의 추상 메서드 구현
     override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        // 이 메서드는 매 아이템이 화면에 보일 때마다 호출됨
        // 이 메서드에 리스트 뷰의 각 아이템에 표시할 뷰를 구성하면 됨
        //    position (출력할 리스트 뷰 내의 아이템 위치)
        //    convertView (재활용되는 아이템의 뷰. 처음 아이템이 작성되기 전엔 null, 이후에는 그 전에 작성했던 뷰를 전달)
        //    parent (부모 뷰. 여기서는 리스트 뷰의 참조를 가리킴)
 
        val vh: ViewHolder
        val view: View
 
        if (convertView == null) {    // null이면 레이아웃을 작성
            view = LayoutInflater.from(parent?.context).inflate(R.layout.item_todo, parent, false)
                       // LayoutInflater : XML 레이아웃 파일을 불러옴
                       // inflate() : XML 레이아웃 파일을 뷰로 전환
                       // false : XML 파일을 불러왔을 경우
            vh = ViewHolder(view)
            view.tag = vh    // tag 프로퍼티에는 모든 데이터형의 객체를 저장할 수 있음
        } else {    // null이 아니면 이전에 작성된 convertView를 재사용
            view = convertView
            vh = view.tag as ViewHolder   // view.tag는 Any형이므로 ViewHolder 타입으로 형변환
        }
 
        // adapterData : RealmBaseAdapter가 제공하는 프로퍼티로서 이를 통해 데이터에 접근할 수 있음
        if (adapterData != null) {   // 데이터가 있으면.
            val item = adapterData!![position]
            vh.textTextView.text = item.title
            vh.dateTextView.text = DateFormat.format("yyyy/MM/dd", item.date)
        }
 
        return view
     }
 
     override fun getItemId(position: Int): Long {
                      // 리스트뷰 클릭 이벤트 처리시 인자로 position, id등이 넘어옴. 이 때의 id 값을 결정
        if (adapterData!= null) {
           return adapterData!![position].id   // adapterView가 Realm 데이터를 가지고 있으므로
                                            // 해당 위치의 id를 반환해줘야 함
        }
 
        return super.getItemId(position)
     }
 }
 
 class ViewHolder(view: View) {
    val dateTextView: TextView = view.findViewById(R.id.text1)
    val textTextView: TextView = view.findViewById(R.id.text2)
 }
 

 

 

4) 할 일 목록 표시

 

 -- MainActivity.kt

package com.tistory.todolist

 import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem
 import androidx.appcompat.app.AppCompatActivity
 import com.google.android.material.snackbar.Snackbar
 import io.realm.Realm
 import io.realm.Sort
 import io.realm.kotlin.where
 import kotlinx.android.synthetic.main.activity_main.*
 import org.jetbrains.anko.listView
 import org.jetbrains.anko.startActivity
 
 class MainActivity : AppCompatActivity() {
 
    val realm = Realm.getDefaultInstance()
 
    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       setSupportActionBar(toolbar)
 
       fab.setOnClickListener { view ->
          Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
             .setAction("Action", null).show()
       }
 
       fab.setOnClickListener {      // 할 일 추가 버튼 클릭 리스너
          startActivity<EditActivity>()
       }
 
       val realmResult = realm.where<Todo>().findAll().sort("date", Sort.DESCENDING)
                                   // 할 일 목록을 날짜순으로 모두 가져옴
       
       val adapter = TodoListAdapter(realmResult)  // 할 일 목록이 담긴 어댑터 생성
       listView().adapter = adapter    // 어댑터 지정 (이 때 목록이 출력됨)
 
       realmResult.addChangeListener { _ -> adapter.notifyDataSetChanged() }  // 데이터가 변경될 경우 어댑터에 적용됨
                      // notifyDataSetChanged() : 데이터 변경을 통지하여 목록을 다시 출력함
       listView().setOnItemClickListener { parent, view, position, id ->   // 리스트 뷰 아이템 클릭시 처리  
          startActivity<EditActivity>("id" to id)   // 기존 id 존재 여부에 따라 새 할 일 추가 또는 수정
       }
    }
 
    override fun onDestroy() {
       super.onDestroy()
 
       realm.close()
    }
 
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
       // Inflate the menu; this adds items to the action bar if it is present.
       menuInflater.inflate(R.menu.menu_main, menu)
       return true
    }
 
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
       // Handle action bar item clicks here. The action bar will
       // automatically handle clicks on the Home/Up button, so long
       // as you specify a parent activity in AndroidManifest.xml.
       return when (item.itemId) {
          R.id.action_settings -> true
          else -> super.onOptionsItemSelected(item)
       }
    }
 }


 

 

실행 결과)

 

 

여기까지 9개의 코틀린 예제를 통해서 안드로이드 주요 기능들을 골고루 훑어봤습니다.

안드로이드 기기가 발전되어 오면서 추가된 여러 센서 기능들과 기기 자체 성능이 다양한 제조업체를 통해 이뤄지다 보니 마치 웹 브라우저에 플러그인들이 더덕더덕 붙어 있는 듯한 느낌입니다.

 

 

 

<<< 이전 글 보기   

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

== 실로폰 ===

 

- 기능 소개

음판을 누르면 소리가 재생

 

 

- 주된 도구

SoundPool   (음원을 관리하고 재생하는 클래스. 안드로이드 5.0 이전과 이후 버전의 동작이 다르므로

              모든 기기에서 잘 동작하도록 버전 분기를 적용할 것임)

 

 

1. 프로젝트 생성

프로젝트명 : Xylophone

miniSdkVersion : 19

기본 액티비티 : Empty

 

레이아웃 에디터에서 미리보기 모드를 가로모드 작업 환경으로 설정합시다.

(실로폰이 가로 모양이니까요)

[미리보기를 수평모드로...]

 

2. 텍스트 뷰로 음판 만들기

음판을 적당하게 배치한 후 한 번에 제약을 추가해 봅시다. (Autoconnect 모드 off)

 

디폴트 텍스트 뷰 삭제 > 팔레트 창 Common > TextView를 끌어다 건반 모양으로 배치

 

   id : do1

   layout_width : 50 dp

   layout_height : match_constraint

   , 아래 여백 : 16  (, 우 여백은 모든 건반 배치후 한 번에 설정할 것임)

   text :

   textAppearance : AppCompat.Large

   textColor : @android.color/white

   background : holo_red_dark

   gravity : center에 체크 (컨텐츠를 가운데 배치하는 역할)

위와 같은 형태로 7개의 테스트 뷰를 추가로 배치.

id

, 아래 여백

text

background

re

24

@android:color/holo_orange_dark

mi

32

@android:color/holo_orange_light

fa

40

@android:color/holo_green_light

sol

48

@android:color/holo_blue_light

la

56

@android:color/holo_blue_dark

si

64

@android:color/holo_purple

do2

72

@android:color/holo_red_dark

모든 건반 배치가 끝나면 컨트롤 키를 누른 상태에서 모든 건반을 클릭하여 전체 건반 선택 >

   마우스 우측 버튼 > 컨텍스트 메뉴에서 Chains > Create Horizontal Chain   (뷰들이 체인으로 연결됨)

 

체인으로 연결된 뷰 중 아무 뷰나 선택 후 컨텍스트 메뉴 > Cycle chain mode 클릭

  (클릭할 때마다 세 가지 모드가 전환됨)

  (아래와 같이 건판이 균일한 간격이 되는 모드를 선택)

[실로폰 음판 모양으로 배치한 텍스트 뷰들]

이렇게 텍스트 뷰들로 실로폰 음판을 만들었습니다.

 

 

3. 재생할 소리 리소스 준비

wav, mp3 와 같은 사운드 파일은 .raw 리소스 디렉터리를 만들어 그 안에 저장해 놓고 사용합니다.

 

1) .res 디렉토리 생성

  프로젝트 창의 .res 디렉터리에서 마우스 우측 버튼 클릭 > 컨텍스트 메뉴 중 'New > Android Resource Directory'

    Resource type : raw

 

2) http://bit.ly/2K9dQjo 에서 실로폰 음계의 wav 파일 다운로드 > .res에 저장

 

 

※ 안드로이드 기기에서 소리 재생

방법1) MediaPlayer 클래스  (음악 및 비디오 파일 재생. 한 번만 재생하고 플레이어를 끝낼 때 유용)

 

  사용 예)

    val mediaPlayer = MediaPlayer.create(this, R.raw.do1)

    button.setOnClickListener{ mediaPlayer.start() }

    ...

    mediaPlayer.release()    // 사용이 끝난 후 해제해 줘야 함

 

 

방법2) SoundPool 클래스   (실로폰과 같이 연속으로 소리를 때마다 계속 내야 할 때 유용)

 

 사용 예)

    val soundPool = SoundPool.Builder().build()

    val soundId = soundPool.load(this, R.raw.do1, 1)    // load(컨텍스트, 소리파일, 우선순위)

    button.setOnClickListener { soundPool.play(soundId, 1.0f, 1.0f, 0, 0, 1.0f) }

         // play(음원id), 왼쪽볼륨 0.0~1.0, 오른쪽 볼륨, 우선순위(0은 최하순위),

         // 반복여부(0:반복안함, -1:반복), 재생속도(배속)

 

 

4. SoundPool 초기화 버전 분기

 

-- MainActivity

class MainActivity : AppCompatActivity() {

 

   ...

 

   private val soundPool = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

      SoundPool.Builder().setMaxStreams(8).build()  // 한꺼번에 재생하는 음원 개수 (8개 동시 재생)  *1

   } else {

      SoundPool(8, AudioManager.STREAM_MUSIC, 0) // 최대 재생 스트림 개수, 음원 종류, 음질 (default: 0)

   }

}

 

 

*1 에서 오류가 표시될 때 Alt+Enter를 눌러 표시되는 제안 중 Sorround with... 를 선택해 주세요.

   (버전에 따른 if 분기를 하는 코드가 생성됨)

 

5. 건반에 동적으로 클릭 이벤트 정의

-- MainActivity (최종 소스 코드)

class MainActivity : AppCompatActivity() {

 

   override fun onCreate(savedInstanceState: Bundle?) {

      super.onCreate(savedInstanceState)

      setContentView(R.layout.activity_main)

 

      sounds.forEach { tune(it)}   // sounds 리스트를 각각 tune()에 전달

   }

 

   private val soundPool = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

      SoundPool.Builder().setMaxStreams(8).build()  // 한꺼번에 재생하는 음원 개수 (8개 동시 재생)  *1

   } else {

      SoundPool(8, AudioManager.STREAM_MUSIC, 0) // 최대 재생 스트림 개수, 음원 종류, 음질 (default: 0)

   }

 

   private val sounds = listOf(  // 리스트 객체

      Pair(R.id.do1, R.raw.do1),

      Pair(R.id.re, R.raw.re),

      Pair(R.id.mi, R.raw.mi),

      Pair(R.id.fa, R.raw.fa),

      Pair(R.id.sol, R.raw.sol),

      Pair(R.id.la, R.raw.la),

      Pair(R.id.si, R.raw.si),

      Pair(R.id.do2, R.raw.do2)

   )

 

   private fun tune(pitch: Pair<Int, Int>) {

      val soundId = soundPool.load(this, pitch.second, 1)  // 음원 id 얻기

      findViewById<TextView>(pitch.first).setOnClickListener {   // 텍스트 뷰의 id에 해당하는 뷰 얻기

         soundPool.play(soundId, 1.0f, 1.0f, 0, 0, 1.0f)

      }

   }

 

   override fun onDestroy() {

      super.onDestroy()

      soundPool.release()

   }

}

}

 

 

실행한 후 각 건반을 터치 해 봅시다.

(건반을 누르면 저장된 음원을 재생하며 소리가 납니다)

[완성한 모양]

 

 

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

=== 손전등 ===

 

- 기능 소개

앱에서 플래시를 켜고 끌 수 있음

위젯을 제공함으로써 앱을 실행하지 않고도 플래시를 켜고 끌 수 있음

 

 

- 주된 도구

Camera Manager (플래시 동작 제어)

Service (보이는 화면 없이 백그라운드에서 실행되는 컴포넌트)

App Widget (런처에 배치하여 앱의 기능을 빠르게 사용)

 

 

1. 프로젝트 생성

프로젝트 명 : Flashlight

minSdkVersion : 23 (Android 6.0 Marshmallow) - 6.0 이상에서 지원되는 방법이 제일 쉽기 때문임

   (5.0 미만 버전들에서는 공식적으로 기능이 제공되지 않으며 제조사별 방법도 다르고 매우 복잡함)

기본 액티비티 : Empty Activity

Anko 라이브러리 설정

 

 

 

2. 손전등 기능 구현

1) 별도의 클래스 파일로 Torch 클래스 작성

  File > New > 'Kotlin File/Class'   (파일명: Torch, 유형: class)

 

  -- Torch.kt

package com.tistory.flashlight

import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager

class Torch(context: Context) {   // CameraManager 객체를 얻어야 하므로 Context를 생성자로 받았음
    private var cameraId: String? = null
    private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE)
            as CameraManager   // getSystemService()의 리턴값이 object형이므로 as로 형변환했음

    init {                         // 클래스가 초기화 될 때 실행됨
        cameraId = getCameraId()
    }

    fun flashOn() {
        if (cameraId != null) cameraManager.setTorchMode(cameraId!!, true)
    }

    fun flashOff() {
        if (cameraId != null) cameraManager.setTorchMode(cameraId!!, false)
    }

    private fun getCameraId():String? { // 카메라 ID는 각각의 내장 카메라에 부여된 고유의 ID이다
                                      // 카메라가 없다면 null을 반환해야 하므로 리턴형을 String?로 지정
        val cameraIds = cameraManager.cameraIdList   // 기기가 가진 모든 카메라 목록
        for (id in cameraIds) {
            val info = cameraManager.getCameraCharacteristics(id)
            val flashAvailable = info.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) // 플래시 가능 여부
            val lensFacing = info.get(CameraCharacteristics.LENS_FACING)   // 카메라 랜즈의 방향
            if (flashAvailable != null && flashAvailable && (lensFacing != null) && (lensFacing > 0)
                && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
                                                              // 플래시가 가능하고 카메라 방향이 뒷방향
                return id
            }
        }
        return null
    }
}

 

 

3. 액티비티에 스위치 제작

액티비티 정중앙에 switch 를 배치  (id: flashSwitch, text: '플래시 On/Off')

Switch는 두 가지 상태 값을 가지는 버튼 객체임

 

-- MainActivity 


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        val torch = Torch(this)

        flashSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
            if (isChecked) {
                torch.flashOn()
            } else {
                torch.flashOff()
            }
        }
    }
}

 

일단 동작을 한 번 확인해 봅시다!

(가운데 버튼을 켜면 플래시가 켜질 거예요)

 

 

4. 손전등 제어에 '서비스'를 활용

액티비티에서 했던 손전등 제어를 이제 서비스에서 구현해 봅시다. ('위젯'을 사용)

  (서비스는 이미 전술했듯 4대 컴포넌트의 하나이며 백그라운드에서 동작)

 

액티비티는 제어 기능은 그대로 살리되, 직접 제어 대신 서비스를 호출만 하도록 바꿔보죠.

  (즉 액티비티는 서비스 호출함으로써, 앱 위젯에서는 직접 서비스를 제어함으로써 Torch클래스를 동작시킴)

 

 

 

※ 안드로이드의 서비스

 

 

서비스 역시 액티비티처럼 생명주기용 콜백메서드들을 가짐

 

onStartCommand() : 일반적으로 실행할 작업을 여기에 작성함

 

onDestroy() : 서비스가 중지될 때 호출됨

  stopSelf()    서비스 내부에서 서비스 중지

  stopService() 서비스 외부에서 서비스 중지

 

 

 

 

 

 

 

 

 

 

 

 

 

위젯 / 서비스 사용

1) 서비스 생성

File > New > Service > Service      (클래스명 : TorchService)

 

2) TorchService 클래스 코딩

class TorchService : Service() {     // 자동으로 Service 클래스를 상속받는 군!

    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

    // 본 TorchService 클래스에서 Torch 클래스를 사용할 것임
    // Torch 클래스의 인스턴스를 얻는 방법으로 onCreate() 와 by lazy 중 하나를 이용할 수 있음
    // onCreate() 콜백서비스를 이용하면 코드가 길어지므로 by lazy를 사용했음
    // by lazy는 torch 객체를 처음 사용할 때 아래 코드가 초기화됨
    private val torch: Torch by lazy {
        Torch(this)
    }

    // 외부에서 startService()로 본 TorchService 서비스를 호출하면
    // onStartCommand()가 호출됨
    // 보통 인텐트에 action 값을 저장하여 호출함
    // (참고: 서비스는 메모리 부족 등의 이유로 강제 종료될 수 있음)
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
 
        when (intent?.action) {

            // 앱에서 실행할 경우
            "on" -> {
                torch.flashOn()
            }

            "off" -> {
                torch.flashOff()
            }
        }

        return super.onStartCommand(intent, flags, startId)  // *1
    }
}

 
/* *1

  onStartCommand()의 반환값

  이 반환값들에 따라 시스템이 강제 종료된 후, 다시 서비스 복원을 어떻게 할지를 결정함

     START_STICKY (null 인텐트로 재시작. 무기한 실행 대기하는 미디어 플레이어에 적합)
     START_NOT_STICKY (재시작 안함)
     START_REDELIVER_INTENT (마지막 인텐트로 재시작. 능동적으로 수행 중인 파일 다운로드 서비스등에 적합)

 */
 

 

3) MainActivity.kt에서 torch 객체에 직접 접근해 스위치를 켜고 끄게 되어 있는 코드를

  인텐트에 "on" 또는 "off" 액션을 보내 TorchService으로 서비스를 시작하도록 수정.


flashSwitch.setOnCheckedChangeListener { buttonView, isChecked ->

      if (isChecked) {
         // torch.flashOn()
         startService(intentFor<TorchService>().setAction("on"))   // *1 Anko 코드
      } else {
         // torch.flashOff()
         startService(intentFor<TorchService>().setAction("off"))
      }
   }

 /* *1
   Anko를 사용하지 않는다면,

   val intent = Intent(this, TorchService::class.jave)
   intent.action = "on"
   startService(intent)
 */
 

 

 

5. 앱 위젯 작성

웹 위젯이란 런처에 배치하여 빠르게 앱 기능을 사용할 수 있게 해 주는 컴포넌트입니다.

간단히 '위젯' 이라고도 부릅니다.

 

1) 위젯 추가

  File > New > Widget > App Widget       (클래스명 : TorchAppWidget)

 

 

※ 위젯 작성 마법사 화면의 옵션 항목들

 

  Placement  위젯 배치 위치

    Home-screen only  (홈 화면에만 배치)

    Home-screen and Keyguard  (홈 화면과 잠금 화면에 배치)

    Keyguard only (API 17+)  (잠금 화면에만 배치)

 

 Resizable (API 12+)  위젯 크기 변경

   Horizontally and vertically  (가로 세로 크기 변경 가능)

   Only Horizontally

   Only vertically

   Not resizable

 

 Minimum Width (cells)  가로 크기를 1~4 중 선택

 Minimum Height (cells)  세로 크기를 1~4 중 선택

 Configuration Screen  위젯의 환경설정 액티비티를 생성

 Source Language  자바와 코틀린 중 선택

 

 

2) 생성된 위젯용 파일들

 

  TorchAppWidget.kt    위젯 동작 작성용 파일

  TorchAppWidget.xlm   위젯의 레이아웃 정의 파일

  dimens.xml dimens.xml (v14)   위젯의 여백 값이 API 14부터 바뀌었음. v14 버전 이하와 이상 분기 파일

  Torch_app_widget_info.xml    각종 설정용 파일

 

 

3) 위젯 레이아웃 수정 - layout/torch_app_widget.xml

  텍스트 뷰 속성 > text 속성 수정 버튼 클릭 > App_name 선택 > "Edit Translations"

  appwidget_text 문자열 값에 "손전등"

 

위젯 레이아웃 전체에 클릭 이벤트를 연결해야 하므로 전체 레이아웃 id를 정합시다.

트리 창에서 RelativeLayout을 선택 > id 속성 : appwidget_layout

 

 

4) TorchAppWidget.kt 코딩


class TorchAppWidget : AppWidgetProvider() {

    // AppWidgetProvider라는 일종의 브로드캐스트 리시버 클래스를 상속
    override fun onUpdate(    // 위젯이 업데이트 돼야 할 때 호출됨됨
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // There may be multiple widgets active, so update all of them
        for (appWidgetId in appWidgetIds) {   // 위젯이 여러 개 라면 모든 위젯을 업데이트
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

    override fun onEnabled(context: Context) {  // 위젯이 처음 생성될 때 호출됨
    }

    override fun onDisabled(context: Context) {  // 위젯이 여러개일 때 마지막 위젯이 제거될 때 호출됨
    }

    companion object {
        internal fun updateAppWidget(   // 위젯을 업데이트 할 때 호출됨
            context: Context,
            appWidgetManager: AppWidgetManager,
            appWidgetId: Int
        ) {

            val widgetText = context.getString(R.string.appwidget_text)

            // 위젯은 액티비티에서 레이아웃을 다루는 것과 다름
            // 위젯에 배치하는 뷰는 따로 있고 RemoteViews 객체로 가져옴
            val views = RemoteViews(context.packageName, R.layout.torch_app_widget)  // 위젯 전체 레이아웃 정보

            // 텍스트 값을 변경
            views.setTextViewText(R.id.appwidget_text, widgetText)

            // 위젯 클릭 시 처리할 작업
            val intent = Intent(context, TorchService::class.java)
            val pendingIntent = PendingIntent.getService(context, 0, intent, 0)
                   // *1   (각 0 : 사용하지 않을 때 0 값을 전달)

            // 클릭 이벤트 연력 (위젯 클릭시 위에서 정의한 인텐트 실행)
            views.setOnClickPendingIntent(R.id.appwidget_layout, pendingIntent)    //   *1

            // 레이아웃 수정이 완료되면 appWidgetManager을 사용해서 위젯을 업데이트 함
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }
}


/* *1
  PendingIntent 객체는 실행할 인텐트 정보를 가지고 있다가 실행함
  어떤 인텐트를 실행힐 지에 따라서 다음의 적당한 메서드를 사용해야 함

    PendingIntent.getActivity()    액티비티 실행
    PendingIntent.getService()    서비스 실행
    PendingIntent.getBroadcast()  브로드캐스트 실행
*/


  

 

 

5) TorchService.kt 수정

  TorchService는 인텐트에 on/off 액션을 지정해서 켜거나 껐으나 위젯은 어떤 경우가

  on인지 off인지 알 수 없으므로 액션을 지정할 수 없습니다. 때문에 액션이 지정되지 않아도

  플래시가 동작하도록 TorchService.kt를 수정해야 합니다.


class TorchService : Service() {
    ...

    private var isRunning = false

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {

            // 앱에서 실행할 경우
            "on" -> {
                torch.flashOn()
                isRunning = true
            }

            "off" -> {
                torch.flashOff()
                isRunning = false
            }

            // 서비스에서 실행할 경우 (이때는 액션 값이 설정되지 않음)
            else -> {
                isRunning = !isRunning
                if (isRunning) {
                    torch.flashOn()
                } else {
                    torch.flashOff()
                }
            }
        }
        ...
    }
}


 

앱을 실행하면 위젯 코드가 반영됩니다.

앱을 실행했다가 바로 종료합시다.

휴대폰의 위젯 모음에 들어가보면 제작한 위젯이 추가되어 있을 것이다. 홈 화면에 끌어다 놓고 위젯으로

손전등을 동작 시켜 봅시다.

 

 

 참고) 위젯 모양 바꾸기

  xml/torch_app_widget_info.xml 파일에 위젯 모양에 대한 정보가 있습니다.

  drawable/example_appwidget_preview.png 아이콘이 지정되어 있음을 확인할 수 있네요.

 

 

 

※ 참고) 앱 위젯에 배치하는 뷰

앱 위젯에 배치하는 뷰는 정해져 있습니다.

 

1) 레이아웃으로 가능

   FrameLayout

   LinearLayout

   RelativeLayout

   GridLayout

 

2) 레이아웃에 배치 가능

   AnalogClock

   Button

   Chronometer

   ImageButton

   ImageView

   ProgressBar

   TextView

   ViewFlipper

   ListView

   GridView

   StackView

   AdapterViewFlipper

 

 

 

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

=== 지도와 GPS ===

 

- 기능 소개

구글 지도에 현재 위치를 표시하고 이동중인 자취를 실선으로 표시.

 

 

- 주로 사용하는 도구

Google Maps Activity (지도를 표시하는 기본 템플릿)

FusedLocationProviderClient (현재 위치 정보 클래스)

play-services-maps (구글 지도 라이브러리)

play-services-location (위치 정보 라이브러리)

 

 

1. 프로젝트 생성

(구글 지도를 사용하는 가장 쉬운 방법이 이렇게 기본 액티비티로 맵을 선택하는 것임)

 

프로젝트 명 : GPSMAP

기본 액티비티 선택 : Google Maps Activity

 

관련 라이브러리 추가

   play-services-maps (구글 지도 라이브러리) - 자동으로 추가됨

   play-services-location (위치 정보 라이브러리)

     (이전 예제에서 처럼 File 메뉴의 Project Structure... 로 검색하여 추가하면 더 편리)

 

Anko 라이브러리도 설정할 것

 

 

2. 구글지도 표시

 

1단계 코딩) 구글 지도 API 키 발급받기와 구글 지도 표시

자동으로 열려있는 google_maps_api.xml에 표시된 웹 링크로 들어가면 구글 API 콘솔화면에서

키를 발급받을 수 있습니다. 이전에 프로젝트를 만든 적이 있다면 기존 프로젝트를 선택하면 됩니다.

 

발급받은 키를 google_maps_api.xml "YOUR_KEY_HERE" 부분에 복사해 넣읍시다.

(발급받은 키는 분실하지 않도록 잘 보관할 것. 사용 횟수 제한도 있고 향후 구글지도는 유료화될 예정임)

[구글 지도 API 키 발급]

 

[구글 지도]

 

※ 자동으로 생성되어 있는 MapsActivity.kt 코드(커맨트 번역)

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

 

    private lateinit var mMap: GoogleMap

 

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_maps)

 

        // (자동 생성된 코드) SupportMapFragment 를 가져와서 지도가 준비되면 알림을 받습니다.

        val mapFragment = supportFragmentManager

            .findFragmentById(R.id.map) as SupportMapFragment

        mapFragment.getMapAsync(this)

    }

 

    /** (자동 생성된 코드)

     * 사용가능한 맵을 조작함. (맵이 준비되면 이 콜백이 실행됨)

     * 이것으로 마커나 선, 리스터를 추가하거나 표시되는 지역 변경 가능 (디폴트로 호주 시드니를 표시됨)

     * Google Play 서비스가 설치되어 있지 않으면

     * SupportMapFragment 안에 서비스를 설치하라는 안내가 표시됨

     * 이 메서드는 Google Play 서비스를 설치하고 이 앱으로 돌아왔을 때만 실행됨

     */

    override fun onMapReady(googleMap: GoogleMap) {

        mMap = googleMap

 

        // 표시 지역을 시드니로 마킹하고 카메라를 이동시킴

        val sydney = LatLng(-34.0, 151.0)

        mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))

        mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))

    }

}

 

 

※ 자동으로 생성되어 있는 activity_maps.xml 디자인

   트리를 보면, name com.google.android.gms....인 특별한 프래그먼트가 하나 배치되어 있는데,

   play-services-maps 라이브러리에서 제공됩니다.

 

2단계 코딩) 주기적으로 현재 위치 정보 업데이트 하기

 

AndroidManifest.xml 에는 다음과 같은 자동으로 위치 권한이 추가 되어 있을 것입니다.

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

 

위치 권한은 위험 권한이므로 실행 중에 권한 요청을 해야 합니다.

(참고로 위치 서비스는 구글 플레이 서비스를 취신 버전으로 업데이트해야 연결됨)

 

// 주기적인 위치 정보 요청

requestLocationUpdates(locationRequest: LocationRequest, locationCallback: LocationCallback, looper: Looper)

   * locationRequiest (위치요청 객체)

   * locationCallback (위치가 갱신되면 호출되는 콜백)

   * looper (특정 루퍼 스레드 지정. 특별한 경우가 아니라면 null)

   ! 주의) 위치 정보를 주기적으로 요청하는 코드는 액티비티가 화면에 보일 때에만 수행되도록 할 것!

          , 아래 코드의 onResume()에서 정보를 요청하고, onPause()에서 요청을 삭제.

 

 

3단계 코딩) 소스 코드 참고

4단계 코딩) 소스 코드 참고

 

 

 

 

- 소스 코드

 

package com.tistory.gpsmap

import android.Manifest
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
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.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import com.google.android.gms.maps.model.PolylineOptions
import org.jetbrains.anko.alert
import org.jetbrains.anko.noButton
import org.jetbrains.anko.toast
import org.jetbrains.anko.yesButton

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {

    private lateinit var mMap: GoogleMap

    /* (1단계 코딩)
    *
    *    ===== 구글 지도 표시 =====
    *     (기본 코드는 자동 생성됨)
    *
    */
    // ① 위치정보를 주기적으로 받는데 필요한 객체들 선언
    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
    private lateinit var locationRequest: LocationRequest
    private lateinit var locationCallback: LocationCallback

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)  // 화면 꺼지지 않게.
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT    // 세로 모드 고정.

        setContentView(R.layout.activity_maps)
        
        // (자동 생성된 코드) SupportMapFragment 를 가져와서 지도가 준비되면 알림을 받습니다.
        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)

        // ② ①에서 선언한 객체들을 onCreate() 마지막에 초기화
        locationInit()
    }

    // ②
    private fun locationInit() {
        fusedLocationProviderClient = FusedLocationProviderClient(this)
        locationCallback = MyLocationCallBack()

        locationRequest = LocationRequest()   // LocationRequest객체로 위치 정보 요청 세부 설정을 함
        locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY   // GPS 우선
        locationRequest.interval = 10000   // 10초. 상황에 따라 다른 앱에서 더 빨리 위치 정보를 요청하면
                                          // 자동으로 더 짧아질 수도 있음
        locationRequest.fastestInterval = 5000  // 이보다 더 빈번히 업데이트 하지 않음 (고정된 최소 인터벌)
    }

    /** (자동 생성된 코드)
     * 사용가능한 맵을 조작함. (맵이 준비되면 이 콜백이 실행됨)
     * 이것으로 마커나 선, 리스터를 추가하거나 표시되는 지역을 변경할 수 있음 (디폴트로 호주 시드니를 표시됨)
     * Google Play 서비스가 설치되어 있지 않으면 SupportMapFragment 안에 서비스를 설치하라는 안내가 표시됨
     * 이 메서드는 Google Play 서비스를 설치하고 이 앱으로 돌아왔을 때만 실행됨
     */
    override fun onMapReady(googleMap: GoogleMap) {
        mMap = googleMap

        // 표시 지역을 시드니로 마킹하고 카메라를 이동시킴
        val sydney = LatLng(-34.0, 151.0)
        mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
        mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
    }

    /* (2단계 코딩)
    *
    *  ===== 현재 위치 요청 하기 =====
    *
    */
    // ③
    private fun addLocationListener() {
        fusedLocationProviderClient.requestLocationUpdates(locationRequest,
            locationCallback,
            null)  // 혹시 안드로이드 스튜디오에서 비정상적으로 권한 요청 오류를 표시할 경우, 'Alt+Enter'로
                   // 표시되는 제안 중, Suppress: Add @SuppressLint("MissingPermission") annotation
                   // 을 클릭할 것
                   // (에디터가 원래 권한 요청이 필요한 코드 주변에서만 권한 요청 코딩을 허용했었기 때문임.
                   //  현재 우리 코딩처럼 이렇게 별도의 메소드에 권한 요청 코드를 작성하지 못하게 했었음)
    }

    // ④ MyActivity 클래스의 내부 클래스로 생성
    inner class MyLocationCallBack: LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult?) {
            super.onLocationResult(locationResult)

            val location = locationResult?.lastLocation   // GPS가 꺼져 있을 경우 Location 객체가
                                                      // null이 될 수도 있음

            location?.run {
                val latLng = LatLng(latitude, longitude)   // 위도, 경도
                mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, 17f))  // 카메라 이동

                Log.d("MapsActivity", "위도: $latitude, 경도: $longitude")     // 로그 확인 용



                /* (4단계 코딩)
                *
                *   이동 경로 그리기 (여기에서는 구글맵에서 이동 자취 그리기용으로 지원해주는
                *                     편리한 메서드를 이용)
                *
                */
                mMap.addPolyline(polylineOptions)         // 선 그리기 (위치 정보가 갱신되면
                                                          // polyLineOptions 객체에 추가되고
                                                          // 지도에 polylineOptions 객체를 추가 함
            }
        }
    }


    /* (3단계 코딩)
    *
    *  ===== 실행 중 권한요청 =====
    *
    *  */
    private val REQUEST_ACCESS_FINE_LOCATION = 1000

    private fun permissionCheck(cancel: () -> Unit, ok: () -> Unit) {   // 전달인자도, 리턴값도 없는
                                                                   // 두 개의 함수를 받음

       if (ContextCompat.checkSelfPermission(this,                  // 권한이 없는 경우
          android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

          if (ActivityCompat.shouldShowRequestPermissionRationale(this,
             Manifest.permission.ACCESS_FINE_LOCATION)) {       // 권한 거부 이력이 있는 경우

             cancel()

          } else {
             ActivityCompat.requestPermissions(this,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                REQUEST_ACCESS_FINE_LOCATION)
          }
       } else {                                                    // 권한이 있는 경우
          ok()
       }
    }

    private  fun showPermissionInfoDialog() {
       alert("위치 정보를 얻으려면 위치 권한이 필요합니다", "권한이 필요한 이유") {
          yesButton {
             ActivityCompat.requestPermissions(this@MapsActivity,  // 첫 전달인자: Context 또는 Activity
                    // this: DialogInterface 객체
                    // this@MapsActivity는 액티비티를 명시적으로 가리킨 것임
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                REQUEST_ACCESS_FINE_LOCATION)
          }
          noButton {  }
       }.show()
    }

    // 권한 요청 결과 처리
    override fun onRequestPermissionsResult(
       requestCode: Int,
       permissions: Array,
       grantResults: IntArray
    ) {
       super.onRequestPermissionsResult(requestCode, permissions, grantResults)

       when (requestCode) {
          REQUEST_ACCESS_FINE_LOCATION -> {
             if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                addLocationListener()
             } else {
                toast("권한이 거부 됨")
             }
          return
          }
       }
    }

    override fun onResume() {
       super.onResume()

       // 권한 요청
       permissionCheck(
          cancel = { showPermissionInfoDialog() },   // 권한 필요 안내창
          ok = { addLocationListener()}      // ③   주기적으로 현재 위치를 요청
       )
    }

    override fun onPause() {
       super.onPause()

       removeLocationListener()    // 앱이 동작하지 않을 때에는 위치 정보 요청 제거
    }

    private fun removeLocationListener() {
       fusedLocationProviderClient.removeLocationUpdates(locationCallback)
    }


    /* (4단계 코딩)
    *
    *   ===== 이동 경로 그리기 =====
    *   MyLocationCallBack 클래스
    */

    private val polylineOptions = PolylineOptions().width(5f).color(Color.RED)

}

 

※ 구글 맵에서 이동 자취 그리기를 위해 지원해주는 편리한 메서드들

   (만약 이들이 지원하지 않는 그래픽 기능이 필요하다면,

    수평 측정기 제작 때처럼 뷰에 그래픽 API를 이용해서 그려야 함)

 

addPolyLine()   - 여러 개의 경로 선을 그림

addCircle()     - 원 그리기

addPolygon()   - 영역 그리기

 

 

구글 지도를 활용하는 프로그램 소스에 특별한 내용은 없습니다.

이 예제는 지도를 그리고 현재 위치를 요청하고 그에 관한 권한을 요청하는 게 전부입니다.

 

지도를 표시하는 것과 이동 자취를 그리는 기능들을 전부 구글 맵용 라이브러리에서

지원하고 있으니 참 편합니다. 좀 더 세부적인 기능들을 구현하는 게 아니라면 제공되는

라이브러리를 쓰면 될 것 같습니다.

 

 

그럼, 이만~

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

코딩의 간결성과 직관적 가독성을 위해 프로그램 작성법이 조금씩 변천되어 왔는데,

특히 함수 표현에 있어 획기적이라고 할 수 있는 표현법이 익명함수와 람다식 입니다.

그리고 프로그래밍 기법에 있어서도 특히 비동기 프로그래밍을 위해

고차함수, 콜백함수들이 도입되었습니다.

 

여기에서 개선됐다는 기준은 80, 90년대의 프로그램 기법에서 개선됐다는 말입니다. ;;

그러니까 이 기술들에 대한 지원이 시작된 것은 2000년대 부터 였던 것 같네요.

그 이후 지금까지 커다란 변화는 없는 것 같습니다.

 

각 용어와 표현 방법에 대해 간략하고 쉽게 정리해봤습니다.

 

람다식

함수를 간결화한 형태.

익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리함.

한편 코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있음.

아래 세 가지 표현은 모두 같은 의미임.

 

① 일반 함수

fun add(x: Int, y: Int) : Int {          // 함수 선언부 (2개의 int형 인자를 받아 int형 값을 리턴)

   return x + y                     // 함수의 내용 본체

}

val sum: Int = add(3, 4)

val sum = add(3, 4)                 // 이렇게 줄임 표현도 가능

 

② 문법적으로 허용된 생략을 한 일반 함수

fun add(x: Int, y: Int) = x + y         // 함수 선언부에 인자 목록은 명시. 리턴 타입은 생략

                                   // 함수 본체의 제일 마직막 표현식에 따라 자동으로 리턴 타입이 추론됨

val sum = add(3, 4)

 

 

③ 람다식

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }   // '{ 인수들 -> 함수본체 }' 부분이 람다식임

val sum: (Int, Int) -> Int = {x, y -> x + y }           // 이렇게 줄여 표현 하든

var sum = {x: Int, y: Int -> x + y }                  // 이렇게 줄여 표현하든 모두 가능

                                                 // (전달인자든 람다식 안에서든 데이터형 명확히 지정됐으므로)

 

                                    // 그리고 람다식이 저장된 변수 sum은 일반 함수처럼 호출이 가능함

sum(1,2)                           // 결과: 3

 

 

 고차 함수 (High-order Function)

 

함수의 전달인자나 반환 값의 데이터형에 함수가 포함된 형태.

 

fun printResult( s: String, r: Float, ff: (num: Float) -> Float) {                //  *1

   val result = ff(r)

   println("$s 는 $result")

}

printResult("원의 넓이", 3.0f, {x -> 3.14f * x*x })                          // *2

printResult("구의 체적", 3.0f, {x -> 4.0f/3.0f * 3.14f * x*x*x })             // *3

 

실행 결과)

원의 넓이 는 28.26

구의 체적 는 113.04001

 

 

*1 함수의 전달인자를 표시하는 방식은,

   문자 : 전후에 함수내에서 사용할 전달인자 이름, 그 전달인자의 데이터형을 명기한 형태입니다.

   결국 전달인자로 사용된 함수 부분은,

      함수명은 ff, 데이터형은 (num: Float) -> Float) 라고 해석할 수 있습니다.

                            

                            (num: Float) -> Float)

                            이 부분이 람다식으로 표현되어 있네요.

                            Float 형 인자 하나를 받고 Float 형으로 리턴하는 형태의 함수가

                            전달인자로 사용될 수 있음을 의미합니다.

 

*2, *3

  그림으로 좀 더 쉽게 설명을 해 보자면,...

   ①,② 각각의 함수가 *1의 전달인자 선언을 통해 printResult()내에서 result 값을 계산하는데 사용되었습니다.

 

  참고로 ①,②는 호출한 함수 printResult에 의해 거꾸로 호출되는데, 이와 같은 함수를 '콜백함수'라고 합니다.

   한편, 이번 예에서는 이 함수들을 이름 없이 (이름이 없으므로 '익명(Anonymous) 함수'라고 부릅니다)

   함수 본체만을 람다식 형태로 전달했지만, 여러 부분에서 이 함수들을 반복해서 참조한다면

   이름을 갖는 함수를 정식으로 정의하고 그 함수명을 전달해줘도 됩니다.

 

 

   참고) 람다식을 사용할 때 몇몇 표현의 융통성

 

      *2, *3의 표현은 (눈치를 챘는지 모르겠지만) 아래의 표현에서 전달인자 데이터형을 생략한 것 입니다.

      printResult("원의 넓이", 3.0f, {x: Int -> 3.14f * x*x }) 

                                                  // (데이터형이 printResult()선언부에 이미 작성되어 있으므로 생략 가능)

 

      *2, *3처럼 함수를 호출할 때 제일 마지막 전달인자가 람다식인 경우,

      람다식을 ()밖에 적어도 됩니다.

      printResult("원의 넓이", 3.0f) {x -> 3.14f * x*x }        // 람다식만 밖으로 꺼내 작성했음

 

      *2, *3처럼 심지어 람다식의 전달인자가 단 하나 뿐이라면, 람다식의 전달인자를 아예 생략해도 됩니다.

      그리고 그 전달인자는 키워드 it으로 참조할 수 있습니다.

      printResult("원의 넓이", 3.0f) {3.14f * it*it }

 

         fun sum( x: Int, y: Int, prt: (num: Int) -> Unit) {       //  Unit는 리턴형이 없다는 의미임(void와 같음)

            val total: Int

            total = x + y

            prt( total )

         }

         sum(3,4) {println(it)}                               // 결과: 7 (람다식의 표현이 거의 깡패 수준이죠?...)

 

 

      뿐만아니라, 고차함수의 전달인자가 람다식 하나 뿐이라면, 고차함수를 호출할 때 ()를 생략할 수도 있습니다.

 

         fun sum(prt: (num: Int) -> Unit) {       // 전달인자가 람다식 하나 뿐이면,

            prt(100)

         }

         sum() {print(it)}

         sum {print(it)}                         // 호출 때 빈 ()를 생략 가능함

 

 

 

 

   ※ 콜백함수의 활용

      비동기 프로그래밍에서 작업시간이 오래 걸리는 함수에게 콜백함수를 전달해주고,

      메인 프로그램은 다음 동작을 계속 진행.

 

      예)

      fun longTimeJob( ..., callback() ) {

         (오래걸리는 작업 - DB, 통신, 사용자이벤트, ...)

         callback()        // 오래걸리는 작업 완료

      }

 

      메인프로그램작업1

      longTimeJob( ..., cBackFun() )     // 콜백함수를 넣어 longTimeJob() 호출 후 다음 작업 계속

      메인프로그램작업2

      ...

      ...

 

 

      참고로, 이런 활용이 UI등에서 사용자 응답 이벤트를 다루는 경우에 이뤄진다면,

      언제 발생할 지 모르는 응답에 대해 대기를 해야 하는 '이벤트 리스너' 함수에게

      그 응답을 처리할 작업을 담고 있는 '이벤트 핸들러(= 이벤트 처리기)' 함수(콜백함수)를 전달하게 됩니다.

      말하자면 다음과 같은 모양이 됩니다.

 

      fun 리스너(핸들러) {

         (센서등 응답 대기...)

         핸들러()

      }

 

      메인프로그램작업1

      리스너(핸들러)

      메인프로그램작업2

      ...

      ...

 

 

      읽을 거리) 그럼 함수의 전달인자로 함수를 받을 수 없었던 예전에는 버튼 이벤트를 어떻게 처리했나?

         (함수가 객체는 받을 수 있었다. 자바에서 변수는 곧 객체)

         이벤트 핸들러 구현용 인터페이스를 개발팀 공용으로 만들어 놓고,

         각 팀원들이 그 인터페이스를 구현한 자신들만의 클래스를 만들고 객체화하여 리스너 함수에 전달함으로써

         콜백함수를 구현했습니다.

         아래 버튼 이벤트 처리의 경우, 인터페이스의 추상메서드 onClick()을 구현한 이벤트 핸들러 객체

         'new onClickListener()'를 전달하고 있습니다.

 

         Button button = findViewID(R.id.button)

         button.setOnClickListener( new onClickListener() {

            @override

            public void onClick(View view) {

                // 이벤트 처리

            }

         }

 

 

간략화된 표현식에 대해서는 오히려 혼동을 주는 경우도 있을 수 있습니다.

각자 편한대로 코딩하면 됩니다. 그래도 내용은 알고 있는 게 좋겠죠. 다른 프로그램을 해독하려면요.

 

그럼, 이만~

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

=== 전자 액자 ===

 

기능 소개

안드로이드 기기에 있는 사진 파일들을 보여줍니다.

3초마다 자동으로 사진이 슬라이드 됩니다.

 

핵심이 되는 주제

프로바이더를 이용해 사진 데이터를 조회

외부 메모리 데이터로의 접근 권한 요청

프래그먼트 생성

Glide 라이브러리 사용

뷰 페이저, 페이저 어댑터의 사용

 

 

1) 프로젝트 생성

프로젝트 명 : MyGallery

Anko 라이브러리 설정할 것

 

2) 프로바이더 사용

컨텐츠 프로바이더는 데이터베이스, 파일, 네트워크등의 데이터를 앱들에게 공유해 주는 컴포넌트다.

사진을 촬영하면 사진 정보가 안드로이드 미디어 데이터베이스에 저장되는데,

이 사진 정보를 '콘텐츠 프로바이더(Contents Provider)'를 통해 얻어올 수 있다.

  (, 사진 정보는 외부저장소에 저장되므로 반드시 외부저장소 읽기 권한 요청 필수!)

콘텐츠 프로바이더를 포함해 안드로이드 기기에는 중요한 4가지 컴포넌트들이 있다.

 

※ 안드로이드 기기의 4대 컴포넌트

  액티비티 (화면 구성)

  콘텐츠 프로바이더 (데이터베이스, 파일, 네트워크 데이터 제공)

  브로드캐스트 리시버 (앱이나 기기에서 발송하는 데이터를 수신)

  서비스 (화면에 표시되지는 않고 백그라운드에서 동작)

 

※ 안드로이드 기기의 2가지 저장소

  내부 저장소 (OS가 설치되어 있으며 유저가 접근할 수 없는 시스템 영역)

  외부 저장소 (유저 영역)

 

※ 안드로이드 기기의 권한의 종류 2가지

  보통(normal) 권한 - 매니페스트에 권한 추가  (: 인터넷 사용 권한)

  위험(dangerous) 권한 - 매니페스트에 권한 추가 + 앱 실행시 사용자의 승인 필수 (: 외부 저장소 읽기 권한)

      주의) 위험 권한을 사용하는 앱의 경우, 이미 사용자의 승인을 얻었다고 해도 권한과 관련된 기능 사용시

            매번 사용자 승인완료 상태를 체크해야 한다. 왜냐하면 사용자가 언제든 권한을 취소할 수 있기 때문.

 

  자주 사용되는 위험 권한들의 예)

STORAGE

READ_EXTERNAL_STORAGE

WRITE_EXTERNAL_STORAGE

LOCATION

ACCESS_FINE_LOCATION

ACCESS_COARSE_LOCATION

SMS

SEND_SMS

RECEIVE_SMS

CAMERA

CAMERA

 

 

※ 사진 정보 가져오기 구현 (콘텐츠 프로바이더 사용법임)

 

  ① 아래 getAllPhotos()*1처럼 contentResolver 객체로 Cursor라는 객체를 얻어오면 그 안에

     사진 데이터가 담겨져 옴

 

class MainActivity : AppCompatActivity() {

       private val REQUEST_READ_EXTERNAL_STORAGE = 1000

       override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
        
          if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
            != PackageManager.PERMISSION_GRANTED) {

            // 권한 승인이 안되어 있는 경우
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
               Manifest.permission.READ_EXTERNAL_STORAGE)) {     // true: 거부한 적이 있음

               // 이전에 이미 권한 거부가 있었을 경우 설명 (Anko 라이브러리를 쓰면 편하다)
               alert("사진을 표시하려면 외부 저장소 권한이 필요합니다!", "권한이 필요한 이유") {
                  yesButton {
                     ActivityCompat.requestPermissions(   // 권한 요청
                        this@MainActivity,
                        arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                        REQUEST_READ_EXTERNAL_STORAGE) // 권한 요청에 대한 분기 처리를 위해
                                                               // 만든 적당한 정수 값임
                  }
                  noButton {  }
               }.show()

            } else {

                // 이전에 권한 거부가 없었을 경우 권한 요청
                ActivityCompat.requestPermissions(this,
                   arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                   REQUEST_READ_EXTERNAL_STORAGE)
            }

          } else {
            // 권한이 이미 승인되어 있는 상태
            getAllPhotos()
          }
       }
    
       private fun getAllPhotos() {
          val photosCursor = contentResolver.query(                        // *1
             MediaStore.Images.Media.EXTERNAL_CONTENT_URI,   // 가져올 데이터의 URI
                                                   // EXTERNAL_CONTENT_URI 는 외부 저장소를 의미함
             null,        // 가져올 항목들을 문자열 배열로 지정 (null : 모든 항목을 가져옴)
             null,        // 조건1 (null : 전체 데이터)
             null,        // 조건2 (조건1과 조합하여 조건 지정)
             MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC")  // 정렬방법 (촬영날짜 내림차순)

          // 이 코드는 사진이 제대로 읽어지는지 로그로 확인해 보려고 작성
          if (photosCursor != null) {      // photosCursor == null 이면 사진이 없는 것임
             while(photosCursor.moveToNext()) {  // PhotosCursor 객체 내부에 데이터 이동용 포인터가 있음
                val uri = photosCursor.getString(
                        photosCursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
                           // 사진의 경로가 저장된 데이터베이스의 컬럼명은 DATA 상수에 정의되어 있음
                Log.d("MainActivity", uri)   // 안드로이드 스튜디오의 Logcat에서
                                          // MainActivity로 필터링 했을 때 사진의 URI가 표시됨
             }
             photosCursor.close()  // 이 객체를 더 이상 사용하지 않으므로 닫아줘야 함 (메모리 누수 방지)
          }
       }
    }

② 매니페스트에 외부 저장소 읽기 권한 설정

 

    -- AndroidManifest.xml --

 

      <manifest ...>

         <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

 

 

일단 이 단계에서 실행해보자. 아직 사진이 표시되지는 않지만 Logcat에 사진 경로가 표시될 것이다.

(사진이 많으면 시간이 좀 걸릴 수도 있음)

[로그: 사진 파일 위치]

  

3) 전자 액자 구현

사진들을 좌우로 돌려볼 수 있도록 '프래그먼트UI' 요소와 'ViewPage'로 구현.

메모리 관리와 성능 향상을 위해서 'Glide' 라이브러리를 활용.

 

※ 프래그먼트 (Fragment; 파편)

웹 페이지 프레임처럼 화면에 구획한 프레임을 말하며 한 번 프레임을 짜놓으면 재사용도 가능하다.

액티비티처럼 생명주기를 가지고 있는데 액티비티보다 더욱 세분화되어 있다.

 

 

 

 

※ 본 예제에서 사용할 세 가지 메서드

onCreate()

  프래그먼트 생성시 호출 (레이아웃 완성 전)

onCreateView()

  프래그먼트에 표시할 뷰를 레이아웃 파일에서 읽어 옴

  (레이아웃 완성 전)

onViewCreated()

  생명주기에는 포함되지 않는 특별 메서드

  완성된 레이아웃 뷰를 받아 이벤트 처리

 

 

※ 참고

onAttach() : 액티비티에 붙을 때 호출

onActivityCreate() : 액티비티의 onCreate() 실행 직후 호출

onStart() : 프래그먼트가 사용자에게 보여질 때 호출

onResume() : 사용자와 상호작용 시작

onPause() : 프래그먼트 일시중지 (사용자와 상호작용 안함)

onStop() : 프래그먼트 중지

onDestroyView() : 프래그먼트 자원 해제

onDestroy() : 프래그먼트 제거

onDetach() : 프래그먼트가 액티비티에서 완전히 제거

 

 

 

 

 

 

 

 

 

① 프래그먼트 생성

File > New > Fragment > Fragment(Blank)

  (이름: PhotoFragment --> 레이아웃 이름은 자동으로 fragment_photo가 될 것임)

  (체크박스해제: 'Include interface callbacks?' ∵ 본 예제에서 사용하지 않음)

 

  아래의 두 개 파일이 생성될 것임 (액티비티 처럼 두 개의 파일로 구성됨)

  app/java/팩키지명/PhotoFragment.kt

  app/res/layout/fragment_photo.xml  ( <-- 디자인에는 텍스트 뷰가 하나 포함되어 생성됨)

 

  ※ 옵션 설명

    Create layout XML? (본 예제에서는 XML을 사용하므로 체크)

    Include fragment factory methods? (팩토리 메서드를 이용해 생성. 생성시 인자도 넘겨줌. 체크)

    Include interface callbacks? (액티비티와 상호 작용하는 콜백 인터페이스를 가져옴.

                                프래그먼트에서 발생한 이벤트를 액티비티로 전달할 경우 체크)

 

② 트리창에서 루트 레이아웃인 FrameLayout에서 컨텍스트 메뉴를 이용해 ConstraintLayout으로 변경(옵션:디폴트)

   (변환이 되면 레이아웃 아이콘 모양이 바뀐다)

   (트리창의 루트 레이아웃 이름은 id가 표시된 것임 --> 본 예제에서는 ID를 사용하지 않으므로 id를 삭제할 것)

 

[프래그먼트 파일 생성]

 

[디폴트 레이아웃 변경]

 

③ 트리창에서 자동으로 생성된 텍스트뷰를 삭제하고,

Autoconnect 모드를 켜고 여백은 0dp로 설정한 후 팔레트창에서 ImageView를 정 중앙에 배치

이미지 리소스 선택창이 나오면 샘플 이미지를 아무거나 선택 (디자인 하는 동안에만 임시로 표시됨)

 

이미지 뷰 속성 >> id: imageView1, layout_width layout_height: match_constraint, scaleType: centerCrop

 

 

Glide 라이브러리 사용 준비

다음과 같이 setImageURI()를 이용해서 이미지 뷰에 사진을 표시할 수도 있지만,

  imageView.setImageURI(Uri.parse("/storage/emulated/0/DCIM/Camera/aaaaa.jpg"))

 

효율적인 리소스 관리를 자동으로 해주고 이미지 비동기 로딩으로 UI의 끊김이 없는

Glide 라이브러리를 이용하자.

 

-- build.gradle 에 의존성 추가후 Sync Now.

(버전은 https://github.com/bumptech/glide 에서 최신 버전을 확인해서 적을 것)

 

dependencies {

   ...

   implementation 'com.github.bumptech.glide:glide:4.11.0'      // 2020.05.13 버전

}

 

build.gradle 파일을 수정하지 않고 직접 안드로이드 스튜디오의 메뉴를 활용하는 방법도 있다.

File > Project Structure 클릭 > Dependencies > '+'눌러 추가 > 검색창에 glide 입력후 검색

자동으로 검색된 라이브러리들 중에 필요한 항목을 선택하고 (자동으로 최신 버전도 표시됨) OK 버튼 클릭.

 

PhotoFragment.kt 파일 코딩

자동으로 작성되어 있는 코드를 아래와 같이 수정하면 된다.

 

 

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

private const val ARG_URI = "uri"  // 클래스 밖에서 상수를 정의하면 컴파일 시 상수가 초기화됨
                              // 컴파일 시 상수 초기화는 프리미티브형(Int, Long, Double등 기본형)만 가능

class PhotoFragment : Fragment() {

    private var uri: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            uri = it.getString(ARG_URI)   // 프래그먼트가 생성되고 onCreate() 호출되면
                                        // ARG_URI키에 저장된 uri값을 얻어서 변수에 저장
        }
    }

    // 프래그먼트에 표시될 뷰를 생성함
    // 액티비티가 아닌 곳에서 레이아웃 리소스를 가져오려면 LayoutInflater객체의 inflate()를 사용함
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_photo, container, false)
    }

    // newInstance()를 이용해서 프래그먼트를 생성할 수 있고 인자로 uri 값을 전달
    // 이 값은 Bundle 객체에 ARG_URI 키로 저장되고 arguments 프로퍼티에 저장됨
    companion object {
        @JvmStatic
        fun newInstance(uri: String) =
            PhotoFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_URI, uri)
                }
            }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Glide.with(this).load(uri).into(imageView1) // 사진을 이미지뷰에 표시
                                               // with(): 사용준비, load(): 이미지 로드, into(): 이미지 표시
    }
}

 

⑥ 액티비티에 ViewPager 추가

activity_main.xml 디자인 창 > Containers > ViewPager를 화면 정중앙에 배치.

   (id: viewPager1, layout_width, layout_height: match_constraint)

   (디자인창을 이용하는 경우에는 안내와 함께 필요한 라이브러리가 자동으로 의존성으로 추가됨)

 

※ 뷰페이저란?

   여러 프래그먼트들을 좌우로 슬라이드하는 뷰. (별도의 라이브러리에 있는 뷰이므로 의존성 추가 필요)

   프래그먼트 목록용 컨테이너('페이저 어댑터'라고 부름)는 다음의 두 가지가 있는데, 이를 뷰페이저에

   연결하면 사진이 표시된다. (본 예제에서는 FragmentStatePagerAdapter를 사용함)

 

   FragmentPagerAdapter (페이지들 로딩후 계속 메모리 상주. 속도는 빠르나 메모리 많이 사용)

   FragmentStatePagerAdapter (보이지 않는 페이지는 메모리에서 제거. 페이지가 많을 때 적합)

 

 

⑦ 페이저 어댑터 작성

File > New > 'Kotlin File/Class'   (이름: MyPagerAdapter1, 종류: Class)

  (자동으로 생성된 코드를 아래와 같이 조금 수정하면 됨)

 

 


import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter

class MyPagerAdapter1(fm: FragmentManager?) : FragmentStatePagerAdapter(fm) {

    private val items = ArrayList()  // 뷰페이저가 표시할 프래그먼트 목록

    override fun getItem(position: Int): Fragment {
        return items[position]
    }

    override fun getCount(): Int {
        return items.size      // 아이템의 개수
    }

    fun updateFragments(items: List) {
        this.items.addAll(items)    // 외부에서 추가
    }
}

 

MainActivity.kt getAllPhotos() 코드 추가


private fun getAllPhotos() {
  val photosCursor = ...

  val fragmentArray = ArrayList()
  if (photosCursor != null) {
    while (photosCursor.moveToNext()) {
      val uri = photosCursor.getString(photosCursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA))
      Log.d("MainActivity", uri)
      fragmentArray.add(PhotoFragment.newInstance(uri)) // 사진을 photosCursor 객체에서 가져올 때마다
                                                        // 새 프래그먼트를 생성하여 추가
    }
    photosCursor.close()
  }

  val adapter1 = MyPagerAdapter1(supportFragmentManager)
                 // 프래그먼트 관리자는 getSupportFragmentManager()로 가져올 수 있는데,
                 // 코틀린에서는 supportFragmentManager 프로퍼티로 접근할 수 있음
  
  adapter1.updateFragments(fragmentArray)
  viewPager1.adapter = adapter1

  ...
}


이 단계에서 실행해 보고 사진이 좌우로 잘 슬라이드되며 표시되는 지 확인해 보자.

(이전 단계에서 사진이 제대로 읽어지는지 로그로 확인해 보려고 작성했던 코드는 리마크할 것

  - 그냥 놔두면 photosCursor.close()에서 오류가 날 수 있다)

 

[실행 화면]

 

4) 슬라이드 쇼 구현

getAllPhotos()에 간단한 코드를 추가하여 3초마다 자동으로 사진이 슬라이드 되도록 만들어 보자.


private fun getAllPhotos() {
   ...

   timer(period = 3000) {
      runOnUiThread {
         if (viewPager1.currentItem < adapter1.count - 1) {
            viewPager1.currentItem = viewPager1.currentItem + 1
         } else {
            viewPager1.currentItem = 0
         }
      }
   }
}

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

수평 측정기

 

기능 소개

기기의 수평 상태를 보여줍니다.

 

핵심이 되는 주제

가속도 센서 활용법

액티비티의 생명 주기에 대한 학습

커스텀 뷰를 만드는 방법

기본적인 그래픽 기능 실습 (원, 선 그리기)

 

사전 지식

1) 액티비티의 생명 주기

   액티비티는 아래의 순서로 생성되고 소멸합니다.

   우리가 구현하려는 기능을 적절한 메소드에 코딩을 해줘야 우리가 원하는 시점에 맞춰 실행이 됩니다. 

모든 앱은 백그라운드 실행중에 메모리 확보등을 이유로 언제든지 강제 종료될 수 있습니다.

그리고 그 후 다시 실행하면 onCreate()부터 다시 호출되는 거죠.

 

센서를 사용하는 모바일 기기용 프로그램을 제작할 때에는 배터리 소모를 항상 고려해야 합니다.

배터리를 절약하려면 꼭 필요한 때에만 센서를 동작시키는 게 좋겠죠?

 

 

2) 안드로이드가 지원하는 센서의 종류 

  ( * 우리는 가속도 센서를 사용할 예정입니다)

종류

용도

 

중력 센서

흔들림, 기울임등 동작 감지

TYPE_GRAVITY

가속도 센서

흔들림, 기울임등 동작 감지

TYPE_ACCELEROMETER

선형 가속도 센서

단일 축을 따라 가속 모니터링

TYPE_LINEAR_ACCELERATION

자기장 센서

나침반

TYPE_MAGNETIC_FIELD

방향 센서

장치 위치 결정

TYPE_ORIENTATION

자이로 센서

회전 감지

TYPE_GYROSCOPE

회전 센서

회전 감지, 모션 감지

TYPE_ROTATION_VECTOR

주변 온도 센서

대기 온도 모니터링

TYPE_AMBIENT_TEMPERATURE

근접 센서

통화중인지 검사

TYPE_PROXIMITY

조도 센서

화면 밝기 제어

TYPE_LIGHT

기압 센서

공기압 모니터링 변화

TYPE_PRESSURE

온도 센서

온도 감지

TYPE_TEMPERATURE

상대 습도 센서

이슬점, 상대 습도 모니터링

TYPE_RELATIVE_HUMIDITY

 

3) 센서 값을 받는 빈도

  빈도가 빈번할 수록 배터리를 많이 사용합니다.

SENSOR_DELAY_FASTEST

빈번하게

SENSOR_DELAY_GAME

게임에 적합한 정도로

SENSOR_DELAY_NORMAL

화면 방향이 전환될 때에 적합한 정도

SENSOR_DELAY_UI

UI 표시에 적합한 정도

 

4) 안드로이드 좌표축

 

※ 가속도 센서의 동작

  가속도 = '중력가속도'를 의미합니다.

 

  중력가속도를 9.8 이라고 할 때,

  중력이 x, y, z 각 방향으로 작용하는 정도에 비례해서 값이 분배됩니다.

    (x, y, z 각 방향의 값의 총합은 항상 9.8이 됨)

 

  음... 그러니까, 쉽게 예를 들어 볼게요.

  휴대폰 화면을 하늘로 향한 상태로 바닥에 놓으면 z축으로만 중력이 작용하므로 --> x=y=0, z=9.8

  휴대폰 좌측 변을 기준으로 화면을 왼쪽 방향으로 90도 세우면 x축으로만 중력이 작용하므로 --> x=9.8, y=z=0

      (우측 변을 기준으로 화면을 오른쪽 방향으로 90도 세우면 x=-9.8, y=z=0)

  휴대폰 하측 변을 기준으로 화면을 정면을 향해 90도 세우면 y축으로만 중력이 작용하므로 --> x=z=0, y=9.8

      (상측 변을 기준으로 화면을 뒤로 향하도록 90도 세우면 x=z=0, y=-9.8)

 

 

※ 센서 사용법 (: 가속도 센서)

  아래는 가속도 센서를 사용하기 위한 코딩입니다. 설명을 덧붙였으니까 천천히 살펴보세요~

  (코드가 눈에 익지 않다면 생소해 보일 수도 있지만, 딱히 어려운 내용은 없습니다)

 

class MainActivity : AppCompatActivity(), SensorEventListener {     // ① 센서 이벤트 리스너 상속

 

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

    }

 

    // ② 센서관리자 객체 얻기

    private val sensorManager1 by lazy {           // 지연된 초기화는 딱 한 번 실행됨

        getSystemService(Context.SENSOR_SERVICE) as SensorManager

    }

 

    // ③ 리스너 등록

    override fun onResume() {              // 앱이 사용될 때에만 동작 (배터리 절약)

        super.onResume()

 

        sensorManager1.registerListener(

            this,                    // 센서 이벤트 값을 받을 리스너 (현재의 액티비티에서 받음)

            sensorManager1.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),     // 센서 종류

            SensorManager.SENSOR_DELAY_NORMAL                             // 수신 빈도

        )

    }

 

    // ④ 필요한 메서드들 재정의

    //   안드로이드 스튜디오에서 추상메서드를 구현하라는 오류 안내를 이용하면 코딩이 편리. (아래 그림 참조)

    //   (구현할 메서드들을 ctrl+A로 전체선택하자)

 

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {    // 센서 정밀도 변경시

    }

 

    override fun onSensorChanged(event: SensorEvent?) {               // 센서 값 변경시

        event?.let {

            Log.d("MainActivity", " x:${event.values[0]}, y:${event.values[1]}, z:${event.values[2]} ")

            // [0] x축값, [1] y축값, [2] z축값

        }

    }

 

    // ⑤ 리스너 해제

    override fun onPause() {                 

        super.onPause()

        sensorManager1.unregisterListener(this)

    }

}

 

참고) 위에서 Log 클래스의 메소드는 다음과 같은 형태로 사용합니다.

 

   Log.d([태그], [출력할 메시지])    // 태그 : 로그캣에는 많은 내용이 표시되므로 필터링할 때 사용함

 

   기타, ...

   Log.e() 에러메시지를 출력할 때 자주 사용함

   Log.w() 경고를 표시할 때 자주 사용함

   Log.i()  정보성 로그를 표시할 때 자주 사용함

   Log.v() 모든 로그를 표시할 때 자주 사용함

 

 

실습

일단 센서만 동작시켜 봅시다.

 

1) 빈 액티비티를 생성 (앱 이름 : TiltSensor)

     잘 모르면 이전의 앱 제작 과정을 참고하세요~

 

2) 레이아웃 작업과 코딩

   여기서는 커스텀 뷰를 제작할 때, 레이아웃 작업을 디자인 창에서 하지 않고 그냥 코딩으로 했습니다.

   코딩으로 이해하는 게 훨씬 쉽기 때문입니다.

class MainActivity : AppCompatActivity(), SensorEventListener {

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

    private val sensorManager1 by lazy {           // 지연된 초기화는 딱 한 번 실행됨
        getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }

    override fun onResume() {
        super.onResume()

        sensorManager1.registerListener(
            this,
            sensorManager1.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
            SensorManager.SENSOR_DELAY_NORMAL
        )
    }

    override fun onPause() {
        super.onPause()
        sensorManager1.unregisterListener(this)
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }

    override fun onSensorChanged(event: SensorEvent?) {
        event?.let {
            Log.d("MainActivity", " x:${event.values[0]}, y:${event.values[1]}, z:${event.values[2]} ")
            // [0] x축값, [1] y축값, [2] z축값
            Log.d(TAG, "onSensorChanged: " +
                    "x : ${event.values[0]}, y : ${event.values[1]}, z : ${event.values[2]}" )
            tiltView.onSensorEvent(event)
        }
    }
}

 

3) 실행 테스트

  여기까지 작업했다면 일단 센서가 올바르게 동작하는지 테스트 해 볼 수 있습니다.

 

 앱을 실행하고 안드로이드 스튜디오 하단의 'Logcat'탭을 클릭한 후,

  ① 연결된 기기, ② 실행중인 프로세스명 , ③ 필터링 태그(MainActivity)를 설정하면

  우리가 작성한 로그만 볼 수 있습니다. x, y, z 축의 가속도 값들이 표시되고 있네요.

 

 

※ 커스텀 뷰 제작 및 사용법

  일단 우리가 만들려는 수평 측정기는 시중에서 판매되는 아래 상품을 모델로 하고 있습니다.

 [시중에서 판매되고 있는 수평 측정기들]

위 수평 측정기들 중 원 모양의 측정기를 화면에 그려 사용합시다.

 

3) 커스텀 뷰 제작을 위한 코딩

   이제 사용자 정의 화면(커스텀 뷰)에 수평 측정기의 원 모양을 화면에 그려서 메인 액티비티에 표시해 보죠.

   그리고 그 화면에 센서 값을 전달하고 값에 따라 원의 위치가 새로 그려지도록 해야 합니다.

 

   일단 커스텀 뷰를 위한 클래스 파일을 추가로 생성해야 합니다.

   우리가 만들 커스텀 뷰의 이름은 TiltView 라고 합시다.

   

  ① File > New > Kotlin File / Class 를 이용하여 빈 클래스 파일을 추가 하세요.

        Name: TiltView

        종류: Class

 

  ② View 상속받기

   'class TileView'에 이어 ': View'를 입력하고 나타나는 자동완성기능에서 View(android.view)를 선택

   빨간 줄이 표시되는 View에 커서를 놓고 'Alt+Enter'를 눌러 제안 목록 중, Context를 인자로 받는 생성자 선택

 

  ③ 추가된 빈 클래스 파일을 열어 아래와 같이 코딩 하세요. 



class TiltView(context: Context?) : View(Context?) {       // View 상속 (View라고 입력하면 여러 생성자가 나옴)
                                        // 그 중에 Context를 인자로 하는 생성자를 선택

    private val paintGreen: Paint = Paint()         // (페인트는 붓에 비유됨)
    private val paintBlack: Paint = Paint()

    private var cX: Float = 0f    // 센터좌표 x
    private var cY: Float = 0f

    init {
            paintGreen.color = Color.GREEN 
            paintBlack.style = Paint.Style.STROKE    // STROKE (외곽선만 그림)
                                                    // FILL | FILL_AND_STROKE | STROKE
    }                                               //  디폴트: FILL

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {   // 뷰의 크기를 얻기 위함
                                                     // 안드로이드 좌표계는 좌측 상단이 (0, 0)
                                                     // w(변경된폭), h, oldw, oldh(변경전높이)
        super.onSizeChanged(w, h, oldw, oldh)

        cX = w / 2f              // 센터좌표 x 산출
        cY = h / 2f
    }

    override fun onDraw(Canvas: Canvas?) {      // (캔버스는 도화지에 비유됨)
        super.onDraw(canvas)

        canvas?.drawCircle(cX-xx, cY-yy, 100f, paintGreen)  // ⓑ원내부. (테두리와 같은 크기의 꽉찬 원)
        canvas?.drawCircle(cX, cY, 100f, paintBlack)        // ⓐ테두리.  x(float), y(float), 반지름(float), 색(Paint!)
        canvas?.drawLine(cX-20, cY, cX+20, cY, paintBlack)  // ⓒ중앙의 십자 수평선   x1,y1,x2,y2,색
        canvas?.drawLine(cX, cY-20, cX, cY+20, paintBlack)  // ⓒ중앙의 십자 수직선
    }

    // 센서 값은 SensorEvent로 전달됨. 이 값을 받아오는 onSensorEvent()를 정의하자.
    private var xx: Float = 0f
    private var yy: Float = 0f

    fun onSensorEvent(event: SensorEvent) {
        // x값: values[0], y값: values[1] 이지만, 화면을 가로방향으로 바꿔 사용하므로 서로 뒤바꿔서 사용
        // 눈에 띄일 정도의 화면 좌표로 사용하기 위해 스케일을 약 20배
        yy = event.values[0] * 20
        xx = event.values[1] * 20
        invalidate()            // 뷰를 다시 그리도록 onDraw()를 다시 호출해 주는 역할
    }
}

 

③ MainActivity의 코드를 아래와 같이 수정하여,
    TiltView를 MainActivity에 등록하고 화면 제어 설정을 하세요.

import android.content.ContentValues.TAG

class MainActivity : AppCompatActivity(), SensorEventListener {
    ...
    
    private lateinit var tiltView: TiltView      // 늦은 초기화
    
    // onCreate() 기존 코딩을 아래와 같이 수정
    override fun onCreate(...) {
        ...
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE  // 화면 방향: 가로
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)   // 화면 꺼짐 방지
        
        tiltView = TiltView(this)        // 커스텀 뷰 배치
        setContentView(tiltView)         // R.layout.activity_main 을 tiltView 로 바꿀 것
                                         //   (이로써 tiltView가 전체 레이아웃이 됨)
    }

    override fun onSensorChanged(event: SensorEvent?) {
        // values[0]:x, values[1]:y, values[2]:z  z값은 불필요
        event?.let {
            Log.d(TAG, "onSensorChanged: " +
                "x : ${event.values[0]}, y : ${event.values[1]}, z : ${event.values[2]}" )
            tiltView.onSensorEvent(event)   // TiltView에 센서 값을 전달
        }
    }
}

 

 

실행한 후, 모바일 기기를 이리저리 기울여 보십시오. 그러면 가운데 동그라미가 움직이며 수평 상태를 표시해 줍니다.

[ 실행 결과 ]

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

나 만의 웹 브라우저

 

기능 소개

웹 서핑용으로 사용할 수 있고 나만의 몇 몇 메뉴와 컨텍스트 메뉴를 가지고 있습니다.

 

핵심이 되는 주제

웹 뷰 (인터넷 사용 권한 필요)

메뉴 구성

컨텍스트 메뉴 구성

암시적 인텐트 사용

 

실습

1) 빈 액티비티를 생성 (앱 이름 : MyWeb) & Anko 라이브러리 추가

     잘 모르면 이전의 앱 제작 과정을 참고하세요~

 

2) 레이아웃 작업

 

① Plain Text 뷰 배치 (id: URLEditText , inputType: textUri, hint: http://, imeOptions: actionSearch)
  (입력 자료형을 textUri로 선택하면 입력할때 소프트키보드도 URL 입력에 편리한 자판 배열로 표시됩니다) 
  (또 hint를 설정하면 사용자에게 웹주소를 입력해야 함을 알려줄 수 있죠)
  actionSearch : 소프트키보드의 서치아이콘(돋보기 처럼 생긴...) 활성화 시켜 줍니다.

② webView 뷰 배치 (id: webView)
  인터넷 권한 추가 (앱 설치시 사용자에게 권한 허용을 요청하게 됨)
 

  -- AndroidManifest.xml

<manifest xmlns:...>
    ...
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifestxmlns>

 

③ 코딩 (웹뷰를 위한)
  -- MainActivity.kt

class MainActivity : AppCompatActivity() {

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

       webView.apply {
          settings.javaScriptEnabled = true      // 자바스크립트 사용 설정
          webViewClient = WebViewClient()     // webViewClient 객체를 생성하여 전달
                                                  // 이 행이 없으면 폰 자체 웹 브라우저가 동작함
       }
       webView.loadUrl("http://www.google.com")  // 구글페이지 로드

       URLEditText.setOnEditorActionListener { _, actionId, _ ->  // 자동완성 기능으로 보면 v, actionId, event
                                                      // (반응한 뷰, 액션ID, 이벤트)
                                                      // 세 개의 인수를 사용하지만 여기선 actionId만 사용
                                                      // (사용하지 않는 인자는 _로 작성)
                                                      // 이 리스너는 editText에 글자가 입력될 때마다 호출됨
         if (actionId == EditorInfo.IME_ACTION_SEARCH) {    // 검색버튼이 눌렸는가?
            webView.loadUrl(URLEditText.text.toString())
            true                                            // true를 반환하며 이벤트 종료
         } else {
            false
         }
            
     }

  override fun onBackPressed() {    // 액티비티에서 뒤로가기 키 이벤트 onBackPressed() 재정의(오버라이드)
     if (webView.canGoBack()) { 
	       webView.goBack()           // 이전페이지로 갈 수 있으면 이전페이지로 이동하고,
     } else {
	       super.onBackPressed()       // 그렇지 않다면 본래의 동작을 수행(즉, 종료)함
     }
  }
     
}

 

일단 이쯤에서 실행하면 기본적인 웹 서핑이 가능합니다.

 

 

이제 옵션 메뉴와 컨텍스트 메뉴를 만들어 볼까요? 일단 메뉴는 별도의 디렉토리로 관리합니다.

  File > New > Android Resource Directory 로 리소스 디렉토리 생성 (Resource type : menu)

 

 

④ 옵션 메뉴 (앱 우측 상단에 …으로 표시되는 메뉴)

  i) 메뉴용 리소스 준비
    생성된 Android > app > res > menu 마우스 우클릭 > Menu resource file (File name: main)

      이렇게 하면 main.xml이 생성됩니다.


    메뉴에 사용할 벡터 이미지 준비 (여기서는 홈 버튼에 사용할 이미지를 준비해야 하는데,

      Vector Asset의 Clip Art의 검색 창에서 home으로 검색하면 곧바로 찾아줍니다~)

  ii) 메뉴 구성하기
    팔레트 창에서 'Menu Item'뷰를 끌어다 컴포넌트트리 창 menu 하위에 배치 (title: 검색사이트)
      검색사이트 메뉴 아이템 하위에 'Menu'뷰를 끌어다 배치
        Menu 뷰 하위에 세 개의 'Menu Itemp'뷰를 끌어다 배치
           (id: action_daum, title: 다음)
           (id: action_google, title: 구글)
           (id: action_naver, title: 네이버)


같은 방법으로 검색사이트 레벨의 메뉴 아이템과 그 하위에 세 개의 메뉴 아이템들을 갖는 메뉴를
      하나 더 생성 (메뉴아이템title: 개발자 정보) 하세요.
        (id: action_call, title: 전화하기)
        (id: action_send_text: 문자보내기)
        (id: action_email: 이메일보내기)

검색사이트 레벨의 메뉴 아이템을 하나 더 추가 하세요.
        (id: action_home, title: Home, icon: 위에서 만든 집 모양 벡터이미지)
        툴바 밖으로 이 메뉴 아이템을 노출시키기 위해 showAsAction을 ifRoom으로 설정해 줍니다.
        (아래 그림에서 보면 집 모양 아이콘이 메뉴에서 빠져나와 앱의 툴바 영역에 나타났음)
         
    ※ 참고

       never

       ifRoom(툴바에 여유가 있을때만)

       withText(글자와 아이콘을 함께 표시)
       collapseActionView (액션 뷰와 결합하면 축소되는 메뉴 생성 가능)

 

[ 최종적인 메뉴 모양 ]

 

  iii) 코딩 (메뉴를 위한)

   -- MainActivity.kt

class MainActivity : AppCompatActivity() {
     ...

     // 액티비티에서 onCreateOptionsMenu()를 오버라이드하여 메뉴 리소스 파일을 지정하면 메뉴가 표시됨
     override fun onCreateOptionsMenu() : Boolean { 
        menuInflater.inflate(R.menu.main, menu)       // MainActivity의 메뉴로 등록 (* inflate 바람넣다. 부풀리다(과장하다))
        return true                          // 반드시 true를 반환하여 액티비티가 있음을 인식시켜줘야 함
     }
     
     // 각 옵션 메뉴 클릭 이벤트 처리기(이벤트 핸들러)
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item?.itemId) {
            R.id.action_daum -> {
                webView.loadUrl("http://www.daum.net")
                return true        // 처리를 끝낸후엔 정상종료 됐음을 알리기 위해 true를 반환
                                 //  (그리고 안드로이드에서는 자신의 작업을 하는 경우를 제외한 모든 경우에
                                 //    super 메소드를 호출하는 것이 기본규칙임
            }
            R.id.action_google, R.id.action_home -> {          // 구글 메뉴 또는 홈버튼
                webView.loadUrl("http://www.google.com")
                return true
            }
            R.id.action_naver -> {
                webView.loadUrl("http://www.naver.com")
                return true
            }
            R.id.action_call -> {
                // 전화 걸기 코드 - '암시적 인텐트'를 사용 (예제 끝 부분에 별도로 설명)
                val intent = Intent(Intent.ACTION_DIAL)
                intent.data = Uri.parse("tel:02-1234-5678")
                if (intent.resolveActivity(packageManager) != null) {
                    startActivity(intent)
                }
                return true
            }
            R.id.action_send_text -> {
                // 메시지 발송 코드 (*k 마지막에 별도 정리)
                return true
            }
            R.id.action_email -> {
                // 이메일 발송 코드 (*k 마지막에 별도 정리)
                return true
            }
        }
        return super.onOptionsItemSelected(item)
     }
  }

 

⑤ 컨텍스트 메뉴 (특정 뷰를 길게 누르고 있을 때 나타나는 메뉴)
  

   이번에는 컨텍스트 메뉴를 추가해 봅시다~


  i) 컨텍스트 메뉴용 리소스 준비
    Android > app > res > menu 마우스 우클릭 > Menu resource file (File name: context)     --> context.xml이 생성됩니다. 

  ii) 메뉴 구성하기
    팔레트 창에서 'Menu Item'뷰 2개를 끌어다 컴포넌트트리 창 menu 하위에 배치 (title: 검색사이트, 기본 브라우저에서 열기)

[ 컨텍스트 메뉴 - 실행하면 길게 누른 지점에 메뉴가 표시됩니다 ]

 

  iii) 코딩 (컨텍스트 메뉴를 위한)

   -- MainActivity.kt

// *1, *2 순으로 작업
   class MainActivity : AppCompatActivity() {
      override fun onCreate(savedInstanceState: Bundle?) {
         ... 
         registerForContextMenu(webView)            // *2
      }

      override fun onCreateContextMenu(             // *1
         menu: ContextMenu?,
         v: View?,
         menuInfo: ContextMenu.ContextMenuInfo?
      ) {
         super.onCreateContextMenu(menu, v, menuInfo)
         menuInflater.inflate(R.menu.context, menu)        // MainActivity의 컨텍스트 메뉴로 등록
      }

      // 컨텍스트 메뉴 클릭 이벤트 처리
      override fun onContextItemSelected(item: MenuItem): Boolean {
         when (item?.itemId) {
             R.id.action_share -> {
                // 페이지 공유 코드 (*k 마지막에 별도 정리)
                return true
             }
             R.id.action_browser -> {
                // 기본 웹 브라우저에서 열기 코드 (*k 마지막에 별도 정리)
                return true
             }
         }
         return super.onContextItemSelected(item)
      }
   }

 

※ 암시적 인텐트
미리 정의된 인텐트들을 말합니다.

사용 예들) 출처: https://developer.android.com/guide/components/intents-common 에서 더 많은 예를 볼 수 있어요.
그런데, Anko 라이브러리를 사용하면 아래의 사용 예들 보다 훨씬 간편하게 코딩할 수 있죠 (거의 한 줄로 처리 가능!)

(일단 암시적 인텐트 사용 예들을 볼까요)

// 전화 걸기
val intent = Intent(Intent.ACTION_DIAL)                // Intent 클래스에 정의된 액션 중 전화거는 액션을 선택한 것임
intent.data = Uri.parse("tel:02-1234-5678")             // "tel:"로 시작하는 Uri는 전화번호를 나타내는 국제표준임
if (intent.resolveActivity(packageManager) != null) {   // intent.resolveActivity()는 인텐트를 수행하는 액티비티가 있는지를 검사
                                                   // 전화 앱이 없는 태블릿 같은 기기에서는 null값을 반환함
  startActivity(intent)
}

// 문자열 보내기
val intent = Intent(Intent.ACTION_SEND)
intent.apply {
   type = "text/plain"
   putExtra(Intent.EXTRA_TEXT, "보낼 문자열")
   var chooser = Intent.createChooser(intent, null)
   if (intent.resolveActivity(packageManager) != null) {
      startActivity(chooser)
   }
}

// 웹 브라우저 띄우기
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("http://www.google.com")
if (intent.resolveActivity(packageManager) != null) {
   startActivity(intent)
}

...
...

 

(이번에는 Anko 라이브러리를 사용한 예 입니다)

Anko라이브러리를 사용한 예)
makeCall(전화번호)             // 전화 걸기
sendSms(전화번호, [문자열])   // 문자 보내기
browse(url)                     // 웹 브라우저에서 열기
share(문자열, [제목])           // 문자열 공유
email(받는메일주소, [제목], [내용])   // 이메일 보내기

 

와! 놀랍도록 간단하군요!!

 

※ 상기 본 프로그램 코드에서 생략했던 코드들을 아래에 정리 했습니다. 채워 넣으세요!

   주의) 특별한 경우가 아니라면 전화걸기에 암시적 인텐트를 사용하지 않을 것을 권장합니다.
         별도의 권한을 필요로 하고 전화번호 입력까지만 제공하면 사용자 의지대로 전화를 걸면 되기 때문이에요.

// 메시지 발송 코드
sendSMS("02-1234-5678", webView.url + "에 들어가봐!")

// 이메일 발송 코드
email("test@daum.net", "이 쇼핑몰이 젤 좋아!", webView.url)

// 페이지 공유 코드
share(webView.url)

// 기본 웹 브라우저에서 열기 코드
browse(webView.url)

  
실행결과) '메뉴>검색사이트>다음'을 눌러본 상태입니다.

잘 동작하는 군요!  수고 하셨습니다~!

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

스톱워치(초시계)

 

기능 소개

타이머 시작, 일시정지, 초기화, 랩타임 표시등의 기능을 갖는 초시계를 제작해 봅니다.

 

핵심이 되는 주제

timer

백그라운드 스레드

runOnUiThread

ScrollView

FloatingActionButton

 

 

실습

1) 빈 액티비티를 생성 (앱 이름 : StopWatch) 

     벡터 이미지를 사용할 것이므로 vectorDrawables.useSupportLibrary = true 설정

      (모듈수준 bundle.gradle의 defaultConfig{} 에 작성하고, 'New Sync' 클릭해 주는 거 있지 않으셨죠?^^ )

 

2) 레이아웃 작업

 

  ① 두 개의 TextView (초, 백분의 1초 시간 표시용)를 배치합니다.

       (id : 각각 secTextView, milliTextView)

 

      참고) 뷰의 컨텍스트 메뉴에서 기준라인을 보이게 함으로써 두 뷰의 글자 하단을 정렬할 수 있어요.

       

[ 텍스트 기준선 정렬 ]

 

② 세 개의 벡터이미지 준비해 주세요.

     play arrow(시작), pause(일시정지), refresh(초기화)

     

[ 세 개의 벡터 이미지 ]

 

③ 2개의 FloatingActionButton(FAB)을 배치해야 하는데요.

   - FAB는 벡터 이미지로 깔끔한 버튼을 만들기에 적합한 이미지합성 버튼입니다.

   - 구글 머티리얼 디자인에 자주 사용됩니다.
   - 컴포넌트 팔레트 창에서 처음 이 컴포넌트를 추가하려고 하면 네트워크 상에서 다운로드 받도록 되어 있습니다.

      (다운로드 받으세요)

   - 다운로드가 끝났으면 FAB를 드래그앤드롭으로 가져다 놓고,

     벡터 이미지(시작, 초기화 이미지)를 선택하고 id와 색상을 적당히 설정하세요.

     총 2개의 버튼을 만들면 됩니다.

      참고) 시작 버튼 id: playFab, 초기화 버튼 id: resetFab

             

[ 두 개의 FAB ]

  ④ 1개의 Button (랩 타임용)을 배치하세요 - id: labButton

  ⑤ 중앙에 ScrollView (랩 타임 표시용)를 배치해 주세요.
     수직으로 차곡차곡 쌓이는 레이아웃을 LinearLayout이라고 합니다.
     (컴포넌트 트리창을 보면 ScrollView 하위에 LinearLayout이 들어있을 거예요. 
      ScrollView 뷰는 자식 뷰룰 하나만 갖는 특수한 뷰이고,
      LinearLayout 뷰는 여러 개의 자식 뷰를 갖습니다)
      LinearLayout 안에 동적으로 타임랩 값들을 새로운 형태로 쌓이도록 만들것입니다.

     컴포넌트 트리 창에서 LinearLayout 뷰를 선택하고 id를 입력해 주세요. ( 예: lapLayout )

[ 최종 레이아웃 ]

  

 

 

3) 코딩

 

timer (타이머)

    안드로이드의 두 가지 스레드 모드가 있습니다.

       ① 메인스레드 (일반적인 UI 가능)

       워커스레드 (작업 시간이 오래 걸리고 화면에 표시되지 않음. 당연히 UI 조작을 불가)

 

  타이머 코딩 형식

    timer (period = 1000) {        // 1000 = 1

        // 워커스레드 (UI조작 불가)

        runOnUiThread {

            // 메인스레드 (UI조작 가능)

        }

    }

 

 

- MainActivity.kt

 

package com.tistory.stopwatch

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
import kotlin.concurrent.timer

class MainActivity : AppCompatActivity() {


    private var time = 0
    private var timerTask: Timer? = null      // null을 허용
    private var isRunning = false
    private var lap = 1

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

        playFab.setOnClickListener {
            isRunning = !isRunning

            if (isRunning) { start() } else { pause() }
        }

        labButton.setOnClickListener {
            recordLapTime()
        }

        resetFab.setOnClickListener {
            reset()
        }
    }

    private fun start() {
        playFab.setImageResource(R.drawable.ic_pause_black_24dp)   // 일시정지 이미지

        timerTask = timer(period=10) {       // 타이머 인터벌 10 ms
            time++

            val sec = time / 100
            val milli = time % 100

            runOnUiThread {                   // UI 조작이 가능한 블럭
                secTextView.text = "$sec"
                milliTextView.text = "$milli"
            }
        }
    }

    private fun pause() {
        playFab.setImageResource(R.drawable.ic_play_arrow_black_24dp)   // 시작 이미지

        timerTask?.cancel()                  // 실행중인 타이머 취소
    }

    private fun recordLapTime() {
        if (!isRunning) return         // 타이머가 실행 중이 아니라면 리턴
        val lapTime = this.time
        val textView = TextView(this)      // 동적으로 TextView 생성
        textView.text = "$lap LAB : ${lapTime / 100}. ${lapTime % 100}"

        lapLayout.addView(textView, 0)     // 0 : 맨 위쪽에 추가
        lap++
    }

    private fun reset() {
        timerTask?.cancel()       // 실행중인 타이머 취소

        // 모든 변수 초기화
        time = 0
        isRunning = false
        playFab.setImageResource(R.drawable.ic_play_arrow_black_24dp)
        secTextView.text = "0"
        milliTextView.text = "0"

        // 모든 랩타임 기록 삭제
        lapLayout.removeAllViews()
        lap = 1
    }

}

 

간단하지만 실행해보면 훌륭하게 동작하는 타이머가 완성됐습니다!

이번 프로그램은 비교적 간단했네요. 

 

다음에는 자신만의 웹 브라우저를 제작해 봅시다~

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

비만도 계산기

 

기능 소개

키와 몸무게를 입력하고 결과보기 버튼을 누르면 새 화면에서 비만도를 문자와 그림으로 표시합니다.

부수 기능으로 마지막으로 입력한 키와 몸무게 자료를 저장합니다.

 

핵심이 되는 주제

입력 및 출력 두 개 화면('액티비티')을 사용합니다.

두 화면 사이에 데이터 전달은 인텐트라는 화면 전환을 사용합니다.

SharedPreference로 자료를 저장합니다.

 

기타 : Anko 라이브러리 사용 방법 ( 라이브러리 의존성 추가)

        벡터 이미지 사용 방법

실습

1) 빈 액티비티를 갖는 프로젝트 생성 (앱 이름 : BMICalculator) 

2) 코드 작성을 쉽게 해주는 Anko 라이브러리 추가 
    
참고: Anko 라이브러리의 구성 
 ① Anko Commons (인텐트, 다이얼로그, 로그)  <-- 우리가 사용할 모듈  
 ② Anko Layouts (레이아웃) 
 ③ Anko SQLite (SQLite) 
 ④ Anko Coroutines (코루틴) 

프로젝트 탐색기 창에서 모듈 수준의 그레이들 파일인 build.gradle 파일을 더블 클릭하여 편집.

 (파일명 뒤에 Module 수준과 Project 수준의 구분이 표시되어 있음)

dependencies 항목에 다음의 Anko 라이브러리를 추가.

 ('라이브러리 의존성 추가'라고 하며 안드로이드 스튜디오에서 자동 다운로드 설치를 하게 됨)

   implementation "org.jetbrains.anko:anko:$anko_version"

[ Anko 라이브러리 추가 ]

 

프로젝트 탐색기 창에서 프로젝트 수준의 그레이들 파일인 build.project 파일을 더블 클릭하여

Anko 라이브러리 버전을 변수로 지정.

 

buildscript {

    ext.kotlin_version = '1.3.50'

    ext.anko_version='0.10.5'

    repositories {

        google()

        jcenter()

       

    }

    dependencies {

    ....

    }

}

 

추가한 그레이들 파일들을 적용하기 위해 에디트창 탭 이름 바로 아래에 'Sync Now'를 클릭해서

프로젝트를 재 빌드.

 

 

3) 레이아웃 작업 1 - , 몸무게 입력 화면

 

'Plain Text' 배치

  ID : heightEditText

  input Type : (숫자만 입력할 있도록) number

  hint : (입력 전에 표시할 문자열) (cm)  --> 문자열이 보이도록 text 란의 Name 삭제

 

Plain Text 뷰도 되지만, 숫자만 입력할 거니까 이번에는 'Number' 뷰를 배치

  ID : weightEditText

 

'Button' 배치

  ID : resultButton

[ 입력 화면 ]

 

 

4) 레이아웃 작업 2 - 결과 화면

에디터 창을 디자인이 아닌 텍스트 모드로 전환한 후, 결과를 출력할 새로운 액티비티 추가.
  File > New > Activity > Empty Activity
  (Activity Name : ResultActivity)

① activity_result.xml 창에 TextView 뷰 배치
   ID : resultTextView
   textAppearance : appCompat.Large

[ 결과 화면 ]

② 이미지 뷰 배치
  
  참고) 안드로이드 스튜디오에서 제공하는 이미지 파일들
    비트맵 이미지: PNG, JPG
    벡터 이미지 : SVG, EPS

   벡터 이미지를 사용해 보자. (에셋 스튜디오에서 생성할 있음)
      프로젝트 창 > res 폴더 우클릭 > New > Vector Asset에서 벡터 이미지 생성
      Clip Art: 에서 원하는 이미지 선택
   같은 방법으로 비만도 정상, 비만, 저체중 세 가지 이미지를 추가.

   
activity_result.xml 화면에서 ImageView 뷰를 배치
   project 항목에서 '정상'용 이미지를 선택하고 OK   (: 스마일 아이콘)
   ImageView 속성 : 사이즈 조정 (: 가로(layout_width), 세로(layout_height : 각각 100 dp),

                         색상(tint) 조정
   
※ 벡터 이미지는 vectorDrawable 리소스로 분류하는데,

   백터드로어블은 Android 5.0부터 동작함.
   우리는 4.4버전으로 생성했으므로 모듈 수준의 build.gradle 파일에

  다음을 추가해 이를 지원케 해야 함. (Sync Now 잊지말고!)

    defaultConfig {
       vectorDrawables.useSupportLibrary = true
    }

[ 결과 화면 ]

 


5) 뒤로가기 기능 추가

 

결과 화면에서 다시 입력화면으로 돌아가기 위한, '뒤로가기' 기능을 상단에 추가해 보죠.

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tistory.bmicalcurator">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".ResultActivity" android:parentActivityName=".MainActivity"></activity>
    </application>

</manifest>

앱을 실행해 보면, 결과화면 상단에 뒤로가기 링크가 표시되고, 터치하면 입력 화면으로 되돌아 갑니다.

[ 뒤로가기 링크 ]

 

 


6) 코딩

간단한 데이터와 함께 다른 화면(액티비티) 구동해 주는 인텐트를 이용합니다.

 

  

 

<< 자료 입력 화면 MainActivity >>

-- MainActivity.kt (코틀린 일반 코딩)

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.content.Intent     // 자동으로 임포트됨
import kotlinx.android.synthetic.main.activity_main.*  // 레이아웃 정보가 자동으로 임포트되어 있음
                                // 이 덕택에 앞에서 추가했던 텍스트뷰나 버튼 사용이 가능한 것임
                                // 자동 임포트는 'kotlin-android-extensions' 플러그인에 의한 기능임

class MainActivity : AppCompatActivity() { 

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

        loadData()

        resultButton.setOnClickListener { 
            saveData(heightEditText.text.toString().toInt(), weightEditText.text.toString().toInt())
                    
            val intt = Intent(this, ResultActivity::class.java) 
            startActivity(intt) 
        } 
    } 
    
    // SharedPreference로 자료 저장
    private fun saveData(h: Int, w: Int) {
        val pref = PreferenceManager.getDefaultSharedPreferences(this)   // 프리퍼런스 객체 생성
        val editor = pref.edit()         // 에디터 객체 얻어오기
                                     // 에디터 객체는 프리퍼런스 객체에 데이터를 넣어주는 역할을 함

        editor.putInt("HEIGHT",  h)            // 키-값 쌍으로 자료 저장
             .putInt("WEIGHT", w)
             .apply()                        // 적용
    }

    // 자료 읽어 오기
    private fun loadData() {
        val pref = PreferenceManager.getDefaultSharedPreferences(this)   // 프리퍼런스 객체 생성
        val h = pref.getInt("HEIGHT", 0)    // 0은 값이 없을 때의 디폴트 설정 값
        val w = pref.getInt("WEIGHT", 0)

        if (h !=0 && w!=0) {
            heightEditText.setText(h.toString())
            weightEditText.setText(w.toString())
        }
    }

} 

 

 

-- MainActivity.kt (Anko 코딩)

 

우리는 Anko 라이브러리를 추가해서 빌드하고 있는데, Anko 라이브러리를 경우 버튼 리스너

다음과 같이 간결하게 적어도 됩니. (, anko 라이브러리를 임포트해 줘야 )

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.startActivity   // 추가할 것

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        
        resultButton.setOnClickListener {
            saveData(heightEditText.text.toString().toInt(), weightEditText.text.toString().toInt())

            // Anko 코딩된 부분
            startActivity<ResultActivity>(
                "weight" to weightEditText.text.toString(),
                "height" to heightEditText.text.toString()
            ) 
        }
    }

    ...
}

 

 

<< 결과 화면 ResultActivity >>

 

-- ResultActivity.kt onCreate()

 

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_result.*

class ResultActivity : AppCompatActivity() {

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

        val h = intent.getStringExtra("height").toInt()      // 인텐트에서 자료 빼오기
        val w = intent.getStringExtra("weight").toInt()
        val bmi = w / Math.pow(h / 100.0, 2.0)

        when {
            bmi >= 35 -> resultTextView.text = "고도 비만"
            bmi >= 30 -> resultTextView.text = "2단계 비만"
            bmi >= 25 -> resultTextView.text = "1단계 비만"
            bmi >= 23 -> resultTextView.text = "과체중"
            bmi >= 18.5 -> resultTextView.text = "정상"
            else -> resultTextView.text = "저체중"
        }

        when {
            bmi >= 23 -> imageView.setImageResource(R.drawable.ic_sentiment_very_dissatisfied_black_24dp)
            bmi >= 18.5 -> imageView.setImageResource(R.drawable.ic_sentiment_satisfied_black_24dp)
            else -> imageView.setImageResource(R.drawable.ic_sentiment_dissatisfied_black_24dp)
        }

        Toast.makeText(this, "BMI : $bmi", Toast.LENGTH_SHORT).show()  // Toast 메시지로 BMI 값 출력
                                                                    // Anko로는 toast("BMI : $bmi")
}

 

 

실행 결과

  [ 입력 화면 ]
[ 결과 화면 ]

 

 

Anko 라이브러리를 추가하며 실습해 봤던, 깃허브를 이용한 '라이브러리 의존성 추가'는 참 편리하고 효과적인 것 같습니다.

 

이 번 실습을 두 세 차례 차근 차근 따라해 보면, 흐름이 파악되실 거예요. 이렇게해서 첫 번째 안드로이드 앱을 만들어 봤습니다. 수고 하셨습니다~

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

[코틀린의 고급 문법 2]

고급 문법 마지막 시간입니다, 힘내자구요~~ 화이팅!

 

람다식

람다식이란 함수를 간결하게 표시한 형태입니다. 익명 클래스나 익명 함수를 간결하게 표현할 수 있어 편리합니다.

코드가 간결해져서 좋긴하지만, 남발할 경우 가독성이 떨어져 디버깅이 힘들어 질 수도 있습니다.

아래 세 가지 표현은 모두 같은 의미입니다. (코딩 간결화의 변천 과정이라고 생각해도 됨)

 

① 일반 함수
fun add(x: Int, y: Int) : Int { return  x + y }

② 문법적으로 허용된 생략을 한 일반 함수
fun add(x: Int, y: Int) = x + y

③ 람다식
var add = {x: Int, y: Int -> x + y }      // { } 부분이 람다식임.  '{ 인수들 -> 함수본문 }' 형태임

사용 예)
println(add(3+4))        // 결과: 7

 

 

 

SAM 변환

JAVA에서 메소드가 하나뿐인 인터페이스는 인터페이스를 구현하는 대신 함수로 구현할 수 있는데,
이것을 'SAM변환'이라고 합니다.  SAM (Simple Abstract Method ; 간단한 추상메소드)

코틀린 함수는 매개변수로 한 개의 추상메소드를 받는 형태를 취할 수 있는데,
이 때는 자바에서 작성된 인터페이스인 경우에 한해,
자바의 SAM변환과 유사하게 함수로 구현된 매개변수를 전달할 수 있습니다.

예를 들어 버튼의 클릭 이벤트를 구현할 때, View.OnClickListener 인터페이스를 구현하는데,
 (이 View.OnClickListener는 onClick() 이라는 추상메소드 한 개만을 가지고 있음)
이 인터페이스를 함수로, 더 나아가 익명메소드 형태로 구현해서 전달해 봅시다.

button.setOnClickListener(object : View.OnClickListener {    //OnClickListener는 OnClick메소드 하나만 포함
   override fun onClick(v: View?) {                        // 익명메소드 형태
       // 클릭시 처리 코드
   }
}
)

그리고 위 익명메소드는 람다식으로 표현할 수 있으므로,

button.setOnClickListener({ v: View? ->                // OnClickListener가 람다식화된 형태
      // 클릭시 처리 코드(람다 블럭)
})


코틀린에서 메소드 호출 시 제일 뒤의 매개변수가 람다식인 경우에는,
가독성을 위해 람다식을 ( ) 밖으로 빼서 작성할 수 있습니다.

button.setOnClickListener( ) { v: View? ->
     // 클릭시 처리 코드(람다 블럭)
}

그리고 람다식이 어떤 메소드의 유일한 매개변수인 경우에는 메소드의 ( ) 를 생략할 수 있습니다. 게다가 컴파일러가 자료형을 추론할 수 있다면 자료형을 생략할 수 있는 것이므로, 최종적으로 다음과 같이 되겠네요.

button.setOnClickListener{ v ->
     // 클릭시 처리 코드(람다 블럭)
}

※ 만약에 '클릭시 처리 코드'에서 v라는 매개변수를 사용하지 않는다면,
v를 _ 기호로 대치할 수도 있습니다.

button.setOnClickListener{ _ ->
       // 클릭시 처리 코드(람다 블럭)
}

※ 또 만약에 그리고 람다식에서 매개변수가 하나 뿐인 경우라면,
매개변수를 아예 생략할 수도 있습니다.

button.setOnClickListener{
    // 클릭시 처리코드(람다 블럭)
}

그리고 이 때 람다블럭내에서는 매개변수를 it로 접근할 수 있습니다.

예를들면,
button.setOnClickListener{
    it.visiblity = view.GONE             // 여기서 it는 View? 형 v를 의미함 ----- (K)
}


위에서 SAM변환을 이용한 익명메소드에서 람다식화, 그리고 코틀린의 편리한 생략 표현 기법들을 총동원해 코드를
간결화시켜봤고 코드들 모두 같은 의미입니다. 어쨌거나 가장 가독성이 뛰어난 형태는 (K)의 코드죠. 앞으로 종종 등장합니다.

 
주의해야 할 점은 코틀린에서의 SAM변환은 자바에서 작성한 인터페이스일 때에만 동작한다는 것~!.


 

확장 함수

코틀린에서는 .연산자를 이용해 이미 정의된 클래스에 메소드를 쉽게 추가할 수 있습니다.
(보통 Java나 C#에서는 final로 상속이 봉인되어 있어서 메소드를 추가하지 못하는 경우가 많은데 편리한 기능인 것 같네요)
(확장 함수 내부에서 해당 객체로의 접근은 this를 사용)

예) Int 클래스에 isEven()을 추가해 보자.
fun Int.isEven( ) = this % 2 == 0    // 'this % 2 == 0'의 결과인 부울 값이 함수의 결과 값이 됨

println(5.isEven())        // 결과: false


 

형 변환

val a = 10L

b = a.toInt()
c = a.toDouble()
d = a.toString()

e = Integer.parseInt(d)      // 문자열 형을 숫자로 변환


※ 일반 클래스 간 형 변환 (as 키워드)
open class Human()
class Man: Human()

val man = Man()
val human = man as Human     // Human 형으로 형 변환


형 체크

val st = "hello"
if (st is String) { println(st.toUpperCase() }



고차 함수

함수를 매개 변수로 전달하거나 함수 형으로 반환할 수 있습니다. 이렇게 사용되는 함수를 '고차 함수'하고 합니다.

fun add( x: Int, y: Int, callback: (sum: Int) -> Unit) {   // 두 개의 Int 매개변수와 한 개의 익명함수 매개변수
                                                 // 매개 변수로 쓰인 익명함수는 한 개의 Int 매개변수를 받고 리턴 값은 없음
    callback( x + y )
}

add(3,4, {println(it)})      // 결과: 7    함수를 { }로 감싸고, 이 함수 내부에서는 반환 값을 it로 접근한 예임



동반 객체

'팩토리 메소드'
코틀린에서는 클래스를 객체화 하는 것과는 별개로 메소드를 이용해 객체를 생성하는 코딩 패턴을 지원하는데, 이를 '팩토리 메소드'라고 합니다.
(나중에 다루게 될 프래그먼트 컴포넌트는 특수한 제약 때문에 팩토리 메소드로만 객체를 생성할 수 있음)

코틀린에서는 타 언어에서 정적인 메소드를 만들 때 사용하는 static 키워드 같은 게 없습니다. 그 대신 '동반 객체 (companion object)'라는 것을 통해 이를 구현합니다. (companio : 동반자, 동료, 친구)

다음은 newInstance() 정적 메소드를 사용해서 Fragment 객체를 생성하는 팩토리 패턴을 구현한 것입니다.
여기서 동반 객체 내부의 메소드는 Fragment 클래스와 아무 관계가 없는 정적인 존재입니다.

class Fragment {
   companion object {
      fun newInstance(): Fragment {    // shs: 함수의 반환 형이 Fragment 형이라... 
         println("생성됨")
      }
   }
}

val fragment = Fragment.newInstance()   // shs: 이거 정적 Fragment 안의 newInstance 멤버메소드를 액세스 하는 것과 비슷한데요...



<<< 코틀린 기본 라이브러리에서 유용한 함수들 >>>

let()

블럭에 자기 자신을 인수로 전달(it로 참조)하고 실행 결과를 반환합니다. 
'안전한 호출' 즉, str이 null이 아닐때만 호출 되도록 ?.연산자를 이용하면 더 좋습니다.

val result = str?.let {            //   결과는 Int형
    Integer.parseInt(it)
}
// fun <T, R> T.let(block: (T) -> R): R


with()

객체를 매개변수로 받고 블럭에 리시버 객체형으로 전달(this로 참조)해 준 후, 실행 결과를 반환합니다.
단, '안전한 호출'이 불가능하기 때문에 반드시 str이 null아닐 때에만 호출해야 합니다.

with(str) {
    println(toUpperCase())     // this.toUpperCase()에서 this.를 생략할 수도 있다.
}
// fun <T, R> with(receiver: T, block T.() -> R): R


apply()

블럭에 객체 자신이 리시버 객체형으로 전달되고 그 객체형으로 반환됩니다.
(주로 객체의 상태를 변경해서 반환할 때 사용함)

val result = car?.apply {
   car.setColor(Color.BLACK)
   car.setPrice(1000000)
}
// fun  T.apply(block: T.() -> Unit): T


run()

run() 함수는 익명 함수처럼 쓰는 법과 객체에서 호출하는 법 모두를 제공합니다.

i) 익명 함수처럼 쓸 때
블럭의 결과를 반환합니다.
블럭 안에 선언된 변수들은 모두 임시로 잠깐 사용하는 변수들인데, 임시 변수를 많이 사용하는 복잡한 계산에 유용하겠네요.

val avg = run{
   val kor = 90
   val eng = 80
   val math = 90

   (kor + eng + math) / 3.0f
}
// fun  run(block: () -> R): R


ii) 객체에서 호출 할 때
객체를 블럭의 리시버 객체로 전달하고 결과를 반환합니다.
안전한 호출이 가능하므로 with()보다 더 유용합니다~ 

str?.run {
   println(toUpperCase())
}
// fun <T, R> T.run(block: T.() -> R): R

 

 

 

 

 

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

[코틀린의 고급 문법 1]

 

특별한 상태 값 null

코틀린에서는, 모든 객체는 생성과 동시에 값을 갖도록 초기화 하는 것을 원칙으로 하며

null 사용을 허용하지 않습니다.

 

즉,

val a: String             // 불허

val a: String = null    // 불허

 

하지만, null을 꼭 사용하겠다면, ?를 사용해서 null을 허용하겠다고 명백히 해줘야 합니다.

val a: String? = null

 

주의) 위 변수 a 사용시 고려할 점

val b: String? = a      // 허용 (a나 b나 동류이므로)

val c: String = a        // Error (a는 부정확한 상태므로 불허)

val c: String = a!!      // a값이 null이 아님을 보증한다는 의미로 !!를 붙여쓰면 허용

 

늦은 초기화

가끔 초기화를 일부러 늦춰야 하는 경우에 사용합니다. 코틀린에서는 두 개의 키워드로 이를 지원합니다.

(앱이 시작될 때 일부 변수들을 늦게 초기화함으로써 연산을 분산시켜 실행 속도를 빠르게 하기도 함)

 

lateinit

var 타입의 늦은 초기화 (int, long, float, double과 같은 프리미티브 자료형에는 사용 불가)

lateinit var a : String      // 허용

 

by lazy

val 타입의 늦은 초기화 (프리미티브 자료형에도 사용 가능)

val a : String by lazy {

    "Hello"

}

 

실습)

val str : String by lazy {
    println("늦은 초기화!")
    "Hello"
}
println(str)       // 늦은 초기화!, Hello      (str 첫 사용시에만 '늦은 초기화!'가 출력됨)
                   //                         (즉, lazy { } 블럭 내부 코드가 한 번만 실행됨)
println(str)       // Hello

 

 

 

안전한 호출

.연산자 대신 ?. 연산자를 사용하면 변수 값이 null이 아닌 경우에만 메소드가 호출됩니다 (편리한 기능이네요!)

var nameUpperCase = if (str != null) str else null       // 이런 문장을
var nameUpperCase = str?.toUpperCase              // 로 간략하게 쓸 수 있다.

또 한 가지 편리한 기능이 있는데, 위에서 str이 null 일 때 nameUpperCase도 null 이 됩니다.
그런데 이 때 엘비스 연산자 ?: 를 사용하면 null 을 대체할 문자열을 대입할 수 있습니다.

var nameUpperCase = str?.toUpperCase ?: "초기화가 안됐어요"  // str이 null인 경우 문자열이 대입됨

 

 

 

컬렉션

데이터 집합을 만드는 자료 구조를 말합니다. (예: 리스트, 맵, ...)
원소들의 내용을 수정할 수 없는 타입과 수정할 수 있는 가변형(mutable) 타입이 있습니다.


<리스트>

원소 변경 불가
val colors1: List = listOf("초록", "주황", "빨강", "파랑", "하양")
val colors1 = listOf("초록", "주황", "빨강", "파랑", "하양")     // 물론 데이터 형을 생략할 수도 있습니다

원소 변경 가능
val colors2: MutableList = mustableListOf("초록", "주황", "빨강", "파랑", "하양")
val colors2 = mustableListOf("초록", "주황", "빨강", "파랑", "하양")

<colors2.add("검정")    // 원소 추가
colors2.removeAt(4)   // 원소 제거
color2[0] = "연두"     // 원소 변경



<맵>
키-값 쌍의 데이터 집합 (중복불가)

원소 변경 불가
val colors3: Map<String, Int> = mapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
val colors3 = mapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
println(colors3["B"])

원소 키 값 변경 가능
val colors3: MutableMap<String, Int> = mutableMapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)
val colors3 = mutableMapOf("A" to 90, "B" to 80, "C" to 70, "D" to 60)


사용 예)
for ((k, v) in colors3) {
   println("$k --- $v")
}



<집합>
원소 변경 불가
val cities: Set = setOf("Seoul", "Incheon", "CheongJu")
val cities = setOf("Seoul", "Incheon", "CheongJu")

원소 변경 가능
val cities: MutableSet = mutableSetOf("Seoul", "Incheon", "CheongJu")
val cities = mutableSetOf("Seoul", "Incheon", "CheongJu")


사용 예)
cities.add("Sokcho")
cities.remove("Incheon")

 

(계속...)

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

[코틀린의 기본 문법 2]

 

class (클래스)

class Person {  }                       // 빈 클래스 형태

 

class Person {                          // 생성자를 정의한 클래스 (*k1)
   constructor(name: String) {
      println(name)
   }
}

 

희한하게 클래스를 아래 처럼도 정의하는군요...

class Person (var name: String) {  }    // (생성자를 갖는) 빈 클래스 (참고: var 생략 가능)
                                        // 굳이 클래스명 옆에 이런 문법을 만들것 까지야...
                                        // 메소드 오버로드는 없나보군요...

 

class Person (name: String) {
   init {                    // init 블럭은 생성자와 함께 제일 먼저 실행됨 (위 *k1과 동일 결과)
      println(name)
   }
}

 

var person1 = Person("원빈")     // 개체 생성
person1.name = "한효주"           // 프로퍼티 쓰기
println(person1.name)            // 프로퍼티 읽기

 

 

접근 제한자

public  : (생략가능) 전체 공개
private : 현재 파일 내에서만 공개
internal : 같은 모듈 내에서만 공개 (모듈? 예를 들면, 한 프로젝트 내에 스마트폰용 모듈, 시계용 모듈, TV용 모듈,
            태블릿용 모듈 등등 여러 모듈들을 제작하는 경우가 있음. 모듈은 여러 개의 파일 조각으로 이뤄져 있음)
protected : 부모로 부터 상속받은 클래스에서만 공개

 

 

클래스 상속

※ 코틀린에서는 기본적으로 상속을 금지합니다. (추상 클래스 제외)
그러나 꼭 사용하고자 한다면 open 키워드를 사용할 수 있습니다.

 

빈 클래스 상속

open class Animal { }
class Dog : Animal( ) { }     // SHS: 반드시 상위 클래스의 생성자를 호출하는 형식임 

 

생성자를 갖는 클래스의 상속

open class Animal(val name: String) { }
class Dog(name: String) : Animal(name) { }

 

중첩 클래스

안쪽 클래스는 바깥 클래스A와 거의 독립적입니다.

안쪽과 바깥쪽 변수명이 같아도 됩니다. 단지 선언된 위치가 클래스A 안쪽일 뿐입니다.

class A {
    class B {          // 중첩 클래스
    }
}
var bb = A.B( )     // 클래스B의 위치를 명시하고 클래스B를 객체화 할 수 있음.

 

(중첩된) 내부 클래스

안쪽 클래스에 inner 키워드를 붙여주면 바깥 클래스 멤버에 접근할 수 있고,

이 때의 내부 클래스B는 클래스A의 멤버로 소속이 바뀝니다.

class A {
    var a=10
 
    inner class B {       // (중첩된) 내부 클래스 - 멤버가 된 클래스
        fun result( ) {
            a = 20        // 바깥 클래스 멤버에 접근 가능
        }
    }
}
var bb = A( ).B( )      // 클래스B는 클래스A의 멤버이므로 객체화된 바깥 클래스를 통해서만 객체화가 가능

 

 

추상 클래스

미구현 메소드가 포함된 클래스를 의미하죠. 키워드: abstract

클래스명과 미구현 메소드명 앞 모두에 키워드를 작성해야 합니다.

미구현 메소드가 단 한 개라도 포함되어 있다면 추상 클래스 입니다.

추상 클래스는 반드시 상속을 목적으로 제작하는 클래스입니다.

반드시 상속한 후, 미구현 메소드를 구현해야 합니다.

이것은 반드시 지켜야 하는 개발 팀원들 간의 약속입니다.

abstract class A {
    abstract fun func1( ) { }
    fun func2( ) { }
}
class B: A( ) {
    override fun func3( ) {
        println("Hello")
    }
}

 

 

인터페이스

추상 클래스와 거의 같지만, 일단 2가지 큰 차이점이 있습니다.

1. 추상 클래스는 하나만 상속 가능

   인터페이스는 복수 개를 상속 받을 수 있음 ('다중 상속')

2. 추상 클래스 내부에는 추상 메소드만 선언 가능 (abstract 키워드 필수)

   인터페이스 내부에서는 추상 메소드 + 일반 메소드 모두 사용가능 (abstract 키워드 불필요)

 

interface Runable {
    fun run( )                  // { } 코드가 없는 추상 메소드
    fun walk( ) = println("걷는다")   // 일반 메소드 포함 가능
}

  class Human : Runable {
   override fun run( ) { println("달린다") }
}

 

 

※ 상속과 인터페이스를 함께 사용해본 예)

class Animal { ... }
interface Runable { ... }
interface Eatable { ... }
class Dog : Animal( ), Runable( ), Eatable( ) { ... }   // 클래스 상속과 인터페이스 다중 상속

 

 

 

 

(계속...)

 

<<< 이전 글 보기      다음 글 보기 >>>

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

 

모든 언어가 그렇듯이 코틀린 언어에도 문법이 있습니다. 여기에서는 코틀린의 기본 문법들을 익혀 봅시다.

프로그래밍 언어들은 단 한 가지 언어라도 입문했었다면 사실상 다른 언어들은 거져 먹는 식입니다.

코틀린 역시 특별히 새로운 문법은 없으니까 아래 글을 한 번 읽어 보는 것만으로도 파악이 끝나실 거예요.

 

문법을 설명하기 전에 먼저 스튜디오 안에 있는 편리한 도구 두 가지를 소개해 드릴게요.

[편리한 도구 - REPL]

REPL (Read-eval-print loop)은 코틀린 명령어(='함수'='메서드' 모두 같은 의미로 받아들여도 됨)를 한 줄씩 입력해서 실행해 볼 수 있는 쉘입니다. (쉘: 인간의 명령과 기계의 동작 사이에서 소통을 도와주는 통역자 같은 프로그램) 

 

REPL을 실행해 보세요 --> 메인메뉴 Tools>Kotlin>Kotlin REPL 클릭하고, 아래 명령을 입력해 보세요.

println("안녕하세요, 코틀린")

   

   명령을 실행하도록 Ctrl+엔터 를 눌러주세요.

   println() 은 괄호 속의 문자열을 출력해 주는 메서드 입니다.

코틀린 REPL

 

[편리한 도구 - 스크래치]

여러 줄의 명령어들을 한꺼번에 실행시켜 볼 수 있는, REPL 보다 좀 더 진보된 쉘입니다.

 

스크래치를 실행해 보세요 --> 메인메뉴 File>New>Scratch File>Kotlin 클릭하고, 아래 소스를 입력해 보세요.

var a=10

println (a * 10)

 

   이들 명령을 실행하도록 창에서 '▶'아이콘을 눌러주세요.

   var 는 변수를 선언하는 명령어이고 a에 10이 대입된 후, 이어지는 문장에서 a*10이 계산되어 100이라는 결과가 출력되는데 재밌는 것은 각 줄마다 처리된 결과가 각 줄 우측에 각각 표시된다는 거죠. 스레기통 아이콘을 누르면 출력 결과가 지워집니다.

참고) 만약 각 행에 밑줄이 그어지면서 런타임 스크립트 오류가 표시되면, Ctrl+Shift+Alt+s를 누르시고 Dependencies>app 에서 Path를 업데이트 시켜주세요. 업데이트 링크가 보일 거예요.

(안보이면 수직 스크롤바를 내려보세요)

스크래치 화면

 

 

[코틀린 기본 문법 1]

기본적인 문법을 간략하게 정리해 보겠습니다.

프로그래밍을 조금이라도 했던 분들은 그냥 이해되시겠지만, 처음 프로그래밍을 접하시는 분들은 지금부터 아래 나열된 각 키워드들에 대해서는 각자 검색등을 통해 지식을 습득하셔야 합니다. 이들에 대한 설명은 두꺼운 서적 한 권 정도 되니까 여기에 세세한 내용들을 적을 수는 없어요. ㅠㅜ

 

수치 자료형 : Int, Long, Short, Byte, Float, Double
 문자 자료형 : Char, String
 기타 : Unit (아무것도 없는 상태를 가리킴. 타 언어의 void에 해당)

 

리터럴 (데이터형을 표현하는 방법입니다)

  1    : Int

  1L   : Long

  1.0   : Double

  1.0f  : Float

 

var는 변수 (값 수정 가능), val은 상수 (값 수정 불가)

var a: Int = 10           // 변수 a를 Int형으로 선언 후 10을 대입
var b: Long = 10L          // 변수 b를 Long형으로 선언 후 10을 대입
val pi: Float = 3.14f      // 상수 pi를 Float형으로 선언 후 3.14를 대입 (값 수정 불가)
val str: String = "안녕"   // 문자열 상수. 여러 줄의 문자열은 """로 감싸면 됨 (값 수정 불가)
val ch: Char = '굿'        // 문자 상수. (값 수정 불가)

 

참고) 다른 언어에서 처럼 변수타입 없이 다음과 같이 써도 되는데, 이런 경우에는 타입이 자동으로 설정됨

var a = 10

val str = "안녕"

 


배열은 var, val 어느 키워드로 선언하든 모두 값 수정이 가능

val arr: Array<Int> = arrayOf(1,2,3,4,5)   // Int형 1차원 배열
 val arr = arrayOf(1,2,3,4,5)               // 자료형을 생략해도 됨 (위와 동일)
    arr[0] = 2                              // 배열

 

++i    // 증감연산자 사용 가능. i++ 라고 해도 됨.

 

== // 문자열 비교
=== // 개체 비교

 

println("입력하신 문자열은 $str 입니다")    // " "내에서 문자열 변수 참조 (이건 마치 PHP 같네요)

 

 

<함수>

fun circleArea(r: Float) : Float {          // Float형 인자 r을 받고 Float형 반환 값을 갖는 함수
     return (r*r*pi)
}

fun circleArea(r: Float) : Pair<String, Float> {  // 두 가지 형 데이터를 반환하는 함수
     return Pair("원의 넓이 = ", r*r*pi)
}

fun circleArea(r: Float) : Unit {          // 반환 값은 Unit형 즉, 없음.
     println(r*r*pi)
 }

 

fun circleArea(r: Float) {     // 반환 값이 없을 때(=return문 없음) 반환타입을 아예 생략해도 됨
   println(r*r*pi)

 

 

※ 식의 결과를 함수의 결과 값으로 대입

fun f( ) : Unit = println("Hi")

fun f( ) = println("Hi")

 

 

<조건문>

if ( a == 10) { println("맞아요") }        // 조건문
 else { println("틀려요") }

val max = if (a>b) a else b

 

when (a) {           // switch 문을 코틀린에서 when 이라고 적음(이건 SQL 같은 모습이죠)
   1 -> println("1 입니다")
   2, 3 -> println("2 또는 3 입니다")
   4..7 -> println("4~7 사이 값입니다")      // ..연산자 사용도 가능하네요
   !8..10 -> println("8~10 사이 값이 아닙니다")
   else -> println("...")
}

 

val str = when (a % 2) {
   0 -> "짝수"
   1 -> "홀수"
}

 

fun isEven(n: Int) = when (n%2) {  // when 문 결과를 함수의 반환 값으로도 사용 가능
  0 -> "짝수"                       // 나잘난 프로그램 덕후나 쓸법한 문법...알아도 쓰지말자!
  1 -> "홀수"
  else -> "Exception Error"         // else 분기식은 생략할 수 없음
}
println(isEven(3))                 // 결과: "홀수"

 

val numbers : Array<Int> = arrayOf(1,2,3,4,10,11,-3,-2,-1,0)
for (i in numbers){               // 타 언어의 foreach문에 대응함
   println(i)                      // 결과: 1;2;3;4;10;11;-3;-2;-1;0
}

for (i in 1..10) {.....}           // 1,2,3,...,10
for (i in 1..10 step 2) {.....}    // 1,3,5,7,9    step은 양수만 가능
for (i in 10 downTo -2 step 2) {.....}  // 10,8,6,4,2,0,-2   10..-2라는 표현은 불가함

 

while (i < 10) {.....}       // while 문

 

do {.....} while(i<10)       // do while 문

 

몇 가지 문법을 살펴봤는데, 변수 선언 방법이나 클래스 상속 부분들이 C#과 같은 언어에 비해

덜 직관적인 것 같아 보이네요;; 뭐라고 표현해야 할까... 코틀린 문법이 마치 공돌이 덕후스러운

오버가 너무 많이 보인다고나 할까요^^;

제가 마소의 열성팬은 아니지만, 마소의 팬이 안될래야 안될 수가 없게 만드네요. ㅎㅎ

 

 

(계속...)

 

<<< 이전 글 보기      다음 글 보기 >>>

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...
곧장 앞서 만든 FirstApp에 약간의 코딩을 하고 화면에서 버튼을 클릭하여 우리가 원하는 글자를 출력하는 앱을 만들어 봅시다.

Activity_main.xml 디자인 탭에는 안드로이드 스튜디오가 디폴트로 만들어준 뷰가 하나 있습니다. HelloWorld 라는 글자를 표시해 주고 있죠. 그 뷰를 선택하고 'Del' 키를 눌러 뷰를 완전히 없애세요. 그럼 진짜 속이 완전히 빈 앱이 되는 것이죠. 원래는 이 상태가 기본입니다!

 

[뷰 추가해 보기]

Autoconnect 옵션을 켜세요. (맙굽자석 아이콘에 금지선이 있으면 안돼요. 금지선이 있으면 옵션이 꺼진 거예요)

 

팔레트에서 'TextView'라는 뷰를 디자인 창의 앱 레이아웃 중앙에 가져다 놓으세요. 참고로 레이아웃 정중앙으로 아이콘을 가져가면 세로및 가로줄이 나타납니다) 이렇게 가져다 놓은 텍스트 뷰의 속성 창에서 text 항목에 앱에서 출력하고 싶은 문자열을 입력해 보세요. 저는 "My first application!"라고 입력해 봤습니다. 글자의 크기를 크게 하기 위해 textApearance를 AppCompat.Display3 으로 바꿔주세요. 컴포넌트 트리 창을 보면 우리가 가져다 놓은 텍스트 뷰가 하나 나열됐음을 알 수 있어요. 그런데 노란 경고 표시가 생겼을 겁니다.

 

 경고 아이콘을 클릭해보세요.

 

경고 이유가 긴 메시지로 출력되는데, 요점은 텍스트를 리소스화 하지 않았다는 것 입니다.

 

메시지 제일 아래쪽에 'Fix' 버튼을 누르면 안드로이드 스튜디오가 이 문제를 수정할 수 있게 이끌어 줍니다.

 

'리소스'란? 번역해 보면, '자원'이죠. 앱 제작에 사용되는 모든 문자열, 사진, 음악 파일, 사진 파일, 뷰 조각 등등을 안드로이드 스튜디오에서 사용할 수 있는 작은 모듈 형태로 만들게 되는데, 이런 모든 데이터들을 포괄적으로 리소스라고 부릅니다. 즉, 앱을 만드는 '자원'인 셈이니까요~ 

 

화면에 'Extract Resource' 창이 나타나 있죠? 리소스화 시켜주는 창입니다. Resource Name에 임의로 영문 이름을 작성하고 OK를 클릭하세요 (다른 세세한 리소스 설정은 안드로이드 스튜디오가 일단 디폴트 설정으로 알아서 작업해 주므로 우선 이렇게 이름을 정해주는 것만으로도 텍스트가 리소스화 됩니다. 저는 이 리소스 이름을 'my_first_application'이라고 적었습니다. (경고 표시가 사라진 것을 확인할 수 있을 거예요)

프로젝트 탐색기>app>res>values>strings.xml을 열어보면 아래 코드와 같이 리소스화 되어 있는 my_first_application을 확인할 수 있습니다.

 

<resources>

   <string name="my_first_application"> My First Application </string>

<resources>

이 textView 뷰를 앞으로 프로그래밍 할 때 'my_first_application'라는 이름으로 프로그램 내에서 참조할 수 있게 된 것입니다. 리소스화라는 게 별거 아닙니다. 이 처럼 <resources>라는 XML 요소로 구체적인 설정 값들을 정의해 놓는 것에 불과합니다. 디자인 창에서 뷰들을 많이 추가해 놓으면 이렇게 정의된 <resources> 요소가 그 수 만큼 점점 늘어나겠죠?

 

참고) 여기서는 경고 속 'Fix' 링크를 통해 문자열을 리소스화 했는데, 이것은 경고메시지 처리 방법을 알려주기 위한 것이었고, 앞으로는 string.xml 에디터 창 위쪽에 있는 'Open editor' 링크로 직접 Extract Resource 창을 열고 입력을 하세요.

 

[다국어 지원 기능 넣기]

 

한편, 여러 나라 사람들이 앱을 사용할 수 있으려면 문자열이 다중 언어를 지원하도록 해줘야 합니다.

위 strings.xml을 더블클릭으로 열고 'open editor' 링크 메뉴를 클릭하세요.

Translation Editor가 열리면 'Add Locale' 아이콘(지구본 모양)을 눌러 'Korean (ko) in South Korea (KR)'를 선택하여 언어를 추가하고 'Default Value'와 'Korean (Ko) in South Korea (KR)'에 앱에서 사용되는 문자열 별로 각 언어에 맞는 번역을 적어 넣으세요. key는 문자열 리소스의 이름이고 아래에 두 개의 문자열 리소스가 보이네요. app_name은 앱 이름, my_first_application은 우리가 추가했던 문자열 뷰의 이름입니다. Default Value는 문자열 표시에 사용되는 디폴트 언어를 나타냅니다. 금방 한국어를 추가했던 방법으로 여러나라 언어들을 계속 추가하면 그 만큼의 다중 언어를 지원하는 앱이 되는 것이에요. 간단하죠?!

 

[뷰의 위치 조정]

뷰에 설정되어 있는 제약을 수정함으로써 뷰의 위치를 바꿔봅시다.

activity_main.xml을 열고 레이아웃 그림에서 아래 쪽에 설정된 제약를 제거해 보세요. 아래 쪽 여백이 사라지면서 뷰가 상단에 붙어 버릴거예요. 속성 창에서 뷰의 상단 여백으로 200 정도를 입력해 봅시다.

 

 

잠깐! 읽어보고 넘어가야 할 사항이 있어요.

 

액티비티란?

안드로이드에서 화면을 액티비티라고 부릅니다. 안드로이드 스튜디오의 에디트 창에 열려있는 MainActivity.kt 파일에는 액티비티를 생성하는 코드가 들어 있습니다. 열어 보세요. 아래와 같은 소스 코드가 보일거예요. 그리소 아래 설명은 그냥 읽어보기만 하고 넘어가세요. 자바나 C# 경험이 있다면 코틀린 언어가 전혀 낯설게 보이지 않을 거예요. '//'는 주석(설명) 입니다. 그런 게 있구나하고 참고만 하세요.

 

package com.tistory.firstapp         // 본 파일 패키지의 위치를 표시함 (디스크 드라이브 ....\com\tistory\firstapp 폴더를 가리킴)
                                             // 도트(.)는 트리(Tree) 구조의 하위 디렉토리 구분용이라고 이해할 것
import android.os.Bundle             // 안드로이드 기본 번들 묶음을 가져옴 (import는 외부 프로그램 모듈들을 본 파일에 포함시켜 줌)
import androidx.appcompat.app.AppCompatActivity  // 액티비티 기본 틀 정의 묶음을 가져옴

class MainActivity : AppCompatActivity() {    // 상위클래스 AppCompatActivity를 상속받아 MainActivity 클래스를 생성

                                                          // 클래스란 화학에서 '분자'와 같은 개념으로 프로그램 세계의 분자라고 할 수 있음

                                                          // 하나의 프로그램 안에 수 많은 클래스들이 '개체'로서 활성화되어 동작하는 것임.

                                                          // 프로그램 세계에서의 그 분자 구조를 class 키워드로 정의함

                                                          // 클래스 내부는 속성 값들과 개체로서 활성화됐을 때 취할 동작이 정의된 함수들로

                                                          // 구성됨. 함수(function)를 최근에는 메소드(method)라고도 부르고 있음

                                                          // 즉 클래스 = 속성 값 + 메소드

                                                          // 자바에서 상속 표현은 extend임 (C#이나 코틀린은 :를 사용)

                                                          // MainActivity는 우리가 제작하는 프로그램 첫 기본 화면임

                                                          // 액티비티를 만들기 위해서는 반드시 AppCompatActivity라는

                                                          // 상위 클래스(=부모 클래스)를 상속받아야 함

                                                          // (상속이란? 부모격 클래스의 '속성 값 + 메소드'를 묵시적으로 복사하는 것)

    override fun onCreate(savedInstanceState: Bundle?) { // onCreate 메서드는 액티비티 시작 때 제일 처음 호출됨

                                                                          // 메소드는 "fun 메소드명(매개변수 묶음)" 형태로 정의하며

                                                                         // override란 상위클래스의 특정 메소드를 재정의 하겠다는 의미임

                                                                         // 반드시 상위클래스의 onCreate 메소드를 오버라이드 하고,
        super.onCreate(savedInstanceState)                   // 반드시 상위클래스(super)의 onCreate 메소드를 호출해야 함
        setContentView(R.layout.activity_main)  // setContentView()로 액티비티의 레이아웃을 설정 함

                                                           // R은 자동 생성되는 리소스 클래스. 모든 리소스는 정수형ID로 관리되고 참조됨

                                                           // 괄호안은 지난 시간에 디자인을 정의했던 res/layout/activity_main.xml을 가리킴

    }
}

 

 

[버튼 배치]

① 팔레트에서 버튼을 레이아웃 중앙 약간 아래 쪽에 가져다 놓고 속성을 다음과 같이 수정합시다.

  상하단 여백에 대한 제약도 추가하시고요.
   ID : 'clickButton'
   text : '클릭해 보세요'

   textApearance : 글자크기를 자기 마음대로 적당히.
   (ID 이름을 짓는 이유는 프로그램 코드에서 우리가 지은 이름으로 컴포넌트들을 참조하기 위해서임)

 

참고)

자바에서는 findViewById()를 이용해서 컴포넌트 파일에서 컴포넌트를 찾아와서 사용해야 했었지만 코틀린에서는 kotlin-android-extensions 라이브러리를 사용하여 xml 파일의 컴포넌트를 자동으로 참조함)

 

② 버튼을 배치했으니 버튼을 눌렀을 때 할 동작을 정의해 줍시다. MainActivity.kt 파일을 열어 주세요.

우선 import로 시작되는 행들이 몇 줄 있는지 확인해 보세요. 두 줄이 있죠?

 

이제 OnCreate()라는 오버라이드된 함수 안에 아래 굵게 표시된 빨간색 코드를 입력해 넣으세요. 입력중에 자동으로 관련 명령어들('메서드'들 이라고 부릅니다)이 자동완성형으로 나열되면서 입력을 도와 줄 것입니다. 혹시라도 자동완성 창이 사라지면 CTRL+Space 를 누름으로써 다시 표시되게 할 수 있습니다. 이 코딩을 하면 위에서 확인해봤던 import 문이 새롭게 하나 추가되어 3 줄이 됐음을 확인할 수 있습니다. 원래 일일이 import 문도 입력해야 되지만, 스튜디오가 알아서 자동으로 작성한 것입니다. 편하죠^^

(참고: 오버라이드란 안드로이드 프로젝트에 기본적으로 정의되어 빌트인 되어 있는 메서드를 새롭게 정의할테니 앞으로는 새롭게 정의된 메서드를 호출하라는 의미입니다. 우린 이렇게 새로 메서드를 정의하면서, 버튼을 클릭했을 때 해야 할 동작을 추가해 넣은 것입니다. 아래는 버튼을 눌렀을 때 '버튼을 눌렀네요'라는 메시지를 출력합니다)

 

class MainActivity : AppCompatActivity() {

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

        clickButton.setOnClickListener {
            textView.text = "버튼을 눌렀네요"
        }
    }
}

 

③ 실행해 봅시다.

 버튼을 누르면 아래와 같이 '버튼을 눌렀네요' 메시지가 나타날 것입니다.

 

위에서 사용한 setOnClickListener는 빈번하게 사용되므로 잘 기억해 둡시다.

 

이렇게 안드로이드 앱을 만들어 봤는데, 아직 뜬 구름 잡는 기분이시죠? 때문에 프로그래밍 언어를 배워야 합니다. 대략 앱 개발 분위기는 파악하셨으리라 생각됩니다. 위에서는 버튼 컴포넌트(기능을 조작할 수 있는 컴포넌트라는 의미에서 '컨트롤'이라고도 해요)을 사용해 봤는데, 앞으로의 학습 방식도 이와 똑같아요. 위와 같이 단순히 컴포넌트들을 하나씩 정복해 가는 거죠.
이제 본격적으로 코틀린 언어를 배워 보자구요. 각오 단단히 하세요. 다음 포스트에 계속~

 

<<< 이전 글 보기      다음 글 보기 >>>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

안드로이드 스튜디오

 >>> 이전 글 보기
앞서 접했던 안드로이드 스튜디오에 대해 몇 가지 설명을 해 드릴게요. 앱 개발을 위해서 스튜디오와 친해져야 하니까요~
차분하게 아래 내용 하나 하나를 직접 조작해 보면 개발 환경에 대해 대략적인 파악을 할 수 있을 거예요.

앞에서 생성한 프로젝트는 안드로이드 스튜디오에서 다음과 같이 열립니다. 아래는 에디터 창에서 MainActivity.kt 탭을 클릭한 상태입니다. activity_main.xml 탭을 선택하면 기본적인 앱 화면의 레이아웃 모양을 시각적으로 확인할 수 있습니다.

프로젝트 탐색기와 에디터

   프로젝트 탐색기는 프로젝트가 저장되는 폴더의 파일들을 트리(Tree) 형태로 요약해서 표시해 줍니다. 폴더들을 열면 그 안에 여러가지 많은 파일들이 보일 것 입니다. 우리가 앱을 작성하면 컴퓨터 내부적으로 여러가지 조각 파일들이 만들어지면서 저장됩니다. 이 프로젝트 탐색기에서 수정하고자 하는 파일을 더블클릭하면 우측 에디터 화면에 파일의 내용이 표시 됩니다. 우리는 에디터에 원하는 기능들을 작성 함으로써('코딩') 프로그램을 만들면 됩니다.

 

알림 및 실행도구 창

   이 창은 안드로이드 스튜디오의 동작 상태를 표시(Logcat 탭)하기도 하고 우리가 프로그램할 명령어들을 테스트(TODO 탭) 해 볼 수도 있는 유용한 공간입니다.

 

프로젝트 탐색기에 있는 파일들의 역할을 잠깐 알아봅시다. 

App > Manifests > AndroidManifest.xml - 위에서 안드로이드 앱이 여러 조각 파일 형태로 저장된다고 했죠? 그 조각 파일들의 종류와 상태 정보가 자동으로 이 파일에 저장됩니다. 이 정보들을 가리켜 '매니페스트 정보'라고 부릅니다.

 

MainActivity.kt 와 Activity_main.xml - 이 두 파일이 우리가 프로그래밍 해줘야 하는 파일입니다. (파일 이름을 외워두세요) 위 안드로이드 스튜디오 화면 에디트 창에 이 두 파일에 대한 탭이 열려있는 것이 보이시죠? MainActivity.kt는 앱의 동작을 프로그램하는 소스 코드이고 Activity_main.xml은 스마트폰에 표시될 화면 모습의 디자인을 프로그램하는 소스 코드입니다. 그러니까 이 두 개의 파일로 앱의 동작과 디자인을 정의합니다. 앱의 가장 핵심이 되는 파일이고 항상 세트로 구성되어 있습니다. 프로그램 작업 때 좋은 팁이 하나 있는데, *.kt 파일 에디터 화면의 좌측 부분에 작은 문서 모양과 그 아래에 <>표시가 있는 아이콘이 있죠? 그 아이콘을 누르면 이렇게 세트로 된 파일의 디자인 정의 파일로 쉽게 이동해서 작업을 할 수 있습니다. 클릭해 보세요. 디자인 파일에서 거꾸로 동작 정의 파일로 이동하려면 ctrl+E를 눌러 최근에 사용했던 파일을 선택하면 됩니다.

 

drawable - 이미지들을 모아놓는 폴더

layout - 화면 레이아웃 정보를 저장하는 폴더

mipmap - 앱 아이콘들을 모아놓는 폴더

values - 여러가지 값 정보들을 저장하는 폴더

 

Gradle Scripts - 빌드 정보들이 저장되어 있는 폴더. 우리가 작성한 코드들을 스마트폰이 실행할 수 있도록 컴퓨터 내부적으로 '컴파일' 작업이 이뤄지고 스마트폰 용 기계어 코드가 만들어지는데, 이 과정을 '빌드'라고 합니다. 우리가 빌드에 대해서 알 필요는 없고 단지 몇 가지 조작 요청은 해줘야 합니다. (build.gradle이라는 파일들을 나중에 수정하게 될 것 입니다) 마치 방앗간에 쌀을 가져다 주고 떡을 만들어 달라고 하면서 (떡이 만들어지는 '빌드' 과정은 우리가 알 필요는 없고) 떡 고물로 뭐뭐를 넣어 무슨 무슨 떡을 만들어 달라고 조작 요청을 하는 것과 같습니다.

 

 

다음은 activity_main.xml 탭을 열어 레이아웃 디자인 화면에 대해 알아봅시다. 

activity_main.xml 탭을 클릭하면 에디터 창이 위와 같은 디자인 창으로 전환됩니다.

 

디자인 창 속에는 팔레트 창, 컴포넌트 트리 창, 속성 창들이 들어있고 한 가운데는 레이아웃 모양을 표시해주는 창이 있습니다.

(텍스트만 표시될 경우, 디자인 창 하단의 'Design' 탭을 클릭해줘야 위 화면을 볼 수 있습니다. 'Text' 탭에는 위 디자인이 프로그래밍 코드로 표현되어 있습니다. 바꿔말하자면 'Text' 탭의 내용을 그대로 그림으로 그려 표시한 것이 'Design' 탭 입니다)

 

팔레트 - 앱 상에서 보이는 모든 요소들을 ''라고 합니다. 버튼, 이미지, 문자열 등등이 모두 뷰 입니다. 팔레트 창은 이런 뷰 조각들을 모아 놓은 창으로서 추가하고 싶은 뷰가 있다면 마우스로 끌어다가 디자인 창 중앙의 레이아웃 위에 올려 놓으면 됩니다. 참 쉽죠~

(프로그래밍 하는 입장에서는 뷰 조각들을 '컴포넌트(요소)'라고 부릅니다.

 

컴포턴트 트리 - 뷰 조각, 그러니까 컴포넌트들이 레이아웃 상에 어떻게 쌓여 있는지를 트리 구조로 표시해 줍니다. 레이아웃 상에서 위 쪽 컴포넌트와 아래 쪽에 가려진 컴포넌트 간에 구분이 안될 때 이 트리구조를 보면 명백하게 알 수 있죠.

 

속성 - 레이아웃 상의 특정 뷰를 클릭하면 그 뷰에 대한 속성이 속성 창에 표시됩니다. 수직 스크롤 바를 움직이거나 'All Attributes'를 눌러 세부 속성으로 확장해 보면, 엄청나게 많은 속성 값들이 표시됩니다. 여기에 표시된 속성들을 'Constraint' 즉, 번역하자면 '제약, 제한'이라고 합니다. 예를 들자면, 위 속성창에는 현재 선택되어 있는 뷰의 상하좌우 여백이 표시되어 있죠. 모두 값이 0으로 설정되어 있습니다. 물론 다른 값으로 조절할 수도 있고요. 여백이 0으로 설정되어 있으므로 현재 선택되어 있는 뷰는 전체 화면으로 표시되게 됩니다. 뷰가 차지할 공간을 공백 설정을 통해 '제한'하는 것이죠. 모든 뷰들이 이런 식으로 화면에 배치됩니다. 이런 뷰 배치방식을 'ConstraintLayout 방식'이라고 부릅니다. 속성 창의 많은 항목들을 변경해보거나 속성 창 속 layout에 있는 값이나 아이콘을 움직이면 디자인 화면 중앙에 실시간으로 그 형태가 표시됩니다. 직접 이것 저것 바꿔보세요. 대략 어떻게 화면의 레이아웃을 조절하는지 알 수 있을 거예요.

 

 디자인창 아이콘들 중에, '위로향한 말굽자석' 아이콘 - AutoConnect를 On/Off 합니다. On (Enable AutoConnection to Parent)으로 하면 뷰를 배치할 때 자동으로 Constraint가 추가됩니다. (이 아이콘 기억해 두세요. 다음 포스트에서 사용할 예정입니다)

 

 디자인창 아이콘들 중에, '0dp' 아이콘 - 기본 여백을 표시하는 숫자입니다.

 

속성창 ID 항목 - 현재 선택된 뷰의 이름을 정해줄 수 있습니다. 그리고 코틀린에서 이 이름을 이용해 뷰에 접근할 수 있습니다.

속성창 layout_width, layout_height - 수평, 수직 배치 형태를 설정합니다. (match_constraint: 꽉차게, wrap_content:컨텐츠 크기에 맞게, Fixed: 임의의 값 지정)

 

 

다음 포스트에서는 위에서 열어놓은 파일에 약간의 코딩을 해서 첫 안드로이드 앱을 만들어 봅시다. (다음 포스트에 계속~)
<<< 이전 글 보기     다음 글 보기 >>>

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

코틀린 (Kotlin) 이란?

본 포스트는 한빛미디어, '안드로이드 생존코딩'에 소개된 예제를 구현해 보면서 누구나 앱 개발 기술을 습득했으면 하는 바램으로 시작합니다. (본 포스트는 작성되는대로 계속해서 업데이트 할 예정입니다)

 

스마트폰의 OS하면 단연코 안드로이드 입니다. 스마트폰에서 80% 이상의 점유율을 보이고 있죠. 안드로이드가 이렇게 까지 광범위하게 사용될 수 있었던 이유는 안드로이드 OS의 저작권 정책이 오픈 소스라는 점일 것 입니다. 즉 누구에게나 무료로 공개되어 있기 때문에 누구나 자유롭게 OS 개선에 참여할 수 있고 그렇게 많은 지성들이 모여 안드로이드의 성능을 끌어 올린 것이죠.

 

스마트폰 앱을 만들려면 그 스마트폰에 사용되는 OS에 맞춰 프로그래밍을 해야 합니다. 그 때 사용하는 프로그래밍 언어는 자바입니다. 그리고 자바 언어를 좀 더 쉽고 간단화 시킨 최신의 언어가 코틀린 입니다. 그러니까 안드로이드로 구동되는 스마트폰 앱을 만들기 위해 '자바' 또는 '코틀린'을 사용할 수 있습니다. (이 언어 외에도 여러 다른 언어들도 있어요. 하지만 머리 복잡하지 않게, 그리고 여기서는 코틀린을 다루기로 했으니까 다른 언어는 일단 제쳐둡시다)

 

안드로이드 앱 개발 환경 구축

안드로이드 앱은 PC에서 개발합니다. 지금부터 PC에 개발용 프로그램 도구들을 설치함으로써 앱 개발 환경을 구축해 봅시다.
PC의 OS는 Windows 10 이나 리눅스 모두 가능한데, 여기에서는 Windows 10을 기준으로 설명하겠습니다.

첫 번째, https://developer.android.com/studio/ 에서 "Android Studio"라는 도구를 다운받고 설치하세요.

 

두 번째, 설치된 안드로이드 스튜디오를 실행하고 첫 화면에서 몇 가지 설정을 하세요.

    (안드로이드 스튜디오의 버전에 따라 화면 표시가 좀 다를 수도 있습니다만, 메뉴를 잘 찾아가며 따라하시면 되요)

            Configure > Setting 클릭!

    ① Editor > File Encodings

        Global Encoding , project Encoding , Default encoding for properties files 모두 UTF-8로 설정 후 Apply 클릭

    ② Editor > General > Auto Import

        'Add unambiguous imports on the fly'와 'Optimize imports on the fly'를 모두 체크한 후 Apply 클릭

 

( ※ shs:  안드로이드 스튜디오는 자동으로 스펠링 체크를 하도록 되어 있는데, 변수명 만들 때 좀 귀찮을 수도 있으므로 옵션을 제거하겠습니다. (setting > Editor > Code Style > inspections 우측 Spelling 항목 언체크)

안드로이드 스튜디오 초기 화면

 

세 번째, 'Start a new Android Studio project'를 클릭

   (초기에 프로젝트 종류 선택 화면이 나오면, 'Empty Activity'를 선택하고  Next)

   아래 사항들을 원하는대로 작성한 후 Finish를 클릭하면 앱 개발용 화면으로 진입하게 됩니다.

      Name : 원하는 앱 이름 작성 ('FirstApp'이라고 적어보자~)

      Pakage name : 만들어질 앱이 구글플레이에 공개될 때, 전세계적 고유의 앱 ID가 됨

                         (도메인 이름 형식으로 적어봤습니다. 자신만의 임의의 이름을 사용해도 됩니다. 'com.tistory.firstApp' )

      Save location : 만들어질 앱이 저장되는 장소 (제 경우는 'C:\_Lab\Android\FirstApp')

      Language : Kotlin

      Minimum API level : 최소 호환 레벨 --> 킷캣 (안드로이드 4.4)로 합시다~

    (위에 표시된 이외에도 여러 항목이 있을 수 있는데, 위와 같이 앱 이름과 API 레벨만 잘 정해주시면 됩니다)

  Finish 버튼을 누르면 안드로이드 스튜디오가 열리면서 프로그래밍 할 수 있는 에디터가 나타납니다.

    activity_main.xml과 MainActivity.kt 탭이 나타나고 코딩을 할 수 있게 되는데, 이에 대해서는 다음 포스트에서 다룹니다.

    이렇게 새로운 프로젝트를 준비한 것만으로도 기본 안드로이드 화면을 출력하는 프로그램 뼈대를

    모두 만든 것입니다.

 

네 번째, PC와 스마트폰 연결

  앱 개발 과정에서 앱을 실행할 때는 두 가지 방법으로 할 수 있습니다.

  ① 스마트폰에서 직접 실행 --> 실행 테스트 속도가 빨라요. ( <-- 이 방법으로 진행할게요)

  ② 에뮬레이터로 실행 --> PC 메모리를 많이 좀 많이 사용하며 실행 테스트 속도가 느려요. (스마트폰 없이 가능)

 

  ①의 방법을 사용하려면, 아래 과정을 따라 하세요. PC와 스마트폰을 USB 케이블로 연결해야 합니다.

   필요한 경우 PC에 USB 드라이버를 설치해야 할 수도 있습니다.

    (https://developer.android.com/studio/run/oem-usb.html 를 참고)

   스마트폰은 '개발자 모드>USB 디버깅'이 활성화 되어 있어야 합니다.

 

   스마트폰의 '개발자 모드' 전환 방법은 폰 제조사마다 약간 다릅니다. (제조사별 방법은 인터넷에 널려있음)

   제 폰은 LG 폰인데, LG 폰의 경우 '설정>휴대폰 정보>소프트웨어 정보'에서 빌드번호 항목을

   여러 번 연속으로 탭함으로써 개발자 모드로 전환할 수 있습니다.

   (개발자 모드로 전환되면 설정 메뉴들 중에 '개발자 옵션'이라는 항목이 추가되며, 개발자 모드를 끌 때는  개발자 옵션에서 '사용 안함'을 하면 됩니다)

    개발자 모드가 되었으면 개발자 옵션에서 'USB 디버깅'을 켜 주세요. (이렇게 설정해야 PC와 연결이 됨)

 

    (PC와 스마트폰을 USB케이블로 연결한 뒤, 안드로이드 스튜디오 화면 하단 'Logcat'을 클릭해보면 폰 이름이 표시되어 있고  PC와 스마트폰간의 통신 상태 로그가 표시될 것 입니다. 그러면 잘 연결된 것 입니다)

 

다섯 번째, 실행

   안드로이드 스튜디오에서 현재 열려있는 프로젝트를(이미 기본적인 프로그램 뼈대가 완성되어 있는 것이라고 했죠?)

   실행해 볼까요?  -----> 메인 메뉴에서 Run>Run>(App)>Run 또는 단축키 Shift+F10

 

   결과) 스마트폰에 빈 화면이 나타나고 'Hello World!'라고 표시될 것 입니다!

 

   

   축하합니다! ^^

   (아직 익숙하지 않은 과정들이지만, 앞으로 앱 개발을 따라하다 보면 많이 친숙해 질 것 입니다.

    앞으로 코틀린 언어를 이용해서 이 빈 화면에 여러가지 기능들을 추가해 봅시다)

 

   다음 포스트에 계속~

    다음 글 보기 >>> 

 

 

 

 

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

     너무 멋집니다. 칼리 리눅스!

[[[[[ 칼리 리눅스 ]]]]]

1. Kali Linux란?

   Offensive Security에서 만든 모의 침투 테스트용 운영체제입니다.

   '백트랙'의 후속 버전으로 2013년 3월 13일에 배포되었습니다.

 

참고로,  
   백트랙은 우분투 기반으로 제작되었고 업그레이드가 안됩니다.

      - 새버전이 배포되면 완전 재설치해야 하므로 많이 번거로웠습니다...
   칼리는 데비안 기반으로 제작되었고 업그레이드가 가능합니다.

      - 새버전이 배포되면 재설치 필요없이 그냥 업그레이드만 하면 됩니다...

Kali 리눅스는 희한하게도 보안 테스트에 필요한 툴(약 300여개) 뿐만 아니라
보안 테스트시 발견한 취약점을 의뢰인에게 보고할 수 있는 레포팅 툴 등도 포함하고 있습니다.

또한 ARM프로세서까지 지원하므로 여러 ARM장비(라즈베리파이, 크롬북 등)에도 설치 가능하다는

재밌는 특징도 있습니다. 여러가지 매력이 뿜뿜 넘치지요~

!! 휴대폰에 설치해볼까? 물론 됩니다만, 휴대폰의 와이파이에는 위의 툴들을 사용할 수 없으므로

  너무 흐뭇해 하시면 안됩니다~ 시간 투자하지 마세요~


칼리는 Live CD 나 Live USB로도 부팅할 수 있기 때문에 CD나 USB 메모리 스틱만 들고다니면서

여러가지 보안 테스트를 시도해 보실 수도 있습니다.

 


2. Kali Linux 배포 주소

 

https://www.kali.org/downloads/

 

  iso 이미지 형식의 설치 버전들을 받을 수도 있고, (웹 페이지 상에서 다운로드를 받으려면 http 링크를,

     토렌트를 통해 다운로드 받으려면 torrent 링크를 클릭하시면 됩니다)

 

  vm (Virtual Machine ; 가상머신) 형식의 이미지를 받을 수도 있습니다.

     (가상머신 이미지 파일로 리눅스를 부팅하시면 로그인 패스워드가 필요합니다.

       root 비밀번호 : toor )

  

 

3. Kali Linux 설치 했을 경우 발생하는 이슈들

문제점1) 한글 깨짐

설치 과정이 끝나고 로그인 하는 화면부터는 한글이 모두 깨져서 뭔가 해괴해 보이고 당황스럽습니다.

[ 한글이 깨져 표시된 로그인 화면 ]


   해결) 일단 대충 root 로그인 하시고, 프롬프트 창 열어서 다음과 같이 업데이트및 나눔 폰트를 설치하시면 됩니다~

        root@kali:/# apt-get update
        root@kali:/# apt-get install fonts-nanum

   참고) vm 이미지를 사용하실 경우에는 이런 문제가 없습니다~

 

문제점2) 미러 사이트 오류

        운영체제를 처음 설치할 때, 미러 사이트를 등록하지 않았기 때문에 업데이트를 할 수 없습니다.

        또는 네트워크 연결이 되지 않아 어쩔 수 없이 미러 사이트를 등록하지 못했습니다.

        미러 사이트를 수동으로 등록하려면 어떻게 해야 하나요?

    해결) 프롬프트 창을 여시고 /etc/apt/sources.list 를 편집하시면 됩니다.

         root@kali:/# vi /etc/apt/sources.list

 

         파일 첫 행에 다음을 추가하신뒤 저장하세요. 그 뒤에 다시 업데이트를 시도하시면 됩니다.

deb http://http.kali.org/kali kali-rolling main non-free contrib  kali-rolling main non-free contrib

   참고) vi 에디터가 아니더라도 사용하기 편한 에디터를 사용하면 됩니다. list 파일은 텍스트 파일입니다.

 

문제점3) 노트북에 설치 중인데 내장된 무선 랜을 인식하지 못하네요 ㅠㅜ 괜히 공들인건가요?

       해결) 향후 업데이트 되어야 할 문제 같은데요. 제 경우에는 USB 타입 무선랜을 설치했고 아무 문제가

              없었습니다. 이 문제에 대해서는 잘 모르겠습니다. 기껏 해볼 수 있는 조치는 업데이트 정도겠네요.

              안돼면 열심히 구글링~하며 다른 경험자 분들의 경험담을 찾아봐야할 것 같습니다.;;

 

 

 

 

 

 

 

              

본 블로그를 찾은 분들에게 오늘도 축복 있으시길...

+ Recent posts