Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
893eda5
[Add] systembars 모듈 — 설계 문서 + 순수 로직(Luma/IconDecider/BarRegistry)
sonms Oct 23, 2026
ce74867
[Add] systembars 공개 API — BarContent/Config, Modifier.systemBars, Hos…
sonms Oct 23, 2026
92aa8e6
[Add] :app systembars 데모 탭 — 스크롤/지정색/Dialog/PixelCopy
sonms Oct 23, 2026
393da20
[Add] systembars 계측 테스트 — 샘플링/레지스트리/리컴포지션/PixelCopy
sonms Oct 23, 2026
30eca2f
[Fix] systembars API 28 실기기 대응 — 캔버스 교체 샘플링, 깊이 우선순위, PixelCopy 안정화
sonms Oct 23, 2026
beb0a45
[Add] systembars 데모 — 비교(modifier 없음) 모드, 인텐트 진입점
sonms Oct 23, 2026
f7a4ef0
[Fix] systembars 스크롤 중 샘플링 누락 수정 + 단일 패스 샘플링, 트레이스 섹션
sonms Oct 23, 2026
d0a530a
[Change] systembars 기본 샘플링 간격 100ms → 300ms (스크롤 프레임 비용 완화)
sonms Oct 23, 2026
baf5cc3
[Docs] systembars README
sonms Oct 23, 2026
8eb4a8b
[Docs] systembars README — 릴리스 빌드 측정 반영
sonms Oct 23, 2026
a8c9141
[Test] systembars 3버튼 스크림 계측 테스트 + espresso 3.7.0 (API 37 호환)
sonms Oct 23, 2026
495ed34
[Docs] systembars ARCHITECTURE 정리 — 성능 결과, 낡은 문구 수정, deprecation 경고 억제
sonms Oct 23, 2026
7f06bd2
[Fix] systembars edge-to-edge 경고를 API 35 미만에서만 검사, 억제 범위 축소
sonms Oct 23, 2026
aa10ea2
[Docs] systembars 공개 API KDoc 한/영 병기, Module.md, Dokka 미문서화 경고 활성화
sonms Oct 23, 2026
76c4aa5
[Fix] SystemBarsHost config 변경 시 컨트롤러를 교체하지 않고 제자리 갱신
sonms Oct 23, 2026
73f8a8a
[Change] systembars 스택에서 가려진 Auto 노드는 샘플링 생략, 다시 이기면 즉시 재샘플
sonms Oct 23, 2026
d21eed5
[Add] systembars 바 영역 칠하기(BarPaint) — None(기본)/Sampled/Solid, API 28 …
sonms Oct 23, 2026
d19ffa2
[Docs] 내비바 스크림 안내 + 데모는 내비바 스크림을 투명으로
sonms Oct 23, 2026
cb54424
[Add] 데모 바 칠하기 모드를 인텐트로 지정(paint=none|sampled|solid) — 벤치마크용
sonms Oct 23, 2026
cf08e61
[Fix] systembars 린트 오류 수정(NewApi/CompositionLocal 읽기), explicitApi 활성화
sonms Oct 23, 2026
f1c9d4f
[Release] systembars 0.1.0 — 버전 올리기, 배포 워크플로에 systembars 태그 추가, READM…
sonms Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
name: Publish to Maven Central

# Tag convention: "<module>-v<version>", e.g. wheelpicker-v0.0.2 or ratingbar-v0.0.2.
# Tag convention: "<module>-v<version>", e.g. wheelpicker-v0.0.2 or systembars-v0.1.0.
# Only the module named in the tag is built, tested, and published.
on:
push:
tags:
- 'wheelpicker-v*'
- 'ratingbar-v*'
- 'systembars-v*'
- 'modifier-node-annotations-v*'
- 'modifier-node-processor-v*'

Expand Down Expand Up @@ -48,7 +49,7 @@ jobs:
# Android library modules test the release variant; Kotlin JVM modules have no
# variants, and the processor's real verification lives in :modifier-node-sample.
case "$MODULE" in
wheelpicker|ratingbar)
wheelpicker|ratingbar|systembars)
TEST_TASKS=":$MODULE:testReleaseUnitTest"
;;
modifier-node-annotations|modifier-node-processor)
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ kotlin {
dependencies {
implementation(project(":wheelpicker"))
implementation(project(":ratingbar"))
implementation(project(":systembars"))

// Modifier.Node 코드젠 — 노드 클래스에 @ModifierNodeFactory 를 붙이면
// Modifier 확장 함수 + ModifierNodeElement 가 생성된다. (modifier-node-sample 참고)
Expand Down
37 changes: 34 additions & 3 deletions app/src/main/java/com/sonms/velvetcompose/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.sonms.velvetcompose
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import android.graphics.Color as AndroidColor
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
Expand Down Expand Up @@ -35,24 +37,48 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sonms.ratingbar.RatingBar
import com.sonms.systembars.SystemBarsConfig
import com.sonms.systembars.SystemBarsHost
import com.sonms.velvetcompose.ui.theme.VelvetComposeTheme
import com.sonms.wheelpicker.HorizontalWheelPicker
import com.sonms.wheelpicker.VerticalWheelPicker
import com.sonms.wheelpicker.state.rememberWheelPickerState
import com.sonms.wheelpicker.style.WheelPickerDefaults

enum class SampleType {
VERTICAL_TIME, HORIZONTAL_TIME, RATING_BAR, MODIFIER_NODE
VERTICAL_TIME, HORIZONTAL_TIME, RATING_BAR, MODIFIER_NODE, SYSTEM_BARS
}

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// 기본 enableEdgeToEdge() 는 API 29 미만(및 3버튼)에서 내비바에 반투명 스크림을 깐다. 데모는 바 칠하기가 보이도록 투명으로 둔다.
enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto(AndroidColor.TRANSPARENT, AndroidColor.TRANSPARENT),
)
setContent {
var selectedSample by remember { mutableStateOf(SampleType.VERTICAL_TIME) }
// adb 로 바로 진입: --es sample SYSTEM_BARS --es bars_demo SCROLL|BASELINE|...
var selectedSample by remember {
mutableStateOf(
SampleType.entries.firstOrNull { it.name == intent.getStringExtra("sample") }
?: SampleType.VERTICAL_TIME,
)
}

VelvetComposeTheme(darkTheme = false) {
if (selectedSample == SampleType.SYSTEM_BARS) {
// 바 뒤까지 그려야 하므로 Scaffold 의 innerPadding 밖에서 전체 화면으로 띄운다.
SystemBarsHost(
config = SystemBarsConfig(intervalMillis = intent.getLongExtra("interval", 300L)),
) {
SystemBarsShowcase(
onBack = { selectedSample = SampleType.VERTICAL_TIME },
initialDemo = intent.getStringExtra("bars_demo"),
initialPaint = intent.getStringExtra("paint"),
)
}
return@VelvetComposeTheme
}
Scaffold (
modifier = Modifier
.fillMaxSize(),
Expand Down Expand Up @@ -85,13 +111,18 @@ class MainActivity : ComponentActivity() {
Button(onClick = { selectedSample = SampleType.MODIFIER_NODE }) {
Text(text = "Modifier.Node")
}

Button(onClick = { selectedSample = SampleType.SYSTEM_BARS }) {
Text(text = "SystemBars")
}
}

when (selectedSample) {
SampleType.VERTICAL_TIME -> VerticalTimePickerSample()
SampleType.HORIZONTAL_TIME -> HorizontalTimePickerSample()
SampleType.RATING_BAR -> VelvetRatingBarSample()
SampleType.MODIFIER_NODE -> ModifierNodeShowcase()
SampleType.SYSTEM_BARS -> Unit
}
}
}
Expand Down
241 changes: 241 additions & 0 deletions app/src/main/java/com/sonms/velvetcompose/SystemBarsShowcase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package com.sonms.velvetcompose

import android.graphics.Color as AndroidColor
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.DialogProperties
import com.sonms.systembars.BarContent
import com.sonms.systembars.BarPaint
import com.sonms.systembars.SystemBarsDialog
import com.sonms.systembars.systemBars

/**
* `:systembars` 라이브 데모. 상태바/내비바 아이콘이 화면 배경에 따라 바뀌는지 눈으로 확인한다.
* 바 뒤까지 그려야 하므로 `Scaffold` 의 innerPadding 없이 전체 화면을 쓴다.
*
* Live demo for `:systembars` — watch the status/navigation bar icons follow the screen behind them.
*/
private enum class BarsDemo(val label: String) {
SCROLL("Auto 스크롤"),
BASELINE("비교(modifier 없음)"),
DECLARED("지정/추출 색"),
DIALOG("Dialog"),
SURFACE("PixelCopy"),
STACKED("겹침(가려진 노드)"),
PAINT("바 칠하기"),
}

private val bandColors = listOf(
Color.White, Color(0xFF101010), Color(0xFF1E88E5), Color(0xFFFFEB3B), Color(0xFFE91E63), Color(0xFFF5F5F5),
)

@Composable
fun SystemBarsShowcase(onBack: () -> Unit, initialDemo: String? = null, initialPaint: String? = null) {
var demo by remember { mutableStateOf(BarsDemo.entries.firstOrNull { it.name == initialDemo } ?: BarsDemo.SCROLL) }

Box(Modifier.fillMaxSize()) {
when (demo) {
BarsDemo.SCROLL -> ScrollDemo(sampled = true)
BarsDemo.BASELINE -> ScrollDemo(sampled = false)
BarsDemo.DECLARED -> DeclaredDemo()
BarsDemo.DIALOG -> DialogDemo()
BarsDemo.SURFACE -> SurfaceDemo()
BarsDemo.STACKED -> StackedDemo()
BarsDemo.PAINT -> PaintDemo(initialPaint)
}

// 컨트롤은 바 영역을 피해 안쪽에 둔다 (statusBarsPadding / navigationBarsPadding).
Column(Modifier.statusBarsPadding().padding(horizontal = 8.dp)) {
TextButton(onClick = onBack) { Text("← 돌아가기") }
}
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.horizontalScroll(rememberScrollState())
.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
BarsDemo.entries.forEach { d ->
FilterChip(selected = demo == d, onClick = { demo = d }, label = { Text(d.label) })
}
}
}
}

/** 흰/검정/파랑/노랑/그라데이션 띠를 스크롤하면서 `Auto` 가 아이콘을 뒤집는지 본다. */
@Composable
private fun ScrollDemo(sampled: Boolean, paint: BarPaint = BarPaint.None) {
LazyColumn(
(if (sampled) Modifier.systemBars(BarContent.Auto, BarContent.Auto, paint, paint) else Modifier).fillMaxSize(),
) {
itemsIndexed(List(30) { it }) { i, _ ->
val c = bandColors[i % bandColors.size]
Box(
Modifier
.fillMaxWidth()
.height(if (i % 7 == 6) 220.dp else 140.dp)
.background(if (i % 7 == 6) Brush.verticalGradient(listOf(Color.White, Color.Black)) else Brush.linearGradient(listOf(c, c))),
contentAlignment = Alignment.Center,
) {
Text("#$i", color = if (c.luminanceIsLow()) Color.White else Color.Black)
}
}
}
}

/** 바 영역 칠하기: 투명(기본) / 샘플 색 단색 / 지정 색 단색. 스크롤하면서 상태바·내비바 띠를 본다. */
@Composable
private fun PaintDemo(initialMode: String? = null) {
var mode by remember {
mutableStateOf(when (initialMode) { "sampled" -> "샘플"; "solid" -> "단색"; else -> "투명" })
}
val solid by androidx.compose.animation.animateColorAsState(Color(0xFF3F51B5), label = "barSolid")
val paint = when (mode) {
"샘플" -> BarPaint.Sampled
"단색" -> BarPaint.Solid(solid)
else -> BarPaint.None
}
Box(Modifier.fillMaxSize()) {
ScrollDemo(sampled = true, paint = paint)
Row(Modifier.align(Alignment.Center), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
listOf("투명", "샘플", "단색").forEach { m ->
FilterChip(selected = mode == m, onClick = { mode = m }, label = { Text(m) })
}
}
}
}

/** 바깥 노드가 가려지는 구조: 안쪽 `ScrollDemo` 가 이기므로 바깥 `Auto` 는 샘플링할 필요가 없다. */
@Composable
private fun StackedDemo() {
Box(Modifier.systemBars(BarContent.Auto, BarContent.Auto).fillMaxSize()) {
ScrollDemo(sampled = true)
}
}

/** `Background(color)` 직접 지정. "추출 색"은 이미지 팔레트에서 뽑은 값을 같은 자리에 넘기면 된다. */
@Composable
private fun DeclaredDemo() {
var color by remember { mutableStateOf(Color(0xFF1E88E5)) }
Box(
Modifier
.systemBars(BarContent.Background(color), BarContent.Background(color))
.fillMaxSize()
.background(color),
contentAlignment = Alignment.Center,
) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
bandColors.forEach { c ->
Box(
Modifier
.size(44.dp)
.clip(CircleShape)
.background(c)
.clickable { color = c },
)
}
}
}
}

/** 다이얼로그는 별도 윈도우라 자체 호스트를 쓰는 `SystemBarsDialog` 로 연다. */
@Composable
private fun DialogDemo() {
var show by remember { mutableStateOf(false) }
Box(
// systemBars 를 background 보다 앞에 둬야 Auto 가 배경까지 샘플링한다.
Modifier.systemBars(BarContent.Auto, BarContent.Auto).fillMaxSize().background(Color.White),
contentAlignment = Alignment.Center,
) {
Button(onClick = { show = true }) { Text("다이얼로그 열기") }
}
if (show) {
SystemBarsDialog(
onDismissRequest = { show = false },
properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false),
) {
Box(
Modifier
.systemBars(BarContent.Dark, BarContent.Dark)
.fillMaxSize()
.background(Color(0xFF101010))
.clickable { show = false },
contentAlignment = Alignment.Center,
) { Text("어두운 다이얼로그 (탭하면 닫힘)", color = Color.White) }
}
}
}

/** `SurfaceView` 는 Compose 레이어가 못 읽으므로 `AutoWindow`(PixelCopy). 탭하면 흰/검정이 바뀐다. */
@Composable
private fun SurfaceDemo() {
var dark by remember { mutableStateOf(false) }
val argb = (if (dark) Color.Black else Color.White).toArgb()
Box(
Modifier
.systemBars(BarContent.AutoWindow, BarContent.AutoWindow)
.fillMaxSize()
.clickable { dark = !dark },
) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
SurfaceView(ctx).apply {
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(h: SurfaceHolder) = Unit
override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, hh: Int) = paint(h, tag as? Int ?: AndroidColor.WHITE)
override fun surfaceDestroyed(h: SurfaceHolder) = Unit
})
}
},
update = { sv ->
sv.tag = argb
if (sv.holder.surface.isValid) paint(sv.holder, argb)
},
)
Text("SurfaceView — 탭하면 색 전환", Modifier.align(Alignment.Center), color = if (dark) Color.White else Color.Black)
}
}

private fun paint(holder: SurfaceHolder, argb: Int) {
val canvas = holder.lockCanvas() ?: return
try { canvas.drawColor(argb) } finally { holder.unlockCanvasAndPost(canvas) }
}

private fun Color.luminanceIsLow(): Boolean = (red * 0.2126f + green * 0.7152f + blue * 0.0722f) < 0.4f
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ rootProject.name = "VelvetCompose"
include(":app")
include(":wheelpicker")
include(":ratingbar")
include(":systembars")
include(":modifier-node-annotations")
include(":modifier-node-processor")
include(":modifier-node-sample")
Loading
Loading