현재 진행중인 사이드 프로젝트에서는 카카오 및 구글 로그인을 사용합니다.
그러므로, 이미지를 촬영 또는 디바이스에서 불러와 이를 수정 및 등록할 수 있도록 구현해야 합니다.
당연히 유수한 ImageCropper 라이브러리가 있지만, 앱 내에서 여러 번 사용되는 기능이 아니기에 직접 구현하기로 했습니다. 단순한 기능 하나를 위해 특정 의존성을 추가하는 행위 자체가 꺼려지기도 했고, 불필요한 패키지 사이즈의 증가는 유저로 하여금 다운로드가 꺼려질 수 있기 때문입니다. 또한 UI 와 관련한 라이브러리는 가능하면 배제하자는 것이 제 주관이기도 합니다.
Tl ; DR
소스 코드는 제 깃허브에서 보실 수 있습니다.
GitHub - jangjh123/Jetpack-Compose-CustomView: Jetpack Compose 를 활용한 CustomView 기록용 Repository
Jetpack Compose 를 활용한 CustomView 기록용 Repository. Contribute to jangjh123/Jetpack-Compose-CustomView development by creating an account on GitHub.
github.com
SectionSelector
유저로 하여금, 이미지 중 원하는 곳만 잘라낼 수 있도록 해야하며, 그 '원하는 곳'을 선택하기 위한 시각적 가이드가 필요합니다.
처음 어프로치할 때는 alpha 값이 0.5f 인 이미지를 띄워두고 그 위에 어떠한 처리를 통해 필요한 사이즈만큼 만 이미지의 alpha 값이 적용되지 않게 하려고 했으나, 아무리 찾아보아도 방법이 없어 어프로치 자체가 잘못되었다는 생각이 들었습니다.
그래서 생각해낸 것이 이미지와 SectionSelector 를 완벽하게 구분해야 한다는 것이었습니다.
이미지를 깔아주고, 그 위에 이미지 사이즈와 같은 레이어를 띄웁니다. 해당 레이어에는 원을 하나 그리고, clipPath() 메서드의 파라미터인 ClipOp.Difference 를 활용하여 원을 제외한 모든 부분을 어둡게 처리합니다.
이는 사이즈 조절 및 이동이 가능하여야하므로, Canvas 의 modifier 에 detectDragGestures 를 정의해주었습니다.
Canvas(modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX.value += dragAmount.x
offsetY.value += dragAmount.y
}
}
.transformable(state = state), onDraw = {
val circlePath = Path().apply {
addOval(
Rect(
if (!movedState.value) {
offsetX.value = center.x
offsetY.value = center.y
movedState.value = true
center
} else {
Offset(offsetX.value, offsetY.value)
}, sizeState.value
)
)
}
clipPath(circlePath, clipOp = ClipOp.Difference) {
drawRect(brush = SolidColor(Color(0x88000000))) // 어두운 배경 처리
}
})
ImageCropper
앱에서는 어떻게 적용될지 모르겠으나, 해당 기능을 미리 구현해두는 과정 중에서는 이미지의 크기를 16 : 9 또는 9 : 16 으로 제한하였습니다.
이미지 사이즈를 미리 지정한 크기에 맞추어 변형해줍니다. 해당 작업을 위해서는 Bitmap 클래스의 createScaledBitmap() 메서드가 필요합니다.
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,
boolean filter)
해당 메서드는 특정 Bitmap 을 사용자 지정 사이즈로 변형시켜줍니다. 파라미터에 직접 지정해주어야 하는 width, height 가 원본과 같으면 그냥 원본이 반환되고, 다르다면 새로운 비트맵을 생성합니다. 이 때, 지정한 width, height 파라미터에 따라 해상도가 변경됩니다. 즉, Bitmap 이기 때문에 해상도가 변경되면 화질 또한 변경될 수 있다는 것입니다.
filter 파라미터의 값을 true 로 넘길 경우, 성능 부하는 커지지만 이미지 품질이 향상됩니다. 권장 기본값은 true 입니다.
해당 이미지 위에 미리 구현해 둔 SectionSelector 를 띄워줍니다. 이 때, 이미지의 사이즈를 구하여 SectionSelector 의 파라미터로 넘겨주면서, SectionSelector 의 내부에는 해당 파라미터를 통해 원의 움직임 및 확대를 제한하는 기능도 구현해두었습니다.
잘라내기
잘라내는 행위를 구현하기 위해서 처음 어프로치에는 이미지나 화면의 특정 부분만큼을 원형으로 가져올 방법이 없을까라는 생각이 지배적이었습니다. 꽤 오랜 시간 찾아보았으나 적당한 답안이 도출되지 않아서,
Scaled 된 Bitmap 에서 원하는 부분을 사각형으로 자르고, 보여줄 때는 원형으로 보여주기로 결론 지었습니다.
화면에 그려지는 Bitmap 의 단위는 기본적으로 Pixel 입니다. 하지만 우리는 UI 를 구현할 때 Pixel 이나 Offset 보단 DP 를 사용하는 경향이 강합니다. 보다 직관적이고, 정수값이므로 사용하기에도 군더더기가 없기 때문입니다.
해당 작업을 진행하면서 가장 난처했던 것은 도무지 통일시킬 수 없는 Pixel 과 Offset, DP 의 조합이었습니다. 이 부분을 해결하기 위한 과정 중 제가 알게 된 사실은 다음과 같습니다.
- 1 Pixel == 1 Offset
- Pixel 은 절대값, Offset 은 비절대값
- Pixel 과 DP 는 서로 변환이 가능
Pixel 과 Offset은 기본적으로 치수가 같습니다. 즉, 화면을 움직이거나 스크롤링하는 경우 등에 Offset 이라는 키워드가 자주 등장하는데, 이 Offset 값은 결국 Pixel 값입니다. 그래서 원의 활동을 제한하거나 이미지의 크기를 결정하거나 하는 행위에 Offset 이 포함되어도 겁먹을 필요가 없습니다. Offset 은 Pixel 과 같고 Pixel 은 DP 로 변환이 가능하기 때문입니다.
이미지를 잘라내는 데에는 Bitmap 클래스의 createBitmap() 메서드가 사용됩니다.
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height)
해당 메서드는 createScaledBitmap() 과는 다르게, Bitmap 에 대한 어떠한 변형도 하지 않습니다. 밀도와 색상 모든 것이 그대로입니다. 파라미터로 전달된 좌측 상단 좌표에 따라 width, height 값만큼 사각형 Bitmap 을 만들어 반환합니다.
다음은 간단합니다. 반환된 비트맵을 원형으로 보여주기만 하면 되겠습니다.
기존 내장된 이미지는 초고화질 이미지입니다. 하지만 이를 Bitmap.createScaledBitmap() 메서드를 통해 작은 사이즈의 새로운 Bitmap 을 생성하였으므로 원을 작게하고 Crop 하면 저화질로 보이는 것입니다. 만약 화질을 그대로 유지하고 싶다면 createScaledBitmap() 메서드를 통해 이미지를 보여주고, 실제로 Crop 하는 것은 원본 이미지에서 Crop 하여야 할 것입니다.
그렇게 되면 SectionSelector 의 위치값에 따라 대응되는 원본 이미지 위치값이 상이할 수 있는데, 이 부분은 Pixel 이나 Offset 이 아닌 비율 개념을 접목시켜 진행하여야 할 것 같습니다. 기회가 된다면 이와 같은 스펙으로 비슷하게 하나 더 만들어 보아도 좋을 것 같다는 생각이 듭니다.
Image, Drawable, Bitmap 을 사용하는 것은 사실 쉽습니다. 메서드를 보고 적절한 파라미터만 넘겨주면 되니 말입니다. 하지만 그 기저부를 파악하고 이를 활용하여 직접 어떠한 기능을 구현하는 것은 마냥 쉬운 것 같지만은 않습니다. 저는 이번 구현을 통해 Pixel 과 Offset, Modifier 의 detectDragGestures, Bitmap 에 관한 이해를 얻을 수 있었습니다.
'Android > Android Custom View' 카테고리의 다른 글
[Jetpack Compose] ImageCropper 라이브러리 없이 구현하기 Part II (2) | 2023.12.01 |
---|---|
[Jetpack Compose] HorizontalPagerIndicator 라이브러리 없이 구현하기 (0) | 2022.12.28 |
[XML] AmbientLightView (0) | 2022.06.10 |
[XML] LineWork (0) | 2022.05.12 |
[XML] HeaderTextView (0) | 2022.05.11 |