반응형
맞춤 페인터 (Custom painter)
Painter 객체
- 그릴 수 있는 항목을 나타내고(Android에서 정의된 Drawable API의 대체 API) 객체를 사용하고 있는 컴포저블의 measure 또는 layout 과정에 영향을 미치는 데 사용됩니다.
- Painter는 지정된 경계 내에서 엄격하게 그려진다.
- cf) 컴포저블의 measure 또는 layout 과정에 영향을 주지 않는DrawModifier와 다릅니다.
- painterResource()를 사용하면 애셋에 맞는 올바른 페인터(예: BitmapPainter 또는 VectorPainter)가 반환됩니다.
(둘의 차이점에 관한 자세한 내용은 ImageBitmap과 ImageVector 섹션을 참고)- BitmapPainter는 ImageBitmap을 사용하여 화면에 Bitmap을 그릴 수 있습니다.
CustomPainter 만들기
- CustomPainter를 만들려면, Painter 클래스를 확장하고 onDraw 메서드를 구현합니다.
이렇게 하면 DrawScope에 액세스하여 맞춤 그래픽을 그릴 수 있습니다.
또한, CustomPainter가 포함된 컴포저블에 영향을 미치는 데 사용되는 intrinsicSize를 재정의할 수 있습니다.
- (1) CustomPainter 구현하기
- (1-1). measure 또는 layout 과정에 영향을 주어야 하는 경우, Painter를 사용해 CustomPainter 구현해야 합니다.
- (1-2). 지정된 경계에서만 렌더링해야 한다면 DrawModifier를 사용해 CustomPainter 구현해야 합니다.
class OverlayImagePainter constructor(
private val image: ImageBitmap,
private val imageOverlay: ImageBitmap,
private val srcOffset: IntOffset = IntOffset.Zero,
private val srcSize: IntSize = IntSize(image.width, image.height),
private val overlaySize: IntSize = IntSize(imageOverlay.width, imageOverlay.height)
) : Painter() {
private val size: IntSize = validateSize(srcOffset, srcSize)
override fun DrawScope.onDraw() {
// draw the first image without any blend mode
drawImage(
image,
srcOffset,
srcSize,
dstSize = IntSize(
this@onDraw.size.width.roundToInt(),
this@onDraw.size.height.roundToInt()
)
)
// draw the second image with an Overlay blend mode to blend the two together
drawImage(
imageOverlay,
srcOffset,
overlaySize,
dstSize = IntSize(
this@onDraw.size.width.roundToInt(),
this@onDraw.size.height.roundToInt()
),
blendMode = BlendMode.Overlay
)
}
/**
* Return the dimension of the underlying [ImageBitmap] as it's intrinsic width and height
*/
override val intrinsicSize: Size get() = size.toSize()
private fun validateSize(srcOffset: IntOffset, srcSize: IntSize): IntSize {
require(
srcOffset.x >= 0 &&
srcOffset.y >= 0 &&
srcSize.width >= 0 &&
srcSize.height >= 0 &&
srcSize.width <= image.width &&
srcSize.height <= image.height
)
return srcSize
}
}
- (2-1) CustomPainter를 Image컴포저블에 적용하기 : 소스 이미지 위에 이미지를 오버레이
val rainbowImage = ImageBitmap.imageResource(id = R.drawable.rainbow)
val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)
val customPainter = remember {
OverlayImagePainter(dogImage, rainbowImage)
}
Image(
painter = customPainter,
contentDescription = stringResource(id = R.string.dog_content_description),
contentScale = ContentScale.Crop,
modifier = Modifier.wrapContentSize()
)
- (2-2) CustomPainter를 다른 컴포저블에 적용하기 : Box컴포저블 위에 이미지를 오버레이
- 맞춤 페인터를 `Modifier.paint(customPainter)`와 함께 사용하여 다른 컴포저블에 콘텐츠를 그릴 수도 있습니다.
val rainbowImage = ImageBitmap.imageResource(id = R.drawable.rainbow)
val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)
val customPainter = remember {
OverlayImagePainter(dogImage, rainbowImage)
}
Box(
modifier =
Modifier.background(color = Color.Gray)
.padding(30.dp)
.background(color = Color.Yellow)
.paint(customPainter)
) { /** intentionally empty **/ }
이미지 성능 최적화를 위한 권장사항
이미지 작업을 할 때는 주의하지 않으면 금방 성능 문제가 발생할 수 있습니다.
Ex) 큰 비트맵으로 작업할 때 OutOfMemoryError가 발생하기가 매우 쉽습니다.
1. 필요한 크기의 비트맵만 로드하기
화면에 이미지를 표시하는 경우 이미지 해상도를 낮추거나 이미지 컨테이너 크기까지만 이미지를 로드해야 합니다.
필요보다 더 큰 이미지를 지속적으로 로드하면 GPU 캐시가 소진되어 UI 렌더링 성능이 저하될 수 있습니다.
1-0. 이미지 크기를 관리 가이드
- 출력 이미지에 영향을 주지 않고 이미지 파일을 가능한 한 작게 축소합니다.
- 이미지 로드 라이브러리를 사용하여 화면의 뷰 크기에 맞게 이미지를 축소합니다.
- 이렇게 하면 화면 로드 성능이 개선됩니다.
- (주의) painterResource를 사용하면 이미지가 화면에 표시되는 컴포저블의 크기에 맞춰 조정되지 않습니다.
작은 컴포저블에 큰 이미지가 있다면 경계에 맞게 이미지를 축소하는 이미지 로드 라이브러리를 사용해야 합니다.
1-1. 가능하면 비트맵 대신 벡터 사용하기
- JPEG 또는 PNG 대신 WebP 형식으로 이미지를 변환합니다.
- 벡터 이미지는 다른 크기로 조정할 때 픽셀화되지 않으므로 비트맵보다 벡터 이미지를 사용하는 것이 좋습니다.
1-2. 다양한 화면 크기(해상도)에 맞는 대체 리소스 제공
- 앱과 함께 이미지를 제공하는 경우 기기 해상도에 따라 다양한 크기의 애셋을 제공하는 것이 좋습니다.
이렇게 하면 기기에서 앱의 다운로드 크기가 줄어들고 해상도가 낮은 기기에 더 낮은 해상도의 이미지가 로드되므로 성능이 개선됩니다. - 다양한 기기 크기에 맞는 대체 비트맵을 제공하는 방법 : 대체 비트맵 문서를 확인
1-3. ImageBitmap 사용 시 그리기 전에 prepareToDraw 호출
- ImageBitmap을 사용할 때 GPU에 텍스처를 업로드하는 프로세스를 시작하려면 실제로 그리기 전에 ImageBitmap#prepareToDraw()를 호출합니다. 그러면 GPU가 텍스처를 준비하고 화면에 시각 요소를 표시하는 성능을 향상할 수 있습니다.
- (대부분의 이미지 로드 라이브러리는 이러한 최적화를 이미 실행하지만) ImageBitmap 클래스를 직접 사용한다면 이 사항에 유의해야 합니다.
1-4. Painter 대신 `IntDrawableRes` 또는 `URL`을 매개변수로 컴포저블에 전달하기
- Painter를 매개변수로 전달하는 대신 URL 또는 드로어블 리소스 ID를 매개변수로서 컴포저블에 전달하는 것이 좋습니다.
- 불안정한 클래스의 경우 데이터가 변경되었는지 컴파일러가 쉽게 추론할 수 없으므로 불필요한 리컴포지션으로 이어질 수 있습니다.
- 이미지 처리의 복잡성으로 인해(예: Bitmaps의 equals 함수를 작성하는 데 드는 높은 계산 비용) Painter API는 안정적인 클래스로 명시하지 않습니다.
2. 비트맵을 필요한 기간보다 오래 메모리에 저장하지 않기
- 메모리에 로드하는 비트맵이 많을수록 기기의 메모리가 부족할 가능성이 커집니다.
- 화면에 대량의 이미지 컴포저블 목록을 로드하는 경우, 큰 목록을 스크롤할 때 메모리가 확보되도록 LazyColumn 또는 LazyRow를 사용합니다.
3. 대용량 이미지를 AAB/APK 파일로 패키징하지 않기
- 앱 다운로드 크기가 커지는 주요 원인 중 하나는 AAB 또는 APK 파일 내에 패키징된 그래픽 때문입니다.
APK Analyzer 도구를 사용하여 필요한 이미지 파일보다 크게 패키징하지 않도록 합니다. - 크기를 줄이거나 이미지를 서버에 두고 필요할 때만 다운로드하는 것이 좋습니다.
출처
반응형