1 /*
2 * Copyright 2018 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.recyclerview.widget
18
19 import android.content.Context
20 import android.view.View
21 import android.view.ViewGroup
22 import android.widget.TextView
23 import androidx.test.ext.junit.runners.AndroidJUnit4
24 import androidx.test.filters.LargeTest
25 import java.util.concurrent.CountDownLatch
26 import java.util.concurrent.TimeUnit
27 import org.hamcrest.CoreMatchers.`is`
28 import org.hamcrest.MatcherAssert.assertThat
29 import org.junit.Assert
30 import org.junit.Ignore
31 import org.junit.Rule
32 import org.junit.Test
33 import org.junit.runner.RunWith
34
35 @LargeTest
36 @RunWith(AndroidJUnit4::class)
37 class RecyclerViewSmoothScrollToPositionTest {
38
39 @Suppress("DEPRECATION")
40 @get:Rule
41 val mActivityTestRule = androidx.test.rule.ActivityTestRule(TestContentViewActivity::class.java)
42
43 @Test
44 @Throws(Throwable::class)
smoothScrollToPosition_calledDuringScrollJustBeforeStop_scrollStateCallbacksCorrectnull45 fun smoothScrollToPosition_calledDuringScrollJustBeforeStop_scrollStateCallbacksCorrect() {
46
47 val recyclerView = setup(500 to 500, 500 to 200, 100)
48
49 val called2ndTime = -1
50
51 // Arrange
52
53 val target = 3
54 val log: MutableList<Int> = mutableListOf()
55 val latch = CountDownLatch(1)
56
57 recyclerView.addOnScrollListener(
58 object : RecyclerView.OnScrollListener() {
59 override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
60 log.add(newState)
61 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
62 latch.countDown()
63 }
64 }
65
66 override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
67 recyclerView.findChildWithTag(target)?.let {
68 if (it.bottom == 500) {
69 log.add(called2ndTime)
70 recyclerView.smoothScrollToPosition(target)
71 }
72 }
73 }
74 }
75 )
76
77 // Act
78 mActivityTestRule.runOnUiThread { recyclerView.smoothScrollToPosition(target) }
79 assertThat(latch.await(2, TimeUnit.SECONDS), `is`(true))
80
81 // Assert
82 assertThat(log.size, `is`(3))
83 assertThat(log[0], `is`(RecyclerView.SCROLL_STATE_SETTLING))
84 assertThat(log[1], `is`(called2ndTime))
85 assertThat(log[2], `is`(RecyclerView.SCROLL_STATE_IDLE))
86 }
87
88 @Ignore("b/291327689")
89 @Test
90 @Throws(Throwable::class)
smoothScroll_whenSmoothScrollerStops_destinationReachednull91 fun smoothScroll_whenSmoothScrollerStops_destinationReached() {
92
93 // Arrange
94
95 val calledOnStart = CountDownLatch(1)
96 val calledOnStop = CountDownLatch(1)
97
98 val layoutManager =
99 object : LinearLayoutManager(mActivityTestRule.activity) {
100
101 override fun smoothScrollToPosition(
102 recyclerView: RecyclerView,
103 state: RecyclerView.State,
104 position: Int
105 ) {
106 val linearSmoothScroller: LinearSmoothScroller =
107 object : LinearSmoothScroller(recyclerView.context) {
108 override fun onStart() {
109 super.onStart()
110 calledOnStart.countDown()
111 }
112
113 override fun onStop() {
114 super.onStop()
115 calledOnStop.countDown()
116 }
117 }
118 linearSmoothScroller.targetPosition = position
119 startSmoothScroll(linearSmoothScroller)
120 }
121 }
122
123 // We are going to traverse through 5 of 10 total screens worth of items to find the
124 // target view.
125 val itemHeight = 100
126 val itemsPerScreen = 5
127 val screensToTraverse = 5
128 val totalScreens = 10
129
130 val targetPosition = itemsPerScreen * screensToTraverse
131
132 val recyclerView =
133 setup(
134 500 to itemHeight * itemsPerScreen,
135 500 to itemHeight,
136 itemsPerScreen * totalScreens,
137 layoutManager = layoutManager
138 )
139
140 // Act
141
142 BaseRecyclerViewInstrumentationTest.mActivityRule.runOnUiThread(
143 Runnable { recyclerView.smoothScrollToPosition(targetPosition) }
144 )
145
146 // Assert
147
148 Assert.assertTrue(
149 "onStart should be called quickly ",
150 calledOnStart.await(2, TimeUnit.SECONDS)
151 )
152 Assert.assertTrue(
153 "onStop should be called eventually",
154 calledOnStop.await(30, TimeUnit.SECONDS)
155 )
156
157 // This needs to be run on the UI thread 1) due to inspecting the results of operations
158 // (such as layout) that may occur after the latch is counted down, and 2) in order to
159 // ensure that it doesn't run concurrently with operations on the UI thread that might
160 // affect the state.
161 mActivityTestRule.runOnUiThread {
162 Assert.assertNotNull(
163 "smoothScrollToPosition should succeed " +
164 "(first visible item: " +
165 layoutManager.findFirstVisibleItemPosition() +
166 ", last visible item: " +
167 layoutManager.findLastVisibleItemPosition() +
168 ")",
169 recyclerView.findViewHolderForLayoutPosition(targetPosition)
170 )
171 }
172 }
173
setupnull174 private fun setup(
175 rvDimensions: Pair<Int, Int>,
176 itemDimensions: Pair<Int, Int>,
177 numItems: Int,
178 context: Context = mActivityTestRule.activity,
179 layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(context)
180 ): RecyclerView {
181
182 val recyclerView = RecyclerView(context)
183
184 recyclerView.layoutParams = ViewGroup.LayoutParams(rvDimensions.first, rvDimensions.second)
185 recyclerView.setBackgroundColor(0x7FFF0000)
186 recyclerView.layoutManager = layoutManager
187 recyclerView.adapter = MyAdapter(itemDimensions, numItems)
188
189 val testContentView = mActivityTestRule.activity.contentView
190 testContentView.expectLayouts(1)
191 mActivityTestRule.runOnUiThread { testContentView.addView(recyclerView) }
192 testContentView.awaitLayouts(2)
193
194 return recyclerView
195 }
196 }
197
findChildWithTagnull198 private fun ViewGroup.findChildWithTag(tag: Int): View? {
199 for (i in 0 until this.childCount) {
200 this.getChildAt(i).also { if (it.tag == tag) return it }
201 }
202 return null
203 }
204
205 private class MyAdapter(val itemDimensions: Pair<Int, Int>, val numItems: Int) :
206 RecyclerView.Adapter<RecyclerView.ViewHolder>() {
207
onCreateViewHoldernull208 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
209 object :
210 RecyclerView.ViewHolder(
211 TextView(parent.context).apply {
212 minWidth = itemDimensions.first
213 minHeight = itemDimensions.second
214 }
215 ) {}
216
onBindViewHoldernull217 override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
218 (holder.itemView as TextView).apply {
219 text = position.toString()
220 tag = position
221 }
222 }
223
getItemCountnull224 override fun getItemCount() = numItems
225 }
226