Skip to content

Commit 64af69f

Browse files
Add scrollableArea snippet to GesturesSnippets (#703)
* Add scrollableArea snippet to GesturesSnippets * Remove unneeded line breaks * Import correct scrollableArea --------- Co-authored-by: Rebecca Franks <riggaroo@google.com>
1 parent 0dcfc0b commit 64af69f

File tree

1 file changed

+186
-0
lines changed
  • compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures

1 file changed

+186
-0
lines changed

compose/snippets/src/main/java/com/example/compose/snippets/touchinput/gestures/GesturesSnippets.kt

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ package com.example.compose.snippets.touchinput.gestures
2020

2121
import android.os.Bundle
2222
import androidx.activity.ComponentActivity
23+
import androidx.compose.foundation.MutatePriority
2324
import androidx.compose.foundation.background
2425
import androidx.compose.foundation.border
2526
import androidx.compose.foundation.clickable
2627
import androidx.compose.foundation.gestures.Orientation
28+
import androidx.compose.foundation.gestures.ScrollScope
29+
import androidx.compose.foundation.gestures.ScrollableState
2730
import androidx.compose.foundation.gestures.detectDragGestures
2831
import androidx.compose.foundation.gestures.detectTapGestures
2932
import androidx.compose.foundation.gestures.draggable
@@ -43,6 +46,7 @@ import androidx.compose.foundation.layout.size
4346
import androidx.compose.foundation.layout.width
4447
import androidx.compose.foundation.lazy.LazyColumn
4548
import androidx.compose.foundation.rememberScrollState
49+
import androidx.compose.foundation.scrollableArea
4650
import androidx.compose.foundation.verticalScroll
4751
import androidx.compose.material.ExperimentalMaterialApi
4852
import androidx.compose.material.FractionalThreshold
@@ -51,12 +55,17 @@ import androidx.compose.material.swipeable
5155
import androidx.compose.material3.Text
5256
import androidx.compose.runtime.Composable
5357
import androidx.compose.runtime.LaunchedEffect
58+
import androidx.compose.runtime.annotation.FrequentlyChangingValue
59+
import androidx.compose.runtime.derivedStateOf
5460
import androidx.compose.runtime.getValue
5561
import androidx.compose.runtime.mutableFloatStateOf
5662
import androidx.compose.runtime.mutableIntStateOf
5763
import androidx.compose.runtime.mutableStateOf
5864
import androidx.compose.runtime.remember
65+
import androidx.compose.runtime.saveable.Saver
66+
import androidx.compose.runtime.saveable.rememberSaveable
5967
import androidx.compose.runtime.setValue
68+
import androidx.compose.runtime.snapshots.Snapshot
6069
import androidx.compose.ui.Alignment
6170
import androidx.compose.ui.Modifier
6271
import androidx.compose.ui.geometry.Offset
@@ -67,12 +76,18 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
6776
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
6877
import androidx.compose.ui.input.nestedscroll.nestedScroll
6978
import androidx.compose.ui.input.pointer.pointerInput
79+
import androidx.compose.ui.layout.Layout
7080
import androidx.compose.ui.platform.ComposeView
7181
import androidx.compose.ui.platform.LocalDensity
7282
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
83+
import androidx.compose.ui.text.style.TextAlign
7384
import androidx.compose.ui.tooling.preview.Preview
7485
import androidx.compose.ui.unit.IntOffset
7586
import androidx.compose.ui.unit.dp
87+
import androidx.compose.ui.unit.sp
88+
import androidx.compose.ui.util.fastCoerceIn
89+
import androidx.compose.ui.util.fastRoundToInt
90+
import kotlin.math.abs
7691
import kotlin.math.roundToInt
7792

7893
@Preview
@@ -174,6 +189,177 @@ private fun ScrollableSample() {
174189
}
175190
// [END android_compose_touchinput_gestures_scrollable]
176191

192+
@Preview
193+
// [START android_compose_touchinput_gestures_scrollableArea]
194+
@Composable
195+
private fun ScrollableAreaSample() {
196+
// [START_EXCLUDE]
197+
// rememberScrollableAreaSampleScrollState() holds the scroll position and other relevant
198+
// information. It implements the ScrollableState interface, making it compatible with the
199+
// scrollableArea modifier
200+
val scrollState = rememberScrollableAreaSampleScrollState()
201+
// [END_EXCLUDE]
202+
Layout(
203+
modifier =
204+
Modifier
205+
.size(150.dp)
206+
.scrollableArea(scrollState, Orientation.Vertical)
207+
.background(Color.LightGray),
208+
// [START_EXCLUDE]
209+
content = {
210+
repeat(40) {
211+
Text(
212+
modifier = Modifier.padding(vertical = 2.dp),
213+
text = "Item $it",
214+
fontSize = 24.sp,
215+
textAlign = TextAlign.Center,
216+
)
217+
}
218+
},
219+
// [END_EXCLUDE]
220+
) { measurables, constraints ->
221+
// [START_EXCLUDE]
222+
var totalHeight = 0
223+
224+
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
225+
val placeables =
226+
measurables.map { measurable ->
227+
val placeable = measurable.measure(childConstraints)
228+
totalHeight += placeable.height
229+
placeable
230+
}
231+
232+
val viewportHeight = constraints.maxHeight
233+
// [END_EXCLUDE]
234+
// Update the maximum scroll value to not scroll beyond limits and stop when scroll
235+
// reaches the end.
236+
scrollState.maxValue = (totalHeight - viewportHeight).coerceAtLeast(0)
237+
238+
// Position the children within the layout.
239+
layout(constraints.maxWidth, viewportHeight) {
240+
// The current vertical scroll position, in pixels.
241+
val scrollY = scrollState.value
242+
val viewportCenterY = scrollY + viewportHeight / 2
243+
244+
var placeableLayoutPositionY = 0
245+
placeables.forEach { placeable ->
246+
// This sample applies a scaling effect to items based on their distance
247+
// from the center, creating a wheel-like effect.
248+
// [START_EXCLUDE]
249+
val itemCenterY = placeableLayoutPositionY + placeable.height / 2
250+
val distanceFromCenter = abs(itemCenterY - viewportCenterY)
251+
val normalizedDistance =
252+
(distanceFromCenter / (viewportHeight / 2f)).fastCoerceIn(0f, 1f)
253+
254+
// Items scale between 0.4 at the edges of the viewport and 1 at the center.
255+
val scaleFactor = 1f - (normalizedDistance * 0.6f)
256+
// [END_EXCLUDE]
257+
// Place the item horizontally centered with a layer transformation for
258+
// scaling to achieve wheel-like effect.
259+
placeable.placeRelativeWithLayer(
260+
x = constraints.maxWidth / 2 - placeable.width / 2,
261+
// Offset y by the scroll position to make placeable visible in the viewport.
262+
y = placeableLayoutPositionY - scrollY,
263+
) {
264+
scaleX = scaleFactor
265+
scaleY = scaleFactor
266+
}
267+
// Move to the next item's vertical position.
268+
placeableLayoutPositionY += placeable.height
269+
}
270+
}
271+
}
272+
}
273+
// [START_EXCLUDE]
274+
/*
275+
* A custom implementation of ScrollableState that manages a scroll position and its maximum allowed
276+
* value.
277+
*
278+
* This is a simplified version of the `ScrollState` used by `Modifier.verticalScroll` and
279+
* `Modifier.horizontalScroll`, demonstrating how to implement a custom state for custom scrollable
280+
* containers.
281+
*/
282+
private class ScrollableAreaSampleScrollState(initial: Int) : ScrollableState {
283+
284+
// The current integer scroll position in pixels.
285+
// This is backed by a mutableStateOf to trigger recomposition when it changes.
286+
@get:FrequentlyChangingValue
287+
var value by mutableIntStateOf(initial)
288+
private set
289+
290+
// The maximum scroll position allowed. This is typically derived from the content size minus
291+
// viewport size.
292+
var maxValue: Int
293+
get() = _maxValueState.intValue
294+
set(newMax) {
295+
_maxValueState.intValue = newMax
296+
Snapshot.withoutReadObservation {
297+
if (value > newMax) {
298+
value = newMax
299+
}
300+
}
301+
}
302+
303+
private var _maxValueState = mutableIntStateOf(Int.MAX_VALUE)
304+
305+
// Accumulates sub-pixel scroll deltas. This ensures that even small, fractional scroll
306+
// movements are accounted for and contribute to the total scroll position over time, preventing
307+
// loss of precision.
308+
private var accumulator: Float = 0f
309+
310+
// The underlying ScrollableState that handles the actual scroll consumption logic. This lambda
311+
// is invoked when a scroll delta is received.
312+
private val scrollableState = ScrollableState {
313+
val absolute = (value + it + accumulator)
314+
val newValue = absolute.coerceIn(0f, maxValue.toFloat())
315+
val changed = absolute != newValue
316+
val consumed = newValue - value
317+
val consumedInt = consumed.fastRoundToInt()
318+
value += consumedInt
319+
accumulator = consumed - consumedInt
320+
321+
if (changed) consumed else it
322+
}
323+
324+
override suspend fun scroll(
325+
scrollPriority: MutatePriority,
326+
block: suspend ScrollScope.() -> Unit,
327+
): Unit = scrollableState.scroll(scrollPriority, block)
328+
329+
override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)
330+
331+
override val isScrollInProgress: Boolean
332+
get() = scrollableState.isScrollInProgress
333+
334+
override val canScrollForward: Boolean by derivedStateOf { value < maxValue }
335+
336+
override val canScrollBackward: Boolean by derivedStateOf { value > 0 }
337+
338+
override val lastScrolledForward: Boolean
339+
get() = scrollableState.lastScrolledForward
340+
341+
override val lastScrolledBackward: Boolean
342+
get() = scrollableState.lastScrolledBackward
343+
344+
companion object {
345+
// Saver for CustomSampleScrollState, allowing it to be saved and restored across
346+
// process death or configuration changes. Only the current scroll 'value' is saved.
347+
val Saver: Saver<ScrollableAreaSampleScrollState, *> =
348+
Saver(save = { it.value }, restore = { ScrollableAreaSampleScrollState(it) })
349+
}
350+
}
351+
352+
@Composable
353+
private fun rememberScrollableAreaSampleScrollState(
354+
initial: Int = 0
355+
): ScrollableAreaSampleScrollState {
356+
return rememberSaveable(saver = ScrollableAreaSampleScrollState.Saver) {
357+
ScrollableAreaSampleScrollState(initial = initial)
358+
}
359+
}
360+
// [END_EXCLUDE]
361+
// [END android_compose_touchinput_gestures_scrollableArea]
362+
177363
// [START android_compose_touchinput_gestures_nested_scroll]
178364
@Composable
179365
private fun AutomaticNestedScroll() {

0 commit comments

Comments
 (0)