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