이 글은 이것이 안드로이드다 with 코틀린(개정판)를 참고하여 작성하였습니다.
작성자 : 김태경
개발환경은 Windows, Android Studio입니다.
취소버튼 구현
private val mUndoPaths = ArrayList<CustomPath>()
fun onClickUndo(){
if(mPath.size > 0){
mUndoPaths.add(mPaths.removeAt(mPaths.size - 1)) // mPaths에서 아이템을 지움. -> 지운 아이템을 받아서 mUndoPaths에 추가.
invalidate() // onDraw 불러옴.
}
}
val ibUndo : ImageButton = findViewById(R.id.ib_undo)
ibUndo.setOnClickListner {
drawingView?.onClickUndo()
}
코루틴
- 안드로이드에서는 작업이 완료될 때까지 UI가 차단됨. 긴 시간이 걸리는 작업의 경우, 안드로이드는 UI 시스템을 최대 5초까지만 지연되도록 만듦.
- 하지만, 코루틴을 사용하면 오래 걸리는 작업을 백그라운드의 다른 스레드로 넘겨서 UI 스레드가 차단되지 않고 계속 실행돼서 사용자가 막힘없이 앱을 사용하도록 만들 수 있음!
코루틴의 기능
- 경량: 코루틴을 실행 중인 스레드를 차단하지 않는 정지를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있음. 정지는 많은 동시 작업을 지원하면서도 차단보다 메모리를 절약함.
- 메모리 누수 감소: 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행함.
- 기본으로 제공되는 취소 지원: 실행 중인 코루틴 계층 구조를 통해 자동으로 취소가 전달됨.
- Jetpack 통합: 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있음. 일부 라이브러리는 구조화된 동시 실행에 사용할 수 있는 자체 코루틴 범위도 제공함.
코루틴 만들기
- build.gradle에서 dependencies에 implementation “androidx.activity:activity-ktx:1.6.0 추가.
- import kotlinx.coroutines.Dispatchers , import kotlinx.coroutines.withContext
- suspend 키워드를 사용한다. → function이 UI를 방해하는 것을 멈추게 함.
val btnExecute: Button = findViewById(R.id.btn_execute)
btnExecute.setOnClickListner {
lifecycleScope.launch{ // lifecycleScope 범위(망가지면 자동으로 취소)에서 실행하도록 설정
execute("Task executed successfully")
}
}
private suspend fun execute(result:String){
withContext(Dispatchers.ID){ // 프로세스가 완료될 때까지 작업을 다른 스레드로 이동시키는 메서드
for(i in 1..100000){
Log.e("delay : ", "" + i)
}
runOnUiThread { // Dispatch.IO 스레드에서 실행 X -> runOnUiThread 메서드 호출 -> UI 스레드에서 실행 O
Toast.makeText(
this@MainActivity, result,
Toast.LENGTH_SHORT
).show()
}
}
}
→ 앱이 중단되지 않고 계속 실행하게 된다.
데이터 쓰기(저장하기)
1. Path 만들기
- name=”captured” → Path 이름 설정
- path="Android/data/com.example.kidsdrawingapp/files" → 저장할 위치 설정
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="captured"
path="Android/data/com.example.kidsdrawingapp/files"/>
</paths>
2. Manifest 파일에서 permission, provider, meta-data 추가
- permission 추가
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- provider 추가
- authorities → authorities 설정
- name → provider 이름 설정
- exported → 다른 앱에서 제공자 사용 여부 설정
- grantUriPermissions → 애플리케이션 구성요소가 권한으로 보호되는 데이터에서 일회성으로 액세스 여부
- meta-data 추가
- name → 일반적으로 android.support.FILE_PROVIDER_PATHS 로 설정
- resource → 만들어놓은 @xml/Path.xml로 설정
<provider
android:authorities="com.example.kidsdrawingapp.fileprovider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true"
>
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/path"
/>
3. bitmap으로 return하는 메서드 만들기
- view는 저장할 수 없기 때문에 bitmap으로 변환해서 저장해야 한다.
private fun getBitmapFromView(view: View) : Bitmap {
val returnedBitmap = Bitmap.createBitmap(view.width,
view.height, Bitmap.Config.ARGB_8888) // view -> bitmap
val canvas = Canvas(returnedBitmap) // canvas = 그림 표현
val bgDrawable = view.background // bgDrawable = 배경
if(bgDrawable != null){ // 배경이 있으면
bgDrawable.draw(canvas) // canvas를 bgDrawable 위에 그림.
}else{ // 배경이 없으면
canvas.drawColor(Color.WHITE) // canvas 컬러 -> 흰색
}
view.draw(canvas) // canvas를 view 위에 그림.
return returnedBitmap
}
4. bitmap을 기기에 저장하기
- 코루틴 활용 → bitmap 압축 및 디렉토리에 파일 저장
private suspend fun saveBitmapFile(mBitmap: Bitmap?): String{
var result = "" // 이미지를 저장할 변수
withContext(Dispatchers.IO){ // 스레드 추가
if(mBitmap != null){
try{ // null 값이 들어오면 오류가 발생할 수 있기 때문에 try 블록 사용
val bytes = ByteArrayOutputStream() // 바이트 배열 출력 스트림을 생성, 초기 버퍼 용량 = 32bytes
mBitmap.compress(Bitmap.CompressFormat.PNG, 90, bytes) // mBitmap을 compress로 압축(PNG 형태, 퀄리티, 출력 스트림)
val f = File(externalCacheDir?.absoluteFile.toString()
+ File.separator + "KidsDrawingApp_" + System.currentTimeMillis() /1000 + ".png"
) // 디렉토리를 포함하여 이름을 저장
val fo = FileOutputStream(f) // 파일에 출력 스트림 적용
fo.write(bytes.toByteArray()) // ByteArrayOutputStream()을 가져와서 ByteArray 만듦
fo.close() // 파일 출력 스트림 닫음 -> 시스템 리소스 해제
result = f.absolutePath // absolutePath를 이용해 결과를 result에 대입
runOnUiThread{
if(result.isNotEmpty()){ // 보관된 장소 알려줌
Toast.makeText(
this@MainActivity,
"File saved successfully :$result",
Toast.LENGTH_SHORT
).show()
}else{ // 결과가 비어있다면, 파일 저장이 잘못되었다고 알려줌
Toast.makeText(
this@MainActivity,
"Something went wrong while saving the file.",
Toast.LENGTH_SHORT
).show()
}
}
}
catch (e: java.lang.Exception){
result = ""
e.printStackTrace()
}
}
}
return result // 문자열 result 반환
}
- 세이브 버튼 눌렀을 시, 발생 구현
val ibSave : ImageButton = findViewById(R.id.ib_save)
ibSave.setOnClickListener{
if(isReadStorageAllowed()){
lifecycleScope.launch{ // lifecycleScope 범위(망가지면 자동으로 취소)에서 실행하도록 설정
val flDrawingView:FrameLayout = findViewById(R.id.fl_drawing_view_container)
saveBitmapFile(getBitmapFromView(flDrawingView))
}
}
}
커스텀 실행 다이얼로그
- 커스텀 실행 다이얼로그 메서드 정의 및 세이브 과정 중 적절한 부분에 커스텀 실행 다이얼로그를 삽입
private fun showProgressDialog(){ // 커스텀 실행 다이얼로그 보이게 하는 메서드
customProgressDialog = Dialog(this@MainActivity)
customProgressDialog?.setContentView(R.layout.dialog_custom_progress)
customProgressDialog?.show()
}
private fun cancelProgressDialog(){ // 커스텀 실행 다이얼로그 사라지게 하는 메서드
if(customProgressDialog != null){
customProgressDialog?.dismiss()
customProgressDialog = null
}
}
val ibSave : ImageButton = findViewById(R.id.ib_save)
ibSave.setOnClickListener{
if(isReadStorageAllowed()){
showProgressDialog() // 이 부분에 쇼우 커스텀 실행 다이얼로그 메서드 삽입
lifecycleScope.launch{
saveBitmapFile(getBitmapFromView(flDrawingView))
}
}
}
private suspend fun saveBitmapFile(mBitmap: Bitmap?): String{
var result = ""
withContext(Dispatchers.IO){
if(mBitmap != null){
try{
val bytes = ByteArrayOutputStream()
mBitmap.compress(Bitmap.CompressFormat.PNG, 90, bytes)
val f = File(externalCacheDir?.absoluteFile.toString()
+ File.separator + "KidsDrawingApp_" + System.currentTimeMillis() /1000 + ".png"
)
val fo = FileOutputStream(f)
fo.write(bytes.toByteArray())
fo.close()
result = f.absolutePath
runOnUiThread{
cancelProgressDialog() // 이 부분에 캔슬 커스텀 실행 다이얼로그 메서드 삽입
if(result.isNotEmpty()){
Toast.makeText(
this@MainActivity,
"File saved successfully :$result",
Toast.LENGTH_SHORT
).show()
}else{
Toast.makeText(
this@MainActivity,
"Something went wrong while saving the file.",
Toast.LENGTH_SHORT
).show()
}
}
}
catch (e: java.lang.Exception){
result = ""
e.printStackTrace()
}
}
}
return result
}
이메일, 왓츠앱 등으로 이미지를 공유하는 공유 기능
private fun shareImage(result:String){
MediaScannerConnection.scanFile(
this@MainActivity, arrayOf(result), null
) { path, uri ->
// 이미지가 저장된 이후에 공유 실행
val shareIntent = Intent()
shareIntent.action = Intent.ACTION_SEND
shareIntent.putExtra(
Intent.EXTRA_STREAM,
uri
)
shareIntent.type =
"image/png"
startActivity(
Intent.createChooser(
shareIntent,
"Share"
)
)
}
}
'GDSC HUFS 4기 > Kotlin Team #6' 카테고리의 다른 글
[6팀]드로잉 앱(권한 데모부터 갤러리 이미지선택까지) (0) | 2022.11.17 |
---|---|
[6팀] 드로잉 앱 - 드로잉 뷰, 브러쉬, DisplayMatrix (0) | 2022.11.14 |
[6팀] 드로잉 앱-캔버스 사용법,이미지 불러오고 내보내기 (0) | 2022.11.14 |
[6팀] 계산기 - XML 사용법과 UI 생성법 배우기 (0) | 2022.10.31 |
[6팀] 코틀린으로 분 단위 계산기 만들기 (1) | 2022.10.31 |