1 /*
2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.input.nestedscroll
18
19 import androidx.compose.ui.ExperimentalComposeUiApi
20 import androidx.compose.ui.Modifier
21 import androidx.compose.ui.geometry.Offset
22 import androidx.compose.ui.node.DelegatableNode
23 import androidx.compose.ui.node.TraversableNode
24 import androidx.compose.ui.node.findNearestAncestor
25 import androidx.compose.ui.node.traverseAncestors
26 import androidx.compose.ui.unit.Velocity
27 import kotlinx.coroutines.CoroutineScope
28
29 /**
30 * This creates a Nested Scroll Modifier node that can be delegated to. In most case you should use
31 * [Modifier.nestedScroll] since that implementation also uses this. Use this factory to create
32 * nodes that can be delegated to.
33 */
nestedScrollModifierNodenull34 fun nestedScrollModifierNode(
35 connection: NestedScrollConnection,
36 dispatcher: NestedScrollDispatcher?
37 ): DelegatableNode {
38 return NestedScrollNode(connection, dispatcher)
39 }
40
41 /** NestedScroll using ModifierLocal as implementation. */
42 internal class NestedScrollNode(
43 var connection: NestedScrollConnection,
44 dispatcher: NestedScrollDispatcher?
45 ) : TraversableNode, NestedScrollConnection, Modifier.Node() {
46
47 // Resolved dispatcher for re-use in case of null dispatcher is passed.
48 private var resolvedDispatcher: NestedScrollDispatcher
49
50 init {
51 resolvedDispatcher = dispatcher ?: NestedScrollDispatcher() // Resolve null dispatcher
52 }
53
54 internal var lastKnownParentNode: NestedScrollNode? = null
55
56 internal val parentNestedScrollNode: NestedScrollNode?
57 get() = if (isAttached) findNearestAncestor() else null
58
59 private val parentConnection: NestedScrollConnection?
60 get() = if (isAttached) parentNestedScrollNode else null
61
62 override val traverseKey: Any = "androidx.compose.ui.input.nestedscroll.NestedScrollNode"
63
64 private val nestedCoroutineScope: CoroutineScope
65 get() =
66 parentNestedScrollNode?.nestedCoroutineScope
67 ?: resolvedDispatcher.scope
68 ?: throw IllegalStateException(
69 "in order to access nested coroutine scope you need to attach dispatcher to the " +
70 "`Modifier.nestedScroll` first."
71 )
72
onPreScrollnull73 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
74 val parentPreConsumed = parentConnection?.onPreScroll(available, source) ?: Offset.Zero
75 val selfPreConsumed = connection.onPreScroll(available - parentPreConsumed, source)
76 return parentPreConsumed + selfPreConsumed
77 }
78
onPostScrollnull79 override fun onPostScroll(
80 consumed: Offset,
81 available: Offset,
82 source: NestedScrollSource
83 ): Offset {
84 val selfConsumed = connection.onPostScroll(consumed, available, source)
85 val parentConsumed =
86 parentConnection?.onPostScroll(
87 consumed + selfConsumed,
88 available - selfConsumed,
89 source
90 ) ?: Offset.Zero
91 return selfConsumed + parentConsumed
92 }
93
onPreFlingnull94 override suspend fun onPreFling(available: Velocity): Velocity {
95 val parentPreConsumed = parentConnection?.onPreFling(available) ?: Velocity.Zero
96 val selfPreConsumed = connection.onPreFling(available - parentPreConsumed)
97 return parentPreConsumed + selfPreConsumed
98 }
99
100 @OptIn(ExperimentalComposeUiApi::class)
onPostFlingnull101 override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
102 val selfConsumed = connection.onPostFling(consumed, available)
103 // if we receive an onPostFling after detaching this node, use the last known parent
104 // if this parent is also detached it will send the signal through the detached parents
105 val parent = if (isAttached) parentConnection else lastKnownParentNode
106 val parentConsumed =
107 parent?.onPostFling(consumed + selfConsumed, available - selfConsumed) ?: Velocity.Zero
108 return selfConsumed + parentConsumed
109 }
110
111 // On receiving a new dispatcher, re-setting fields
updateDispatchernull112 private fun updateDispatcher(newDispatcher: NestedScrollDispatcher?) {
113 resetDispatcherFields() // Reset fields of current dispatcher.
114
115 // Update dispatcher associated with this node.
116 if (newDispatcher == null) {
117 resolvedDispatcher = NestedScrollDispatcher()
118 } else if (newDispatcher != resolvedDispatcher) {
119 resolvedDispatcher = newDispatcher
120 }
121
122 // Update fields of the newly set dispatcher.
123 if (isAttached) {
124 updateDispatcherFields()
125 }
126 }
127
onAttachnull128 override fun onAttach() {
129 // NOTE: It is possible for the dispatcher of a yet-to-be-removed node above this one in the
130 // chain is being used here where the dispatcher's modifierLocalNode will not be null. As a
131 // result, we should not check to see if the dispatcher's node is null, we should just set
132 // it assuming that it is not going to be used by the previous node anymore.
133 updateDispatcherFields()
134 }
135
136 @OptIn(ExperimentalComposeUiApi::class)
onDetachnull137 override fun onDetach() {
138 // cache parent for detached clean up access in the dispatcher and in this node.
139 lastKnownParentNode = findNearestAttachedAncestor()
140 resolvedDispatcher.lastKnownParentNode = lastKnownParentNode
141 resetDispatcherFields()
142 }
143
144 /**
145 * If the node changes (onAttach) or if the dispatcher changes (node.update). We'll need to
146 * reset the dispatcher properties accordingly.
147 */
148 @OptIn(ExperimentalComposeUiApi::class)
updateDispatcherFieldsnull149 private fun updateDispatcherFields() {
150 resolvedDispatcher.nestedScrollNode = this
151 // reset lastKnownParentNodes
152 resolvedDispatcher.lastKnownParentNode = null
153 lastKnownParentNode = null
154 resolvedDispatcher.calculateNestedScrollScope = { nestedCoroutineScope }
155 resolvedDispatcher.scope = coroutineScope
156 }
157
resetDispatcherFieldsnull158 private fun resetDispatcherFields() {
159 // only null this out if the modifier local node is what we set it to, since it is possible
160 // it has already been reused in a different node
161 if (resolvedDispatcher.nestedScrollNode === this) resolvedDispatcher.nestedScrollNode = null
162 }
163
updateNodenull164 internal fun updateNode(
165 connection: NestedScrollConnection,
166 dispatcher: NestedScrollDispatcher?
167 ) {
168 this.connection = connection
169 updateDispatcher(dispatcher)
170 }
171 }
172
findNearestAttachedAncestornull173 private fun <T : TraversableNode> T.findNearestAttachedAncestor(): T? {
174 var node: T? = null
175 traverseAncestors {
176 if (it.node.isAttached) {
177 node = it
178 false
179 } else {
180 true
181 }
182 }
183 return node
184 }
185