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