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") bringIntoViewRequesternull95fun 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