[Android/Flutter 교육] 40일차

MSU·2024년 2월 23일

Android-Flutter

목록 보기
43/85
post-thumbnail

월요일 평가 예정 내용

  • 안드로이드의 특징들
  • 안드로이드 가상 머신의 이름
  • 안드로이드 4대 구성 요소 중 Activity의 정의
  • 안드로이드 4대 구성 요소 중 Service의 정의
  • 안드로이드 4대 구성 요소 중 Content Provider의 정의
  • 안드로이드 4대 구성 요소 중 Broadcast Receiver의 정의
  • AndroidManifest.xml 의 역할
  • ViewBinding을 사용하기 위한 작업(설정) 과정
  • ViewBinding 클래스의 네이밍 규칙
  • 사용했던 View들의 특징
  • 사용했던 Layout들의 특징
  • 각 메뉴들의 특징
  • AdapterView (RecyclerView)에 대한 내용
  • 여러 메시징 도구들의 특징들
  • SQLiteDatabase에 관한 내용들
  • 단위에 대한 내용
  • 각종 센서
  • GPS Provider에 관련된 내용
  • layout 폴더내의 리소스 관리에 관련된 내용
  • values 폴더내의 리소스 관리에 관련된 내용

추후 진행 과정

LBS 프로젝트(GPS, 구글지도, open api, Http 네트워크)
메모 프로젝트(종합, sqlite)
게시판 프로젝트(종합, firebase 클라우드 서비스)
Flutter 프로젝트(클론코딩)

쇼핑몰 팀프로젝트
개인프로젝트

사진

사진을 찍는 액티비티를 실행하고 사진을 찍으면 옛날 카메라 성능에 맞추어 사진의 원본이 아니라 화질을 저화질로 맞추어 보여준다.
따라서 사진을 찍으면 원본 사진을 저장하고 저장한 파일을 불러오는 과정이 필요하다.

사진을 찍는 Activity는 이미 만들어진 사진 어플의 Activity이므로 이걸 통제할 수 없다.
따라서 우리가 작업하는 Activity에서 사진을 저장할 경로를 만든다음에 사진 어플의 Activity를 실행할때 전달해주는 방식으로 작업한다.

코드 작성 순서

1. res/xml 폴더에 xml 파일을 만들어주고 이 파일에 사진이 저장될 외부저장소까지의 경로를 기록해준다. (res/xml/file_path.xml)

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
    name="storage/emulated/0"
    path="."/>
</paths>

이후에는 직접 작성하지 않고 이 xml파일을 복사에서 사용해도 된다.

2. AndroidManifest.xml에 1에서 만든 파일의 경로를 지정해준다.

        <!-- 촬영한 사진을 저장하는 프로바이더 -->
        <provider
            android:authorities="kr.co.lion.android52_picture.file_provider"
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path"/>
        </provider>

    </application>

</manifest>

3. 촬영된 사진이 저장된 경로 정보를 가지고 있는 Uri 객체를 만들고 카메라가 사진을 찍으면 Uri객체에 담긴 경로로 사진을 저장하게 해준다.

이전에는 외부저장소까지의 경로를 직접 작성할 수 있었다.
하지만 안드로이드 보안문제때문에 외부저장소 경로를 직접 정하는게 아니라 안드로이드 os로부터 받아오는 방식으로 작업해야 한다.

class MainActivity : AppCompatActivity() {

    lateinit var activityMainBinding: ActivityMainBinding

    // Activity 실행을 위한 런처
    lateinit var cameraLauncher: ActivityResultLauncher<Intent>

    // 촬영된 사진이 저장된 경로 정보를 가지고 있는 Uri 객체
    lateinit var contentUri:Uri

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

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        // 사진 촬영을 위한 런처 생성
        val contract1 = ActivityResultContracts.StartActivityForResult()
        cameraLauncher = registerForActivityResult(contract1){

        }

        activityMainBinding.apply {

            // 카메라로 사진 찍기
            // step1) res/xml 폴더에 xml 파일을 만들어주고 이 파일에 사진이 저장될 외부저장소까지의
            // 경로를 기록해준다. (이 예제에서는 res/xml/file_path.xml)
            // step2) AndroidManifest.xml 에 1에서 만든 파일의 경로를 지정해준다.
            button.setOnClickListener {
                // 촬영한 사진이 저장될 경로
                // 외부 저장소 중에 애플리케이션 영역 경로를 가져온다.
                val rootPath = getExternalFilesDir(null).toString()
                // 이미지 파일명을 포함한 경로
                val picPath = "${rootPath}/tempImage.jpg"
                // File 객체 생성
                val file = File(picPath)
                // 사진이 저장될 위치를 관리할 Uri 생성
                // AndroidManifest.xml에 등록한 provider의 authorities
                val a1 = "kr.co.lion.android52_picture.file_provider"
                contentUri = FileProvider.getUriForFile(this@MainActivity, a1, file)

                if(contentUri != null){
                    // 실행할 액티비티를 카메라 액티비티로 지정한다.
                    // 단말기에 설치되어 있는 모든 애플리케이션이 가진 액티비티 중에 사진촬영이
                    // 가능한 액티비가 실행된다.
                    val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                    // 이미지가 저장될 경로를 가지고 있는 Uri 객체를 인텐트에 담아준다.
                    cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri)
                    // 카메라 액티비티 실행
                    cameraLauncher.launch(cameraIntent)
                }
            }

        }
    }
}

getExternalFilesDir() 메서드는 외부 저장소에서 애플리케이션 영역의 경로를 가져온다.
FileProvider.getUriForFile() 메서드는 프로바이더를 통해 외부저장소까지의 경로를 안드로이드 OS에서 받아와준다.

외부저장소까지의 경로 + 외부 저장소에서 애플리케이션 영역의 경로 + 파일명 을 합쳐서 전체 경로를 contentUri 변수에 담고 이를 Intent에 담아서 카메라 액티비티를 실행할 때 전달하는 것이다.

4. 사진을 찍고 돌아온 후

사진을 찍고 확정하면 resultCode에 RESULT_OK가 담겨온다.
취소하면 RESULT_CANCELED가 담겨온다.

        // 사진 촬영을 위한 런처 생성
        val contract1 = ActivityResultContracts.StartActivityForResult()
        cameraLauncher = registerForActivityResult(contract1){
            // 사진을 사용하겠다고 한 다음에 돌아온 경우
            if(it.resultCode == RESULT_OK){
                // 사진 객체를 생성한다.
                val bitmap = BitmapFactory.decodeFile(contentUri.path)
                activityMainBinding.imageView.setImageBitmap(bitmap)

                // 사진 파일을 삭제한다.
                val file = File(contentUri.path)
                file.delete()
            }
        }

에뮬레이터에서 테스트할 경우

사진을 찍고 오면 이미지뷰에 찍은 사진이 원본대로 제대로 불러와진다

실제 단말기에서 테스트할 경우

사진이 회전되어서 나온다.

사진이 회전되어서 나오는 이유는 애초에 스마트폰으로 사진을 찍을 때 가로로 눕혀서 찍는 것을 상정하기 때문에 단말기에서 사진을 찍으면 90도 돌아가서 찍히게 된다.
이러한 현상을 개발자가 직접 코드로 수정해줘야 한다.

사진 결과물 회전시키기

사진의 회전 각도값을 반환하는 메서드

    // 사진의 회전 각도값을 반환하는 메서드
    // ExifInterface : 사진, 영상, 소리 등의 파일에 기록한 정보
    // 위치, 날짜, 조리개값, 노출 정도 등 다양한 정보가 기록된다.
    // ExifInterface 정보에서 사진 회전 각도값을 가져와서 그만큼 다시 돌려준다.
    fun getDegree(uri:Uri) : Int {
        // 사진 정보를 가지고 있는 객체를 담을 변수.
        // 사용자가 ExifInterface를 지우면 null이 반환될 수 있다.
        var exifInterface:ExifInterface? = null

        // 안드로이드 os 버전에 따라 분기가 필요하다.
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
            // 이미지 데이터를 가져올 수 있는 Content Provider의 Uri를 추출한다.
            // val photoUri = MediaStore.setRequireOriginal(uri) 
            // photoUri는 권한이 없는 객체여서 나중에 실행 시 에러가 나므로 주석처리하고 그냥 uri를 사용
            
            // ExifInterface 정보를 읽어올 스트림을 추출한다.
            val inputStream = contentResolver.openInputStream(uri)!!
            // ExifInterface 객체를 생성한다.
            exifInterface = ExifInterface(inputStream)
        } else {
            // ExifInterface 객체를 생성한다.
            exifInterface = ExifInterface(uri.path!!)
        }

        // exifInterface가 null이 아닐때만
        if(exifInterface != null){
            // 반환할 각도 값을 담을 변수
            var degree = 0
            // ExifInterface 객체에서 회전 각도값을 가져온다.
            // defaultValue는 아무값이나 넣어주면 된다.
            var ori = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)

            degree = when(ori){
                ExifInterface.ORIENTATION_ROTATE_90 -> 90
                ExifInterface.ORIENTATION_ROTATE_180 -> 180
                ExifInterface.ORIENTATION_ROTATE_270 -> 270
                else -> 0
            }

            return degree
        }

        return 0
    }

ExifInterface 객체에서 다양한 태그 정보를 가져올 수 있다.

사진 결과물 회전시키기 위한 메서드

    // 회전시키는 메서드
    fun rotateBitmap(bitmap:Bitmap, degree:Float):Bitmap{
        // 회전 이미지를 생성하기 위한 변환 행렬
        val matrix = Matrix()
        matrix.postRotate(degree)

        // 회전 행렬을 적용하여 회전된 이미지를 생성한다.
        // 첫 번째 : 원본 이미지
        // 두번째와 세번째 : 원본 이미지에서 사용할 부분의 좌측상단 x,y 좌표
        // 네번째와 다섯번째 : 원본 이미지에서 사용할 부분의 가로 세로 길이
        // 여기에서는 이미지데이터 전체를 사용할 것이기 때문에 전체 영역으로 잡아준다.
        // 여섯번째 : 변환 행렬. 적용해준 변환행렬이 무엇이냐에 따라 이미지 변형 방식이 달라진다. (matrix.postRotate 회전)
        // 일곱번째 : 필터 적용. 원본 그대로 적용하고자 할 때에는 false를 넣어준다.
        val rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)

        return rotateBitmap
    }

이미지 사이즈를 조정하는 메서드

    // 이미지 사이즈를 조정하는 메서드
    fun resizeBitmap(bitmap:Bitmap, targetWidth:Int):Bitmap{
        // 이미지 확대/축소 비율을 구한다.
        val ratio = targetWidth.toDouble() / bitmap.width.toDouble()
        // 세로 길이를 구한다.
        val targetHeight = (bitmap.height * ratio).toInt()
        // 크기를 조정한 Bitmap을 생성한다.
        val resizedBitmap = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, false)

        return resizedBitmap
    }

회전된 이미지를 구하기

        // 사진 촬영을 위한 런처 생성
        val contract1 = ActivityResultContracts.StartActivityForResult()
        cameraLauncher = registerForActivityResult(contract1){
            // 사진을 사용하겠다고 한 다음에 돌아온 경우
            if(it.resultCode == RESULT_OK){
                // 사진 객체를 생성한다.
                val bitmap = BitmapFactory.decodeFile(contentUri.path)

                // 회전 각도값을 구한다.
                val degree = getDegree(contentUri)
                // 회전된 이미지를 구한다.
                val bitmap2 = rotateBitmap(bitmap, degree.toFloat())
                // 크기를 조정한 이미지를 구한다.
                val bitmap3 = resizeBitmap(bitmap2, 1024)

                activityMainBinding.imageView.setImageBitmap(bitmap)

                // 사진 파일을 삭제한다.
                val file = File(contentUri.path)
                file.delete()
            }
        }

이미지뷰에 사진이 눕혀지지 않고 제대로 나오는 것을 확인할 수 있다.

앨범으로부터 사진 가져오기

권한 설정

    </application>

    <!-- 앨범으로 부터 사진을 가져오기 위한 권한 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>

</manifest>
// MainActivity.kt

// 확인할 권한 목록
    val permissionList = arrayOf(
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.ACCESS_MEDIA_LOCATION,
    )
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        // 권한 확인
        requestPermissions(permissionList,0)

런처 작성

// MainActivity.kt

lateinit var albumLauncher: ActivityResultLauncher<Intent>

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

        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding.root)

        // 권한 확인
        requestPermissions(permissionList,0)
        
        // 앨범 실행을 위한 런처
        val contract2 = ActivityResultContracts.StartActivityForResult()
        albumLauncher = registerForActivityResult(contract2){
            // 사진 선택을 완료한 후 돌아왔다면
            if(it.resultCode == RESULT_OK){
                // 선택한 이미지의 경로 데이터를 관리하는 Uri 객체를 추출한다.
                val uri = it.data?.data!!
                if(uri != null){
                    // 안드로이드 Q(10) 이상이라면
                    val bitmap = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
                        // 이미지를 생성할 수 있는 객체를 생성한다.
                        val source = ImageDecoder.createSource(contentResolver, uri)
                        // Bitmap을 생성한다.
                        ImageDecoder.decodeBitmap(source)
                    }else{
                        // 컨텐츠 프로바이더를 통해 이미지 데이터에 접근한다.
                        val cursor = contentResolver.query(uri, null, null, null, null)
                        if(cursor != null){
                            cursor.moveToNext()
                            // 이미지의 경로를 가져온다.
                            val idx = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
                            val source = cursor.getString(idx)

                            // 이미지를 생성한다.
                            BitmapFactory.decodeFile(source)
                        }else{
                            null
                        }
                    }

                    // 회전 각도값을 가져온다.
                    val degree = getDegree(uri)
                    // 회전 이미지를 가져온다.
                    val bitmap2 = rotateBitmap(bitmap!!, degree.toFloat())
                    // 크기를 줄인 이미지를 가져온다.
                    val bitmap3 = resizeBitmap(bitmap2, 1024)

                    activityMainBinding.imageView.setImageBitmap(bitmap3)
                }
            }
        }

버튼을 클릭하면 앨범으로 이동하여 사진을 선택후 가져오기


            button2.setOnClickListener {
                // 앨범에서 사진을 선택할 수 있도록 셋팅된 인텐트를 생성한다.
                val albumIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
                // 실행할 액티비티의 타입을 설정(이미지를 선택할 수 있는 것이 뜨게 한다)
                albumIntent.setType("image/*")
                // 선택할 수 있는 파일들의 MimeType을 설정한다.
                // 여기서 선택한 종류의 파일만 선택이 가능하다. 모든 이미지로 설정한다.
                val mimeType = arrayOf("image/*")
                // val mimeType = arrayOf("image/jpeg")
                albumIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimeType)

                // 액티비티를 실행한다.
                albumLauncher.launch(albumIntent)
            }

GPS

  • Global Positioning System
  • 단말기와 네트워크 망, 위성등을 모두 연결해 현재 위치를 측정할 수 있는 시스템
  • 안드로이드 단말기 내부에서 가용한 위치 측정 수단을 모두 동원해 위치를 측정하고 있다.
  • 측정된 위치 값을 애플리케이션에 가져다 사용할 수 있다.

Provider

  • 안드로이드는 위치 측정을 위해 정보 제공자를 선택해서 사용한다.
  • 가급적이면 모든 위치 정보 제공자를 사용하는 것이 좋다
  • GPS Provider : GPS 위성과 통신하여 3각 측량 방법을 이용해 위치 정보를 습득한다. 위성과의 전파 송수신이 방해가 되는 요인(방해전파, 실내, 건물 내, 계곡 등)이 있으면 정확한 위치를 측정할 수 없다.
  • Network Provider : 이동통신 기지국, Wifi ap 등 통신망을 통해 위치 측정
    설치 장소의 위도와 경도를 제공하거나 시청과 계약하여 설치한 Wifi 통신망은 시청의 위도와 경도를 제공하는 경우도 있다. -> 통신망 계약자(모뎀)의 주소지
  • Passive Provider : 직접 위치를 측정하는 것이 아닌 다른 애플리케이션이 구한 값을 받아 측정
profile
안드로이드공부

0개의 댓글