1 /*
<lambda>null2 * 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.viewinterop
18
19 import android.content.Context
20 import android.os.Build
21 import android.util.AttributeSet
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.view.ViewGroup
25 import android.widget.TextView
26 import androidx.annotation.LayoutRes
27 import androidx.annotation.RequiresApi
28 import androidx.compose.foundation.background
29 import androidx.compose.foundation.layout.Box
30 import androidx.compose.foundation.layout.fillMaxSize
31 import androidx.compose.foundation.layout.fillMaxWidth
32 import androidx.compose.foundation.layout.height
33 import androidx.compose.foundation.layout.padding
34 import androidx.compose.foundation.lazy.LazyColumn
35 import androidx.compose.material.Text
36 import androidx.compose.runtime.Composable
37 import androidx.compose.ui.Alignment
38 import androidx.compose.ui.Modifier
39 import androidx.compose.ui.geometry.Offset
40 import androidx.compose.ui.graphics.Color
41 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
42 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
43 import androidx.compose.ui.input.nestedscroll.nestedScroll
44 import androidx.compose.ui.platform.ComposeView
45 import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
46 import androidx.compose.ui.platform.testTag
47 import androidx.compose.ui.tests.R
48 import androidx.compose.ui.unit.Velocity
49 import androidx.compose.ui.unit.dp
50 import androidx.coordinatorlayout.widget.CoordinatorLayout
51 import androidx.core.view.ViewCompat
52 import androidx.lifecycle.Lifecycle
53 import androidx.recyclerview.widget.LinearLayoutManager
54 import androidx.recyclerview.widget.RecyclerView
55 import androidx.test.core.app.ActivityScenario
56
57 internal const val MainTestList = "mainList"
58 internal const val OuterBoxLayout = "outerBoxLayout"
59 internal const val AndroidViewContainer = "androidView"
60
61 internal class NestedScrollInteropAdapter :
62 RecyclerView.Adapter<NestedScrollInteropAdapter.SimpleTextViewHolder>() {
63 val items = (1..200).map { it.toString() }
64
65 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleTextViewHolder {
66 return SimpleTextViewHolder(
67 LayoutInflater.from(parent.context)
68 .inflate(R.layout.android_in_compose_nested_scroll_interop_list_item, parent, false)
69 )
70 }
71
72 override fun onBindViewHolder(holder: SimpleTextViewHolder, position: Int) {
73 holder.bind(items[position])
74 }
75
76 override fun getItemCount(): Int = items.size
77
78 class SimpleTextViewHolder(view: View) : RecyclerView.ViewHolder(view) {
79 fun bind(item: String) {
80 itemView.findViewById<TextView>(R.id.list_item).text = item
81 }
82 }
83 }
84
85 internal open class InspectableNestedScrollConnection() : NestedScrollConnection {
86 var offeredFromChild = Offset.Zero
87 var velocityOfferedFromChild = Velocity.Zero
88 var consumedDownChain = Offset.Zero
89 var velocityConsumedDownChain = Velocity.Zero
90 var notConsumedByChild = Offset.Zero
91 var velocityNotConsumedByChild = Velocity.Zero
92
onPreScrollnull93 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
94 offeredFromChild += available
95 return Offset.Zero
96 }
97
onPostScrollnull98 override fun onPostScroll(
99 consumed: Offset,
100 available: Offset,
101 source: NestedScrollSource
102 ): Offset {
103 consumedDownChain += consumed
104 notConsumedByChild += available
105 return Offset.Zero
106 }
107
onPreFlingnull108 override suspend fun onPreFling(available: Velocity): Velocity {
109 velocityOfferedFromChild += available
110 return Velocity.Zero
111 }
112
onPostFlingnull113 override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
114 velocityConsumedDownChain += consumed
115 velocityNotConsumedByChild += available
116 return Velocity.Zero
117 }
118
resetnull119 fun reset() {
120 offeredFromChild = Offset.Zero
121 velocityOfferedFromChild = Velocity.Zero
122 consumedDownChain = Offset.Zero
123 velocityConsumedDownChain = Velocity.Zero
124 notConsumedByChild = Offset.Zero
125 velocityNotConsumedByChild = Velocity.Zero
126 }
127 }
128
129 internal class TestNestedScrollParentView(context: Context, attrs: AttributeSet) :
130 CoordinatorLayout(context, attrs) {
131
132 private val unconsumed = IntArray(2)
133 val unconsumedOffset: Offset
134 get() = unconsumed.toReversedOffset()
135
136 private val offeredToParent = IntArray(2)
137 val offeredToParentOffset: Offset
138 get() = offeredToParent.toReversedOffset()
139
140 private val velocityOfferedToParent = FloatArray(2)
141 val velocityOfferedToParentOffset: Velocity
142 get() = velocityOfferedToParent.toReversedVelocity()
143
144 private val velocityUnconsumed = FloatArray(2)
145 val velocityUnconsumedOffset: Velocity
146 get() = velocityUnconsumed.toReversedVelocity()
147
onNestedPreScrollnull148 override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
149 super.onNestedPreScroll(target, dx, dy, consumed, type)
150 offeredToParent[0] += dx
151 offeredToParent[1] += dy
152 }
153
onStartNestedScrollnull154 override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
155 unconsumed.fill(0)
156 offeredToParent.fill(0)
157 return super.onStartNestedScroll(child, target, axes, type)
158 }
159
onNestedScrollnull160 override fun onNestedScroll(
161 target: View,
162 dxConsumed: Int,
163 dyConsumed: Int,
164 dxUnconsumed: Int,
165 dyUnconsumed: Int,
166 type: Int,
167 consumed: IntArray
168 ) {
169 super.onNestedScroll(
170 target,
171 dxConsumed,
172 dyConsumed,
173 dxUnconsumed,
174 dyUnconsumed,
175 type,
176 consumed
177 )
178 unconsumed[0] += dxConsumed
179 unconsumed[1] += dyConsumed
180 }
181
onNestedPreFlingnull182 override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
183 velocityOfferedToParent[0] += velocityX
184 velocityOfferedToParent[1] += velocityY
185 return super.onNestedPreFling(target, velocityX, velocityY)
186 }
187
onNestedFlingnull188 override fun onNestedFling(
189 target: View,
190 velocityX: Float,
191 velocityY: Float,
192 consumed: Boolean
193 ): Boolean {
194 velocityUnconsumed[0] += velocityX
195 velocityUnconsumed[0] += velocityY
196 return super.onNestedFling(target, velocityX, velocityY, consumed)
197 }
198 }
199
200 internal class AllConsumingInspectableConnection : InspectableNestedScrollConnection() {
onPreScrollnull201 override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
202 super.onPreScroll(available, source)
203 return available
204 }
205
onPostScrollnull206 override fun onPostScroll(
207 consumed: Offset,
208 available: Offset,
209 source: NestedScrollSource
210 ): Offset {
211 super.onPostScroll(consumed, available, source)
212 return available
213 }
214
onPreFlingnull215 override suspend fun onPreFling(available: Velocity): Velocity {
216 super.onPreFling(available)
217 return available
218 }
219
onPostFlingnull220 override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
221 super.onPostFling(consumed, available)
222 return available
223 }
224 }
225
226 internal class RecyclerViewConsumptionTracker {
227 private var consumedByRecyclerView = intArrayOf(0, 0)
228 private var velocityConsumedByRecyclerView = intArrayOf(0, 0)
229
230 val deltaConsumed
231 get() = consumedByRecyclerView.toOffset()
232
233 val velocityConsumed
234 get() = velocityConsumedByRecyclerView.toComposeVelocity()
235
trackDeltaConsumptionnull236 fun trackDeltaConsumption(dx: Int, dy: Int) {
237 consumedByRecyclerView[0] += dx
238 consumedByRecyclerView[1] += dy
239 }
240
trackVelocityConsumednull241 fun trackVelocityConsumed(velocityX: Int, velocityY: Int) {
242 velocityConsumedByRecyclerView[0] += velocityX
243 velocityConsumedByRecyclerView[1] += velocityY
244 }
245
resetnull246 fun reset() {
247 consumedByRecyclerView.fill(0)
248 velocityConsumedByRecyclerView.fill(0)
249 }
250 }
251
252 @Composable
NestedScrollInteropTestAppnull253 internal fun NestedScrollInteropTestApp(modifier: Modifier = Modifier, content: (Context) -> View) {
254 Box(modifier.fillMaxSize().testTag(OuterBoxLayout)) {
255 AndroidView(content, modifier = Modifier.testTag(AndroidViewContainer))
256 }
257 }
258
259 @Composable
NestedScrollDeepNestednull260 internal fun NestedScrollDeepNested(
261 modifier: Modifier,
262 enabled: Boolean,
263 connection: NestedScrollConnection? = null
264 ) {
265 // Box (Compose) + AndroidView (View) +
266 // Box (Compose)
267 val outerModifier = if (connection == null) Modifier else Modifier.nestedScroll(connection)
268 NestedScrollInteropTestApp(modifier) { context ->
269 LayoutInflater.from(context)
270 .inflate(R.layout.test_nested_scroll_coordinator_layout_without_toolbar, null)
271 .apply {
272 with(findViewById<ComposeView>(R.id.compose_view)) {
273 setContent {
274 Box(modifier = outerModifier) { ComposeInViewWithNestedScrollInterop() }
275 }
276 }
277 }
278 .also { ViewCompat.setNestedScrollingEnabled(it, enabled) }
279 }
280 }
281
282 @Composable
ComposeInViewWithNestedScrollInteropnull283 internal fun ComposeInViewWithNestedScrollInterop() {
284 LazyColumn(
285 modifier =
286 Modifier.nestedScroll(rememberNestedScrollInteropConnection()).testTag(MainTestList)
287 ) {
288 items(200) { item ->
289 Box(
290 modifier =
291 Modifier.padding(16.dp).height(56.dp).fillMaxWidth().background(Color.Gray),
292 contentAlignment = Alignment.Center
293 ) {
294 Text(item.toString())
295 }
296 }
297 }
298 }
299
300 @RequiresApi(Build.VERSION_CODES.M)
301 @Composable
NestedScrollInteropWithViewnull302 internal fun NestedScrollInteropWithView(
303 modifier: Modifier = Modifier,
304 enabled: Boolean,
305 recyclerViewConsumptionTracker: RecyclerViewConsumptionTracker
306 ) {
307 NestedScrollInteropTestApp(modifier) { context ->
308 LayoutInflater.from(context)
309 .inflate(R.layout.android_in_compose_nested_scroll_interop, null)
310 .apply {
311 with(findViewById<RecyclerView>(R.id.main_list)) {
312 layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
313 adapter = NestedScrollInteropAdapter()
314 setOnScrollChangeListener { _, _, _, oldX, oldY ->
315 recyclerViewConsumptionTracker.trackDeltaConsumption(oldX, oldY)
316 }
317 onFlingListener =
318 object : RecyclerView.OnFlingListener() {
319 override fun onFling(velocityX: Int, velocityY: Int): Boolean {
320 recyclerViewConsumptionTracker.trackVelocityConsumed(
321 velocityX,
322 velocityY
323 )
324 return false
325 }
326 }
327 }
328 }
329 .also { ViewCompat.setNestedScrollingEnabled(it, enabled) }
330 }
331 }
332
333 internal fun ActivityScenario<*>.createActivityWithComposeContent(
334 @LayoutRes layout: Int,
335 enableInterop: Boolean,
336 modifier: Modifier = Modifier,
337 content: @Composable () -> Unit
338 ) {
activitynull339 onActivity { activity ->
340 activity.setTheme(R.style.Theme_MaterialComponents_Light)
341 activity.setContentView(layout)
342 with(activity.findViewById<ComposeView>(R.id.compose_view)) {
343 setContent {
344 val nestedScrollInterop =
345 if (enableInterop)
346 modifier.nestedScroll(rememberNestedScrollInteropConnection())
347 else modifier
348 Box(nestedScrollInterop) { content() }
349 }
350 }
351 }
352 moveToState(Lifecycle.State.RESUMED)
353 }
354
355 @Composable
RecyclerViewAndroidViewnull356 internal fun RecyclerViewAndroidView(interopEnabled: Boolean) {
357 AndroidView(
358 factory = { context ->
359 LayoutInflater.from(context)
360 .inflate(R.layout.android_in_compose_nested_scroll_interop, null)
361 .apply {
362 with(findViewById<RecyclerView>(R.id.main_list)) {
363 layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
364 adapter = NestedScrollInteropAdapter()
365 }
366 }
367 .also { ViewCompat.setNestedScrollingEnabled(it, interopEnabled) }
368 },
369 modifier = Modifier.testTag(AndroidViewContainer)
370 )
371 }
372
IntArraynull373 private fun IntArray.toOffset() = Offset(this[0].toFloat(), this[1].toFloat())
374
375 private fun IntArray.toComposeVelocity() =
376 Velocity((this[0] * -1).toFloat(), (this[1] * -1).toFloat())
377
378 private fun IntArray.toReversedOffset(): Offset {
379 require(size == 2)
380 return Offset(this[0] * -1f, this[1] * -1f)
381 }
382
FloatArraynull383 private fun FloatArray.toReversedVelocity(): Velocity {
384 require(size == 2)
385 return Velocity(this[0] * -1f, this[1] * -1f)
386 }
387
absnull388 internal fun abs(velocity: Velocity) =
389 Velocity(kotlin.math.abs(velocity.x), kotlin.math.abs(velocity.y))
390