본문 바로가기

안드로이드 Android

안드로이드 개발 (20) Camera Capture 및 Image button,ripple

안녕하세요 안드로이드 개발자 Loner입니다. 오늘 겪었던 사소한 이슈나 구현해본 것들을 정리하려고 합니다. 

 

(1) imageButton 

개인적으로 ImageButton을 좋아합니다. 왜냐하면 android:src = "이미지 경로" 를 사용할 수 있기 때문 입니다.

즉, android:background 설정을 바꾸지 않았기 때문에 ripple 효과가 사라지지 않고 그대로 남아있기 때문입니다. 이미지를 클릭해서 이벤트를 만들어내야하는 경우 가능하면 imageButton을 사용하는것을 선호합니다. 

    <ImageButton
        android:id="@+id/ib_noticeBoardUploadImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="40dp"
        android:src="@drawable/ic_iconfinder_plus_empty_24"
        android:background="@drawable/bg_noticeboard_upload_ripple"
        app:layout_constraintBottom_toTopOf="@+id/btn_boardUpload"
        app:layout_constraintStart_toStartOf="@+id/et_boardUploadDescribe" />

그리고 android:src 과 android:background를 같이 써서 둘을 혼합해서 사용 할 수 있습니다. 일반 Button의 경우 android:src를 사용할 수 없기 때문에 완전히 background로 만들어야하는것과 달리 ImageButton은 이점에 있어서 상당히 유용합니다. 

src와 백그라운드를 같이 사용한 이미지버튼

ImageView만 사용해서 아이콘을 설정해 클릭이벤트를 넣어봤거나 그림과 백그라운드를 혼합해야하는 경우 ImageButton을 사용해보는것이 어떨까요? 오늘 imageButton을 사용하는 김에 블로그에 적어봤습니다.

 

(2) ripple

아래 예제를 먼저 보시겠습니다. 

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid android:color="#ffffff" />

    <size
        android:width="68dp"
        android:height="68dp" />
    <stroke
        android:width="1dp"
        android:color="#000" />
</shape>

 

클릭 반응 모습

 

좌측 하단 동그란 + 버튼을 클릭하면 사진 촬영화면으로 넘어갑니다.

하지만 사용자가 버튼을 클릭했을때 자신이 정확히 어디를 눌렀는지 ui로 피드백을 하지 않습니다.

 

개인적으로 사용자의 상호작용 하는 부분들을 중요시 여깁니다. 사용자가 어떠한 ui를 클릭을 했을 때 사소한 반응 하나하나가 앱의 디테일을 살려준다 생각하고 좋은 평가를 받을 수 있을거라 생각합니다. 그래서 대표적으로 ripple이라는 효과가 있습니다.

 

 ripple 효과로 앱의 반응을 표시해주는것이 사용자와 소통하는 방법중 하나라고 생각합니다. ripple를 통해 사용자는 내가 무엇을 클릭했는지 시각적으로 부담스럽지 않는 선에서 확인 할 수 있고 디자인에 따라 ripple를 더하면 더 품격있는 앱처럼 보일거라 생각이 듭니다. 

 

기본적으로 Button과 ImageButton에 ripple이 적용되어있습니다. 하지만 보통 별다른 셋팅없이 Button이나 ImageButton을 만들어서 ripple를 사용하게 됩니다. 하지만 해당 뷰의 background를 변경했을시 background가 다른 값으로 할당이 되기 때문에 ripple 효과가 사라집니다.

 

그렇다면 직접 ripple를 만들어줘야합니다.

 

 ripple을 xml로 만든 예제는 다음과 같습니다.

 

예제

<?xml version="1.0" encoding="UTF-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#4E4E4E">
    <item>
        <shape android:shape="oval">
            <solid android:color="#ffffff" />
            <size
                android:width="68dp"
                android:height="68dp" />
            <stroke
                android:width="1dp"
                android:color="#000" />
        </shape>
    </item>

</ripple>

ripple를 상위태그로 감싸서 기존 xml background를 만들면 됩니다.

- android:color = 는 리플을 눌렀을때 색감을 지정해줍니다. 

- 위 같이 설정해놓으면 자동적으로 물결이 번지고 물결이 백그라운드를 설정한 크기만큼 전부 번지는걸 확인할 수 있습니다. 

 

 

결과

 

- 아까와 달리 잔물결이 퍼지는 모습을 잠시 확인 할 수 있습니다.

- 사용자는 자신이 어디를 클릭했는지 파악할 수 있고 앱은 사용자가 클릭을 했음을 알립니다. 

 

그외 통해 보조색상이나 잔 물결의 반경을 설정할 수 있습니다. 

아래링크를 참고하면 좋을것 같습니다.

https://developer.android.com/reference/android/graphics/drawable/RippleDrawable

 

RippleDrawable  |  Android 개발자  |  Android Developers

 

developer.android.com

 

 

(3) Camera Capture

Camera Capture 후 이미지 업로드는 스마트폰 앱에서 거의 필수적인 기능이 되버렸습니다. 오래전부터 사용해왔던 기능이기 때문에 많은 사용자에게 이미 익숙한 기능입니다. 하지만 흔히 하는 기능이라 단순하게 접근했다가 간혹 작은 실수로  시간을 오래끄는 경우가 있는데 이때 개발자는 삽질의 길로 빠집니다.

 

안드로이드에서 카메라 촬영 후 이미지 저장은 Intent 액션으로 ACTION_IMAGE_CAPTURE를 사용해서 

startActivityForResult()로 인텐트를 넘기거나 registerForActivityResult()를 사용해서 .launch(intent)를 사용해 넘깁니다. 

 

아래 예제는 startActivityForResult를 사용한 예제입니다. (개인적으로 registerForActivityResult를 더 선호합니다. 한번 써보시길 추천)

 

    val REQUEST_IMAGE_CAPTURE = 1

    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            takePictureIntent.resolveActivity(packageManager)?.also {
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
            }
        }
    }

 

카메라 촬영 후 체크시 콜백으로 data를 받게 되는데 여기서 문제는 원본 사진의 uri를 받지 못합니다. 카메라 사진을 캡쳐만 하고 썸네일을 사용할 정도의 적은 화질의 bitmap만 제공될뿐 입니다. 

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            val imageBitmap = data.extras.get("data") as Bitmap
            imageView.setImageBitmap(imageBitmap)
        }
    }
    

위처럼 bitmap만 받을 수 있고 상당히 저퀄리티 사진에 실망하실수 있습니다. 

( 적은 썸네일 사진을 원한게 아니라면 위 예제 두개 다 지워버립시다. ) 

그렇기 때문에 원본 사이즈를 카메라 촬영후 갤러리에 이 이미지에 넣고 싶다면 추가적인 방법이 필요합니다. 

 

 

- 원본 사이즈 사진 Uri 얻는법 - 

  <manifest ...>
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                         android:maxSdkVersion="18" />
        ...
    </manifest>
    

매니페스트 권한 설정 해준 뒤 

 

  lateinit var currentPhotoPath: String

    @Throws(IOException::class)
    private fun createImageFile(): File {
        //파일 이름의 일부분 만들기 (예제는 날짜기준)
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val storageDir: File = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File.createTempFile(
                "JPEG_${timeStamp}_", /* prefix */
                ".jpg", /* suffix */
                storageDir /* directory */
        ).apply {
            // 아래 Path를 적어놓으면 사진 클릭후 콜백시 바로 이미지뷰에 사진을 표시한다했을때
            //아래 변수를 사용하면 됩니다!!
            currentPhotoPath = absolutePath
        }
    }
    

 

- 중복되지 않는 파일이름을 가진 파일을 만들어야합니다. 위 예제는 날짜로 중복을 피헀습니다.

- File.crateTempFile() 로 임시 파일을 만듭니다.

- 그리고 currentPhotoPath에 파일 path를 할당해서 사진 클릭후 바로 이미지 뷰에 선택한 사진을 보여줄 수 있습니다!

 

그다음 아래 예제를 만들어줍시다. 

   val REQUEST_IMAGE_CAPTURE = 1

    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            // Ensure that there's a camera activity to handle the intent
            takePictureIntent.resolveActivity(packageManager)?.also {
                // Create the File where the photo should go
                val photoFile: File? = try {
                    createImageFile()
                } catch (ex: IOException) {
                    // Error occurred while creating the File
                    ...
                    null
                }
                // Continue only if the File was successfully created
                photoFile?.also {
                    val photoURI: Uri = FileProvider.getUriForFile(
                            this,
                            "com.example.android.fileprovider",
                            it
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
                }
            }
        }
    }
    

-인텐트를 생성하면서 Temp파일도 같이 생성해줍니다. 

-생성한 Temp파일인 photoFile 를 통해서 FileProvider로 URI를 다시 만들어야합니다.

  -> API 24 이상 기기에서 패키지 경계를 넘어 file:// URI를 전달하면 FileUriExposedException이 발생하기 때문에 

      FileProvider를 이용해서 content::// URI를 반환합니다. 

 

- 위 함수를 사용하면 갤러리화면으로 이동하게 됩니다.

 

 

FileProvider는 ContentProvider로 파일을 만들어 앱과 관련된 파일의 안전한 공유하는 클래스입니다 . 

 

FileProvider의 설명은 아래링크를 참고하세요.

https://developer.android.com/reference/androidx/core/content/FileProvider

 

FileProvider  |  Android 개발자  |  Android Developers

From class android.content.ContentProvider ContentProviderResult[] applyBatch(String arg0, ArrayList arg1) ContentProviderResult[] applyBatch(ArrayList arg0) void attachInfo(Context arg0, ProviderInfo arg1) int bulkInsert(Uri arg0, ContentValues[] arg1) Bu

developer.android.com

 

 

FileProvider를 사용한다면 ContentProvider를 사용하는 것이기 때문에 매니페스트에 선언을 해줘야합니다.

(4대 컴포넌트는 반드시 매니페스트 선언)

<application>
   ...
   <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.example.android.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"></meta-data>
    </provider>
    ...
</application>

 

그다음 XML를 하나 만들어야합니다.

하위 디렉터리에 대한 콘텐츠 URI를 요청하겠다고 FileProvider에 알립니다 .

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

- 위 예에서는 "Pictures" 하위 디렉토리에 대한 콘텐츠 URI를 FileProvider에게 요청함

 

위 같이 다 셋팅을 했다면 이제 원본 uri를 얻을 수 있습니다.

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
             //아까 설정해놨던 uri!
             imageView.setImageURI(currentPhotoPath)
        }
    }

이러면 이제 원본 화질의 uri를 얻을 수 있습니다.

 

 

이상 카메라 캡쳐 및 리플 및 이미지 버튼에 대해 적어봤습니다. Camera Capture 같은 경우는 생각보다 셋팅해야 할게 많아서 꽤 귀찮게 느껴지는 작업이긴 하네요 이런 부분들은 구글 안드로이드 개발팀이 좀 신경써줫으면 좋겟다는 개인적인 바람입니다. 이상입니다.