Skip to content

Instantly share code, notes, and snippets.

@benigumocom
Last active July 6, 2026 08:19
Show Gist options
  • Select an option

  • Save benigumocom/f9525ad7833ab00e385343cf5f893e76 to your computer and use it in GitHub Desktop.

Select an option

Save benigumocom/f9525ad7833ab00e385343cf5f893e76 to your computer and use it in GitHub Desktop.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppNavigationScaffold() {
val topBarHeight = 64.dp
val bottomBarHeight = 80.dp
val navController = rememberNavController()
var barsVisible by remember { mutableStateOf(true) }
val animatedTopOffset by animateDpAsState(
targetValue = if (barsVisible) 0.dp else -topBarHeight,
animationSpec = tween(durationMillis = 200),
label = "TopBarOffset"
)
val animatedBottomOffset by animateDpAsState(
targetValue = if (barsVisible) 0.dp else bottomBarHeight,
animationSpec = tween(durationMillis = 200),
label = "BottomBarOffset"
)
Scaffold(
topBar = {
TopAppBar(
title = { Text("共通タイトル") },
modifier = Modifier.height(topBarHeight).offset(y = animatedTopOffset)
)
},
bottomBar = {
NavigationBar(
modifier = Modifier.height(bottomBarHeight).offset(y = animatedBottomOffset)
) {
NavigationBarItem(
icon = { Icon(Icons.Default.Home, null) },
label = { Text("Home") },
selected = true,
onClick = { navController.navigate("home") }
)
NavigationBarItem(
icon = { Icon(Icons.Default.Settings, null) },
label = { Text("Setting") },
selected = false,
onClick = { navController.navigate("setting") }
)
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.fillMaxSize()
) {
composable("home") {
HomeScreen(
topBarHeight = topBarHeight,
bottomBarHeight = bottomBarHeight,
onScrollDirectionChanged = { visible -> barsVisible = visible }
)
}
composable("setting") {
SettingScreen(
topBarHeight = topBarHeight,
bottomBarHeight = bottomBarHeight,
onScrollDirectionChanged = { visible -> barsVisible = visible }
)
}
}
}
}
/**
* ホーム画面
*/
@Composable
fun HomeScreen(
topBarHeight: Dp,
bottomBarHeight: Dp,
onScrollDirectionChanged: (Boolean) -> Unit
) {
val scrollState = rememberLazyListState()
// 拡張関数をここでサクッと呼び出す
scrollState.trackScrollDirection(onScrollDirectionChanged)
LazyColumn(
state = scrollState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(top = topBarHeight, bottom = bottomBarHeight)
) {
items(50) { index ->
ListItem(headlineContent = { Text("ホームコンテンツ $index") })
HorizontalDivider()
}
}
}
/**
* 設定画面
*/
@Composable
fun SettingScreen(
topBarHeight: Dp,
bottomBarHeight: Dp,
onScrollDirectionChanged: (Boolean) -> Unit
) {
val scrollState = rememberLazyListState()
// どの画面でも state から直感的に生やせる
scrollState.trackScrollDirection(onScrollDirectionChanged)
LazyColumn(
state = scrollState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(top = topBarHeight, bottom = bottomBarHeight)
) {
items(30) { index ->
ListItem(headlineContent = { Text("設定項目 $index") })
HorizontalDivider()
}
}
}
@Composable
fun ScrollAwareLazyColumn(
topBarHeight: Dp,
bottomBarHeight: Dp,
onScrollDirectionChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(), // 外部から制御したい時のために一応保持
content: LazyListScope.() -> Unit
) {
// スクロール方向の検知ロジックをここに内包
LaunchedEffect(state) {
var previousPosition = 0
snapshotFlow { state.firstVisibleItemIndex * 10000 + state.firstVisibleItemScrollOffset }
.collect { currentPosition ->
if (state.isScrollInProgress) {
val delta = currentPosition - previousPosition
if (abs(delta) > 15) {
val visible = currentPosition < previousPosition
onScrollDirectionChanged(visible)
}
}
previousPosition = currentPosition
}
}
LazyColumn(
state = state,
modifier = modifier.fillMaxSize(),
// 共通の上下余白をここで強制適用
contentPadding = PaddingValues(top = topBarHeight, bottom = bottomBarHeight),
content = content
)
}
@Composable
fun LazyListState.trackScrollDirection(onScrollDirectionChanged: (Boolean) -> Unit) {
LaunchedEffect(this) {
var previousPosition = 0
snapshotFlow { firstVisibleItemIndex * 10000 + firstVisibleItemScrollOffset }
.collect { currentPosition ->
if (isScrollInProgress) {
val delta = currentPosition - previousPosition
if (abs(delta) > 15) {
val visible = currentPosition < previousPosition
onScrollDirectionChanged(visible)
}
}
previousPosition = currentPosition
}
}
}
fun Modifier.trackScrollDirection(
scrollState: LazyListState,
onScrollDirectionChanged: (Boolean) -> Unit
): Modifier = composed {
LaunchedEffect(scrollState) {
var previousPosition = 0
snapshotFlow { scrollState.firstVisibleItemIndex * 10000 + scrollState.firstVisibleItemScrollOffset }
.collect { currentPosition ->
if (scrollState.isScrollInProgress) {
val delta = currentPosition - previousPosition
if (abs(delta) > 15) {
val visible = currentPosition < previousPosition
onScrollDirectionChanged(visible)
}
}
previousPosition = currentPosition
}
}
this
}
@benigumocom

Copy link
Copy Markdown
Author

Compose State を Flow に変換する唯一の方法、それが snapshotFlow
https://android.benigumo.com/20260706/snapshotflow/

【Jetpack Compose】スクロール位置を正しく復元する
https://android.benigumo.com/20260624/restore-scroll-position/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment