Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

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

Billcorea 2025. 11. 17. 15:52
반응형

 

 

🦾 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)

핵심 접근법은 다음과 같습니다.

  1. Density와 fontScale을 명시적으로 처리
    • LocalDensity와 사용자 fontScale을 제한하여 극단적 값으로 레이아웃이 깨지지 않게 함.
    • sp 단위로 정의한 기준 텍스트 크기를 Density로 px로 변환한 다음, 캔버스 스케일 계수(화면 비율)를 곱해 최종 textSize를 결정.
  2. 확대/축소 및 드래그 제약(clamp)
    • 컨테이너(canvasSize 또는 부모 containerSize)와 콘텐츠(폼)의 실제 크기를 비교해 허용 가능한 translation 범위를 계산합니다.
    • 콘텐츠가 뷰보다 클 때는 minX = vw - contentW, maxX=0 처럼 좌표를 제한해 사용자가 빈 공간을 보지 못하게 함.
  3. 그리기 순서와 보이는 선 문제 해결
    • 배경색/패널을 먼저 그리고, 이후에 라인들을 그리거나 특정 선은 마지막에 다시 그려 겹침 문제를 해결했습니다.
  4. 문자열 리소스화
    • 하드코딩된 한글을 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)

 

반응형