Skip to content

Commit

Permalink
Merge branch 'jb-main' into integration
Browse files Browse the repository at this point in the history
# Conflicts:
#	libraryversions.toml
  • Loading branch information
MatkovIvan committed Dec 9, 2024
2 parents 76ea6e2 + f8ef71b commit fccc3e0
Show file tree
Hide file tree
Showing 24 changed files with 385 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -365,24 +365,24 @@ internal class LazyGridScrollbarAdapter(
scrollState.scrollBy(value)
}

override fun averageVisibleLineSize(): Double{
private val lineIsKnown = { itemInfo: LazyGridItemInfo -> itemInfo.line() != unknownLine }

override fun averageVisibleLineSize(): Double {
val visibleItemsInfo = scrollState.layoutInfo.visibleItemsInfo
val indexOfFirstKnownLineItem = visibleItemsInfo.indexOfFirst { it.line() != unknownLine }
if (indexOfFirstKnownLineItem == -1)
return 0.0
val reallyVisibleItemsInfo = // Non-exiting visible items
visibleItemsInfo.subList(indexOfFirstKnownLineItem, visibleItemsInfo.size)

// Compute the size of the last line
val lastLine = reallyVisibleItemsInfo.last().line()
val lastLineSize = reallyVisibleItemsInfo

// First and last visible, non-exiting LazyGridItemInfo
val first = visibleItemsInfo.firstOrNull(lineIsKnown) ?: return 0.0
val last = visibleItemsInfo.last(lineIsKnown)

// Compute the size (e.g. height for vertical grid) of the last line
val lastLine = last.line()
val lastLineSize = visibleItemsInfo
.asReversed()
.asSequence()
.filter(lineIsKnown)
.takeWhile { it.line() == lastLine }
.maxOf { it.mainAxisSize() }

val first = reallyVisibleItemsInfo.first()
val last = reallyVisibleItemsInfo.last()
val lineCount = last.line() - first.line() + 1
val lineSpacingSum = (lineCount - 1) * lineSpacing
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,8 @@ internal fun AssertThat<Float>.isEqualTo(f: Float, eps: Float = 0f) {
}
}

internal fun AssertThat<Int>.isNotEqualTo(i: Int) {
assertNotEquals(i, t)
}

internal fun <T> AssertThat<T>.isNotEqualTo(i: T) {
assertNotEquals(i, t)
internal fun <T> AssertThat<T>.isNotEqualTo(a: Any?) {
assertNotEquals(a, t)
}

internal fun AssertThat<Int>.isEqualTo(i: Int, d: Int = 0) {
Expand Down Expand Up @@ -128,6 +124,9 @@ internal fun <K, T : Collection<*>> AssertThat<T>.contains(vararg items: K) {
}
}

internal fun AssertThat<*>.isZero() = isEqualTo(0)

internal fun AssertThat<*>.isNonZero() = isNotEqualTo(0)

internal fun AssertThat<*>.isNull() {
assertEquals(null, t, message ?: "$t expected to be null")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.materialize
import androidx.compose.ui.modifier.ModifierLocalConsumer
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.platform.*
import androidx.compose.ui.test.*
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import androidx.compose.ui.util.fastForEach
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.test.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -1295,6 +1296,181 @@ class ScrollableTest {
}
}

@Test
fun scrollable_nestedFling_shouldCancelWhenHitTheBounds() = runSkikoComposeUiTest {
var latestAvailableVelocity = Velocity.Zero
var onPostFlingCalled = false
val connection =
object : NestedScrollConnection {
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
latestAvailableVelocity = available
onPostFlingCalled = true
return super.onPostFling(consumed, available)
}
}
setContent {
Box(
Modifier.scrollable(
state = rememberScrollableState { it },
orientation = Orientation.Vertical
)
) {
Box(Modifier.nestedScroll(connection)) {
Column(
Modifier.testTag("column")
.verticalScroll(
rememberScrollState(with(density) { (5 * 200.dp).roundToPx() })
)
) {
repeat(10) { Box(Modifier.size(200.dp)) }
}
}
}
}

onNodeWithTag("column").performTouchInput { swipeDown() }

/**
* Because previously the animation was being completely consumed by the child fling, the
* nested scroll connection in the middle would see a zero post fling velocity, even if the
* child hit the bounds.
*/
runOnIdle {
assertThat(onPostFlingCalled).isTrue()
assertThat(latestAvailableVelocity.y).isNonZero()
}
}

@Test
fun scrollable_nestedFling_parentShouldFlingWithVelocityLeft() = runSkikoComposeUiTest {
var postFlingCalled = false
var lastPostFlingVelocity = Velocity.Zero
var flingDelta = 0.0f
val fling =
object : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
assertThat(initialVelocity).isEqualTo(lastPostFlingVelocity.y)
scrollBy(100f)
return initialVelocity
}
}
val topConnection =
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// accumulate deltas for second fling only
if (source == NestedScrollSource.SideEffect && postFlingCalled) {
flingDelta += available.y
}
return super.onPreScroll(available, source)
}
}

val middleConnection =
object : NestedScrollConnection {
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
postFlingCalled = true
lastPostFlingVelocity = available
return super.onPostFling(consumed, available)
}
}
val columnState = ScrollState(with(density) { (5 * 200.dp).roundToPx() })
setContent {
Box(
Modifier.nestedScroll(topConnection)
.scrollable(
flingBehavior = fling,
state = rememberScrollableState { it },
orientation = Orientation.Vertical
)
) {
Column(
Modifier.nestedScroll(middleConnection)
.testTag("column")
.verticalScroll(columnState)
) {
repeat(10) { Box(Modifier.size(200.dp)) }
}
}
}

onNodeWithTag("column").performTouchInput { swipeDown() }

runOnIdle {
assertThat(columnState.value).isZero() // column is at the bounds
assertThat(postFlingCalled)
.isTrue() // we fired a post fling call after the cancellation
assertThat(lastPostFlingVelocity.y)
.isNonZero() // the post child fling velocity was not zero
assertThat(flingDelta).isEqualTo(100f) // the fling delta as propagated correctly
}
}

@Test
fun scrollable_nestedFling_parentShouldFlingWithVelocityLeft_whenInnerDisappears() = runSkikoComposeUiTest {
var postFlingCalled = false
var postFlingAvailableVelocity = Velocity.Zero
var postFlingConsumedVelocity = Velocity.Zero
var flingDelta by mutableFloatStateOf(0.0f)
var preFlingVelocity = Velocity.Zero

val topConnection =
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// accumulate deltas for second fling only
if (source == NestedScrollSource.SideEffect) {
flingDelta += available.y
}
return super.onPreScroll(available, source)
}

override suspend fun onPreFling(available: Velocity): Velocity {
preFlingVelocity = available
return super.onPreFling(available)
}

override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
postFlingCalled = true
postFlingAvailableVelocity = available
postFlingConsumedVelocity = consumed
return super.onPostFling(consumed, available)
}
}

val columnState = ScrollState(with(density) { (50 * 200.dp).roundToPx() })

setContent {
Box(Modifier.nestedScroll(topConnection)) {
if (flingDelta.absoluteValue < 100) {
Column(Modifier.testTag("column").verticalScroll(columnState)) {
repeat(100) { Box(Modifier.size(200.dp)) }
}
}
}
}

onNodeWithTag("column").performTouchInput { swipeUp() }
waitForIdle()
// removed scrollable
onNodeWithTag("column").assertDoesNotExist()
runOnIdle {
// we fired a post fling call after the disappearance
assertThat(postFlingCalled).isTrue()

// fling velocity in onPostFling is correctly propagated
assertThat(postFlingConsumedVelocity + postFlingAvailableVelocity)
.isEqualTo(preFlingVelocity)
}
}

@Test
@Ignore // TODO: test failing on desktop
fun scrollable_bothOrientations_proxiesPostFling() = runSkikoComposeUiTest {
Expand Down Expand Up @@ -2148,7 +2324,7 @@ class ScrollableTest {
Modifier.scrollable(
state,
Orientation.Vertical,
NoOpOverscrollEffect
null
)
)
}
Expand Down Expand Up @@ -2300,6 +2476,42 @@ class ScrollableTest {
}
}

@Test
@Ignore // TODO(https://youtrack.jetbrains.com/issue/CMP-7220) Fails on iOS
fun onDensityChange_shouldUpdateFlingBehavior() = runSkikoComposeUiTest {
var density by mutableStateOf(density)
var flingDelta = 0f
val fixedSize = 400
setContent {
CompositionLocalProvider(LocalDensity provides density) {
Box(
Modifier.size(with(density) { fixedSize.toDp() })
.testTag(scrollableBoxTag)
.scrollable(
state =
rememberScrollableState {
flingDelta += it
it
},
orientation = Orientation.Vertical
)
)
}
}

onNodeWithTag(scrollableBoxTag).performTouchInput { swipeUp() }

waitForIdle()

density = Density(density.density * 2f)
val previousDelta = flingDelta
flingDelta = 0.0f

onNodeWithTag(scrollableBoxTag).performTouchInput { swipeUp() }

runOnIdle { assertThat(flingDelta).isNotEqualTo(previousDelta) }
}

private fun SkikoComposeUiTest.setScrollableContent(scrollableModifierFactory: @Composable () -> Modifier) {
setContentAndGetScope {
Box {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestinationBuilder
import androidx.navigation.NavDestinationDsl
import androidx.navigation.NavType
import kotlin.jvm.JvmSuppressWildcards
import kotlin.reflect.KClass
import kotlin.reflect.KType

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavType
import androidx.navigation.get
import kotlin.jvm.JvmSuppressWildcards
import kotlin.reflect.KClass
import kotlin.reflect.KType

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.graphics.Color
Expand Down Expand Up @@ -170,6 +172,7 @@ fun Slider(
val onValueChangeFinishedState = rememberUpdatedState(onValueChangeFinished)
val tickFractions = remember(steps) { stepsToTickFractions(steps) }

val focusRequester = remember { FocusRequester() }
BoxWithConstraints(
modifier
.minimumInteractiveComponentSize()
Expand All @@ -182,6 +185,7 @@ fun Slider(
valueRange,
steps
)
.focusRequester(focusRequester)
.focusable(enabled, interactionSource)
.slideOnKeyEvents(
enabled,
Expand Down Expand Up @@ -229,6 +233,7 @@ fun Slider(
rememberUpdatedState<(Float) -> Unit> { velocity: Float ->
val current = rawOffset.floatValue
val target = snapValueToTick(current, tickFractions, minPx, maxPx)
focusRequester.requestFocus()
if (current != target) {
scope.launch {
animateToTarget(draggableState, current, target, velocity)
Expand Down
Loading

0 comments on commit fccc3e0

Please sign in to comment.