Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snapping #16

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
7 changes: 2 additions & 5 deletions app/src/main/java/me/onebone/toolbar/ParallaxActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
Expand Down Expand Up @@ -66,6 +62,7 @@ fun ParallaxEffect() {
modifier = Modifier.fillMaxSize(),
state = state,
scrollStrategy = ScrollStrategy.EnterAlwaysCollapsed,
snapConfig = SnapConfig(),
toolbarModifier = Modifier.background(MaterialTheme.colors.primary),
toolbar = {
// Collapsing toolbar collapses its size as small as the that of
Expand Down
34 changes: 13 additions & 21 deletions lib/src/main/java/me/onebone/toolbar/AppBarContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,14 @@ package me.onebone.toolbar

import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.*
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import kotlin.math.max
import kotlin.math.roundToInt

@Deprecated(
"Use AppBarContainer for naming consistency",
Expand All @@ -53,13 +44,13 @@ import kotlin.math.roundToInt
fun AppbarContainer(
modifier: Modifier = Modifier,
scrollStrategy: ScrollStrategy,
collapsingToolbarState: CollapsingToolbarState,
collapsingToolbarScaffoldState: CollapsingToolbarScaffoldState,
content: @Composable AppbarContainerScope.() -> Unit
) {
AppBarContainer(
modifier = modifier,
scrollStrategy = scrollStrategy,
collapsingToolbarState = collapsingToolbarState,
collapsingToolbarScaffoldState = collapsingToolbarScaffoldState,
content = content
)
}
Expand All @@ -76,15 +67,15 @@ fun AppBarContainer(
modifier: Modifier = Modifier,
scrollStrategy: ScrollStrategy,
/** The state of a connected collapsing toolbar */
collapsingToolbarState: CollapsingToolbarState,
collapsingToolbarScaffoldState: CollapsingToolbarScaffoldState,
content: @Composable AppbarContainerScope.() -> Unit
) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert contract(i.e. parameter or behavior) changes in AppBarContainer.kt as it is in maintenance mode and thus should not be changed until it is removed!

val offsetY = remember { mutableStateOf(0) }
val flingBehavior = ScrollableDefaults.flingBehavior()
val snapStrategy = null

val (scope, measurePolicy) = remember(scrollStrategy, collapsingToolbarState) {
AppbarContainerScopeImpl(scrollStrategy.create(offsetY, collapsingToolbarState, flingBehavior)) to
AppbarMeasurePolicy(scrollStrategy, collapsingToolbarState, offsetY)
val (scope, measurePolicy) = remember(scrollStrategy, collapsingToolbarScaffoldState) {
AppbarContainerScopeImpl(scrollStrategy.create(collapsingToolbarScaffoldState, flingBehavior, snapStrategy)) to
AppbarMeasurePolicy(scrollStrategy, collapsingToolbarScaffoldState)
}

Layout(
Expand Down Expand Up @@ -118,8 +109,7 @@ private object AppBarBodyMarker

private class AppbarMeasurePolicy(
private val scrollStrategy: ScrollStrategy,
private val toolbarState: CollapsingToolbarState,
private val offsetY: State<Int>
private val collapsingToolbarScaffoldState: CollapsingToolbarScaffoldState
): MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
Expand Down Expand Up @@ -151,6 +141,7 @@ private class AppbarMeasurePolicy(
}
}

val toolbarState = collapsingToolbarScaffoldState.toolbarState
val placeables = nonToolbars.map { measurable ->
val childConstraints = if(scrollStrategy == ScrollStrategy.ExitUntilCollapsed) {
constraints.copy(
Expand All @@ -175,16 +166,17 @@ private class AppbarMeasurePolicy(

height += (toolbarPlaceable?.height ?: 0)

val offsetY = collapsingToolbarScaffoldState.offsetY
return layout(
width.coerceIn(constraints.minWidth, constraints.maxWidth),
height.coerceIn(constraints.minHeight, constraints.maxHeight)
) {
toolbarPlaceable?.place(x = 0, y = offsetY.value)
toolbarPlaceable?.place(x = 0, y = offsetY)

placeables.forEach { placeable ->
placeable.place(
x = 0,
y = offsetY.value + (toolbarPlaceable?.height ?: 0)
y = offsetY + (toolbarPlaceable?.height ?: 0)
)
}
}
Expand Down
60 changes: 29 additions & 31 deletions lib/src/main/java/me/onebone/toolbar/CollapsingToolbar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,11 @@ import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.ParentDataModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.*
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
Expand All @@ -57,7 +46,7 @@ import kotlin.math.roundToInt

@Stable
class CollapsingToolbarState(
initial: Int = Int.MAX_VALUE
initial: Int = CollapsingToolbarDefaults.InitialHeight
): ScrollableState {
/**
* [height] indicates current height of the toolbar.
Expand Down Expand Up @@ -136,8 +125,23 @@ class CollapsingToolbarState(
)
fun feedScroll(value: Float): Float = dispatchRawDelta(value)

/**
* @return Remaining velocity after fling
*/
suspend fun fling(flingBehavior: FlingBehavior, velocity: Float): Float {
var left = velocity
scroll {
with(flingBehavior) {
left = performFling(left)
}
}

return left
}

// TODO: A strange jump in snap speed is often observed
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bug I mentioned. You can see a strange expanding before the snap will applied. This sometimes works even in wrong direction. Look at the demonstration GIF:

When I release the finger you can see that snapping is moving in wrong direction (to top) before it finally snap to full size.

I tried to debug it and I suppose that a bug is in implementation of EnterAlwaysCollapsedNestedScrollConnection#onPostScroll() method. Unfortunately I don't know how NestedScrollConnection and math in EnterAlwaysCollapsedNestedScrollConnection work. I will be glad if you help me with it. Try to build an example app and reproduce this bug.

@ExperimentalToolbarApi
suspend fun expand(duration: Int = 200) {
suspend fun expand(duration: Int = CollapsingToolbarDefaults.ExpandDuration) {
val anim = AnimationState(height.toFloat())

scroll {
Expand All @@ -149,8 +153,9 @@ class CollapsingToolbarState(
}
}

// TODO: A strange jump in snap speed is often observed
@ExperimentalToolbarApi
suspend fun collapse(duration: Int = 200) {
suspend fun collapse(duration: Int = CollapsingToolbarDefaults.CollapseDuration) {
val anim = AnimationState(height.toFloat())

scroll {
Expand All @@ -162,20 +167,6 @@ class CollapsingToolbarState(
}
}

/**
* @return Remaining velocity after fling
*/
suspend fun fling(flingBehavior: FlingBehavior, velocity: Float): Float {
var left = velocity
scroll {
with(flingBehavior) {
left = performFling(left)
}
}

return left
}

override val isScrollInProgress: Boolean
get() = scrollableState.isScrollInProgress

Expand All @@ -189,7 +180,7 @@ class CollapsingToolbarState(

@Composable
fun rememberCollapsingToolbarState(
initial: Int = Int.MAX_VALUE
initial: Int = CollapsingToolbarDefaults.InitialHeight
): CollapsingToolbarState {
return remember {
CollapsingToolbarState(
Expand All @@ -216,6 +207,13 @@ fun CollapsingToolbar(
)
}

object CollapsingToolbarDefaults {
const val InitialHeight = Int.MAX_VALUE
const val Edge = 0.5f
const val ExpandDuration = 200
const val CollapseDuration = 200
}

private class CollapsingToolbarMeasurePolicy(
private val collapsingToolbarState: CollapsingToolbarState
): MeasurePolicy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
package me.onebone.toolbar

import android.os.Bundle
import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
Expand All @@ -45,6 +48,24 @@ class CollapsingToolbarScaffoldState(
get() = offsetYState.value

internal val offsetYState = mutableStateOf(initialOffsetY)

@ExperimentalToolbarApi
suspend fun expandOffset(duration: Int = CollapsingToolbarDefaults.ExpandDuration) {
val anim = AnimationState(offsetY.toFloat())

anim.animateTo(0f, tween(duration)) {
offsetYState.value = value.toInt()
}
}

@ExperimentalToolbarApi
suspend fun collapseOffset(duration: Int = CollapsingToolbarDefaults.CollapseDuration) {
val anim = AnimationState(offsetY.toFloat())

anim.animateTo(-toolbarState.minHeight.toFloat(), tween(duration)) {
offsetYState.value = value.toInt()
}
}
}

private class CollapsingToolbarScaffoldStateSaver: Saver<CollapsingToolbarScaffoldState, Bundle> {
Expand Down Expand Up @@ -75,14 +96,15 @@ fun CollapsingToolbarScaffold(
modifier: Modifier,
state: CollapsingToolbarScaffoldState,
scrollStrategy: ScrollStrategy,
snapConfig: SnapConfig? = null,
toolbarModifier: Modifier = Modifier,
toolbar: @Composable CollapsingToolbarScope.() -> Unit,
body: @Composable () -> Unit
) {
val flingBehavior = ScrollableDefaults.flingBehavior()

val nestedScrollConnection = remember(scrollStrategy, state) {
scrollStrategy.create(state.offsetYState, state.toolbarState, flingBehavior)
scrollStrategy.create(state, flingBehavior, snapConfig)
}

val toolbarState = state.toolbarState
Expand Down
Loading