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
17package androidx.recyclerview.widget;
18
19import static org.hamcrest.CoreMatchers.equalTo;
20import static org.hamcrest.CoreMatchers.is;
21import static org.junit.Assert.assertThat;
22
23import android.content.Context;
24import android.support.test.InstrumentationRegistry;
25import android.support.test.filters.LargeTest;
26import android.support.test.filters.SdkSuppress;
27import android.support.test.rule.ActivityTestRule;
28import android.support.test.runner.AndroidJUnit4;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.TextView;
32
33import org.junit.Rule;
34import org.junit.Test;
35import org.junit.runner.RunWith;
36
37@RunWith(AndroidJUnit4.class)
38@LargeTest
39public class RecyclerViewFocusTest {
40
41    private static final int RV_HEIGHT_WIDTH = 200;
42    private static final int ITEM_HEIGHT_WIDTH = 100;
43
44    private RecyclerView mRecyclerView;
45    private TestLinearLayoutManager mTestLinearLayoutManager;
46    private TestContentView mTestContentView;
47
48    @Rule
49    public ActivityTestRule<TestContentViewActivity> mActivityRule =
50            new ActivityTestRule<>(TestContentViewActivity.class);
51
52    @Test
53    public void focusSearch_layoutInterceptsAndReturnsNotNull_valueReturned() throws Throwable {
54        setupRecyclerView(true, RecyclerView.VERTICAL, true);
55        View expectedView = new View(mActivityRule.getActivity());
56        View currentlyFocusedView = mRecyclerView.getChildAt(0);
57        mTestLinearLayoutManager.mOnInterceptFocusSearchReturnValue = expectedView;
58
59        View actualView = mRecyclerView.focusSearch(currentlyFocusedView, View.FOCUS_FORWARD);
60
61        assertThat(actualView, is(equalTo(expectedView)));
62    }
63
64    @Test
65    public void focusSearch_noAdapter_onFocusSearchFailedNotCalled() throws Throwable {
66        setupRecyclerView(false, RecyclerView.VERTICAL, true);
67        View currentlyFocusedView = mRecyclerView.getChildAt(1);
68
69        mRecyclerView.focusSearch(currentlyFocusedView, View.FOCUS_FORWARD);
70
71        assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false));
72    }
73
74    @Test
75    public void focusSearch_layoutFrozen_onFocusSearchFailedNotCalled() throws Throwable {
76        setupRecyclerView(true, RecyclerView.VERTICAL, true);
77        mRecyclerView.setLayoutFrozen(true);
78        View currentlyFocusedView = mRecyclerView.getChildAt(1);
79
80        mRecyclerView.focusSearch(currentlyFocusedView, View.FOCUS_FORWARD);
81
82        assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false));
83    }
84
85    @Test
86    public void focusSearch_focusedViewNull_onFocusSearchFailedNotCalled() throws Throwable {
87        setupRecyclerView(true, RecyclerView.VERTICAL, true);
88        mRecyclerView.focusSearch(null, View.FOCUS_FORWARD);
89        assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false));
90    }
91
92    /*
93        Failures, null is returned
94        Tests to verify when onFocusSearchFailed is called.
95    */
96
97    @Test
98    public void focusSearch_verticalAndHasChildInDirection_findsCorrectChild() throws Throwable {
99        setupRecyclerView(true, RecyclerView.VERTICAL, true);
100        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_FORWARD, 0, 1);
101        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_BACKWARD, 1, 0);
102        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_DOWN, 0, 1);
103        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_UP, 1, 0);
104    }
105
106    @Test
107    public void focusSearch_horizontalAndHasChildInDirection_findsCorrectChild() throws Throwable {
108        setupRecyclerView(true, RecyclerView.HORIZONTAL, true);
109        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_FORWARD, 0, 1);
110        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_BACKWARD, 1, 0);
111        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_RIGHT, 0, 1);
112        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_LEFT, 1, 0);
113    }
114
115    @Test
116    @SdkSuppress(minSdkVersion = 17)
117    public void focusSearch_horizontalRtlAndHasChildInDirection_findsCorrectChild()
118            throws Throwable {
119        setupRecyclerView(true, RecyclerView.HORIZONTAL, false);
120        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_FORWARD, 0, 1);
121        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_BACKWARD, 1, 0);
122        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_RIGHT, 1, 0);
123        focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(View.FOCUS_LEFT, 0, 1);
124    }
125
126    @Test
127    public void focusSearch_verticalAndHasChildInDirection_doesNotCallOnFocusSearchFailed()
128            throws Throwable {
129        setupRecyclerView(true, RecyclerView.VERTICAL, true);
130        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
131                View.FOCUS_FORWARD, 0);
132        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
133                View.FOCUS_BACKWARD, 1);
134        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
135                View.FOCUS_DOWN, 0);
136        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
137                View.FOCUS_UP, 1);
138    }
139
140    @Test
141    public void focusSearch_horizontalAndHasChildInDirection_doesNotCallOnFocusSearchFailed()
142            throws Throwable {
143        setupRecyclerView(true, RecyclerView.HORIZONTAL, true);
144        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
145                View.FOCUS_FORWARD, 0);
146        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
147                View.FOCUS_BACKWARD, 1);
148        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
149                View.FOCUS_RIGHT, 0);
150        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
151                View.FOCUS_LEFT, 1);
152    }
153
154    @Test
155    @SdkSuppress(minSdkVersion = 17)
156    public void focusSearch_horizontalRtlAndHasChildInDirection_doesNotCallOnFocusSearchFailed()
157            throws Throwable {
158        setupRecyclerView(true, RecyclerView.HORIZONTAL, false);
159        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
160                View.FOCUS_FORWARD, 0);
161        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
162                View.FOCUS_BACKWARD, 1);
163        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
164                View.FOCUS_RIGHT, 1);
165        focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
166                View.FOCUS_LEFT, 0);
167    }
168
169    @Test
170    public void focusSearch_verticalAndDoesNotHaveChildInDirection_callsOnFocusSearchFailed()
171            throws Throwable {
172        setupRecyclerView(true, RecyclerView.VERTICAL, true);
173        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_FORWARD, 1);
174        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_BACKWARD, 0);
175        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_DOWN, 1);
176        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_UP, 0);
177    }
178
179    @Test
180    public void focusSearch_horizontalAndDoesNotHaveChildInDirection_callsOnFocusSearchFailed()
181            throws Throwable {
182        setupRecyclerView(true, RecyclerView.HORIZONTAL, true);
183        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_FORWARD, 1);
184        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_BACKWARD, 0);
185        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_RIGHT, 1);
186        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_LEFT, 0);
187    }
188
189    @Test
190    @SdkSuppress(minSdkVersion = 17)
191    public void focusSearch_horizontalRtlAndDoesNotHaveChildInDirection_callsOnFocusSearchFailed()
192            throws Throwable {
193        setupRecyclerView(true, RecyclerView.HORIZONTAL, false);
194        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_FORWARD, 1);
195        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_BACKWARD, 0);
196        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_RIGHT, 0);
197        focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(View.FOCUS_LEFT, 1);
198    }
199
200    private void focusSearch_simpleFindFocusSucceeds_returnsCorrectValue(int direction,
201            int startingChild, int expectedChild) {
202        View currentlyFocusedView = mRecyclerView.getChildAt(startingChild);
203        View expectedResult = mRecyclerView.getChildAt(expectedChild);
204
205        View actualResult = mRecyclerView.focusSearch(currentlyFocusedView, direction);
206
207        assertThat(actualResult, is(equalTo(expectedResult)));
208    }
209
210    private void focusSearch_simpleFindFocusSucceeds_doesNotCallOnFocusSearchFailedCalled(
211            int direction, int startingChild) {
212        mTestLinearLayoutManager.mOnFocusSearchFailedCalled = false;
213        View currentlyFocusedView = mRecyclerView.getChildAt(startingChild);
214
215        mRecyclerView.focusSearch(currentlyFocusedView, direction);
216
217        assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(false));
218    }
219
220    private void focusSearch_simpleFindFocusFails_callsOnFocusSearchFailed(int direction,
221            int startingChild) {
222        mTestLinearLayoutManager.mOnFocusSearchFailedCalled = false;
223        View currentlyFocusedView = mRecyclerView.getChildAt(startingChild);
224        mRecyclerView.focusSearch(currentlyFocusedView, direction);
225        assertThat(mTestLinearLayoutManager.mOnFocusSearchFailedCalled, is(true));
226    }
227
228    private void setupRecyclerView(final boolean hasAdapter, int orientation, boolean ltr)
229            throws Throwable {
230        final TestContentViewActivity testContentViewActivity = mActivityRule.getActivity();
231        mTestContentView = testContentViewActivity.getContentView();
232
233        mTestLinearLayoutManager = new TestLinearLayoutManager(testContentViewActivity,
234                orientation, false);
235
236        mRecyclerView = new RecyclerView(InstrumentationRegistry.getContext());
237        if (!ltr) {
238            mRecyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
239        }
240        mRecyclerView.setBackgroundColor(0xFFFF0000);
241        mRecyclerView.setLayoutParams(
242                new TestContentView.LayoutParams(RV_HEIGHT_WIDTH, RV_HEIGHT_WIDTH));
243        mRecyclerView.setLayoutManager(mTestLinearLayoutManager);
244
245        if (hasAdapter) {
246            mRecyclerView.setAdapter(
247                    new TestAdapter(100, ITEM_HEIGHT_WIDTH, ITEM_HEIGHT_WIDTH));
248        }
249
250        mTestContentView.expectLayouts(1);
251        mActivityRule.runOnUiThread(new Runnable() {
252            @Override
253            public void run() {
254                mTestContentView.addView(mRecyclerView);
255            }
256        });
257        mTestContentView.awaitLayouts(2);
258    }
259
260    private class TestLinearLayoutManager extends LinearLayoutManager {
261
262        boolean mOnFocusSearchFailedCalled = false;
263        View mOnInterceptFocusSearchReturnValue;
264        View mViewToReturnFromonFocusSearchFailed;
265
266        TestLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
267            super(context, orientation, reverseLayout);
268        }
269
270        @Override
271        public View onInterceptFocusSearch(View focused, int direction) {
272            return mOnInterceptFocusSearchReturnValue;
273        }
274
275        @Override
276        public View onFocusSearchFailed(View focused, int focusDirection,
277                RecyclerView.Recycler recycler, RecyclerView.State state) {
278            mOnFocusSearchFailedCalled = true;
279            return mViewToReturnFromonFocusSearchFailed;
280        }
281    }
282
283    private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
284
285        private int mItemCount;
286        private int mItemLayoutWidth;
287        private int mItemLayoutHeight;
288
289        TestAdapter(int itemCount, int itemLayoutWidth, int itemLayoutHeight) {
290            mItemCount = itemCount;
291            mItemLayoutWidth = itemLayoutWidth;
292            mItemLayoutHeight = itemLayoutHeight;
293        }
294
295        @Override
296        public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
297            TextView textView = new TextView(parent.getContext());
298            textView.setLayoutParams(
299                    new ViewGroup.LayoutParams(mItemLayoutWidth, mItemLayoutHeight));
300            textView.setFocusableInTouchMode(true);
301            return new TestViewHolder(textView);
302        }
303
304        @Override
305        public void onBindViewHolder(TestViewHolder holder, int position) {
306            ((TextView) holder.itemView).setText("Position: " + position);
307        }
308
309        @Override
310        public int getItemCount() {
311            return mItemCount;
312        }
313    }
314
315    private class TestViewHolder extends RecyclerView.ViewHolder {
316
317        TestViewHolder(View itemView) {
318            super(itemView);
319        }
320    }
321}
322