이 글은 Today I Learn 시리즈의 69번째 기록입니다. (총 133개)

메트로놈 비주얼을 다듬으면서 나온 안드로이드 기법들. 발자국 인디케이터가 줄바꿈에서 세트째 찢어지던 문제를 그룹 레이아웃으로 푸는 법, 줄 수를 화면 폭으로 계산하는 법, 버튼 글자가 꺾이지 않게 막는 법, 그리고 스피너 두 개를 분수처럼 보이게 만드는 스타일링까지.


레이아웃 / 그룹핑

세트 단위 줄바꿈 — 중첩 LinearLayout 그룹

9/8 박자면 발자국이 [큰1+작2] × 3세트다. 점 9개를 반으로 나눠 두 줄에 뿌리면 큰 발자국과 그 작은 발자국들이 다른 줄로 갈라진다. “한 박 = 한 세트”라는 도메인 단위를 레이아웃 구조에도 그대로 반영해야 한다 — 박마다 작은 LinearLayout 그룹을 만들고, 줄에는 점이 아니라 그룹만 얹는다.

fun makeBeatGroup(): LinearLayout {
    val group = LinearLayout(this).apply {
        orientation = LinearLayout.HORIZONTAL
        gravity = Gravity.CENTER_VERTICAL
    }
    group.addView(makeDot(true))                           // 큰 발자국
    for (s in 1 until subs) group.addView(makeDot(false))  // 작은 발자국들
    return group
}

for (count in rowBeats) {
    val row = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL }
    repeat(count) { row.addView(makeBeatGroup()) }   // 줄에는 세트만
    container.addView(row)
}

주의할 불변식 하나 — 점등은 indicatorViews 리스트를 인덱스 순서로 돌기 때문에, 그룹으로 감싸더라도 리스트에 넣는 순서가 오디오 서브디비전 순서와 같아야 소리와 불빛이 어긋나지 않는다.


줄 수는 점 개수가 아니라 실제 폭으로

처음엔 “점 5개 이상이면 2줄”처럼 개수로 판정했는데, 이 방식은 점 크기·화면 폭·가로모드가 바뀔 때마다 어긋난다. 필요한 총 폭을 계산해 화면 폭과 비교하고, 넘치는 만큼 줄 수를 올림 나눗셈으로 구하는 게 맞다.

val groupWidth = (largeSize + 2 * largeMargin) + (subs - 1) * (smallSize + 2 * smallMargin)
val availWidth = resources.displayMetrics.widthPixels - horizontalPadding

// 올림 나눗셈: 필요한 줄 수 (1 ~ beats)
val rows = ((beats * groupWidth + availWidth - 1) / availWidth).coerceIn(1, beats)
val perRow = (beats + rows - 1) / rows

가로모드는 화면이 넓어 자동으로 1줄이 되고, 12/4처럼 극단적인 경우엔 3줄 이상으로 늘어난다. 분기 코드 없이 같은 식 하나로 다 커버된다.


한 요소를 키우면 이웃이 밀린다

BPM 숫자를 50→68sp로 키웠더니 옆의 ±5/±1 버튼 칸이 좁아져 버튼 글자가 두 줄로 꺾였다. weight로 나눠 갖는 행에서는 한 요소의 크기가 형제 요소의 폭을 직접 깎는다. 키우는 쪽을 타협(68→56sp)하고, 버튼에는 줄바꿈 자체를 금지시켰다.

<!-- 버튼 스타일 -->
<item name="android:maxLines">1</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:paddingRight">8dp</item>
<item name="android:minWidth">64dp</item>

스타일 / 드로어블

shape drawable에 테마색 스트로크

둥근 테두리 박스를 그릴 때 색을 #...으로 박으면 테마(다크 ↔ 강아지)를 바꿔도 테두리가 그대로다. drawable XML 안에서도 ?attr/를 쓸 수 있어서(API 21+), 테마 속성을 참조하면 테마 전환에 자동으로 따라온다.

<shape android:shape="rectangle">
    <solid android:color="@android:color/transparent" />
    <corners android:radius="16dp" />
    <stroke android:width="1.5dp" android:color="?attr/colorPrimary" />
</shape>

커스텀 스피너 아이템으로 분수 UI

기본 스피너 룩(작은 글자, 배경, 화살표)으로는 박자표로 안 읽혔다. 스피너의 “선택된 상태” 모양은 어댑터에 넘기는 레이아웃이 결정하므로, 굵은 큰 숫자 + 투명 배경 전용 레이아웃을 만들어 물린다. 분자·분모 스피너를 세로로 쌓고 사이에 가로줄 View를 넣으면 분수가 된다.

<!-- spinner_ts_selected_item.xml -->
<TextView
    android:gravity="center"
    android:textColor="?attr/mtInk"
    android:textSize="22sp"
    android:textStyle="bold" />
ArrayAdapter.createFromResource(
    this, R.array.numerator_options, R.layout.spinner_ts_selected_item
)

드롭다운 팝업 모양은 setDropDownViewResource로 따로 지정하므로, 선택 상태만 크게 하고 팝업은 일반 목록으로 둘 수 있다.


MaterialButton cornerRadius — 스타일에서 일괄 라운드

버튼 모서리는 뷰마다 만지지 않고 스타일에 cornerRadius를 둬서 일괄 적용한다. 아웃라인 버튼과 채움 버튼에 각각 다른 반경을 주면 위계도 생긴다.

<style name="Widget.SimpleMetronome.OutlinedAccent" parent="...OutlinedButton">
    <item name="cornerRadius">18dp</item>
</style>
<style name="Widget.SimpleMetronome.ToggleAccent" parent="...Button">
    <item name="cornerRadius">22dp</item>
    <item name="android:textStyle">bold</item>
</style>

SeekBar 틴트 3종

시크바는 색이 세 군데로 나뉜다 — 지나온 트랙, 남은 트랙, 손잡이. 각각 따로 틴트해야 테마에 온전히 녹는다.

<SeekBar
    android:progressTint="?attr/colorPrimary"           <!-- 지나온 트랙 -->
    android:progressBackgroundTint="?attr/mtIndicatorOff" <!-- 남은 트랙 -->
    android:thumbTint="?attr/mtAccent" />                 <!-- 손잡이 -->

설계

중복 컨트롤은 지운다

박자표 도입 전에는 “Sub per Beat”(분할) 컨트롤이 필요했지만, 박자표는 그 정보를 이미 담고 있다 — 6/8을 고르면 셋잇단이고, 4/4를 고르면 4분음표다. 표현이 겹치는 컨트롤을 남겨두면 “6/8 + 분할 2”처럼 서로 모순되는 조합이 생긴다. 컨트롤을 지우니 하단이 [박자표 Play] 둘로 줄었고, 3칸 GridLayout도 단순한 가로 LinearLayout으로 바뀌었다.

Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 습관만들기 Pawbit TIL 1 27 insertOnConflictUpdate vs DoUpdate(target) 28 세션 필터 29 아코디언(Accordiaon) UI를 펼친상태로 만들기 30 input의 step 31 습관만들기 Pawbit TIL 2 32 습관만들기 Pawbit TIL 3 33 Word Cloud 34 Google Sheets를 데이터 버스로(with AppSheet) 35 Django 모델 텍스트 필드 자동 수집 패턴 36 localStorage로 섹션 토글 상태 유지 37 순차 ID 생성(`select_for_update()` + `max()` 조합) 38 역참조 검색과 distinct() 39 xlsx 다운로드와 로딩 오버레이 충돌 40 Android 파일 공유 MIME 타입 41 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 42 크레센도 Phaser + TypeScript TIL 1 43 크레센도 Phaser + TypeScript TIL 2 44 크레센도 Phaser + TypeScript TIL 3 45 크레센도 Phaser + TypeScript TIL 4 46 크레센도 Phaser + TypeScript TIL 5 47 여둘까 Office Layout TIL 1 48 습관만들기 Pawbit TIL 4 49 여둘까 Office Layout TIL 2 50 여둘까 Office Layout TIL 3 51 크레센도 Phaser + TypeScript TIL 6 52 UTF-8 BOM과 PowerShell 파일 쓰기 53 소리꽃 KeyBloom TIL 1 54 소리꽃 KeyBloom TIL 2 55 소리꽃 KeyBloom TIL 3 56 소리꽃 KeyBloom TIL 4 57 소리꽃 KeyBloom TIL 5 58 습관만들기 Pawbit TIL 5 59 여둘까 Office Layout TIL 4 60 소리꽃 KeyBloom TIL 6 61 소리꽃 KeyBloom TIL 7 62 소리꽃 KeyBloom TIL 8 63 메트로놈 Simple Metronome TIL 1 64 소리꽃 KeyBloom TIL 9 65 메트로놈 Simple Metronome TIL 2 66 소리꽃 KeyBloom TIL 10 67 습관만들기 Pawbit TIL 6 68 크레센도 Phaser + TypeScript TIL 7 69 메트로놈 Simple Metronome TIL 3 읽는 중 70 습관만들기 Pawbit TIL 7 71 정적 블로그 SEO 정비와 Pagefind 검색 도입 72 크레센도 Phaser + TypeScript TIL 8 73 메트로놈 Simple Metronome TIL 4 74 습관만들기 Pawbit TIL 8 75 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 76 메트로놈 Simple Metronome TIL 5 77 모래게임 Sandrop TIL 1 78 습관만들기 Pawbit TIL 9 79 메트로놈 Simple Metronome TIL 6 80 모래게임 Sandrop TIL 2 81 모래게임 Sandrop TIL 3 82 습관만들기 Pawbit TIL 10 83 습관만들기 Pawbit TIL 11 84 메트로놈 Simple Metronome TIL 7 85 모래게임 Sandrop TIL 4 86 모래게임 Sandrop TIL 5 87 습관만들기 Pawbit TIL 12 88 모래게임 Sandrop TIL 6 89 소리꽃 KeyBloom TIL 11 90 모래게임 Sandrop TIL 7 91 모래게임 Sandrop TIL 8 92 소리꽃 KeyBloom TIL 12 93 모래게임 Sandrop TIL 9 94 소리꽃 KeyBloom TIL 13 95 모래게임 Sandrop TIL 10 96 소리꽃 KeyBloom TIL 14 97 소리꽃 KeyBloom TIL 15 98 소리꽃 KeyBloom TIL 16 99 콘티온 Conti On TIL 1 100 소리꽃 KeyBloom TIL 17 101 콘티온 Conti On TIL 2 102 습관만들기 Pawbit TIL 13 103 콘티온 Conti On TIL 3 104 모래게임 Sandrop TIL 11 105 소리꽃 KeyBloom TIL 18 106 여둘까 Office Layout TIL 5 107 모래게임 Sandrop TIL 12 108 모래게임 Sandrop TIL 13 109 콘티온 Conti On TIL 4 110 모래게임 Sandrop TIL 14 111 소리꽃 KeyBloom TIL 19 112 소리꽃 KeyBloom TIL 20 113 소리꽃 KeyBloom TIL 21 114 소리꽃 KeyBloom TIL 22 115 습관만들기 Pawbit TIL 14 116 모래게임 Sandrop TIL 15 117 모래게임 Sandrop TIL 16 118 소리꽃 KeyBloom TIL 23 119 소리꽃 KeyBloom TIL 24 120 소리꽃 KeyBloom TIL 25 121 소리꽃 KeyBloom TIL 26 122 온실 GreenHouse TIL 1 123 콘티온 Conti On TIL 5 124 모래게임 Sandrop TIL 17 125 소리꽃 KeyBloom TIL 27 126 온실 GreenHouse TIL 2 127 콘티온 Conti On TIL 6 128 모래게임 Sandrop TIL 18 129 콘티온 Conti On TIL 7 130 모래게임 Sandrop TIL 19 131 소리꽃 KeyBloom TIL 28 132 모래게임 Sandrop TIL 20 133 모래게임 Sandrop TIL 21