How to Continuously Scroll With One Swipe Android
Compose provides a variety of APIs to help you detect gestures that are generated from user interactions. The APIs cover a wide range of use cases:
-
Some of them are high-level and designed to cover the most commonly used gestures. For example, the
clickable
modifier allows easy detection of a click, and it also provides accessibility features and displays visual indicators when tapped (such as ripples). -
There are also less commonly used gesture detectors that offer more flexibility on a lower level, like
PointerInputScope.detectTapGestures
orPointerInputScope.detectDragGestures
but don't include the extra features.
Tapping and pressing
The clickable
modifier allows apps to detect clicks on the element it's applied to.
@Composable fun ClickableSample() { val count = remember { mutableStateOf(0) } // content that you want to make clickable Text( text = count.value.toString(), modifier = Modifier.clickable { count.value += 1 } ) }
When more flexibility is needed, you can provide a tap gesture detector via the pointerInput
modifier:
Modifier.pointerInput(Unit) { detectTapGestures( onPress = { /* Called when the gesture starts */ }, onDoubleTap = { /* Called on Double Tap */ }, onLongPress = { /* Called on Long Press */ }, onTap = { /* Called on Tap */ } ) }
Scroll modifiers
The verticalScroll
and horizontalScroll
modifiers provide the simplest way to allow the user to scroll an element when the bounds of its contents are larger than its maximum size constraints. With the verticalScroll
and horizontalScroll
modifiers you don't need to translate or offset the contents.
@Composable fun ScrollBoxes() { Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .verticalScroll(rememberScrollState()) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }
The ScrollState
allows you to change the scroll position or get its current state. To create it with default parameters, use rememberScrollState()
.
@Composable private fun ScrollBoxesSmooth() { // Smoothly scroll 100px on first composition val state = rememberScrollState() LaunchedEffect(Unit) { state.animateScrollTo(100) } Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .padding(horizontal = 8.dp) .verticalScroll(state) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }
Scrollable modifier
The scrollable
modifier differs from the scroll modifiers in that scrollable
detects the scroll gestures, but does not offset its contents. A ScrollableState
is required for this modifier to work correctly. When constructing ScrollableState
you must provide a consumeScrollDelta
function which will be invoked on each scroll step (by gesture input, smooth scrolling or flinging) with the delta in pixels. This function must return the amount of scrolling distance consumed, to ensure the event is properly propagated in cases where there are nested elements that have the scrollable
modifier.
The following snippet detects the gestures and displays a numerical value for an offset, but does not offset any elements:
@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()) } }
Nested Scrolling
Compose supports nested scrolling, in which multiple elements react to a single scroll gesture. A typical example of nested scrolling is a list inside another list, and a more complex case is a collapsing toolbar.
Automatic nested scrolling
Simple nested scrolling requires no action on your part. Gestures that initiate a scrolling action are propagated from children to parents automatically, such that when the child can't scroll any further, the gesture is handled by its parent element.
Automatic nested scrolling is supported and provided out of the box by some of Compose's components and modifiers: verticalScroll
, horizontalScroll
, scrollable
, Lazy
APIs and TextField
. This means that when the user scrolls an inner child of nested components, the previous modifiers propagate the scrolling deltas to the parents that have nested scrolling support.
The following example shows elements with a verticalScroll
modifier applied to it inside a container that also has a verticalScroll
modifier applied to it.
val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White) Box( modifier = Modifier .background(Color.LightGray) .verticalScroll(rememberScrollState()) .padding(32.dp) ) { Column { repeat(6) { Box( modifier = Modifier .height(128.dp) .verticalScroll(rememberScrollState()) ) { Text( "Scroll here", modifier = Modifier .border(12.dp, Color.DarkGray) .background(brush = gradient) .padding(24.dp) .height(150.dp) ) } } } }
Using the nestedScroll modifier
If you need to create an advanced coordinated scroll between multiple elements, the nestedScroll
modifier gives you more flexibility by defining a nested scrolling hierarchy. As mentioned in the previous section, some components have built-in nested scroll support. However, for composables that aren't scrollable automatically, such as Box
or Column
, scroll deltas on such components won't propagate in the nested scroll system and the deltas won't reach the NestedScrollConnection
nor the parent component. To resolve this, you can use nestedScroll
to confer such support to other components, including custom components.
Nested scrolling interop (experimental)
When you try to nest scrollable View
elements in scrollable composables, or the other way around, you might encounter issues. Most noticeable ones would happen when you scroll the child and reach its start or end bounds and expect the parent to take the scrolling over. However, this expected behaviour either might not happen or might not work as expected.
This issue is a result of the expectations built in scrollable composables. Scrollable composables have a "nested-scroll-by-default" rule, which means that any scrollable container must participate in the nested scroll chain, both as a parent via NestedScrollConnection
, and as a child via NestedScrollDispatcher
. The child would then drive a nested scroll for the parent when the child is at the bound. As an example, this rule allows Compose Pager
and Compose LazyRow
to work well together. However, when interoperability scrolling is being done with ViewPager2
or RecyclerView
, since these don't implement NestedScrollingParent3
, the continuous scrolling from child to parent is not possible.
To enable nested scrolling interop API between scrollable View
elements and scrollable composables, nested in both directions, you can use the nested scrolling interop API to mitigate these issues, in the following scenarios.
A cooperating parent View containing a child ComposeView
A cooperating parent View
is one that already implements NestedScrollingParent3
and therefore is able to receive scrolling deltas from a cooperating nested child composable. ComposeView
would act as a child in this case and would need to (indirectly) implement NestedScrollingChild3
. One example of a cooperating parent is androidx.coordinatorlayout.widget.CoordinatorLayout
.
If you need nested scrolling interoperability between scrollable View
parent containers and nested scrollable child composables, you can use rememberNestedScrollInteropConnection()
.
rememberNestedScrollInteropConnection()
allows and remembers the NestedScrollConnection
that enables nested scroll interoperability between a View
parent that implements NestedScrollingParent3
and a Compose child. This should be used in conjunction with a nestedScroll
modifier. Since nested scrolling is enabled by default on the Compose side, you can use this connection to enable both nested scroll on the View
side and add the necessary glue logic between Views
and composables.
A frequent use case is using CoordinatorLayout
, CollapsingToolbarLayout
and a child composable, shown in this example:
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="100dp" android:fitsSystemWindows="true"> <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:layout_scrollFlags="scroll|exitUntilCollapsed"> <!--...--> </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_width="match_parent" android:layout_height="match_parent"/> </androidx.coordinatorlayout.widget.CoordinatorLayout>
In your Activity or Fragment, you need to set up your child composable and the required NestedScrollConnection
:
@ExperimentalComposeUiApi open class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<ComposeView>(R.id.compose_view).apply { setContent { val nestedScrollInterop = rememberNestedScrollInteropConnection() // Add the nested scroll connection to your top level @Composable element // using the nestedScroll modifier. LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) { items(20) { item -> Box( modifier = Modifier .padding(16.dp) .height(56.dp) .fillMaxWidth() .background(Color.Gray), contentAlignment = Alignment.Center ) { Text(item.toString()) } } } } } } }
A parent composable containing a child AndroidView
This scenario covers the implementation of nested scrolling interop API on the Compose side - when you have a parent composable containing a child AndroidView
. The AndroidView
implements NestedScrollDispatcher
, since it acts as a child to a Compose scrolling parent, as well as NestedScrollingParent3
, since it acts as a parent to a View
scrolling child. Compose parent will then be able to receive nested scroll deltas from a nested scrollable child View
.
The following example shows how you can achieve nested scrolling interop in this scenario, along with a Compose collapsing toolbar:
@Composable private fun NestedScrollInteropComposeParentWithAndroidChildExample() { val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() } val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } // Sets up the nested scroll connection between the Box composable parent // and the child AndroidView containing the RecyclerView val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Updates the toolbar offset based on the scroll to enable // collapsible behaviour val delta = available.y val newOffset = toolbarOffsetHeightPx.value + delta toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f) return Offset.Zero } } } Box( Modifier .fillMaxSize() .nestedScroll(nestedScrollConnection) ) { TopAppBar( modifier = Modifier .height(ToolbarHeight) .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) } ) AndroidView( { context -> LayoutInflater.from(context) .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply { with(findViewById<RecyclerView>(R.id.main_list)) { layoutManager = LinearLayoutManager(context, VERTICAL, false) adapter = NestedScrollInteropAdapter() } }.also { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(it, true) } }, // ... ) } } private class NestedScrollInteropAdapter : Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() { val items = (1..10).map { it.toString() } override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): NestedScrollInteropViewHolder { return NestedScrollInteropViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.list_item, parent, false) ) } override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) { // ... } class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) { fun bind(item: String) { // ... } } // ... }
This example shows how you can use the API with a scrollable
modifier:
@Composable fun ViewInComposeNestedScrollInteropExample() { Box( Modifier .fillMaxSize() .scrollable(rememberScrollableState { // View component deltas should be reflected in Compose // components that participate in nested scrolling it }, Orientation.Vertical) ) { AndroidView( { context -> LayoutInflater.from(context) .inflate(android.R.layout.list_item, null) .apply { // Nested scrolling interop is enabled when // nested scroll is enabled for the root View ViewCompat.setNestedScrollingEnabled(this, true) } } ) } }
And finally, this example shows how nested scrolling interop API is used with BottomSheetDialogFragment
to achieve a successful drag and dismiss behaviour:
@OptIn(ExperimentalComposeUiApi::class) class BottomSheetFragment : BottomSheetDialogFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false) rootView.findViewById<ComposeView>(R.id.compose_view).apply { setContent { val nestedScrollInterop = rememberNestedScrollInteropConnection() LazyColumn( Modifier .nestedScroll(nestedScrollInterop) .fillMaxSize() ) { item { Text(text = "Bottom sheet title") } items(10) { Text( text = "List item number $it", modifier = Modifier.fillMaxWidth() ) } } } return rootView } } }
Note that rememberNestedScrollInteropConnection()
will install a NestedScrollConnection
in the element you attach it to. NestedScrollConnection
is responsible for transmitting the deltas from the Compose level to the View
level. This enables the element to participate in nested scrolling, but it doesn't enabling scrolling of elements automatically. To composables that aren't scrollable automatically, such as Box
or Column
, scroll deltas on such components won't propagate in the nested scroll system and the deltas won't reach the NestedScrollConnection
provided by rememberNestedScrollInteropConnection()
, therefore those deltas won't reach the parent View
component. To resolve this, make sure you also set scrollable modifiers to these types of nested composables. You can refer to the previous section on Nested scrolling for more detailed information.
A non-cooperating parent View containing a child ComposeView
A non-cooperating View is one that does not implement the necessary NestedScrolling
interfaces on the View
side. Note that this means that nested scrolling interoperability with these Views
doesn't work out of the box. Non-cooperating Views
are RecyclerView
and ViewPager2
.
Dragging
The draggable
modifier is the high-level entry point to drag gestures in a single orientation, and reports the drag distance in pixels.
It's important to note that this modifier is similar to scrollable
, in that it only detects the gesture. You need to hold the state and represent it on screen by, for example, moving the element via the offset
modifier:
var offsetX by remember { mutableStateOf(0f) } Text( modifier = Modifier .offset { IntOffset(offsetX.roundToInt(), 0) } .draggable( orientation = Orientation.Horizontal, state = rememberDraggableState { delta -> offsetX += delta } ), text = "Drag me!" )
If you need to control the whole dragging gesture, consider using the drag gesture detector instead, via the pointerInput
modifier.
Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } Box( Modifier .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consumeAllChanges() offsetX += dragAmount.x offsetY += dragAmount.y } } ) }
Swiping
The swipeable
modifier lets you drag elements which, when released, animate towards typically two or more anchor points defined in an orientation. A common usage for this is to implement a 'swipe-to-dismiss' pattern.
It's important to note that this modifier does not move the element, it only detects the gesture. You need to hold the state and represent it on screen by, for example, moving the element via the offset
modifier.
The swipeable state is required in the swipeable
modifier, and can be created and remembered with rememberSwipeableState()
. This state also provides a set of useful methods to programmatically animate to anchors (see snapTo
, animateTo
, performFling
, and performDrag
) as well as properties to observe the dragging progress.
The swipe gesture can be configured to have different threshold types, such as FixedThreshold(Dp)
and FractionalThreshold(Float)
, and they can be different for each anchor point from-to combination.
For more flexibility, you can configure the resistance
when swiping past the bounds and, also, the velocityThreshold
which will animate a swipe to the next state, even if the positional thresholds
have not been reached.
@Composable fun SwipeableSample() { val width = 96.dp val squareSize = 48.dp val swipeableState = rememberSwipeableState(0) val sizePx = with(LocalDensity.current) { squareSize.toPx() } val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states Box( modifier = Modifier .width(width) .swipeable( state = swipeableState, anchors = anchors, thresholds = { _, _ -> FractionalThreshold(0.3f) }, orientation = Orientation.Horizontal ) .background(Color.LightGray) ) { Box( Modifier .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } .size(squareSize) .background(Color.DarkGray) ) } }
Multitouch: Panning, zooming, rotating
To detect multitouch gestures used for panning, zooming and rotating, you can use the transformable
modifier. This modifier does not transform elements by itself, it only detects the gestures.
@Composable fun TransformableSample() { // set up all transformation states var scale by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> scale *= zoomChange rotation += rotationChange offset += offsetChange } Box( Modifier // apply other transformations like rotation and zoom // on the pizza slice emoji .graphicsLayer( scaleX = scale, scaleY = scale, rotationZ = rotation, translationX = offset.x, translationY = offset.y ) // add transformable to listen to multitouch transformation events // after offset .transformable(state = state) .background(Color.Blue) .fillMaxSize() ) }
If you need to combine zooming, panning and rotation with other gestures, you can use the PointerInputScope.detectTransformGestures
detector.
Source: https://developer.android.com/jetpack/compose/gestures
Postar um comentário for "How to Continuously Scroll With One Swipe Android"