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

FEATURE - Snapping #83

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Note that the `CollapsingToolbarScaffoldState` is stable, which means that a cha
val state = rememberCollapsingToolbarScaffoldState()
val offsetY = state.offsetY // y offset of the layout
val progress = state.toolbarState.progress // how much the toolbar is expanded (0: collapsed, 1: expanded)
val offsetProgress = state.offsetProgress // how much the toolbar offset (EnterAlways, EnterAlwaysCollapsed) is expanded (0: collapsed, 1: expanded)
val totalProgress = state.totalProgress // how much the toolbar height and offset is expanded (0: collapsed, 1: expanded)

Text(
text = "Hello World",
Expand All @@ -65,8 +67,19 @@ Text(
)
```

Also, it is possible to trigger collapse/expansion animations manually:
```kotlin
val state = rememberCollapsingToolbarScaffoldState()
state.toolbarState.expand() // expand toolbar
state.toolbarState.collapse() // collapse toolbar
state.offsetExpand() // expand toolbar offset (EnterAlways, EnterAlwaysCollapsed)
state.offsetCollapse() // collapse toolbar offset (EnterAlways, EnterAlwaysCollapsed)
state.expand() // expand altogether toolbar and offset
state.collapse() // collapse altogether toolbar and offset
```

## parallax, pin, road
You can tell children of CollapsingToolbar how to deal with a collapse/expansion. This works almost the same way to the `collapseMode` in the `CollapsingToolbarLayout` except for the `road` modifier.
You can tell children of `CollapsingToolbar` how to deal with a collapse/expansion. This works almost the same way to the `collapseMode` in the `CollapsingToolbarLayout` except for the `road` modifier.

```kotlin
CollapsingToolbar(/* ... */) {
Expand All @@ -76,6 +89,8 @@ CollapsingToolbar(/* ... */) {
}
```

To properly set the `minHeight`/`maxHeight` for `CollapsingToolbar` use `Modifier.pin()` on the child, because `CollapsingToolbar` determines its `minHeight`/`maxHeight` by the smallest/largest child.

### road modifier
The `road()` modifier allows you to place a child relatively to the toolbar. It receives two arguments: `whenCollapsed` and `whenExpanded`. As the name suggests, these describe how to place a child when the toolbar is collapsed or expanded, respectively.
This can be used to display a title text on the toolbar which is moving as the scroll is fed.
Expand All @@ -99,7 +114,7 @@ The above code orders the title `Text` to be placed at the _CenterStart_ positio


## Scroll Strategy
`ScrollStrategy` defines how CollapsingToolbar consumes scroll. You can set your desired behavior by providing `scrollStrategy` to `CollapsingToolbarScaffold`:
`ScrollStrategy` defines how `CollapsingToolbar` consumes scroll. You can set your desired behavior by providing `scrollStrategy` to `CollapsingToolbarScaffold`:

```kotlin
CollapsingToolbarScaffold(
Expand All @@ -119,3 +134,25 @@ CollapsingToolbarScaffold(

### ScrollStrategy.ExitUntilCollapsed
![ExitUntilCollapsed](img/exit-until-collapsed.gif)

## Snap Config
`SnapConfig` defines how `CollapsingToolbar` snaps to its edges. You can enable snapping by providing `snapConfig` to `CollapsingToolbarScaffold`:

```kotlin
CollapsingToolbarScaffold(
/* ... */
snapConfig = SnapConfig() // "collapseThreshold = 0.5" by default
) {
/* ... */
}
```

### Snap for ScrollStrategy.EnterAlways
![Snap for EnterAlways](img/snap-enter-always.gif)

### Snap for ScrollStrategy.EnterAlwaysCollapsed
![Snap for EnterAlwaysCollapsed](img/snap-enter-always-collapsed.gif)

### Snap ScrollStrategy.ExitUntilCollapsed
![Snap for ExitUntilCollapsed](img/snap-exit-until-collapsed.gif)

5 changes: 3 additions & 2 deletions app/src/main/java/me/onebone/toolbar/ParallaxActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import me.onebone.toolbar.ui.theme.CollapsingToolbarTheme

class ParallaxActivity: ComponentActivity() {
class ParallaxActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Expand All @@ -79,6 +79,7 @@ fun ParallaxEffect() {
modifier = Modifier.fillMaxSize(),
state = state,
scrollStrategy = ScrollStrategy.EnterAlwaysCollapsed,
snapConfig = SnapConfig(),
toolbarModifier = Modifier.background(MaterialTheme.colors.primary),
enabled = enabled,
toolbar = {
Expand Down Expand Up @@ -127,7 +128,7 @@ fun ParallaxEffect() {
modifier = Modifier
.padding(16.dp)
.align(Alignment.BottomEnd),
onClick = { }
onClick = { }
) {
Text(text = "Floating Button!")
}
Expand Down
Binary file added img/snap-enter-always-collapsed.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/snap-enter-always.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/snap-exit-until-collapsed.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 26 additions & 33 deletions lib/src/main/java/me/onebone/toolbar/AppBarContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ 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
Expand All @@ -40,28 +38,22 @@ import androidx.compose.ui.layout.Placeable
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",
replaceWith = ReplaceWith(
"AppBarContainer(modifier, scrollStrategy, collapsingToolbarState, content)",
"AppBarContainer(modifier, snapConfig, scrollStrategy, state, content)",
"me.onebone.toolbar"
)
)
@Composable
fun AppbarContainer(
modifier: Modifier = Modifier,
snapConfig: SnapConfig? = null,
scrollStrategy: ScrollStrategy,
collapsingToolbarState: CollapsingToolbarState,
state: CollapsingToolbarScaffoldState,
content: @Composable AppbarContainerScope.() -> Unit
) {
AppBarContainer(
modifier = modifier,
scrollStrategy = scrollStrategy,
collapsingToolbarState = collapsingToolbarState,
content = content
)
}

@Deprecated(
Expand All @@ -74,17 +66,17 @@ fun AppbarContainer(
@Composable
fun AppBarContainer(
modifier: Modifier = Modifier,
snapConfig: SnapConfig? = null,
scrollStrategy: ScrollStrategy,
/** The state of a connected collapsing toolbar */
collapsingToolbarState: CollapsingToolbarState,
state: CollapsingToolbarScaffoldState,
content: @Composable AppbarContainerScope.() -> Unit
) {
val offsetY = remember { mutableStateOf(0) }
val flingBehavior = ScrollableDefaults.flingBehavior()

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

Layout(
Expand All @@ -100,15 +92,15 @@ interface AppbarContainerScope {

internal class AppbarContainerScopeImpl(
private val nestedScrollConnection: NestedScrollConnection
): AppbarContainerScope {
) : AppbarContainerScope {
override fun Modifier.appBarBody(): Modifier {
return this
.then(AppBarBodyMarkerModifier)
.nestedScroll(nestedScrollConnection)
}
}

private object AppBarBodyMarkerModifier: ParentDataModifier {
private object AppBarBodyMarkerModifier : ParentDataModifier {
override fun Density.modifyParentData(parentData: Any?): Any {
return AppBarBodyMarker
}
Expand All @@ -118,9 +110,8 @@ private object AppBarBodyMarker

private class AppbarMeasurePolicy(
private val scrollStrategy: ScrollStrategy,
private val toolbarState: CollapsingToolbarState,
private val offsetY: State<Int>
): MeasurePolicy {
private val state: CollapsingToolbarScaffoldState,
) : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
Expand All @@ -132,33 +123,35 @@ private class AppbarMeasurePolicy(

val nonToolbars = measurables.filter {
val data = it.parentData
if(data != AppBarBodyMarker) {
if(toolbarPlaceable != null)
if (data != AppBarBodyMarker) {
if (toolbarPlaceable != null)
throw IllegalStateException("There cannot exist multiple toolbars under single parent")

val placeable = it.measure(constraints.copy(
minWidth = 0,
minHeight = 0
))
val placeable = it.measure(
constraints.copy(
minWidth = 0,
minHeight = 0
)
)
width = max(width, placeable.width)
height = max(height, placeable.height)

toolbarPlaceable = placeable

false
}else{
} else {
true
}
}

val placeables = nonToolbars.map { measurable ->
val childConstraints = if(scrollStrategy == ScrollStrategy.ExitUntilCollapsed) {
val childConstraints = if (scrollStrategy == ScrollStrategy.ExitUntilCollapsed) {
constraints.copy(
minWidth = 0,
minHeight = 0,
maxHeight = max(0, constraints.maxHeight - toolbarState.minHeight)
maxHeight = max(0, constraints.maxHeight - state.toolbarState.minHeight)
)
}else{
} else {
constraints.copy(
minWidth = 0,
minHeight = 0
Expand All @@ -179,12 +172,12 @@ private class AppbarMeasurePolicy(
width.coerceIn(constraints.minWidth, constraints.maxWidth),
height.coerceIn(constraints.minHeight, constraints.maxHeight)
) {
toolbarPlaceable?.place(x = 0, y = offsetY.value)
toolbarPlaceable?.place(x = 0, y = state.offsetY)

placeables.forEach { placeable ->
placeable.place(
x = 0,
y = offsetY.value + (toolbarPlaceable?.height ?: 0)
y = state.offsetY + (toolbarPlaceable?.height ?: 0)
)
}
}
Expand Down
Loading