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 package androidx.recyclerview.widget
17
18 import android.content.Context
19 import android.util.AttributeSet
20 import android.view.MotionEvent
21 import android.view.MotionEvent.ACTION_CANCEL
22 import android.view.MotionEvent.ACTION_DOWN
23 import android.view.MotionEvent.ACTION_MOVE
24 import android.view.MotionEvent.ACTION_UP
25 import android.view.View
26 import android.view.ViewGroup
27 import android.widget.FrameLayout
28 import androidx.test.core.app.ApplicationProvider
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.LargeTest
31 import com.google.common.truth.Truth.assertThat
32 import org.junit.Before
33 import org.junit.Test
34 import org.junit.runner.RunWith
35
36 val ActionDown = MotionEventItem(0, ACTION_DOWN, 500f, 500f)
37 val ActionMove1 = MotionEventItem(100, ACTION_MOVE, 500f, 400f)
38 val ActionMove2 = MotionEventItem(200, ACTION_MOVE, 500f, 300f)
39 val ActionMove3 = MotionEventItem(300, ACTION_MOVE, 500f, 200f)
40 val ActionUp = MotionEventItem(400, ACTION_UP, 500f, 200f)
41
42 @RunWith(AndroidJUnit4::class)
43 @LargeTest
44 class RecyclerViewOnItemTouchListenerTest {
45 private lateinit var parent: MyFrameLayout
46 private lateinit var recyclerView: RecyclerView
47 private lateinit var childView: MyView
48 private lateinit var onItemTouchListener: MyOnItemTouchListener
49
50 @Before
setupnull51 fun setup() {
52 val context = ApplicationProvider.getApplicationContext<Context>()
53
54 childView = MyView(context)
55 childView.minimumWidth = 1000
56 childView.minimumHeight = 1000
57
58 recyclerView = RecyclerView(context)
59 recyclerView.layoutManager = LinearLayoutManager(context)
60 recyclerView.adapter = InternalTestAdapter(childView)
61
62 onItemTouchListener = MyOnItemTouchListener()
63 recyclerView.addOnItemTouchListener(onItemTouchListener)
64
65 parent = MyFrameLayout(context)
66 parent.addView(recyclerView)
67
68 val measureSpec = View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)
69 parent.measure(measureSpec, measureSpec)
70 parent.layout(0, 0, 1000, 1000)
71 }
72
73 @Test
listenerDoesntIntercept_rvChildDoesntClick_correctListenerCallsnull74 fun listenerDoesntIntercept_rvChildDoesntClick_correctListenerCalls() {
75 listenerDoesntIntercept_correctListenerCalls(false)
76 }
77
78 @Test
listenerDoesntIntercept_rvChildClicks_correctListenerCallsnull79 fun listenerDoesntIntercept_rvChildClicks_correctListenerCalls() {
80 listenerDoesntIntercept_correctListenerCalls(true)
81 }
82
listenerDoesntIntercept_correctListenerCallsnull83 private fun listenerDoesntIntercept_correctListenerCalls(childClickable: Boolean) {
84 childView.isClickable = childClickable
85
86 parent.dispatchMotionEventItem(ActionDown)
87 parent.dispatchMotionEventItem(ActionMove1)
88 parent.dispatchMotionEventItem(ActionUp)
89
90 assertThat(onItemTouchListener.motionEventItems)
91 .isEqualTo(listOf(ActionDown to true, ActionMove1 to true, ActionUp to true))
92 }
93
94 @Test
listenerInterceptsDown_rvChildClicks_correctListenerCallsnull95 fun listenerInterceptsDown_rvChildClicks_correctListenerCalls() {
96 listenerInterceptsDown_correctListenerCalls(false)
97 }
98
99 @Test
listenerInterceptsDown_rvChildDoesntClick_correctListenerCallsnull100 fun listenerInterceptsDown_rvChildDoesntClick_correctListenerCalls() {
101 listenerInterceptsDown_correctListenerCalls(true)
102 }
103
listenerInterceptsDown_correctListenerCallsnull104 private fun listenerInterceptsDown_correctListenerCalls(childClickable: Boolean) {
105 childView.isClickable = childClickable
106
107 onItemTouchListener.motionEventItemToStartIntecepting = ActionDown
108
109 parent.dispatchMotionEventItem(ActionDown)
110 parent.dispatchMotionEventItem(ActionMove1)
111 parent.dispatchMotionEventItem(ActionUp)
112
113 assertThat(onItemTouchListener.motionEventItems)
114 .isEqualTo(
115 listOf(
116 ActionDown to true,
117 ActionDown to false,
118 ActionMove1 to false,
119 ActionUp to false
120 )
121 )
122 }
123
124 @Test
listenerInterceptsMove_rvChildDoesntClick_correctListenerCallsnull125 fun listenerInterceptsMove_rvChildDoesntClick_correctListenerCalls() {
126 listenerInterceptsMove_correctListenerCalls(false)
127 }
128
129 @Test
listenerInterceptsMove_rvChildClicks_correctListenerCallsnull130 fun listenerInterceptsMove_rvChildClicks_correctListenerCalls() {
131 listenerInterceptsMove_correctListenerCalls(true)
132 }
133
134 @Test
listenerInterceptsMove_rvChildClicks_correctListenerCallsAndSendsCancelnull135 fun listenerInterceptsMove_rvChildClicks_correctListenerCallsAndSendsCancel() {
136 val actionCancelFromMove1 = MotionEventItem(100, ACTION_CANCEL, 500f, 400f)
137 val secondOnItemTouchListener = MyOnItemTouchListener()
138 recyclerView.addOnItemTouchListener(secondOnItemTouchListener)
139
140 listenerInterceptsMove_correctListenerCalls(true)
141
142 assertThat(secondOnItemTouchListener.motionEventItems)
143 .isEqualTo(listOf(ActionDown to true, actionCancelFromMove1 to true))
144 }
145
listenerInterceptsMove_correctListenerCallsnull146 private fun listenerInterceptsMove_correctListenerCalls(childClickable: Boolean) {
147 childView.isClickable = childClickable
148 onItemTouchListener.motionEventItemToStartIntecepting = ActionMove1
149
150 parent.dispatchMotionEventItem(ActionDown)
151 parent.dispatchMotionEventItem(ActionMove1)
152 parent.dispatchMotionEventItem(ActionMove2)
153 parent.dispatchMotionEventItem(ActionUp)
154
155 assertThat(onItemTouchListener.motionEventItems)
156 .isEqualTo(
157 listOf(
158 ActionDown to true,
159 ActionMove1 to true,
160 ActionMove2 to false,
161 ActionUp to false
162 )
163 )
164 }
165
166 @Test
listenerInterceptsDown_childOnTouchNotCallednull167 fun listenerInterceptsDown_childOnTouchNotCalled() {
168 childView.isClickable = true
169 onItemTouchListener.motionEventItemToStartIntecepting = ActionDown
170
171 parent.dispatchMotionEventItem(ActionDown)
172 parent.dispatchMotionEventItem(ActionUp)
173
174 assertThat(childView.motionEventItems).isEmpty()
175 }
176
177 @Test
listenerInterceptsMove_childOnTouchCalledWithCorrectEventsnull178 fun listenerInterceptsMove_childOnTouchCalledWithCorrectEvents() {
179 childView.isClickable = true
180 onItemTouchListener.motionEventItemToStartIntecepting = ActionMove1
181
182 parent.dispatchMotionEventItem(ActionDown)
183 parent.dispatchMotionEventItem(ActionMove1)
184
185 assertThat(childView.motionEventItems)
186 .isEqualTo(listOf(ActionDown, ActionMove1.toCancelledVersion()))
187 }
188
189 @Test
listenerInterceptsUp_childOnTouchCalledWithCorrectEventsnull190 fun listenerInterceptsUp_childOnTouchCalledWithCorrectEvents() {
191 childView.isClickable = true
192 onItemTouchListener.motionEventItemToStartIntecepting = ActionUp
193
194 parent.dispatchMotionEventItem(ActionDown)
195 parent.dispatchMotionEventItem(ActionUp)
196
197 assertThat(childView.motionEventItems)
198 .isEqualTo(listOf(ActionDown, ActionUp.toCancelledVersion()))
199 }
200
201 @Test
listenerInterceptsThenParentIntercepts_correctListenerCallsnull202 fun listenerInterceptsThenParentIntercepts_correctListenerCalls() {
203 onItemTouchListener.motionEventItemToStartIntecepting = ActionMove1
204 parent.motionEventItemToStartIntecepting = ActionMove3
205
206 parent.dispatchMotionEventItem(ActionDown)
207 parent.dispatchMotionEventItem(ActionMove1)
208 parent.dispatchMotionEventItem(ActionMove2)
209 parent.dispatchMotionEventItem(ActionMove3)
210 parent.dispatchMotionEventItem(ActionUp)
211
212 assertThat(onItemTouchListener.motionEventItems)
213 .isEqualTo(
214 listOf(
215 ActionDown to true,
216 ActionMove1 to true,
217 ActionMove2 to false,
218 ActionMove3.toCancelledVersion() to false
219 )
220 )
221 }
222 }
223
224 data class MotionEventItem(val eventTime: Long, val action: Int, val x: Float, val y: Float)
225
toMotionEventnull226 private fun MotionEventItem.toMotionEvent(): MotionEvent =
227 MotionEvent.obtain(0, eventTime, action, x, y, 0)
228
229 private fun MotionEventItem.toCancelledVersion() = copy(action = ACTION_CANCEL)
230
231 private fun MotionEvent.toMotionEventItem() = MotionEventItem(eventTime, actionMasked, x, y)
232
233 private fun View.dispatchMotionEventItem(motionEventItem: MotionEventItem) {
234 motionEventItem.toMotionEvent().also {
235 dispatchTouchEvent(it)
236 it.recycle()
237 }
238 }
239
240 private class InternalTestAdapter(var view: View) : RecyclerView.Adapter<MyViewHolder>() {
onCreateViewHoldernull241 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MyViewHolder(view)
242
243 override fun onBindViewHolder(holder: MyViewHolder, position: Int) {}
244
getItemCountnull245 override fun getItemCount() = 1
246 }
247
248 private class MyViewHolder internal constructor(itemView: View?) :
249 RecyclerView.ViewHolder(itemView!!)
250
251 private class MyOnItemTouchListener : RecyclerView.OnItemTouchListener {
252 var motionEventItemToStartIntecepting: MotionEventItem? = null
253 var motionEventItems = mutableListOf<Pair<MotionEventItem, Boolean>>()
254
255 override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
256 motionEventItems.add(e.toMotionEventItem() to true)
257 return e.toMotionEventItem() == motionEventItemToStartIntecepting
258 }
259
260 override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
261 motionEventItems.add(e.toMotionEventItem() to false)
262 }
263
264 override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
265 }
266
267 private class MyView : View {
268 var motionEventItems = mutableListOf<MotionEventItem>()
269
270 constructor(context: Context?) : super(context)
271
272 constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
273
274 constructor(
275 context: Context?,
276 attrs: AttributeSet?,
277 defStyleAttr: Int
278 ) : super(context, attrs, defStyleAttr)
279
onTouchEventnull280 override fun onTouchEvent(event: MotionEvent): Boolean {
281 motionEventItems.add(event.toMotionEventItem())
282 return super.onTouchEvent(event)
283 }
284 }
285
286 private class MyFrameLayout(context: Context) : FrameLayout(context) {
287
288 var motionEventItemToStartIntecepting: MotionEventItem? = null
289
onInterceptTouchEventnull290 override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
291 super.onInterceptTouchEvent(ev)
292 return ev!!.toMotionEventItem() == motionEventItemToStartIntecepting
293 }
294 }
295