You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 03:05:34