앱 프로젝트 - 05 - 1 ( 전자액자 ) - Layout(화면 가로 그리기), Android Permission, View Animation, Activity Lifecycle -> 액티비티의 생명주기, Content Provider (SAF - Storage Access Framework) -> 로컬 이미지(사진) 가져오기, timer 쓰래드, 화면 가로로 설정하기

하이루·2022년 1월 18일
0

소개

스마트폰에 저장된 사진들을 보여주는 앱


레이아웃 소개


시작하기에 앞서 알고갈 것들

Android Permission -> 사용자에게 권한 요청하기 (권한 팝업)

--> 로컬에 저장된 사진을 가져오는 등의 특정 기능들은 사용자가 권한을 부여해줘야만 가능함
--> 따라서 사용자에게 권한 요청을 할 필요가 있음 ( 일반적으로 권한 팝업이 뜸 )

[권한 요청의 과정]

A
-> A 부분에서 권한이 이미 있는지 확인 -> 있으면 바로 C로 가서 해당 기능 실행
-> 없으면 B로 넘어가서 권한 요청팝업을 띄움 -> B로 이동

B
-> 5b를 보면 권한요청 팝업에서 유저에게 왜 권한이 필요한지 설명할 수 있는데, 이 팝업을 개발자가 구성할 수 있다. ( 있어도되고 없어도 됨 )
-> 6번에서 권한요청 팝업을 통해 권한을 요청,, 이후 허용하면 C로 가서 해당 기능 실행 , 허용하지 않았다면 해당 기능이 실행되지 않음

권한 요청의 방법

  1. 이미 권한이 부여되어 있는지 확인

  2. 권한이 없다면 권한요청 팝업을 띄움

    ( 권한요청에 대한 설명 팝업이 있다면 그것부터 띄움 --> 여기서 권한이 필요한 이유 설명 )

권한 요청 코드

예시 1))))))) -> Manifest에서 먼저 권한에 대한 명시

Manifest에서 권한을 명시해야만 사용자에게 권한을 요청하던지 할 수 있음

예시 2))))))) -> 버튼을 클릭했을 경우, 권한을 확인하고 요청을 하는 코드


    private fun initAddPhotoButton(){
        addPhotoButton.setOnClickListener {
            
            // TODO 권한을 요청하는 부분
            when {
                
                // 이미 권한이 부여되어 있는지 확인
                ContextCompat.checkSelfPermission(
                    this,
                    android.Manifest.permission.READ_EXTERNAL_STORAGE
                ) == PackageManager.PERMISSION_GRANTED -> {
                    //TODO 권한이 잘 부여되었을 때 갤러리에서 사진을 선택하는 기능
                }

                // 위에서 권한이 부여되어 있지 않다고 했으므로, 권한 부여의 필요성을 설명하기 위한 팝업이 있는지 확인
                shouldShowRequestPermissionRationale(android.Manifest.permission.READ_EXTERNAL_STORAGE) -> {
                    //TODO 교육용 팝업 확인 후 권한 팝업을 띄우는 기능
                }
                
                // 권한부여가 되어있지 않고, 교육용팝업도 없으므로 권한부여 요청
                else -> {
                    //TODO 권한을 요청하는 팝업을 띄우는 기능
                    requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), 1000)
                    
                }


            }
        }

    }

1. ContextCompat.checkSelfPermission() -> 해당 앱이 사용자로부터 Permission을 이미 부여받았는지 확인

 ContextCompat.checkSelfPermission( this, android.Manifest.permission.READ_EXTERNAL_STORAGE )

해당 코드는 권한이 있는지 확인하는 코드로 첫번째 파라미터로 현재 위치를 받고, 두번째 파라미터로 어떤 Permission에 대한 것인지를 받는다.
(위의 경우, 이미지를 가져올 권한에 대한 Permission이 있는지 확인)

이후ㅡ,

  • Permission이 있다면 PackageManager.PERMISSION_GRANTED을 반환
  • Permission이 없다면 PackageManager.PERMISSION_DENIED을 반환

2. shouldShowRequestPermissionRationale() -> 사용자에게 Permission을 요청하기에 앞서 교육용 팝업을 띄울지 확인

 shouldShowRequestPermissionRationale(android.Manifest.permission.READ_EXTERNAL_STORAGE)
  • 첫번째 파라미터로 어떤 Permission에 대한 것인지를 받는다.

이 코드는 해당 Permission에 대해 권한요청에 앞서 교육용팝업을 띄울 것이라고 했는지 확인하는 코드이다.

3. requestPermissions() -> Permission을 사용자에게 요청

권한 요청 팝업을 띄우는 메소드이다.

  • 첫번째 파라미터로 권한요청을 띄울 Permission들을 Array형태로 받고,

  • 두번째 파라미터로 requestCode를 받는다. -> 아래의 onRequestPermissionsResult에서 사용 ( 어떤 requestPermissions인지 식별하기 위해 사용 )

  • 여기서 Permission을 Array형태로 받는 이유는 필요한 Permission이 많을 경우 하나의 Array에 넣어서 한번에 Permission받기 위해서이다.

예시 3))))))) onRequestPermissionResult -> 바로 위에 requestPermissions 를 통해 사용자에게 권한요청을 한 후, 사용자가 선택을 마치면 실행됨 ( 오버라이드 메소드 )

    
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {

          
            1000 -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    //ToDo 권한이 부여됬다는 뜻
                    // 권한 부여 이후에 일어날 일들이 여기에 들어감

                } else {
                    Toast.makeText(this,"권한을 거부하셨습니다.",Toast.LENGTH_SHORT).show()
                }

            }
            else -> {

            }

        }
    }

해당 메소드는 실행시에

  • 첫번째 파라미터(requestCode)requestPermissions()메소드 실행시 담은 requestCode를 가져온다

    이 requestCode의 번호를 바탕으로 어떤 지점의 requestPermissions()으로부터 왔는지 파악할 수 있다.
    ( 각각의 영역에서 실행될 Permission 요청들은 많지만 onRequestPermissionResult함수는 하나이기 때문에 이런 구분이 필요 )

  • 두번째 파라미터(permissions)requestPermissions()메소드 실행시 Array로 담은 Permission들을 Array형태 그대로로 가져온다.

  • 세번째 파라미터(grantResults)로 두번째 파라미터에서 받은 각 Permission에 대해 Grant인지 Denied인지 Array형태로 가져온다.

    예를들어)
    permissions[0]에 대해서 권한이 주어졌으면, grantResult[0]이 PackageManager.PERMISSION_GRANTED일 것이고,
    permissions[0]에 대해서 권한이 주어지지 않았으면, grantResult[0]이 PackageManager.PERMISSION_DENIED가 된다.


View Animation


Content Provider --> 로컬에서 컨텐츠 가져오기

로컬에서 컨텐츠들에 접근하기 위해 사용

이 중에서 이번에는 이미지에 대한 것을 사용할 것
--> 로컬에서 이미지를 가져오는 것이므로 당연하게도 일차적으로 Permission을 받았어야 가능함

Content Provider를 통해 이미지를 가져오는 방식은
직접 이미지를 가져오는 화면을 구현해서 가져오는 방법이 있고,
일반적으로 사용하는 SAF기능을 사용해서 가져오는 방법이 있다.

여기서는 Content Provider의 여러 기능들 중에 SAF를 이용해서 사진을 가져올 것이다.

SAF - Storage Access Framework

--> 간결하고 사용자 친화적임 ( 대부분이 사용하는 방식이므로 UI가 사용자에게 익숙함 )

--> SAF방식은 우리가 일반적으로 사용하는 Intent로 액티비티 여는 방식과 같다.

예제 코드 01 -> Intent설정을 통해 SAF 실행 ( 사진 선택 창 나타남 )

    private fun navigatePhotos(){
 
        val intent = Intent(Intent.ACTION_GET_CONTENT)
        intent.type = "image/*"
        startActivityForResult(intent,2000)

    }

navigatePhotos라는 함수를 새로 만들어서 사용했다.

  • Intent는 첫번째 파라미터로 intent를 통해 실행하고자 하는 액티비티의 주소값을 받는다.

    SAF에서는 Intent.ACTION_GET_CONTENT을 넣어줬는데, Intent.ACTION_GET_CONTENT는 ( SAF를 이용해서 Content를 가져오는 기능을 가진 ) 안드로이드에 내장되어있는 액티비티이다.

  • intent의 type 변수의 값을 "image/*"로 설정했다.
    --> 이 부분은 SAF를 통해 가져올 컨텐츠를 추리는 부분이다.
    "image/*"로 하는 것으로 png, jpg등등의 여러 이미지 타입의 확장자를 가진 컨텐츠들만 필터링해서 가져올 수 있게됨

  • startActivityForResult를 통해 액티비티 실행
    startActivityForResult는
    첫번째 파라미터로 실행시킬 Intent를 받고,
    두번째 파라미터로 requestCode를 받는다.

    또한 startActivityForeResult로 액티비티를 실행시킨 경우 startActivity와는 다르게
    해당 액티비티가 종료되면 onActivityResult() 함수를 실행시킨다. ( 이때 requestCode를 해당 함수에 넘겨줌 )

예제코드 02 -> 사진을 선택하면 액티비티가 종료되며 onAcitivityResult함수를 통해 선택한 사진에 대한 데이터를 받아서 처리함


......

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

......

    
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        
        if (resultCode != Activity.RESULT_OK){
            return
        }

        when (requestCode) {
            2000 -> {
                val selectedImageUri: Uri? = data?.data
                
                if (selectedImageUri != null) {
                  
                     imageView.setImageURI(selectedImageUri)
                
                } else {
                     Toast.makeText(this, "사진을 가져오지 못했습니다", Toast.LENGTH_SHORT).show()
                }
           
            }
            else -> {
                Toast.makeText(this,"사진을 가져오지 못했습니다",Toast.LENGTH_SHORT).show()
            }
        }
    }

onAcitivityResult() 오버라이드 함수startActivityForResult()로 실행시킨 액티비티가 종료되었을 때 실행되는 함수이다.

onAcitivityResult() 함수는

  • 첫번째 파라미터(requestCode)startActivityForResult()의 두번째 파라미터로 받은 requestCode를 가져온다.

    해당 requestCode를 통해 이 액티비티가 어떤 액티비티인지 구분하고 그에 따라 처리할 수 있다.
    ( 각각의 영역에서 실행될 액티비티들은 많지만 onActivityResult함수는 하나이기 때문에 이런 구분이 필요 )

  • 두번째 파라미터(resultCode)로 해당 액티비티가 정상적으로 실행하여 리턴을 내놓았는지 여부를 가져온다.

    정상적으로 진행되었다면 Activity.RESULT_OK을 보내도록 코딩하며 ( setResult()함수를 이용 ),
    정상적으로 진행되지 않았다면 Activity.RESULT_CANCELED를 보내도록 코딩한다.
    따라서 정상적으로 진행되었다면 Activity.RESULT_OK가 들어갈 것이다.

  • 세번째 파라미터(data)로 실행되었던 액티비티에서 데이터를 Intent에 담아서 가져온다.

그리고 SAF의 경우
이렇게 가져온 데이터는 Uri 타입의 데이터이므로 고려해서 처리해주면 된다.


Intent를 통해 액티비티를 실행시킬 때 startActivityForResult()가 왜 필요할까??

이유는 Activity끼리는 데이터를 주고 받을 수 없기 때문 !

따라서 Intent를 이용하여 데이터를 주고 받기 위해서 startActivityForResult()를 이용하는 것 !


Activity Lifecycle --> 액티비티의 생명주기는 어떻게 될까?

Activity Lifecycle에 대한 공식문서 : https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko

  1. Activity가 Launched

  2. onCreate() 함수 실행 ( Activity가 Launched되었을 때 첫번째로 호출되는 함수 )
    --> 일반적으로 여기서 사용자 인터페이스 선언(xml 선언), 멤버 변수 정의, 일부 UI구성 등등의 활동을 한다.

  3. onStart() 함수 실행

  4. onResume() 함수 실행
    -> 이 함수까지 실행된 이후에야 화면이 사용자에게 나타남 ( Activity running )

5-1. onPause()
--> 사용자가 활동을 떠나는 첫번째 신호
-> Activity running상태에서(즉, 앱 실행도중), 카톡을 하러 간다던가, 전화를 하러 간다던가 등 잠깐 다른 것을 하러 갈 때
onPause() 함수가 실행됨

-> onPause()는 아주 잠깐 실행되므로 저장 작업을 실행하기에는 시간이 부족할 수 있습니다. 그러므로 onPause()를 사용하여 애플리케이션 또는 사용자 데이터를 저장하거나, 네트워크 호출을 하거나, 데이터베이스 트랜잭션을 실행해서는 안 됩니다.
--> 이런 부하가 큰 작업의 경우 onStop()에서 처리하는 것이 바람직함

5-2. onPause() 상태에서 다른 것들을 하다가 다시 앱 화면으로 돌아가면
onResume()함수가 실행된 후 다시 Activity running상태에 돌입함

--> 즉, onResume()은 액티비티가 뷰 위에 올라올 때마다 실행되는 함수임 ( 앱이 기기의 메인화면을 잡을 때마다 실행된다고 보면 됨 )
--> 따라서 앱이 실행될 때마다 실행되어야하는 기능은 onResume()에 구현해줘야 함

6-1. onStop()
-> onPause() 이후에 해당 액티비티가 완전히 뷰를 벗어나게 되면, onStop()함수가 실행됨

--> 앱이 사용자에게 보이지 않는 동안 앱은 필요하지 않은 리소스를 해제하거나 조정해야 합니다.

onStop()을 이용하여 CPU를 비교적 많이 소모하는 종료 작업을 실행해야 합니다.
예를 들어 정보를 데이터베이스에 저장할 적절한 시기를 찾지 못했다면 onStop() 상태일 때 저장할 수 있습니다.

6-2. onStop()상태에서 앱을 다시 불러오면 onRestart()함수를 거친 뒤 onStart()함수를 실행, 이후 onResume()함수를 거쳐서
다시 Activity running상태로 돌입한다.
( 이처럼 onResume()은 액티비티가 뷰 위에 올라올 때마다 실행되는 함수임
--> 따라서 앱이 실행될 때마다 실행되어야하는 기능은 onResume()에 구현 )

6-3. onStop()상태에서 만약 너무 많은 프로그램이 메모리에 올라가 있다면, 필요에 따라 메모리에서 해당 앱을 지워버릴 수도 있다.( App process killed )
--> 그 경우에는 다시 앱으로 가면 onCreate()함수부터 다시 실행시키게 된다.

  1. 만약 앱을 종료한다고 하면, onStop()이후에 onDestroy()함수를 실행한 뒤에 Activity shut down상태가 되어 앱이 종료된다.

View animation

View animation은 View의 상태를 서서히 변화시켜주는 기능을 의미한다.

예시) 이미지뷰를 fadeIn 시켜주는 animation 만들기

......

    private val imageView:ImageView by lazy {
        findViewById(R.id.imageView11)
    }

......

  private fun fadeIn(){

        imageView.alpha = 0f
        imageView.animate()
            .alpha(1.0f)
            .setDuration(1 * 1000)
            .start()

    }

......

내가 만든 fadeIn() 함수를 사용하면 imageView의 alpha(선명도)가 0~1로 1초에 걸쳐서 변화한다.

View Animation을 만드는 방법

1. animate()확장함수를 통해 animation을 만들기 시작,

2. animate()함수의 하부에서 해당 뷰의 속성을 변화시킴
--> 이때 설정한 내용들은 animation이 해당 컴포넌트의 상태를 어떤 방향으로 변화시킬지를 나타낸다. ),

( 즉, setDuration()으로 설정한 시간에 걸쳐서 서서히 해당 상태로 변화하게 된다. )

예를들어, 해당 부분에서 뷰의 좌표값을 변경했다면, animation을 실행했을 때 setDuration()으로 설정한 시간에 걸쳐서
해당 뷰가 변경한 좌표값에 도달한다. --> 이것이 사용자에게는 뷰가 이동하는 것처럼 보이게 된다.

3. 마지막으로 start()함수를 통해 해당 animation을 시작해주면 된다.

setDuration()을 통해 해당 animation이 대상의 상태를 몇 초에 걸쳐서 변화시킬지 나타낸다.


timer 쓰래드 ++ Activity LifeCycle에 따른 조정


......

private var timer: Timer? = null

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_photoframe)
        
        // startTimer()

    }

......
private fun starttimer() {
	


   timer = timer(period = 5 * 1000){

        print("5초마다 등장")

        runOnUiThread{
        }
    }
}

......

    override fun onStop() {
        super.onStop()

        timer?.cancel()
    }

    override fun onStart() {
        super.onStart()

        starttimer()
    }

    override fun onDestroy() {
        super.onDestroy()

        timer?.cancel()
    }

......

timer 쓰래드는 period로 설정한 시간마다 블록의 내용을 반복한다.
그리고 새로운 쓰래드이므로 UI쓰래드에 연결시키기 위해서 runOnUiThread를 사용해 줄 수도 있다.

++++++++++
위에 설명한 Activity LifeCyle에 따라서 위의 timer쓰래드는 조정해줄 필요가 있다.
--> 이것을 고려하지 않을 경우 잘못하면 앱이 종료되고 나서도 해당 쓰래드만 돌아가는 사태도 발생할 수 있다.

  • 앱이 잠시 멈추거나, 앱이 종료했을 경우 해당 쓰래드를 종료시켜주어야 한다.

    따라서 앱이 멈출 때 호출되는 메소드인 onStop()메소드
    앱이 종료되었을 떄 호출되는 메소드인 onDestroy()메소드에
    해당 쓰래드를 종료시키는 코드를 추가
    한다.
    ( 사실 onStop()에서만 해줘도 문제없지만, 확실히 종료시키기 위해 onDestroy()에도 해준 것이다. )

  • 앱이 최초로 실행되거나, 앱이 멈췄다가 다시 실행될 경우에 쓰래드를 다시 실행시켜줘야 한다.

    따라서 앱이 시작하거나, onStop()상태에서 다시 실행상태로 돌아올 때 호출되는 메소드인 onStart()에
    해당 쓰래드를 실행시키는 코드를 추가
    한다.
    ( 또한 onStart()는 앱이 최초로 실행될 때에도 호출되는 메소드이므로 onCreate()에 굳이 쓰래드의 실행을 써줄 필요가 없다.
    --> onCreate()는 최초 실행시에만 한번 실행되므로 )


    화면 가로로 설정하기

    Manifest에서 설정할 수 있다.

activity의 속성으로 screenOrientaion을 "landscape"로 설정하면 해당 activity는 가로로 설정된다.


코드 소개

MainActivity.kt


package com.example.aop_part2_chapter5

import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {

    private val addPhotoButton: Button by lazy {
        findViewById<Button>(R.id.addPhotoButton)
    }

    private val startPhotoFrameModeButton: Button by lazy {
        findViewById<Button>(R.id.startPhotoFrameModeButton)
    }

    private val imageViewList: List<ImageView> by lazy {
        mutableListOf<ImageView>()
            .apply {
                add(findViewById<ImageView>(R.id.imageView11))
                add(findViewById<ImageView>(R.id.imageView12))
                add(findViewById<ImageView>(R.id.imageView13))
                add(findViewById<ImageView>(R.id.imageView21))
                add(findViewById<ImageView>(R.id.imageView22))
                add(findViewById<ImageView>(R.id.imageView23))

            }
    }

    private val imageUriList: MutableList<Uri> = mutableListOf()


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

        initAddPhotoButton()
        initStartPhotoFrameModeButton()
        // 이렇게 onCreate()함수에는 모듈화된 함수만 넣는 방식을 추상화라고 한다.( 추상화로 하면 이후에 코드 파악에 도움이 됨 )

        
    }

    private fun initAddPhotoButton() {
        addPhotoButton.setOnClickListener {

            // TODO 권한을 요청하는 부분
            when {

                // 이미 권한이 부여되어 있는지 확인
                ContextCompat.checkSelfPermission(
                    this,
                    android.Manifest.permission.READ_EXTERNAL_STORAGE
                ) == PackageManager.PERMISSION_GRANTED -> {
                    //TODO 권한이 잘 부여되었을 때 갤러리에서 사진을 선택하는 기능
                    navigatePhotos()
                }


                // 위에서 권한이 부여되어 있지 않다고 했으므로, 권한 부여의 필요성을 설명하기 위한 팝업이 있는지 확인
                shouldShowRequestPermissionRationale(android.Manifest.permission.READ_EXTERNAL_STORAGE) -> {
                    //TODO 교육용 팝업 확인 후 권한 팝업을 띄우는 기능
                    showPermissionContextPopup()
                }

                // 권한부여가 되어있지 않고, 교육용팝업도 없으므로 권한부여 요청
                else -> {
                    //TODO 권한을 요청하는 팝업을 띄우는 기능
                    requestPermissions(
                        arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),
                        1000
                    )
                    // requestPermissions이 실행되면 onRequestPermissionResult라는 함수가 자동으로 실행되면서 requestCode가 전달됨 -> 이 Code에 따라 해당 함수에서 다른 반응을 하도록 만들 수 있음

                }
            }
        }
    }


    private fun initStartPhotoFrameModeButton() {
        startPhotoFrameModeButton.setOnClickListener {
            val intent = Intent(this,PhotoFrameActivity::class.java)
            imageUriList.forEachIndexed { index, uri ->
                intent.putExtra("photo${index}", uri.toString())
            }
            // forEach를 통해 List의 각 값에 대해 해당 람다함수 실행 + index값이 필요하므로 forEachIndexed를 사용

            intent.putExtra("photoListSize",imageUriList.size)

            startActivity(intent)
            
            
        }
    }


    private fun navigatePhotos() {

        // Intent.ACTION_GET_CONTENT는 SAF를 이용해서 Content를 가져오는 기능을 가진, 안드로이드에 내장되어있는 액티비티를 가져옴
        val intent = Intent(Intent.ACTION_GET_CONTENT)

        intent.type = "image/*"
        // png, jpg등등의 여러 이미지 타입들만 필터링해서 컨텐츠로 가져올 수 있게됨

        startActivityForResult(intent, 2000)
        // startActivityForResult를 통해 실행하는 이유는 SAF기능도 다른 액티비티를 실행하여 기능하는 것이기 때문이다.
        // 이후 이렇게 실행된 액티비티에서 선택한 결과값은 onActivityResult라는 오버라이드 메소드를 실행시켜서 처리된다.

    }


    // startActivityForResult()를 통해 Intent에 해당하는 액티비티를 실행시킨 뒤, 해당 액티비티가 종료되면 결과값과 함꼐 아래의 onActivityResult 오버라이드 메소드가 실행된다.
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        // resultCode파라미터로 Activity.RESULT_OK가 들어왔다는 말은 사용자가 사진을 선택해서 결과가 반환되어 보내졌다는 의미이다.
        // 만약 사용자가 취소를 눌렀다면 Activity.RESULT_OK 반환되지 나타나지 않았을 것이다.
        if (resultCode != Activity.RESULT_OK) {
            return
        }

        when (requestCode) {
            2000 -> {
                // 혹시라도 데이터를 보내는 액티비티에서 null을 보냈을 때 오류가 발생할 수 있기 때문에 nullable하게 선언
                val selectedImageUri: Uri? = data?.data
                if (selectedImageUri != null) {

                    if (imageUriList.size == 6) {
                        Toast.makeText(this, "이미 사진이 꽉 찼습니다", Toast.LENGTH_SHORT).show()
                        return
                    }
                    imageUriList.add(selectedImageUri)
                    imageViewList[imageUriList.size - 1].setImageURI(selectedImageUri)
                } else {
                    Toast.makeText(this, "사진을 가져오지 못했습니다", Toast.LENGTH_SHORT).show()
                }
            }
            else -> {
                Toast.makeText(this, "사진을 가져오지 못했습니다", Toast.LENGTH_SHORT).show()

            }

        }

    }


    // requestPermissions메소드( Permission을 사용자에게 요청하는 메소드 )를 실행한 후, 사용자의 선택이 끝나면 실행되는 코드
    // 사용자가 권한을 부여해줬는지에 대한 결과를 나타냄
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {

            // 사용자에게 권한을 요청했을 때 requestCode로 1000을 보내도록 설정했음
            1000 -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    //ToDo 권한이 부여됬다는 뜻
                    // 권한 부여 이후에 일어날 일들이 여기에 들어감
                    navigatePhotos()

                } else {
                    Toast.makeText(this, "권한을 거부하셨습니다.", Toast.LENGTH_SHORT).show()
                }

            }
            else -> {

            }

        }
    }

    private fun showPermissionContextPopup() {
        AlertDialog.Builder(this)
            .setTitle("권한이 필요합니다.")
            .setMessage("전자액자 앱에서 사진을 불러오기 위해 권한이 필요합니다.")
            .setPositiveButton("동의하기", { dialog, which ->
                requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), 1000)
            })
            .setNegativeButton("취소하기", { dialog, which -> })
            .create()
            .show()
    }


}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <LinearLayout
        android:id="@+id/firstRowLinearLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintDimensionRatio="h,3:1"
        >
<!--        app:layout_constraintDimensionRatio="1:1" 속성-->
<!--        해당 컴포넌트를 width:height로 크기지정하는 속성 ( 따라서 이 경우 하나는 크기가 지정되어 있어야함 ) (특정 크기 혹은 wrap_content등)-->

<!--        하지만 가로세로 모두 0dp일 경우(match_parent)에도 사용할 수 있는데, 위와 같이 "h혹은w,1:1" 등으로 설정하면 된다. (제약,비율)-->
<!--        이때 의미는 예를 들어, w,1:3 -> h를 1:3에 따라 Constraint(제약)에 맞춰 3이라고 했을 때 w를 1으로 크기지정하겠다는 의미이다.-->
<!--        반면에 h,1:3의 의미는 -> w를 1:3에 따라 Constraint(제약)에 맞춰 1이라고 했을 때 h를 3으로 크기지정하겠다는 의미이다.-->

<!--        즉, 위의 속성의 의미는 w를 Constrains에 맞춰 3으로 봤을 때 h를 1로 크기지정하겠다는 뜻이다. >> h = 1/3w -->

        <ImageView
            android:id="@+id/imageView11"
            android:scaleType="centerCrop"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>
<!--        scaleType -> 이미지가 ImageView에 대해 어떻게 나타날지 정하는 속성-->
<!--        딱맞게 조정?, 벗어나는 부분 잘라서?, 어느 특정 크기에 맞게?, 중앙정렬로 벗어나는부분 잘라서? 등등-->
<!--        >> centerCrop은 중앙정렬로 View벗어나면 자르게 하는 설정임-->


        <ImageView
            android:id="@+id/imageView12"
            android:scaleType="centerCrop"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>

        <ImageView
            android:id="@+id/imageView13"
            android:scaleType="centerCrop"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>


    </LinearLayout>

    <LinearLayout
        android:id="@+id/secondRowLinearLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/firstRowLinearLayout"
        app:layout_constraintDimensionRatio="h,3:1"
        >


        <ImageView
            android:id="@+id/imageView21"
            android:scaleType="centerCrop"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>



        <ImageView
            android:id="@+id/imageView22"
            android:scaleType="centerCrop"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>

        <ImageView
            android:id="@+id/imageView23"
            android:scaleType="centerCrop"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>


    </LinearLayout>


    <Button
        android:id="@+id/addPhotoButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="사진 추가하기"
        app:layout_constraintBottom_toTopOf="@+id/startPhotoFrameModeButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/startPhotoFrameModeButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="전자액자 실행하기"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

이 코드에서 주목할 부분

app:layout_constraintDimensionRatio="1:1" 속성


......
    <LinearLayout
        android:id="@+id/firstRowLinearLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintDimensionRatio="h,3:1"
        >
      
......
      
	</LinearLayout>

......

해당 컴포넌트를 width:height로 크기지정하는 속성
( 따라서 이 경우 하나는 크기가 지정되어 있어야함 --> 특정 크기 혹은 wrap_content등 )

하지만 가로세로 모두 0dp일 경우(match_parent)에도 사용할 수 있는데,
위와 같이 "h혹은w,1:1" 등으로 설정하면 된다. (제약,비율)

이때 의미는 예를 들어, w,1:3 -> h를 1:3에 따라 Constraint(제약)에 맞춰 3이라고 했을 때 w를 1으로 크기지정하겠다는 의미이다.
반면에 h,1:3의 의미는 -> w를 1:3에 따라 Constraint(제약)에 맞춰 1이라고 했을 때 h를 3으로 크기지정하겠다는 의미이다.

즉, 위의 속성의 의미는 w를 Constrains에 맞춰 3으로 봤을 때 h를 1로 크기지정하겠다는 뜻이다. >> h = 1/3w

출처 : https://shinjekim.github.io/android/2019/08/07/Android-ConstraintLayout/#dimension-constraints


ImageView의 ScaleType


        <ImageView
            android:id="@+id/imageView11"
            android:scaleType="centerCrop"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"/>

scaleType --> 이미지가 ImageView에 대해 어떻게 나타날지 정하는 속성

딱맞게 조정?, 벗어나는 부분 잘라서?, 어느 특정 크기에 맞게?, 중앙정렬로 벗어나는부분 잘라서? 등등
centerCrop은 중앙정렬로 View벗어나면 자르게 하는 설정임


PhotoFrameActivity.kt

package com.example.aop_part2_chapter5

import android.net.Uri
import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import java.util.*
import kotlin.concurrent.timer

class PhotoFrameActivity : AppCompatActivity() {

    private var currentPosition = 0

    private val photoList = mutableListOf<Uri>()

    private var timer: Timer? = null

    private val photoImageView: ImageView by lazy {
        findViewById(R.id.photoImageView)
    }

    private val backgroundPhotoImageView: ImageView by lazy {
        findViewById(R.id.backgroundPhotoImageView)
    }


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

        getPhotoUriFromIntent()

    }


    private fun getPhotoUriFromIntent() {
        val size = intent.getIntExtra("photoListSize", 0)
        for (i in 0..size) {

            // let함수 사용 -> 앞의 리턴값이 null이면 let의 람다함수는 생략
            intent.getStringExtra("photo${i}")?.let {
                photoList.add(Uri.parse(it))
                // 데이터를 intent에 담아서 가져올 때 String으로 변환해서 가져왔으므로 다시 Uri로 변환시켜서 사용
            }
        }

    }

    private fun starttimer() {

        // timer는 쓰래드이다. timer쓰래드를 사용하면 period주기로 람다함수의 내용을 반복한다.
        // 그리고 timer도 쓰래드이므로, 앱에 영향을 주려면 UI쓰래드와 연결해서 사용해야한다.
       timer =  timer(period = 5 * 1000) {
            runOnUiThread {
                val current = currentPosition
                val next = if (photoList.size <= currentPosition + 1) 0 else currentPosition + 1

                backgroundPhotoImageView.setImageURI(photoList[current])

                photoImageView.alpha = 0f
                photoImageView.setImageURI(photoList[next])
                photoImageView.animate()
                    .alpha(1.0f)
                    .setDuration(1000)
                    .start()

                currentPosition=next

            }

        }
    }

    override fun onStop() {
        super.onStop()

        timer?.cancel()
    }

    override fun onStart() {
        super.onStart()

        starttimer()
    }

    override fun onDestroy() {
        super.onDestroy()

        timer?.cancel()
    }

}

이 코드에서 주목할 부분

Intent에 담아서 보내준 데이터 받기


 private fun getPhotoUriFromIntent() {
        val size = intent.getIntExtra("photoListSize", 0)
        for (i in 0..size) {

            // let함수 사용 -> 앞의 리턴값이 null이면 let의 람다함수는 생략
            intent.getStringExtra("photo${i}")?.let {
                photoList.add(Uri.parse(it))
                // 데이터를 intent에 담아서 가져올 때 String으로 변환해서 가져왔으므로 다시 Uri로 변환시켜서 사용
            }
        }
    }

--> intent.getStringExtra(받을 데이터 이름)로 String타입의 데이터를 받아서 사용하고 있음

그리고 let 함수를 사용해서 데이터가 있으면 처리하고, 없으면 생략하는 방식으로
혹시 모를 null 문제에 대비하고 있음


Thread를 Activity Lifecycle에 맞게 처리

   override fun onStop() {
        super.onStop()

        timer?.cancel()
    }

    override fun onStart() {
        super.onStart()

        starttimer()
    }

    override fun onDestroy() {
        super.onDestroy()

        timer?.cancel()
    }

쓰래드를 Activity Lifecycle에 맞게 적절히 처리해주고 있음
--> 자세한 설명은 위에 있음


activity_photoframe.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/backgroundPhotoImageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/black"
        android:scaleType="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/photoImageView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/black"
        android:scaleType="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <!--    View가 겹쳐있을 경우 뒤에 있는 View가 앞에 오게 됨 -> 코드를 내려 읽으면서 덮여씌워지기 때문-->

</androidx.constraintlayout.widget.ConstraintLayout>

AndroidManifest.xml


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

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

    <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/Theme.Aoppart2chapter5">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <activity android:name=".PhotoFrameActivity"
            android:screenOrientation="landscape"/>
<!--        해당 액티비티는 가로 화면으로 나타남-->
    </application>

</manifest>
profile
ㅎㅎ

0개의 댓글