如何在Jetpack Compose中用Motion Layout实现滚动折叠搜索栏
在Jetpack Compose中实现滚动联动的搜索栏隐藏/显示效果
你可以通过滚动状态监听+Compose基础动画或者MotionLayout来实现这个需求,以下是两种可行方案:
方案一:基础滚动监听+Offset动画(轻量高效)
核心思路是通过共享ViewModel传递滚动状态,控制父布局中搜索栏的偏移量实现隐藏/显示:
1. 定义共享ViewModel
用于跨组件传递搜索栏的显示状态:
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class MainViewModel : ViewModel() { private val _isSearchBarVisible = MutableStateFlow(true) val isSearchBarVisible: StateFlow<Boolean> = _isSearchBarVisible fun updateSearchBarVisibility(visible: Boolean) { viewModelScope.launch { _isSearchBarVisible.emit(visible) } } }
2. 父布局(ParentScreen)实现搜索栏动画
使用animateDpAsState实现平滑的偏移动画:
@Composable fun ParentScreen(viewModel: MainViewModel = viewModel()) { val isSearchBarVisible by viewModel.isSearchBarVisible.collectAsStateWithLifecycle() // 根据显示状态计算偏移量,60dp为搜索栏预估高度,可根据实际调整 val searchBarOffset by animateDpAsState( targetValue = if (isSearchBarVisible) 0.dp else (-60).dp, animationSpec = tween(durationMillis = 300) ) Column(modifier = Modifier.fillMaxSize()) { // 带偏移动画的搜索栏 SearchBar( modifier = Modifier .offset(y = searchBarOffset) .fillMaxWidth() ) // TabView + ViewPager 组件 TabView() } } @Composable fun SearchBar(modifier: Modifier = Modifier) { TextField( value = "", onValueChange = {}, placeholder = { Text("搜索") }, modifier = modifier .padding(horizontal = 16.dp, vertical = 8.dp) .background(Color.White, RoundedCornerShape(8.dp)) .padding(horizontal = 16.dp) ) }
3. 子页面(HomeScreen)监听滚动状态
在LazyColumn中监听滚动方向,更新ViewModel的状态:
@Composable fun HomeScreen(viewModel: MainViewModel = viewModel()) { val listState = rememberLazyListState() var lastScrollOffset by remember { mutableStateOf(0) } LaunchedEffect(listState) { // 监听列表滚动偏移量变化 snapshotFlow { listState.firstVisibleItemScrollOffset } .collect { currentOffset -> val scrollDelta = currentOffset - lastScrollOffset // 设置滚动阈值,避免频繁触发动画 when { scrollDelta > 10 -> viewModel.updateSearchBarVisibility(false) // 向下滚动隐藏 scrollDelta < -10 -> viewModel.updateSearchBarVisibility(true) // 向上滚动显示 } lastScrollOffset = currentOffset } } LazyColumn(state = listState) { items(100) { index -> Text( text = "列表项 $index", modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 12.dp) ) } } }
方案二:使用MotionLayout(适合复杂动画场景)
如果需要更复杂的联动动画(比如搜索栏缩放+TabView位移),可以用MotionLayout实现:
1. 添加MotionLayout依赖
在build.gradle中添加:
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"
2. 创建MotionScene配置文件
在res/xml/motion_scene_searchbar.xml中定义动画状态:
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:motion="http://schemas.android.com/apk/res-auto"> <Transition motion:constraintSetStart="@+id/start" motion:constraintSetEnd="@+id/end" motion:duration="300"/> <ConstraintSet android:id="@+id/start"> <Constraint android:id="@id/searchBar"> <Layout android:width="match_parent" android:height="wrap_content" android:topToTopOf="parent"/> </Constraint> <Constraint android:id="@id/tabView"> <Layout android:width="match_parent" android:height="0dp" android:topToBottomOf="@id/searchBar" android:bottomToBottomOf="parent"/> </Constraint> </ConstraintSet> <ConstraintSet android:id="@+id/end"> <Constraint android:id="@id/searchBar"> <Layout android:width="match_parent" android:height="wrap_content" android:topToTopOf="parent" android:translationY="-60dp"/> <!-- 隐藏搜索栏 --> </Constraint> <Constraint android:id="@id/tabView"> <Layout android:width="match_parent" android:height="0dp" android:topToTopOf="parent" android:bottomToBottomOf="parent"/> </Constraint> </ConstraintSet> </MotionScene>
3. 父布局中使用MotionLayout
@Composable fun ParentScreenWithMotionLayout(viewModel: MainViewModel = viewModel()) { val isSearchBarVisible by viewModel.isSearchBarVisible.collectAsStateWithLifecycle() val context = LocalContext.current val motionScene = remember { MotionScene(content = context.resources.openRawResource(R.xml.motion_scene_searchbar).readBytes()) } MotionLayout( motionScene = motionScene, // 根据状态切换动画进度 progress = if (isSearchBarVisible) 0f else 1f, modifier = Modifier.fillMaxSize() ) { SearchBar(modifier = Modifier.layoutId("searchBar")) TabView(modifier = Modifier.layoutId("tabView")) } }
注意事项
- 滚动阈值(示例中的10)可根据实际交互体验调整,避免频繁触发动画
- 如果多个Tab页面需要触发搜索栏变化,可在对应页面重复滚动监听逻辑
- 使用
collectAsStateWithLifecycle需要添加依赖:implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2"
内容的提问来源于stack exchange,提问作者Sanjay
相关产品推荐
相关产品推荐

