1 /*
2  * Copyright 2021 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 @file:JvmMultifileClass
18 @file:JvmName("BringIntoViewRequesterKt")
19 
20 package androidx.compose.foundation.relocation
21 
22 import androidx.compose.runtime.annotation.RememberInComposition
23 import androidx.compose.runtime.collection.mutableVectorOf
24 import androidx.compose.ui.Modifier
25 import androidx.compose.ui.geometry.Rect
26 import androidx.compose.ui.node.ModifierNodeElement
27 import androidx.compose.ui.platform.InspectorInfo
28 import androidx.compose.ui.relocation.BringIntoViewModifierNode
29 import androidx.compose.ui.relocation.bringIntoView
30 import kotlin.js.JsName
31 import kotlin.jvm.JvmMultifileClass
32 import kotlin.jvm.JvmName
33 
34 /**
35  * Can be used to send [bringIntoView] requests. Pass it as a parameter to
36  * [Modifier.bringIntoViewRequester()][bringIntoViewRequester].
37  *
38  * For instance, you can call [bringIntoView()][bringIntoView] to make all the scrollable parents
39  * scroll so that the specified item is brought into the scroll viewport.
40  *
41  * @sample androidx.compose.foundation.samples.BringIntoViewSample
42  * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
43  */
44 sealed interface BringIntoViewRequester {
45     /**
46      * Bring this item into bounds by making all the [BringIntoViewModifierNode] parents to bring
47      * their content appropriately.
48      *
49      * This method will not return until this request is satisfied or a newer request interrupts it.
50      * If this call is interrupted by a newer call, this method will throw a
51      * [CancellationException][kotlinx.coroutines.CancellationException].
52      *
53      * @param rect The rectangle (In local coordinates) that should be brought into view. If you
54      *   don't specify the coordinates, the coordinates of the
55      *   [Modifier.bringIntoViewRequester()][bringIntoViewRequester] associated with this
56      *   [BringIntoViewRequester] will be used.
57      * @sample androidx.compose.foundation.samples.BringIntoViewSample
58      * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
59      */
bringIntoViewnull60     suspend fun bringIntoView(rect: Rect? = null)
61 }
62 
63 /**
64  * Create an instance of [BringIntoViewRequester] that can be used with
65  * [Modifier.bringIntoViewRequester][bringIntoViewRequester]. A child can then call
66  * [BringIntoViewRequester.bringIntoView] to send a request to any [BringIntoViewModifierNode]
67  * parent so that they adjust its content to bring this item into view.
68  *
69  * Here is a sample where a composable is brought into view:
70  *
71  * @sample androidx.compose.foundation.samples.BringIntoViewSample
72  *
73  * Here is a sample where a part of a composable is brought into view:
74  *
75  * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
76  */
77 @JsName("funBringIntoViewRequester")
78 @RememberInComposition
79 fun BringIntoViewRequester(): BringIntoViewRequester {
80     return BringIntoViewRequesterImpl()
81 }
82 
83 /**
84  * Modifier that can be used to send [bringIntoView][BringIntoViewRequester.bringIntoView] requests.
85  *
86  * The following example uses a `bringIntoViewRequester` to bring an item into the parent bounds.
87  * The example demonstrates how a composable can ask its parents to scroll so that the component
88  * using this modifier is brought into the bounds of all its parents.
89  *
90  * @sample androidx.compose.foundation.samples.BringIntoViewSample
91  * @param bringIntoViewRequester An instance of [BringIntoViewRequester]. This hoisted object can be
92  *   used to send [bringIntoView] requests to parents of the current composable.
93  */
94 @Suppress("ModifierInspectorInfo")
bringIntoViewRequesternull95 fun Modifier.bringIntoViewRequester(bringIntoViewRequester: BringIntoViewRequester): Modifier =
96     this.then(BringIntoViewRequesterElement(bringIntoViewRequester))
97 
98 private class BringIntoViewRequesterImpl : BringIntoViewRequester {
99     val nodes = mutableVectorOf<BringIntoViewRequesterNode>()
100 
101     override suspend fun bringIntoView(rect: Rect?) {
102         nodes.forEach { it.bringIntoView { rect } }
103     }
104 }
105 
106 private class BringIntoViewRequesterElement(private val requester: BringIntoViewRequester) :
107     ModifierNodeElement<BringIntoViewRequesterNode>() {
createnull108     override fun create(): BringIntoViewRequesterNode {
109         return BringIntoViewRequesterNode(requester)
110     }
111 
updatenull112     override fun update(node: BringIntoViewRequesterNode) {
113         node.updateRequester(requester)
114     }
115 
inspectablePropertiesnull116     override fun InspectorInfo.inspectableProperties() {
117         name = "bringIntoViewRequester"
118         properties["bringIntoViewRequester"] = requester
119     }
120 
equalsnull121     override fun equals(other: Any?): Boolean {
122         return (this === other) ||
123             (other is BringIntoViewRequesterElement) && (requester == other.requester)
124     }
125 
hashCodenull126     override fun hashCode(): Int {
127         return requester.hashCode()
128     }
129 }
130 
131 /**
132  * A node that manages the state of modifier implementations for [bringIntoViewRequester]. It
133  * provides access to the next [BringIntoViewModifierNode] via [bringIntoView].
134  */
135 internal class BringIntoViewRequesterNode(private var requester: BringIntoViewRequester) :
136     Modifier.Node() {
137     override val shouldAutoInvalidate: Boolean = false
138 
onAttachnull139     override fun onAttach() {
140         updateRequester(requester)
141     }
142 
updateRequesternull143     fun updateRequester(requester: BringIntoViewRequester) {
144         disposeRequester()
145         if (requester is BringIntoViewRequesterImpl) {
146             requester.nodes += this
147         }
148         this.requester = requester
149     }
150 
disposeRequesternull151     private fun disposeRequester() {
152         if (requester is BringIntoViewRequesterImpl) {
153             (requester as BringIntoViewRequesterImpl).nodes -= this
154         }
155     }
156 
onDetachnull157     override fun onDetach() {
158         disposeRequester()
159     }
160 }
161