본문 바로가기

다우 & Android

자연스러운 ImageView 회전 적용하기 (카카오톡 이미지 편집)

안녕하세요!
'개발세끼'의 막내를 담당하고 있는 '세끼'라고 합니다.

Android 개발을 하면서 이미지 뷰 에디터를 개발할 때가 있었는데요,
화면 회전 기능 개발시 생겼던 이슈와 해결방법에 대해서 글을 적어보려고 합니다. 

화면 회전 기능을 개발하기위해 구글링을 해보니, 가장 많이 보이는 글은 Bitmap 조작이었습니다.
아래와 같은 코드처럼 말이죠.

val matrix = Matrix().apply { postRotate(90f) }
val bitmapSource = ((this.drawable) as BitmapDrawable).bitmap
val rotatedBitmap = Bitmap.createBitmap(bitmapSource, 0, 0, bitmapSource.width, bitmapSource.height, matrix, true)
imageView.setImageBitmap(rotatedBitmap)


위의 코드를 실행시켜 보니, 이미지가 정확하게 90도 회전한 상태로 imageVIew에 setting 되었습니다.
다만 두 가지 정도 이유로 매번 회전 할 때마다 위의 코드를 실행시키는 것은 좋지 않겠다는 생각이 들었습니다.

비트맵 변경의 문제점

첫 번째 이유로는 성능이었습니다. 최근 모바일 디바이스들의 성능이 급격히 좋아지면서 이미지 파일 하나의 용량이 상당히 커졌는데요. 위의 코드로 실행해보니 갤럭시 s8에서 3MB가 넘는 이미지를 회전할 경우 꽤나 버벅거리더군요. 참고로 갤럭시 s8 기본 카메라로 사진을 찍었을 때 용량이 3MB였습니다. 앞으로 더 큰 용량의 이미지가 사용될 텐데, 미래를 생각해서라도 피해야 할 방법이라고 판단했습니다 (그러고 보니 Bitmap을 직접 변경하라는 글들은 꽤나 옛날 글들이 많았던 것 같네요.)

두 번째 이유로는 사용자경험이었습니다. 비트맵을 직접 조작한 후 바뀐 Bitmap을 imageView에 갈아 끼우는 방식의 경우, 비트맵 연산 딜레이가 끝나면 이미지 뷰를 갑자기 변경시켜버립니다. 요즘처럼 애니메이션이 중요해진 시대에는 맞지 않는다고 판단했습니다.

이미지 회전에 대한 고민을 하면서 여러가지 앱을 찾아보았는데, 카카오톡의 이미지 편집 기능을 보니 화면 회전에 자연스러운 애니메이션이 적용되었으며 전혀 버벅거림이 없더군요(이미지 용량이 얼마나 크던지 상관없었습니다). 정확하게는 모르겠지만 회전 버튼을 클릭할 때마다 실제 비트맵을 조작하는 것은 아닌 것 같았습니다.

카카오톡의 이미지 첨부시 회전 기능은 아래와 같이 부드럽게 동작합니다. 심지어 가로 세로 비율도 디바이스 크기에 딱 맞게 조절이 되고요.

카카오톡 이미지 첨부시 회전 기능과 동일

해결 방법

계속 보다 보니 위에서 언급한 두 가지 문제점을 모두 커버할 수 있는 방법이 떠오르더라구요.
카카오톡이 어떻게 했는지는 모르겠지만 저는 Bitmap 변경은 최대한 마지막 순간까지 미루고, 유저가 보는 ImageView만 회전시키기로 했습니다.
ImageView만 회전시키는 것은 자유롭게 애니메이션이 적용 가능할 뿐만 아니라 Bitmap은 건들지 않으므로 성능상의 이슈가 거의 없습니다. 겉모습만 화려하게 회전되는 것 뿐, 실제로 이미지 데이터(Bitmap)가 변경되는 것이 아니며 그러한 기술적 내용이 사용자 입장에서는 중요한 게 아니겠더라구요.
사용자 입장에서는 마음대로 회전을 시키다가, 원하는 회전 모습일 때 "저장"버튼을 누르면 그만인 것이죠. 우리는 겉모습인 ImageView만 회전시켜주다가, 사용자가 "저장" 버튼을 누를 때만(앱마다 다르겠지만) 실제 Bitmap을 변경시키면 됩니다.

이미지 뷰를 애니메이션 + 길이 조정과 함께 회전시키기

이미지 뷰를 회전시키는 방법은 아래와 같이 정말 간단하게 작동시킬 수 있습니다.

rotateButton.setOnClickListener {
        val currentDegree = imageView.rotation
        ObjectAnimator.ofFloat(imageView, View.ROTATION, currentDegree, currentDegree + 90f)
            .setDuration(300)
            .start()
}


ObjectAnimator를 사용하면 View의 회전뿐만 아니라, 투명도, 색상 등 미리 정의된 다양한 애니메이션을 사용할 수 있는데요, 위의 코드를 그대로 사용하면 한 가지 문제가 있습니다. 이미지의 가로 세로 길이가 동일하다면 전혀 문제 될 게 없지만, 둘 중 하나의 비율이 더 길 경우, 예를 들어 세로 길이가 더 길 경우 90도 회전을 하면 이미지가 일부분 잘리게 되는 현상이 발생합니다. 

이미지를 회전시켜주긴 하지만, 말 그대로 회전만 시켜줄 뿐 화면 크기에 맞게 반응형으로 맞춰주진 않는다는 것이죠.
이래서는 카카오톡 이미지 편집처럼 동작하지 않습니다.

위에 첨부된 이미지 파일처럼 동작하는 코드는 아래와 같습니다.
참고로 이미지 로딩을 위해서 이미지 로딩 라이브러리인  coil을 사용했습니다. c
oil은 코틀린 코루틴으로 작동하는 가벼운 image loading 라이브러리이며 기타 설정 dsl을 지원하고 있습니다. 코루틴을 사용하지만 라이브러리 자체에 코루틴이 내장되어있으므로 따로 코루틴을 설치할 필요는 없습니다.

(코드는 여기서도 볼 수 있습니다.)

코드는 복잡해 보이지만, 핵심은 heightGap 하나입니다. 

우리는 이미지 뷰의 height를 기준으로 기능을 동작시킬 것입니다. 세로가 더 긴 이미지에서 90도 회전시킬 경우, 세로 길이를 디바이스의 가로길이로 맞춰야 하는데, 이미지의 세로(높이)가 100이고 디바이스 가로(너비) 길이가 70이라고 한다면 이미지의 높이를 30만큼 줄여나가야 합니다. 목표하는 길이까지의 gap은 -30인 것이죠.

이와 반대로 옆으로 길게 누워있는 이미지라면, 90도 회전 시 imageView의 길이가 디바이스의 높이와 같게 늘어나야 합니다. 이 상황은 옆으로 누워있는 이미지 뷰의 높이(70)가 디바이스의 가로길이(70)와 동일할 것이므로, 회전과 함께 이미지 뷰의 길이(70)를 디바이스 세로 길이(100)로 만들어 주어야 합니다. 즉, 누워있는 이미지뷰의 높이(70)와 회전 이후 목표 길이(100)인 deviceHeight의 차이가 gap이 됩니다. 목표하는 길이까지의 gap은 +30이죠.

따라서 최종 식은 아래와 같습니다.

imageView.height = imageView.height + gap

여기에 ValueAnimator을 사용하여 gap을 0부터 서서히 30까지 만들면 자연스러운 애니메이션이 될 것입니다.

비트맵을 변경하려는 순간에는 imageView.rotation을 이용

이렇게 해서 imageView는 자연스럽게 회전이 되게 되었습니다. 사용자가 마지막으로 "수정" 버튼을 눌렀을 때는 실제로 Bitmap을 변경시켜줘야 할 수가 있는데요, 이럴 때는 imageView.rotation으로 회전 값을 알아온 다음에, 그 값만큼 딱 한 번만 비트맵 연산을 해주면 됩니다.

결론

이미지 용량은 날이 갈수록 커지고 있습니다. 그에 따라 많은 성능 이슈가 생길 수 있고, 만들고자 하는 앱의 UI&UX가 모두 다르기 때문에 이미지 회전을 구글에서 검색한 후 무조건 최 상단에 있다고 그대로 복사 붙여 넣기 하는 방식은 좋지 않은 것 같습니다. 더 좋은 방법이 있을지 모르겠지만, 저 같은 경우에는 위의 방법을 사용해 이미지 용량에 상관없이 동일한 퍼포먼스와 적당한 UX를 구현했습니다. 더 좋은 방법이 있다면 댓글로 알려주시면 감사하겠습니다^^!