onebone/compose-collapsing-toolbar

Content is scrollable even when it's 1 item

Opened this issue · 6 comments

The content is scrollable even when it's 1 item in the list and half of the screen is empty.
How to detect if the content takes the whole screen and make it scrollable only then ?

I looked into the behavior of androidx's CollapsingToolbarLayout, looks like it collapses only if the height of a body layout is large enough to scroll which is different from the current behavior of my library.
One point I want to think of is when the height of the body layout is somewhere between body hole + collapsed toolbar and body hole + expanded toolbar. AndroidX leaves some space if the actual height of body layout is smaller than that of body hole, maybe I could give some options to choose this behavior?

스크린샷 2022-03-07 오후 11 38 29

Thank you for so quick response.

Yes, in CollapsingToolbarLayout it checks if the content height is more than the screen height and only then make it collapsable when scrolling.
In your library it scrolls and collapse always.

Sorry but I didn't get your question:
AndroidX leaves some space if the actual height of body layout is smaller than that of body hole, maybe I could give some options to choose this behavior?

Could you please rephrase it ?

Is there any workaround for this case? For example disable vertical scroll if content fits to screen. But how to detect "if content fits"?

I meant the edge case where the body content's height is not tall enough to cover the whole screen when the toolbar is collapsed but it fills the screen when toolbar is expanded.
By the way, I didn't tested myself though did you try calculating available height with CollapsingToolbarState.maxHeight as a workaround?

This workaround works for me:

@Composable
public fun CollapsingToolbarScaffold(
    state: CollapsingToolbarScaffoldState,
    modifier: Modifier = Modifier,
    scrollStrategy: ScrollStrategy = ScrollStrategy.ExitUntilCollapsed,
    enabled: Boolean = true,
    toolbarModifier: Modifier = Modifier,
    toolbar: @Composable CollapsingToolbarScope.() -> Unit,
    content: @Composable BoxWithConstraintsScope.() -> Unit,
) {
    var collapsable by remember { mutableStateOf(false) }

    LaunchedEffect(collapsable) {
        @OptIn(ExperimentalToolbarApi::class)
        if (!collapsable) state.toolbarState.expand()
    }

    CollapsingToolbarScaffold(
        modifier = modifier,
        toolbarModifier = toolbarModifier,
        state = state,
        enabled = collapsable && enabled, // Force disable collapsing if it is not needed
        scrollStrategy = scrollStrategy,
        toolbar = toolbar,
        body = {
            var contentHeight by remember { mutableStateOf(0) }
            BoxWithConstraints(Modifier.onGloballyPositioned { contentHeight = it.size.height }) {
                collapsable = with(LocalDensity.current) {
                    val toolbarMaxOffset = state.toolbarState.maxHeight - state.toolbarState.minHeight
                    contentHeight > maxHeight.toPx() - toolbarMaxOffset
                }
                content()
            }
        },
    )
}

UPD: Simplified workaround

Look at the first version of workaround
@Composable
public fun CollapsingToolbarScaffold(
    state: CollapsingToolbarScaffoldState,
    modifier: Modifier = Modifier,
    scrollStrategy: ScrollStrategy = ScrollStrategy.ExitUntilCollapsed,
    enabled: Boolean = true,
    toolbarModifier: Modifier = Modifier,
    toolbar: @Composable CollapsingToolbarScope.() -> Unit,
    content: @Composable CollapsingToolbarContentScope.() -> Unit,
) {
    CollapsingToolbarScaffold(
        modifier = modifier,
        toolbarModifier = toolbarModifier,
        state = state,
        enabled = enabled,
        scrollStrategy = scrollStrategy,
        toolbar = toolbar,
        body = {
            var contentHeight by remember { mutableStateOf(0) }
            BoxWithConstraints(Modifier.onGloballyPositioned { contentHeight = it.size.height }) {
                CollapsingToolbarContentScopeImpl(
                    scope = this,
                    density = LocalDensity.current,
                    toolbarOffset = state.toolbarState.maxHeight - state.toolbarState.minHeight,
                    contentHeightPx = contentHeight,
                ).content()
            }
        },
    )
}

@Stable
public interface CollapsingToolbarContentScope : BoxWithConstraintsScope {
    /** The content height in [Dp]. */
    public val contentHeight: Dp

    /** The max height that may fit without toolbar collapsing. */
    public val maxHeightWhenExpanded: Dp

    /** Returns `true` if content can not fit on screen without toolbar collapsing. */
    public val collapsingNeeded: Boolean
}

private data class CollapsingToolbarContentScopeImpl(
    private val scope: BoxWithConstraintsScope,
    private val density: Density,
    private val toolbarOffset: Int,
    private val contentHeightPx: Int,
) : CollapsingToolbarContentScope, BoxWithConstraintsScope by scope {

    override val contentHeight: Dp
        get() = with(density) { contentHeightPx.toDp() }
    override val maxHeightWhenExpanded: Dp
        get() = maxHeight - with(density) { toolbarOffset.toDp() }
    override val collapsingNeeded: Boolean
        get() = contentHeight > maxHeightWhenExpanded
}

With this you can disable scrolling and make toolbar always expanded if content fits on the screen looking at collapsingNeeded flag in CollapsingToolbarContentScope:

val state = rememberCollapsingToolbarScaffoldState()
CollapsingToolbarScaffold(
    state = state,
    toolbar = { /* Toolbar content here */ }
) {
    LaunchedEffect(collapsingNeeded) {
        // We don't want the state when scroll is disabled but toolbar is not expanded
        @OptIn(ExperimentalToolbarApi::class)
        if (!collapsingNeeded) state.toolbarState.expand()
    }

    Column(
        // Disable scrolling if content fits on screen
        modifier = Modifier.verticalScroll(rememberScrollState(), enabled = collapsingNeeded),
    ) {
        // Content here
    }
}

Hi, I have already created PR for this: #85

By the way, if you want to use this feature, I created a separated remote dependency, while this PR is under review:

Add it in your root build.gradle at the end of repositories:

all projects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}
}

Step 2. Add the dependency

dependencies {
        implementation 'com.github.GIGAMOLE:ComposeCollapsingToolbar:latest-version'
}

Or you can simply download it from there:

https://github.com/GIGAMOLE/ComposeCollapsingToolbar/releases