1.  대화상자

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


1)   기본 대화상자

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

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


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

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

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

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"

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

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

</LinearLayout>
class MainActivity : AppCompatActivity() {

    lateinit var button: Button

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

        button = findViewById(R.id.button)

        button.setOnClickListener {

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

 

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


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

 


 

2)  목록 대화상자

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

class MainActivity : AppCompatActivity() {

    lateinit var button: Button

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

        button = findViewById(R.id.button)

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

 

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

 

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

 

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


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

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

 

val checkArray = booleanArrayOf(true, false, false)


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

 

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


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

 

 

 

 

 

 

 

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


1.  메뉴

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

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

1)  XML을 이용한 옵션 메뉴

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

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

 

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

 

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

 

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

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

 

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

 

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

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

 

 

 

인플레이터 Inflater


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

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

 


 

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

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

class MainActivity : AppCompatActivity() {

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

        title = "배경색 바꾸기"

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

    }

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

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

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

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

 

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

 

 

 


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

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

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

 

 

 

 

 

 

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


 

🚀  SD 카드 특정 폴더의 이미지 파일을 보여주는 간단한 이미지 뷰어 앱 만들기

 

1.  화면 디자인 및 편집

 

1) MyPictureView 클래스

커스텀 위젯 (Custom Widget, Custom View)을 직접 만들어서 activity_main.xml에 넣어서 사용.
  ➡️ 커스텀 위젯은 지정된 이미지 파일을 출력하는 역할

 

MyPictureView.kt

 

    ✓  onDraw() 메서드를 오버라이딩

class MyPictureView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    var imagePath : String? = null

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        try {
            if (imagePath != null) {
                val bitmap = BitmapFactory.decodeFile(imagePath)
                canvas.scale(2f, 2f, 0f, 0f)
                canvas.drawBitmap(bitmap!!, 0f, 0f, null)
                bitmap.recycle()
            }
        } catch ( e : Exception) {

        }
    }
}

 

var imagePath : String? = null


    ✓  이미지 파일의 경로 및 파일 이름을 저장할 변수

 

if (imagePath != null) {
val bitmap = BitmapFactory.decodeFile(imagePath)
canvas.scale(2f, 2f, 0f, 0f)
canvas.drawBitmap(bitmap!!, 0f, 0f, null)
bitmap.recycle()
}


    ✓ imagePath에 값이 있으면(경로 및 파일이름이 지정되었다면) 화면에 그림 파일을 출력

 


 

2) activity_main.xml

  • 가로 레이아웃에 버튼 2개를 생성
  • 커스텀 위젯인 MyPictureView를 생성
  • 위젯의 이름은 btnPrev, btnNext, myPictureView
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

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

        <Button
            android:id="@+id/btnPrev"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="이전 그림" />

        <Button
            android:id="@+id/btnNext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="다음 그림" />

    </LinearLayout>

    <kr.abc.stream_practice.MyPictureView
        android:id="@+id/myPictureView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

 

 

👾  /storage/emulated/0/Pictures 에 이미지 업로드 후 AndroidManifest.xml 에 권한 설정

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:requestLegacyExternalStorage="true"

 


 

2.  kotlin 코드 작성 및 수정

class MainActivity : AppCompatActivity() {
    // 전역변수 선언
    lateinit var btnPrev: Button
    lateinit var btnNext: Button
    lateinit var myPicture: MyPictureView
    
    var curIndex: Int = 1 // 이미지 파일의 인덱스로 사용할 변수
    var imageFiles: Array<File>? = null // SD 카드에서 읽어올 이미지 파일의 배열

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

        title = "간단 이미지 뷰어"
        
        // 접근 권한 요청
        ActivityCompat.requestPermissions(
            this,
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE
        )

        btnPrev = findViewById(R.id.btnPrev)
        btnNext = findViewById(R.id.btnNext)
        myPicture = findViewById(R.id.myPictureView)

        imageFiles =
            File(Environment.getExternalStorageDirectory().absolutePath + "/Pictures").listFiles()

        // 파일 목록 출력
        for (i in imageFiles!!.indices) {
            var fileName = if (imageFiles!![i].isDirectory == true)
                "<폴더> " + imageFiles!![i].toString()
            else
                "<파일> " + imageFiles!![i].toString()
            println(fileName)
        }

        // 첫 번째 파일을 커스텀 위젯에 출력
        // 해당 인덱스의 이미지 파일 이름을 myPicture에 전달한다는 뜻
        myPicture.imagePath = imageFiles!![curIndex].toString()

        btnPrev.setOnClickListener {
            if (curIndex <= 1) {
                Toast.makeText(applicationContext, "첫번째 그림입니다", Toast.LENGTH_SHORT).show()
            } else {
                myPicture.imagePath = imageFiles!![--curIndex].toString()
                myPicture.invalidate()
            }
        }

        btnNext.setOnClickListener {
            if (curIndex >= imageFiles!!.size-1) {
                Toast.makeText(applicationContext, "마지막 그림입니다.", Toast.LENGTH_SHORT).show()
            } else {
                myPicture.imagePath = imageFiles!![++curIndex].toString()
                myPicture.invalidate()
            }
        }
    }
}

 

 


첫 번째 이미지에서 이전 버튼을 누르면 마지막 이미지가 뜨거나,
마지막 이미지에서 다음 버튼을 누르면 첫 번째 이미지가 뜨게 하기
class MainActivity : AppCompatActivity() {
    lateinit var btnPrev: Button
    lateinit var btnNext: Button
    lateinit var textIndex: TextView
    lateinit var textTotal: TextView
    lateinit var myPicture: MyPictureView
    var curIndex: Int = 1
    var imageFiles: Array<File>? = null

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

        title = "간단 이미지 뷰어"
        ActivityCompat.requestPermissions(
            this,
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE
        )

        btnPrev = findViewById(R.id.btnPrev)
        btnNext = findViewById(R.id.btnNext)

        textIndex = findViewById(R.id.textIndex)
        textTotal = findViewById(R.id.textTotal)

        myPicture = findViewById(R.id.myPictureView)

        imageFiles =
            File(Environment.getExternalStorageDirectory().absolutePath + "/Pictures").listFiles()

        for (i in imageFiles!!.indices) {
            var fileName = if (imageFiles!![i].isDirectory == true)
                "<폴더> " + imageFiles!![i].toString()
            else
                "<파일> " + imageFiles!![i].toString()
            println(fileName)
        }

        myPicture.imagePath = imageFiles!![curIndex].toString()
        textTotal.text = (imageFiles!!.size-1).toString()
        textIndex.text = curIndex.toString()

        btnPrev.setOnClickListener {

            if (curIndex == 1) {
                curIndex = imageFiles!!.size
            } else {
                myPicture.imagePath = imageFiles!![--curIndex].toString()
                textIndex.text = curIndex.toString()
                myPicture.invalidate()
            }

        }

        btnNext.setOnClickListener {

            if (curIndex >= imageFiles!!.size-1) {
               curIndex = 0
            } else {
                myPicture.imagePath = imageFiles!![++curIndex].toString()
                textIndex.text = curIndex.toString()
                myPicture.invalidate()
            }

        }

    }
}

 

 

 

 

 

 

 

 

 

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


✏️  일기장 앱을 내장 메모리가 아닌 sd카드의 mydiary 폴더에 저장되도록 작업. 단, SD카드에 mydiary가 없으면 kotlin 코드에서 자동 생성되게 함

 

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

    <DatePicker
        android:id="@+id/datePicker"
        android:datePickerMode="spinner"
        android:calendarViewShown="false"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:padding="20dp"
        android:background="#E7D9F6"
        android:layout_weight="1"
        android:lines="8"/>

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:enabled="false"
        android:text="BUTTON" />

</LinearLayout>

 

 

MainActivity 코드
class MainActivity : AppCompatActivity() {
    lateinit var datePicker: DatePicker
    lateinit var editText: EditText
    lateinit var btn: Button
    lateinit var fileName: String 

    /* 추가된 코드 */
    lateinit var savePath: String // 저장 경로
    var isFlag = false // 디렉토리 생성을 저장

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

        title = "간단 일기장"

        // 접근 권한 요청
        ActivityCompat.requestPermissions(this,
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE)

        datePicker = findViewById(R.id.datePicker)
        editText = findViewById(R.id.editText)
        btn = findViewById(R.id.btn)

        val calendar = Calendar.getInstance()
        val calendarYear = calendar.get(Calendar.YEAR)
        val calendarMonth = calendar.get(Calendar.MONTH)
        val calendarDay = calendar.get(Calendar.DAY_OF_MONTH)
        
        // 외부 경로 지정
        savePath = Environment.getExternalStorageDirectory().absolutePath + "/MyDiary"

        fileName = "${calendarYear}_${calendarMonth + 1}_${calendarDay}.txt"
        val str = readDiary(fileName) // 날짜에 해당하는 일기 파일을 읽기
        editText.setText(str) // 에디트텍스트에 일기 내용을 출력
        btn.isEnabled = true // 버튼 활성화

        datePicker.init(
            calendarYear,
            calendarMonth,
            calendarDay,
            DatePicker.OnDateChangedListener() { datePicker: DatePicker, year: Int, month: Int, day: Int ->
                makeDir() // 새로운 디렉토리 생성 함수 ( 있으면 다시 생성 x )
                
                fileName = "${year}_${month + 1}_${day}.txt"
                val string = readDiary(fileName) // 날짜에 해당하는 파일을 읽기
                editText.setText(string) // 에디트텍스트에 일기 내용을 출력
                Toast.makeText(applicationContext, fileName, Toast.LENGTH_SHORT).show()
                btn.isEnabled = true // 버튼 활성화
            })


        btn.setOnClickListener {
            val outputStream = FileOutputStream("$savePath/$fileName") // 지정 경로 이름으로 저장
            val string = editText.text.toString() // 입력값 저장
            outputStream.write(string.toByteArray())
            outputStream.close()
            Toast.makeText(applicationContext, "${fileName} 이 저장됨", Toast.LENGTH_SHORT).show()
        }
    }


    private fun readDiary(fileName: String): String? {
        var diaryStr: String? = null
        val inputStream: FileInputStream

        try {
            inputStream = FileInputStream("${savePath}/${fileName}") // 지정 파일 불러오기
            val txt = ByteArray(inputStream.available())
            inputStream.read(txt)
            inputStream.close()
            diaryStr = txt.toString(Charsets.UTF_8).trim()
            btn.text = "수정하기"
        } catch (e: IOException) {
            editText.hint = "일기 없음"
            btn.text = "새로 저장"
        }
        return diaryStr;
    }

    private fun makeDir() {
        if (!isFlag) { // 외부에 디렉토리가 생성되어 있지 않으면 생성
            // 저장 경로 생성
            val myDir = File(savePath)
            myDir.mkdir()
            isFlag = true
        }
    }

}

 

 

 

 

 

 

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


1.  activity_main.xml

데이터 피커, 에디트텍스트, 버튼을 1개씩 생성

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <DatePicker
        android:id="@+id/datePicker"
        android:datePickerMode="spinner"
        android:calendarViewShown="false"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:padding="10dp"
        android:background="#F0FFE6"
        android:layout_weight="1"
        android:lines="8"/>

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:enabled="false"
        android:text="BUTTON" />

</LinearLayout>

 

android:lines="8"

 

    ✓  에디트텍스트를 8행으로 설정

 

android:enabled="false"


    ✓  초기에 버튼을 disable 되게 하고, java 코드에서 enable 되게 함

 


 

2.  Kotlin 코드 작성 및 수정

  • activity_main의 3개 위젯에 대응할 위젯 변수 3개를 선언
  • 파일 이름을 저장할 문자열 변수 1개를 선언. 파일 이름을 '연_월_일.txt'로 지정
  • 위젯 변수에 activity_main.xml의 위젯을 대입
  • 데이트피커를 설정
  • Calendar 클래스를 이용하여 현재 날짜의 년, 월, 일을 구한 후 데이트피커를 초기화 ➡️ 데이트피커의 날짜가 변경되면 변경된 날짜에 해당하는 일기 파일(연_월_일.txt)의 내용을 에디트텍스트로 보여줌
  • 현재 날짜의 파일(연_월_일.txt)을 읽어 일기의 내용을 반환하는 readDiary() 메서드 완성
class MainActivity : AppCompatActivity() {
    lateinit var datePicker: DatePicker
    lateinit var editText: EditText
    lateinit var btn: Button
    lateinit var fileName: String // 파일 이름 저장

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

        title = "간단 일기장"

        datePicker = findViewById(R.id.datePicker)
        editText = findViewById(R.id.editText)
        btn = findViewById(R.id.btn)

        val calendar = Calendar.getInstance()
        val calendarYear = calendar.get(Calendar.YEAR)
        val calendarMonth = calendar.get(Calendar.MONTH)
        val calendarDay = calendar.get(Calendar.DAY_OF_MONTH)

        fileName = "${calendarYear}_${calendarMonth + 1}_${calendarDay}.txt"
        val str = readDiary(fileName) // 날짜에 해당하는 일기 파일을 읽기
        editText.setText(str) // 에디트텍스트에 일기 내용을 출력
        btn.isEnabled = true // 버튼 활성화


        datePicker.init(
            calendarYear,
            calendarMonth,
            calendarDay,
            DatePicker.OnDateChangedListener() { datePicker: DatePicker, year: Int, month: Int, day: Int ->
                fileName = "${year}_${month + 1}_${day}.txt"
                val string = readDiary(fileName) // 날짜에 해당하는 파일을 읽기
                editText.setText(string) // 에디트텍스트에 일기 내용을 출력
                Toast.makeText(applicationContext, fileName, Toast.LENGTH_SHORT).show()
                btn.isEnabled = true // 버튼 활성화
            })

        btn.setOnClickListener {
            val outputStream = openFileOutput(fileName, Context.MODE_PRIVATE)
            val string = editText.text.toString()
            outputStream.write(string.toByteArray())
            outputStream.close()
            Toast.makeText(applicationContext, "${fileName} 이 저장됨", Toast.LENGTH_SHORT).show()
        }
    }

    private fun readDiary(fileName: String): String? {
        var diaryStr: String? = null
        val inputStream: FileInputStream

        try {
            inputStream = openFileInput(fileName)
            val txt = ByteArray(inputStream.available())
            inputStream.read(txt)
            inputStream.close()
            diaryStr = txt.toString(Charsets.UTF_8).trim()
            btn.text = "수정하기"
        } catch (e: IOException) {
            editText.hint = "일기 없음"
            btn.text = "새로 저장"
        }
        return diaryStr;
    }

}

 

inputStream = openFileInput(fileName)


    ✓ 일기 파일을 열어 입력 파일 스트림에 저장. 파일이 없으면 예외가 발생해서 catch 구문이 실행

 


 

3.   프로젝트 실행 및 결과 확인

선택된 날짜에 쓴 일기가 있다면 일기 내용이 보이고 버튼이 <수정하기>로 바뀜
선택된 날짜에 일기가 없다면 에디트텍스트에 '일기 없음' 힌트가 보이고 버튼이 <새로 저장>으로 바뀜

 

 

 


4. 파일 확인

AVD가 켜진 상태에서 Divice File Explorer 실행
View  ▶️  Tool Windows  ▶️  Divice File Explorer

 

 

 

 

 

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


1.  파일 처리의 응용

음악, 영상, 그림 파일 등은 여러 응용 프로그램에서 사용되는 경우가 많음.
  📍  예를 들어 MP3 파일을 안드로이드에 내장된 앱에서 들을 수도 있고 별도의 음악 플레이어로 들을 수도 있음
         ➡️  이런 경우 SD 카드에 저장하여 활용

안드로이드에서는 SD 카드에 저장된 데이터에 특별한 인증 절차 없이 접근 가능하며 제한된 공간의 내장 메모리보다 훨씬 큰 공간을 사용할 수 있으며 확장성도 뛰어남. 실제 안드로이드폰의 경우 주로 마이크로 SD 카드를 장착하여 사용. AVD에도 가상 SD 카드를 장착할수 있음.

 

AVD Manager를 실행 후 해당 AVD 이름의 오른쪽에 있는 Edit 아이콘을 클릭한후 Show Advanced Settings를 클릭하면 사용하는 AVD에 장착된 SD 카드를 확인할 수 있다. 만약 가상 SD 카드가 만들어져 있지 않으면 원하는 크기를 새로 입력.

 

 

Target SDK 버전 변경


    Android SDK 30 버전에서 SD 카드의 처리 방법이 대폭 변경되어 접근 거부가 발생할 수 있음
      ➡️  이럴 때는 Target SDK 버젼을 29로 변경

android {
    compileSdk 32

    defaultConfig {
        applicationId "kr.somin.sdkstream"
        minSdk 26
        targetSdk 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

 

1)  SD 카드에서 파일 읽기

💫  폴더 경로가 다른 것만 제외하면 SD 카드의 기본적인 경로는 내장 메모리에서 파일을 읽을 때와 별다른 차이가 없음
💫  먼저 Device File Explorer에서 /sdcard 폴더 또는 /storage/emulated/0 폴더에 적당한 텍스트 파일을 upload
💫  AVD에서 sd 카드의 경로는 절대적인 것이 아니며 SDK 버전에 따라 달라질수 있음

 

 

💫  AndroidManifest.xml 파일에 SD 카드를 사용할 수 있도록 퍼미션을 지정해야 함
       ➡️  AndroidManifest.xml에 퍼미션 및 application 관련 속성을 추가

  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application
        android:requestLegacyExternalStorage="true"

 

SD 카드에서 파일 읽기
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnRead"
        android:text="SD 카드에서 파일 읽기" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editSD"
        android:lines="10" />

</LinearLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val btnRead = findViewById<Button>(R.id.btnRead)
        val editSD = findViewById<EditText>(R.id.editSD)
        
        ActivityCompat.requestPermissions(this, 
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE)
        
        btnRead.setOnClickListener {
            try {
                val inputStream = FileInputStream("/storage/emulated/0/Download/test.txt")
                val txt = ByteArray(inputStream.available())
                inputStream.read(txt)
                editSD.setText(txt.toString(Charsets.UTF_8))
                inputStream.close()
            } catch (e: IOException) {
                println(e.printStackTrace())
                Toast.makeText(applicationContext, "에러", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

 

ActivityCompat.requestPermissions(this,
    arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
    Context.MODE_PRIVATE)


    앱에서 파일 엑세스 작업을 허용할지를 묻는 창이 나옴. <허용>을 클릭.

val inputStream = FileInputStream("/storage/emulated/0/Download/test.txt")


    파일 입력 스트림을 생성할 때 SD 카드의 절대 경로를 FileInputStream() 생성자에 지정

 


2)  SD 카드에 폴더 및 파일 생성

SD 카드의 파일을 좀 더 정밀하게 처리하려면 우선 Environment 클래스의 정적 메소드를 이용하여 SD 카드의 동작 여부와 관련 폴더 경로를 구해야 함

SD 카드에 폴더 및 파일 생성
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnMkDir"
        android:text="SD 카드에 디렉토리 생성" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnRmDir"
        android:text="SD 카드에 디렉토리 삭제" />

</LinearLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_01)
        
        ActivityCompat.requestPermissions(this, 
            arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE),
            Context.MODE_PRIVATE)

        val btnMkdir = findViewById<Button>(R.id.btnMkDir)
        val btnRmdir = findViewById<Button>(R.id.btnRmDir)

        val strSDpath = Environment.getExternalStorageDirectory().absolutePath
        val myDir = File("${strSDpath}/MyDir")

        btnMkdir.setOnClickListener {
            myDir.mkdir()
        }

        btnRmdir.setOnClickListener {
            myDir.delete()
        }
    }
}

 

val strSDpath = Environment.getExternalStorageDirectory().absolutePath

 

    ✓  SD 카드의 절대 경로를 돌려주는데, 대개 /sdcard 폴더 또는 /storage/emulated/0

    ✓  getExternalStorageDirectory() 이외에도 시스템 폴더를 반환하는 getRootDirectory(),

         데이터 폴더를 반환하는 getDataDirectory()등이 있음

 

val myDir = File("${strSDpath}/MyDir")


    ✓  SD 카드의 경로 아래에 MyDir 폴더를 생성하기 위한 File형 변수를 설정

 

 

 


3)  특정 폴더의 하위 폴더 및 파일 목록

지정한 폴더의 하위 폴더 및 파일 목록에 접근하려면 File.listFiles() 메서드를 사용  ➡️  반환형은 File[] 형

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnFileList"
        android:text="시스템 폴더의 폴더/파일 목록" />

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

</LinearLayout>
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_02)

        val btnFileList = findViewById<Button>(R.id.btnFileList)
        val editFileList = findViewById<EditText>(R.id.editFileList)
        btnFileList.setOnClickListener {
            val sysDir = Environment.getRootDirectory().absolutePath
            val sysFiles = File(sysDir).listFiles()

            var strFname: String
            for (i in sysFiles.indices) {
                if (sysFiles[i].isDirectory == true)
                    strFname = "<폴더> " + sysFiles[i].toString()
                else
                    strFname = "<파일> " + sysFiles[i].toString()

                editFileList.setText(editFileList.text.toString() + "\n" + strFname)
            }
        }
    }
}

 

val sysDir = Environment.getRootDirectory().absolutePath


    ✓  안드로이드 시스템 경로를 반환

 

val sysFiles = File(sysDir).listFiles()

 

    ✓  시스템 폴더의 폴더 및 파일 목록을 구해서 배열로 반환

 

if (sysFiles[i].isDirectory == true)


    ✓  현재 파일이 폴더인지 파일인지 확인

 

 

 

 

 

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

+ Recent posts