I am trying to implement collapsing toolbar in my Detail screen using Jetpack compose : https://github.com/alirezaeiii/Compose-MultiModule-Cache/blob/master/feature_list/src/main/java/com/android/sample/app/feature/list/ui/detail/DetailsScreen.kt
val toolbarHeightPx = with(LocalDensity.current) {
278.dp.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()
// attach as a parent to the nested scroll system
.nestedScroll(nestedScrollConnection)
) {
DetailsContent(
scrollState = scrollState,
onNamePosition = { newNamePosition ->
// Comparing to Float.MIN_VALUE as we are just interested on the original
// position of name on the screen
if (detailScroller.namePosition == Float.MIN_VALUE) {
detailScroller =
detailScroller.copy(namePosition = newNamePosition)
}
},
item = item,
boxHeight = with(LocalDensity.current) {
440.dp + toolbarOffsetHeightPx.value.toDp()
},
imageHeight = with(LocalDensity.current) {
420.dp + toolbarOffsetHeightPx.value.toDp()
},
sendNotification = sendNotification,
contentAlpha = { contentAlpha.value }
)
DetailsToolbar(
toolbarState, item.name, pressOnBack,
contentAlpha = { contentAlpha.value }
)
}
The idea is taken from sunflower Google Github project. When we scroll up it works as expected but when we scroll down, it will not sometimes fully scrolled. toolbarOffsetHeightPx should become 0 when we scroll down, but sometimes it is a negative value that cause image does not fully scrolled. It is not stable at all and 0 or any negative value may happen. it happens since we have :
boxHeight = with(LocalDensity.current) {
440.dp + toolbarOffsetHeightPx.value.toDp()
},
imageHeight = with(LocalDensity.current) {
420.dp + toolbarOffsetHeightPx.value.toDp()
}
Why is that and how to resolve it?
I reported it as a minor bug in issue tracker : https://issuetracker.google.com/issues/238177355
Related
Looking at the documentation for LazyRow I was wondering if it was possible to reduce the scroll speed, it looks like LazyRow inherits from ScrollState but I can't find anything useful on how to reduce the speed of the scroll
LazyRow doesn't have a parameter to customize scroll speed so you have to do it manually.
You could first capture the scroll gesture something like the below (from google example here ):
#Composable
fun ScrollableSample() {
// actual composable state
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
Then you have to implement the scroll state at your desired speed for the LazyRow by manually changing the LazyRow's state which is one of the LazyRow parameters.
you will also have to disable LazyRow userscrolling
something like this:
LazyRow(
...
state = yourCustomState,
userScrollEnabled = false,
...
){ ... }
below is a complete working solution:
var stateIsGood = rememberLazyListState()
var offset = 0f
LazyRow(
modifier = Modifier
.scrollable(
orientation = Orientation.Horizontal,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
vm.viewModelScope.launch {
//dividing by 8 the delta for 8 times slower horizontal scroll,
//you can change direction by making it a negative number
stateIsGood.scrollBy(-delta/8)
}
delta
}
),
state = stateIsGood,
userScrollEnabled = false,
) {
item {
Text(text = "Header")
}
// Add 3 items
items(3) { index ->
Text(text = "SCROLL ME First List items : $index")
}
// Add 2 items
items(2) { index ->
Text(text = "Second List Items: $index")
}
// Add another single item
item {
Text(text = "Footer")
}
}
As a shorter alternative to David's Code,
#Composable
fun LazyStack(ssd: Int = 1) { // Scroll-Speed-Divisor
val lazyStackState = rememberLazyListState()
val lazyStackScope = rememberCoroutineScope()
LazyColumn(
modifier = Modifier.pointerInput(Unit) {
detectVerticalDragGestures { _, dragAmount ->
lazyStackScope.launch {
lazyStackState.scrollBy(-dragAmount / ssd).log("checkpoint")
}
}
},
state = lazyStackState,
userScrollEnabled = false
) {...}
}
And yes, LazyStack is short for LazyStackOverflower
instead of prevent userScroll we can use flingBehavior parameter. When we update initialVelocity it should change fling effect.
#Composable
fun maxScrollFlingBehavior(): FlingBehavior {
val flingSpec = rememberSplineBasedDecay<Float>()
return remember(flingSpec) {
ScrollSpeedFlingBehavior(flingSpec)
}
}
private class ScrollSpeedFlingBehavior(
private val flingDecay: DecayAnimationSpec<Float>
) : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
// Prevent very fast scroll
val newVelocity =
if (initialVelocity > 0F) minOf(initialVelocity, 15_000F)
else maxOf(initialVelocity, -15_000F)
return if (abs(newVelocity) > 1f) {
var velocityLeft = newVelocity
var lastValue = 0f
AnimationState(
initialValue = 0f,
initialVelocity = newVelocity,
).animateDecay(flingDecay) {
val delta = value - lastValue
val consumed = scrollBy(delta)
lastValue = value
velocityLeft = this.velocity
// avoid rounding errors and stop if anything is unconsumed
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
}
velocityLeft
} else newVelocity
}
}
and you can use like this
LazyColumn(
modifier = Modifier.fillMaxSize(),
flingBehavior = maxScrollFlingBehavior()
) {
// Content Here
}
If you want to implement your own logic, you can update initialVelocity param. Higher its 'absolute' value, the faster it moves.
I need to use swipe to reveal when swiping right and swipe to dismiss when swiping left with jetpack compose android how can I achieve that ?
You can modify this. Try it yourself or I may as well help in a while, but it should be fairly easy. Try it.
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
// Detect a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
I've solved such a problem with using this library dependency
de.charlex.compose:revealswipe:1.0.0
See here
Check this implementation here without additional libraries.
https://proandroiddev.com/swipe-to-reveal-in-jetpack-compose-6ffa8928a4c2
#Composable
fun DraggableCardComplex(
card: CardModel,
isRevealed: Boolean,
cardOffset: Float,
onExpand: () -> Unit,
onCollapse: () -> Unit,
) {
val offsetX by remember { mutableStateOf(0f) }
val transitionState = remember {
MutableTransitionState(isRevealed).apply {
targetState = !isRevealed
}
}
val transition = updateTransition(transitionState)
val offsetTransition by transition.animateFloat(
label = "cardOffsetTransition",
transitionSpec = { tween(durationMillis = ANIMATION_DURATION) },
targetValueByState = { if (isRevealed) cardOffset - offsetX else -offsetX },
)
Card(
modifier = Modifier
.offset { IntOffset((offsetX + offsetTransition).roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures { change, dragAmount ->
..
}
},
content = { CardTitle(cardTitle = card.title) }
)
}
There is requirements to create ChipGroup with maximum lines/rows allowed, for all items that are not shown there should be last chip with text like "+3 more items".
I have tried FlowRow from accompanist, which works fine, but number of rows cannot be limited there (or I don't know how to :))
Another option I have tried is to implement custom layout. I have managed to limit number of rows (implementation similar to FlowRow with additional conditions), but I'm not sure how to append last item from layout(...placementBlock: Placeable.PlacementScope.() -> Unit)
Any help is appreciated
In such cases SubcomposeLayout should be used: it allows you to insert a composable depending on already measured views.
Applying this to FlowRow would take more time, because of many other arguments, so I've took my own simplified variant as the base.
#Composable
fun ChipVerticalGrid(
modifier: Modifier = Modifier,
spacing: Dp,
moreItemsView: #Composable (Int) -> Unit,
content: #Composable () -> Unit,
) {
SubcomposeLayout(
modifier = modifier
) { constraints ->
val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0)
var currentRow = 0
var currentOrigin = IntOffset.Zero
val spacingValue = spacing.toPx().toInt()
val mainMeasurables = subcompose("content", content)
val placeables = mutableListOf<Pair<Placeable, IntOffset>>()
for (i in mainMeasurables.indices) {
val measurable = mainMeasurables[i]
val placeable = measurable.measure(contentConstraints)
fun Placeable.didOverflowWidth() =
currentOrigin.x > 0f && currentOrigin.x + width > contentConstraints.maxWidth
if (placeable.didOverflowWidth()) {
currentRow += 1
val nextRowOffset = currentOrigin.y + placeable.height + spacingValue
if (nextRowOffset + placeable.height > contentConstraints.maxHeight) {
var morePlaceable: Placeable
do {
val itemsLeft = mainMeasurables.count() - placeables.count()
morePlaceable = subcompose(itemsLeft) {
moreItemsView(itemsLeft)
}[0].measure(contentConstraints)
val didOverflowWidth = morePlaceable.didOverflowWidth()
if (didOverflowWidth) {
val removed = placeables.removeLast()
currentOrigin = removed.second
}
} while (didOverflowWidth)
placeables.add(morePlaceable to currentOrigin)
break
}
currentOrigin = currentOrigin.copy(x = 0, y = nextRowOffset)
}
placeables.add(placeable to currentOrigin)
currentOrigin = currentOrigin.copy(x = currentOrigin.x + placeable.width + spacingValue)
}
layout(
width = maxOf(constraints.minWidth, placeables.maxOfOrNull { it.first.width + it.second.x } ?: 0),
height = maxOf(constraints.minHeight, placeables.lastOrNull()?.run { first.height + second.y } ?: 0),
) {
placeables.forEach {
val (placeable, origin) = it
placeable.place(origin.x, origin.y)
}
}
}
}
Usage:
val words = LoremIpsum().values.first().split(" ").map { it.filter { it.isLetter()} }
val itemView = #Composable { text: String ->
Text(
text,
modifier = Modifier
.background(color = Color.Gray, shape = CircleShape)
.padding(vertical = 3.dp, horizontal = 5.dp)
)
}
ChipVerticalGrid(
spacing = 7.dp,
moreItemsView = {
itemView("$it more items")
},
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(7.dp)
) {
words.forEach { word ->
itemView(word)
}
}
Result:
I have a column which has many items; based on the scroll, I want to show/hide the Floating action button, in case the scroll is down, hide it and in case the scroll is up, show it.
My code is working partially, but the scrolling is buggy. Below is the code. Need help.
Column(
Modifier
.background(color = colorResource(id = R.color.background_color))
.fillMaxWidth(1f)
.verticalScroll(scrollState)
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState {
offset.value = it
coroutineScope.launch {
scrollState.scrollBy(-it)
}
it
},
)
) { // 10-20 items }
Based on the offset value (whether positive/negative), I am maintaining the visibility of FAB.
You can use the nestedScroll modifier.
Something like:
val fabHeight = 72.dp //FabSize+Padding
val fabHeightPx = with(LocalDensity.current) { fabHeight.roundToPx().toFloat() }
val fabOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = fabOffsetHeightPx.value + delta
fabOffsetHeightPx.value = newOffset.coerceIn(-fabHeightPx, 0f)
return Offset.Zero
}
}
}
Since composable supports nested scrolling just apply it to the Scaffold:
Scaffold(
Modifier.nestedScroll(nestedScrollConnection),
scaffoldState = scaffoldState,
//..
floatingActionButton = {
FloatingActionButton(
modifier = Modifier
.offset { IntOffset(x = 0, y = -fabOffsetHeightPx.value.roundToInt()) },
onClick = {}
) {
Icon(Icons.Filled.Add,"")
}
},
content = { innerPadding ->
Column(
Modifier
.fillMaxWidth(1f)
.verticalScroll(rememberScrollState())
) {
//....your code
}
}
)
It can work with a Column with verticalScroll and also with a LazyColumn.
I'm looking forward to a way to implement a Collapsing effect while scrolling on LazyColumn list. I have been checking the docs but I didn't found nothing related. How can I implement it?
At the moment I'm using a BottomNavigation setted inside my Scaffold and I can add the inner paddings to the screen coming from the scaffold content lambda. But I didn't find any kind of scrollable state or something close to it.
You can use the nestedScroll modifier.
Something like:
val bottomBarHeight = 48.dp
val bottomBarHeightPx = with(LocalDensity.current) { bottomBarHeight.roundToPx().toFloat() }
val bottomBarOffsetHeightPx = remember { mutableStateOf(0f) }
// connection to the nested scroll system and listen to the scroll
// happening inside child LazyColumn
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = bottomBarOffsetHeightPx.value + delta
bottomBarOffsetHeightPx.value = newOffset.coerceIn(-bottomBarHeightPx, 0f)
return Offset.Zero
}
}
}
and then apply the nestedScroll to the Scaffold:
Scaffold(
Modifier.nestedScroll(nestedScrollConnection),
scaffoldState = scaffoldState,
//..
bottomBar = {
BottomAppBar(modifier = Modifier
.height(bottomBarHeight)
.offset { IntOffset(x = 0, y = -bottomBarOffsetHeightPx.value.roundToInt()) }) {
IconButton(
onClick = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}
) {
Icon(Icons.Filled.Menu, contentDescription = "Localized description")
}
}
},
content = { innerPadding ->
LazyColumn(contentPadding = innerPadding) {
items(count = 100) {
Box(
Modifier
.fillMaxWidth()
.height(50.dp)
.background(colors[it % colors.size])
)
}
}
}
)