kotlin Jetpack合成折叠工具栏

hgc7kmma  于 2022-11-16  发布在  Kotlin
关注(0)|答案(8)|浏览(210)

我找不到任何关于这个问题的文档,在Compose中有类似于CollapsingToolbar的东西吗?
所有我发现的是提到它here,但没有关于如何设置它

pexxcrt2

pexxcrt21#

Material Design 3的Jetpack合成实施包括4种类型的顶部应用栏(https://m3.material.io/components/top-app-bar/implementation):

  • CenterAlignedTopAppBar
  • SmallTopAppBar
  • MediumTopAppBar
  • LargeTopAppBar

https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary
它们都有一个scrollBehavior参数,可以用来折叠工具栏。

  • TopAppBarDefaults.pinnedScrollBehavior
  • TopAppBarDefaults.enterAlwaysScrollBehavior
  • TopAppBarDefaults.exitUntilCollapsedScrollBehavior

https://developer.android.com/reference/kotlin/androidx/compose/material3/TopAppBarDefaults

**注:**此API目前被注解为实验性。

样品使用:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Test() {
    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState())
    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            MediumTopAppBar(
                title = { Text(text = "Scroll Behavior Test") },
                navigationIcon = {
                    IconButton(onClick = { /*TODO*/ }) {
                        Icon(imageVector = Icons.Default.Menu, contentDescription = "")
                    }
                },
                scrollBehavior = scrollBehavior
            )
        }
    ) {
        LazyColumn(modifier = Modifier.fillMaxWidth()) {
            items((1..50).toList()) { item ->
                Text(modifier = Modifier.padding(8.dp), text = "Item $item")
            }
        }
    }
}
hujrc8aj

hujrc8aj2#

我找到了一个由Samir Basnet(来自KotlinSlack频道)创建的解决方案,这对我很有用,我希望它能帮助其他人...

@Composable
fun CollapsingEffectScreen() {
    val items = (1..100).map { "Item $it" }
    val lazyListState = rememberLazyListState()
    var scrolledY = 0f
    var previousOffset = 0
    LazyColumn(
        Modifier.fillMaxSize(),
        lazyListState,
    ) {
        item {
            Image(
                painter = painterResource(id = R.drawable.recife),
                contentDescription = null,
                contentScale = ContentScale.FillWidth,
                modifier = Modifier
                    .graphicsLayer {
                        scrolledY += lazyListState.firstVisibleItemScrollOffset - previousOffset
                        translationY = scrolledY * 0.5f
                        previousOffset = lazyListState.firstVisibleItemScrollOffset
                    }
                    .height(240.dp)
                    .fillMaxWidth()
            )
        }
        items(items) {
            Text(
                text = it,
                Modifier
                    .background(Color.White)
                    .fillMaxWidth()
                    .padding(8.dp)
            )
        }
    }
}

结果如下:

5sxhfpxr

5sxhfpxr3#

我在Android文档中发现了这个问题,我认为您在问题中链接的文档是在讨论如何使用嵌套滚动来实现这一点。

val toolbarHeight = 48.dp
    val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {

                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }
    Box(
        Modifier
            .fillMaxSize()

            .nestedScroll(nestedScrollConnection)
    ) {

        LazyColumn(contentPadding = PaddingValues(top = toolbarHeight)) {
            items(100) { index ->
                Text("I'm item $index", modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp))
            }
        }
        TopAppBar(
            modifier = Modifier
                .height(toolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) },
            title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") }
        )
    }
tez616oj

tez616oj4#

您可以使用compose-collapsing-toolbar库。

安装implementation "me.onebone:toolbar-compose:2.1.0"

用法-Exemple

预览

以下是来自该库的www.example.com的一些gif图像Readme.md:
第一次

lhcgjxsq

lhcgjxsq5#

您可以按照文档中的示例创建一个工具栏,该工具栏在每次向上/向下滚动时都会展开/折叠。
要创建一个仅在列表滚动到顶部时才展开的工具栏,可以对原始示例稍作修改:

val toolbarHeight = 48.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
var toolbarOffsetHeightPx by remember { mutableStateOf(0f) }
var totalScrollOffsetPx = remember { 0f }

val nestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {

            val delta = available.y
            totalScrollOffsetPx += delta
            
            if (totalScrollOffsetPx in -toolbarHeightPx..0f) {
                toolbarOffsetHeightPx = totalScrollOffsetPx
            }

            return Offset.Zero
        }
    }
}

通过这样做,您可以灵活地创建自己的CollapsibleScaffold,它可以接受scrollBehaviourappBarLayoutlist等参数。
例如,通过这种方式,你还可以通过编程计算应用栏的高度,摆脱大量的样板文件,使屏幕上使用的代码整洁干净。

qcuzuvrc

qcuzuvrc6#

合成-折叠-工具栏Jetpack合成的折叠工具栏布局的简单实现
https://github.com/onebone/compose-collapsing-toolbar

jecbmhm3

jecbmhm37#

我有一些特定的需求,所以我创建了一个简单的impl,它测量navigationIcons和Trainling图标,并试图适应它们之间的内容。忽略重载和测试代码,它不到200行,应该很容易定制为您的特定需求。
https://gist.github.com/fabriciovergara/5de1e8b114fb484bf5f6808a0a107b24

@Composable
fun CollapsibleScaffold(
    state: LazyListState,
    modifier: Modifier = Modifier,
    topBar: @Composable () -> Unit = {},
    content: @Composable (insets: PaddingValues) -> Unit
) {
    CollapsibleScaffoldInternal(
        offsetState = rememberOffsetScrollState(state),
        modifier = modifier,
        topBar = topBar,
        content = content
    )
}

@Composable
private fun CollapsibleScaffoldInternal(
    offsetState: State<Int>,
    modifier: Modifier = Modifier,
    topBar: @Composable () -> Unit = {},
    content: @Composable (insets: PaddingValues) -> Unit
) {
    Scaffold(modifier = modifier, backgroundColor = Color.Transparent) { insets ->
        Box {
            content(
                PaddingValues(
                    top = CollapsibleTopAppBarDefaults.maxHeight + 8.dp,
                    bottom = 16.dp
                )
            )
            CompositionLocalProvider(
                LocalScrollOffset provides offsetState,
                LocalInsets provides insets
            ) {
                topBar()
            }
        }
    }
}

@Composable
fun CollapsibleTopAppBar(
    modifier: Modifier = Modifier,
    actions: (@Composable RowScope.() -> Unit)? = null,
    navigationIcon: (@Composable () -> Unit)? = null,
    content: (@Composable CollapsibleTopAppBarScope.() -> Unit) = { }
) {
    CollapsibleTopAppBarInternal(
        scrollOffset = LocalScrollOffset.current.value,
        insets = LocalInsets.current,
        modifier = modifier.background(Color.Transparent),
        navigationIcon = navigationIcon,
        actions = actions,
        content = content
    )
}

@Composable
private fun CollapsibleTopAppBarInternal(
    scrollOffset: Int,
    insets: PaddingValues,
    modifier: Modifier = Modifier,
    navigationIcon: (@Composable () -> Unit)? = null,
    actions: (@Composable RowScope.() -> Unit)? = null,
    content: @Composable CollapsibleTopAppBarScope.() -> Unit
) {
    val density = LocalDensity.current
    val actionsSize = remember { mutableStateOf(IntSize.Zero) }
    val navIconSize = remember { mutableStateOf(IntSize.Zero) }
    val actionWidth = with(density) { actionsSize.value.width.toDp() }
    val backWidth = with(density) { navIconSize.value.width.toDp() }
    val bodyHeight = CollapsibleTopAppBarDefaults.maxHeight - CollapsibleTopAppBarDefaults.minHeight
    val maxOffset = with(density) {
        bodyHeight.roundToPx() - insets.calculateTopPadding().roundToPx()
    }

    val offset = min(scrollOffset, maxOffset)
    val fraction = 1f - kotlin.math.max(0f, offset.toFloat()) / maxOffset
    val currentMaxHeight = bodyHeight * fraction

    BoxWithConstraints(modifier = modifier) {
        val maxWidth = maxWidth
        Row(
            modifier = Modifier
                .height(CollapsibleTopAppBarDefaults.minHeight)
                .fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier.onGloballyPositioned {
                    navIconSize.value = it.size
                }
            ) {
                if (navigationIcon != null) {
                    navigationIcon()
                }
            }

            Spacer(modifier = Modifier.weight(1f))

            Row(
                modifier = Modifier
                    .widthIn(0.dp, maxWidth / 3)
                    .onGloballyPositioned { actionsSize.value = it.size }
            ) {
                if (actions != null) {
                    actions()
                }
            }
        }

        val scaleFraction = (fraction / CollapsibleTopAppBarDefaults.startScalingFraction).coerceIn(0f, 1f)
        val paddingStart = if (fraction > CollapsibleTopAppBarDefaults.startScalingFraction) {
            0.dp
        } else {
            lerp(backWidth, 0.dp, scaleFraction)
        }

        val paddingEnd = if (fraction > CollapsibleTopAppBarDefaults.startScalingFraction) {
            0.dp
        } else {
            lerp(actionWidth, 0.dp, scaleFraction)
        }

        /**
         *  When content height reach minimum size, we start translating it to fit the toolbar
         */
        val startTranslateFraction = CollapsibleTopAppBarDefaults.minHeight / CollapsibleTopAppBarDefaults.maxHeight
        val translateFraction = (fraction / startTranslateFraction).coerceIn(0f, 1f)
        val paddingTop = if (fraction > startTranslateFraction) {
            CollapsibleTopAppBarDefaults.minHeight
        } else {
            lerp(0.dp, CollapsibleTopAppBarDefaults.minHeight, translateFraction)
        }

        BoxWithConstraints(
            modifier = Modifier
                .padding(top = paddingTop, start = paddingStart, end = paddingEnd)
                .height(max(CollapsibleTopAppBarDefaults.minHeight, currentMaxHeight))
                .fillMaxWidth()
                .align(Alignment.BottomStart)
        ) {
            val scope = remember(fraction, this) {
                CollapsibleTopAppBarScope(fraction = fraction, scope = this)
            }
            content(scope)
        }
    }
}

@Composable
private fun rememberOffsetScrollState(state: LazyListState): MutableState<Int> {
    val offsetState = rememberSaveable() { mutableStateOf(0) }
    LaunchedEffect(key1 = state.layoutInfo.visibleItemsInfo) {
        val fistItem = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == 0 }
        val offset = fistItem?.offset?.absoluteValue ?: Int.MAX_VALUE
        offsetState.value = offset
    }
    return offsetState
}

object CollapsibleTopAppBarDefaults {
    // Replicating the value in androidx.compose.material.AppBar.AppBarHeight which is private
    val minHeight = 56.dp
    val maxHeight = 320.dp

    /**
     *  When content height reach this point we start applying padding start and end
     */
    const val startScalingFraction = 0.5f
}
zbdgwd5y

zbdgwd5y8#

这是我在作曲时用来制作折叠效果的

  • Constraint layout - compose使用.json5文件创建约束集。创建开始、结束和过渡效果。
  • Motion Layout将所有小部件添加到合成功能中的运动布局。
  • 标识列表中滚动的进度。

结果+ Source Code**

添加此依赖项。
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha03")

**STEP 1:**在原始资源文件夹中创建collapse_toolbar.json5文件
折叠工具栏.json5

{
  ConstraintSets: {
    start: {
      box: {
        width: 'spread',
        height: 230,
        start: ['parent', 'start'],
        end: ['parent', 'end'],
        top: ['parent', 'top'],
        custom: {
          background: '#FF74d680'
        }
      },
      help_image:{
        width: 80,
        height: 120,
        end: ['box', 'end', 16],
        top: ['box', 'top', 16],
        bottom: ['box', 'bottom',8]
      },
      close_button:{
        start: ['parent', 'start',8],
        bottom: ['box', 'bottom',8]
      },
      title: {
        start: ['close_button', 'end', 16],
        bottom: ['close_button', 'bottom'],
        top: ['close_button', 'top']
      }

    },
    end: {
      help_image:{
        width: 10,
        height: 10,
        bottom: ['box', 'bottom'],
        end: ['box', 'end']
      },
      box: {
        width: 'spread',
        height: 56,
        start: ['parent', 'start'],
        end: ['parent', 'end'],
        top: ['parent', 'top'],
        custom: {
          background: '#FF378b29'
        }
      },
      close_button:{
        start: ['box', 'start', 16],
        bottom: ['box', 'bottom', 16],
        top: ['box', 'top', 16]
      },
      title: {
        start: ['close_button', 'end', 8],
        bottom: ['close_button', 'bottom'],
        top: ['close_button', 'top']
      }

    }
  },
  Transitions: {
    default: {
      from: 'start',
      to: 'end',
      pathMotionArc: 'startVertical',
      // key here must be Key with capital K
      KeyFrames: {
        KeyAttributes: [
          {
            target: ['box'],
            frames: [0, 20, 50, 80, 100]
//            rotationZ: [0,  360]
          },
          {
            target: ['close_button'],
            frames: [0, 20, 60, 80, 100],
//            translationY: [20, 40, 65, 85, 100]
//            alpha: [1, 0.5, 0.5, 0.7, 1]
          },
          {
            target: ['title'],
            frames: [0, 100],
//            translationY: [20,100]
//            alpha: [1, 0.5, 0.5, 0.7, 1]
          },
          {
            target: ['help_image'],
            frames: [0, 30, 50, 80, 100],
            scaleX: [1, 0.8, 0.6, 0.3, 0],
            scaleY: [1, 0.8, 0.6, 0.3, 0],
            alpha: [1, 0.8, 0.6, 0.3, 0]
          }
        ]
      }
    }
  }
}

**STEP 2:**创建可组合函数并添加Motion Layout
主要活动.kt

@ExperimentalComposeUiApi
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val lazyScrollState = rememberLazyListState()
            Scaffold(
                modifier = Modifier
                    .fillMaxSize(),
                topBar = {
                    CollapsingToolbar(lazyScrollState)
                },
            ) { paddingValues ->
                Column(modifier = Modifier.padding(paddingValues)) {
                    LazyColumn(
                        modifier = Modifier
                            .fillMaxSize()
                            .background(color = Color.White)
                            .animateContentSize(),
                        state = lazyScrollState
                    ) {
                        items(100) { index ->
                            Text(modifier = Modifier.padding(36.dp), text = "Item: $index")
                            Divider(color = Color.Black, thickness = 1.dp)
                        }

                    }
                }
            }
        }
    }
}

@OptIn(ExperimentalMotionApi::class)
@Composable
fun CollapsingToolbar(lazyScrollState: LazyListState) {
    val context = LocalContext.current
    val motionScene = remember {
        context.resources.openRawResource(R.raw.collapse_toolbar).readBytes().decodeToString()
    }

    val progress by animateFloatAsState(
        targetValue = if (lazyScrollState.firstVisibleItemIndex in 0..1) 0f else 1f,
        tween(500)
    )
    val motionHeight by animateDpAsState(
        targetValue = if (lazyScrollState.firstVisibleItemIndex in 0..1) 230.dp else 56.dp,
        tween(500)
    )

    MotionLayout(
        motionScene = MotionScene(content = motionScene),
        progress = progress,
        modifier = Modifier
            .fillMaxWidth()
            .background(backgroundColor)
            .height(motionHeight)
    ) {

        val boxProperties = motionProperties(id = "box")
//        val startColor = Color(boxProperties.value.color("custome"))
        Box(
            modifier = Modifier
                .layoutId("box")
                .background(boxProperties.value.color("background"))
        )

        Image(
            modifier = Modifier
                .layoutId("help_image"),
            painter = painterResource(id = R.drawable.help),
            contentDescription = ""
        )

        Icon(
            modifier = Modifier.layoutId("close_button"),
            imageVector = Icons.Filled.Close,
            contentDescription = "",
            tint = Color.White
        )

        Text(
            modifier = Modifier.layoutId("title"),
            text = "Help",
            color = Color.White,
            fontSize = 18.sp
        )

    }
}

相关问题