반응형
🦾 Android | 서식 캔버스 폼 - 이 앱 개발의 기본 지식 정리

개요 (Intro)
- 오늘의 목표 / 배경: 이 프로젝트는 Jetpack Compose의
Canvas를 이용해 계산서 형태의 서식을 그리는 UI를 구현합니다. 이미지 기반 폼을 확대/축소/드래그 하여 전체 레이아웃을 확인할 수 있게 하는 것이 핵심입니다. - 해결하려던 문제: 화면 크기/해상도, 글자 크기(사용자 폰트 스케일) 대응, 핀치-줌 및 팬(드래그) 동작과 콘텐츠 영역의 이동 한계(클램핑)를 안정적으로 처리해야 했습니다.
- 사용한 기술 스택: Kotlin, Jetpack Compose(Canvas), Compose gesture APIs(
pointerInput,detectTransformGestures), Android resource <strings.xml>, LocalDensity/LocalConfiguration.
📅 날짜: 2025.11.17
🎯 목표: Canvas 기반 전자세금계산서 UI 구현과 사용자 폰트/스케일/제약 처리 정리
🧰 기술: Kotlin, Jetpack Compose(Canvas), pointerInput, LocalDensity, string resources
문제 정의 (Problem / Motivation)
- Canvas로 서식을 직접 그릴 때의 주요 고려사항
- 픽셀 단위 vs dp/sp: 텍스트와 선의 두께는 Density와 사용자 fontScale에 영향을 받음.
- 확대/축소 시 드래그 범위: 콘텐츠를 확대했을 때 전체를 드래그로 볼 수 있게 클램프 범위를 계산해야 함.
- 레이어 순서 문제: 먼저 그린 선이 뒤에 그려지는 배경 요소에 가려질 수 있음(선은 적절한 순서로 재그리기 필요).
- 구체적 오류/도전 과제
- 상단 레이블의 상단선이 보이지 않는 문제 — 그린 순서(선 먼저, 배경 나중)를 수정해야 했음.
- 문자열 하드코딩: UI 텍스트(한글)가 소스에 하드코딩되어 있어 번역/유지보수에 불리함.
// 예시: Contact 데이터 구조 (프로젝트의 Constants.kt 발췌)
data class Contact(
val regNo: String,
val branch: String,
val companyName: String,
val name: String,
val businessPlace: String,
val businessType: String,
val businessClass: String,
val email: String,
val email2: String = ""
)
해결 과정 (How I Solved It)
핵심 접근법은 다음과 같습니다.
- Density와 fontScale을 명시적으로 처리
LocalDensity와 사용자 fontScale을 제한하여 극단적 값으로 레이아웃이 깨지지 않게 함.- sp 단위로 정의한 기준 텍스트 크기를 Density로 px로 변환한 다음, 캔버스 스케일 계수(화면 비율)를 곱해 최종 textSize를 결정.
- 확대/축소 및 드래그 제약(clamp)
- 컨테이너(canvasSize 또는 부모 containerSize)와 콘텐츠(폼)의 실제 크기를 비교해 허용 가능한 translation 범위를 계산합니다.
- 콘텐츠가 뷰보다 클 때는 minX = vw - contentW, maxX=0 처럼 좌표를 제한해 사용자가 빈 공간을 보지 못하게 함.
- 그리기 순서와 보이는 선 문제 해결
- 배경색/패널을 먼저 그리고, 이후에 라인들을 그리거나 특정 선은 마지막에 다시 그려 겹침 문제를 해결했습니다.
- 문자열 리소스화
- 하드코딩된 한글을
strings.xml로 이동하고, 코드에서는stringResource(id = R.string.xxx)를 사용하도록 수정했습니다.
- 하드코딩된 한글을
// 캔버스 내에서 텍스트 크기 계산(요약)
val effectiveFontScale = userFontScale.coerceIn(0.75f, 1.2f)
val effectiveDensity = Density(density.density, effectiveFontScale)
val baseTextPx = with(effectiveDensity) { 11.sp.toPx() }
val globalSmallPx = baseTextPx * scaleFactorCanvas * 1.3f
또한, 코드 유지보수를 위해 문자열 리소스 분리와 함수화(텍스트 정렬/자동 크기조절)를 적용했습니다.
결과 (Result)
- Canvas 기반 폼이 안정적으로 렌더링되고 확대/축소/드래그 동작이 자연스럽게 동작하도록 개선했습니다.
- 텍스트 크기와 배치가 사용자 fontScale에 영향받으나 레이아웃 붕괴를 최소화하도록 보정 로직을 넣어 안정성 향상.
- 하드코딩 문자열을 리소스화하여 향후 다국어 지원과 유지보수가 쉬워졌습니다.
✅ 상단 레이블 선 문제 해결(그리기 순서 수정) ✅ 문자열 리소스 분리 완료 ✅ 확대/축소 시 전체 영역을 드래그해 확인할 수 있도록 클램프 로직 반영
느낀 점 / 회고 (Reflection)
- Canvas로 직접 그리는 구현은 유연하지만, 모든 것을 수동으로 계산해야 하기 때문에 레이아웃 버그가 생기기 쉽습니다. 따라서 작은 유틸 함수(문자열 정렬, 자동 폰트 축소, 클램프 계산)를 잘 분리해두는 것이 큰 도움이 됩니다.
- 사용자 폰트 스케일이나 화면 종횡비 같은 환경 차이를 고려하지 않으면 UI가 깨지기 쉬우므로, 기본값과 허용 범위를 명확히 두는 것이 좋습니다.
- 리소스 분리는 초기 작업량을 늘리지만 중장기적으로 유지보수 비용을 크게 낮춰줍니다.
참고자료 (References)
- [Jetpack Compose - Canvas documentation](https://developer.android.com/jetpack/compose/graphics)
- [Compose Pointer Input and Gestures](https://developer.android.com/jetpack/compose/gestures)
- [Android Developers - Supporting multiple screens](https://developer.android.com/training/multiscreen)
반응형
'모바일 앱(안드로이드)' 카테고리의 다른 글
| 🩺 Android | Health Connect 걸음 수 집계 캐시 & 상단바 최소 높이 적용 (1) | 2025.11.21 |
|---|---|
| 🪄 Wear OS | Complication 탭 액션 구현 및 타일/칩 UI 정리 (1) | 2025.11.19 |
| 개발일기: Wear OS Complication 클릭 시 앱 실행하기 (1) | 2025.11.15 |
| Android | Jetpack Compose로 Photo Picker 구현 (백포트 없이) (1) | 2025.11.13 |
| 개발일기: Wear OS Tile Chip 너비 문제 해결 (2) | 2025.11.11 |