blob: b0c8f7fe48be717f49d03b95b95de901ec5f1002 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static androidx.recyclerview.widget.RecyclerView.VERTICAL;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.test.R;
import androidx.recyclerview.test.RecyclerViewTestActivity;
import org.hamcrest.BaseMatcher;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Description;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* This class tests RecyclerView focus search failure handling by using a real LayoutManager.
*/
@LargeTest
@RunWith(Parameterized.class)
public class FocusSearchNavigationTest {
@Rule
public ActivityTestRule<RecyclerViewTestActivity> mActivityRule =
new ActivityTestRule<>(RecyclerViewTestActivity.class);
private final int mOrientation;
private final int mLayoutDir;
public FocusSearchNavigationTest(int orientation, int layoutDir) {
mOrientation = orientation;
mLayoutDir = layoutDir;
}
@Parameterized.Parameters(name = "orientation:{0},layoutDir:{1}")
public static List<Object[]> params() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return Arrays.asList(
new Object[]{VERTICAL, ViewCompat.LAYOUT_DIRECTION_LTR},
new Object[]{HORIZONTAL, ViewCompat.LAYOUT_DIRECTION_LTR},
new Object[]{HORIZONTAL, ViewCompat.LAYOUT_DIRECTION_RTL}
);
} else {
// Do not test RTL before API 17
return Arrays.asList(
new Object[]{VERTICAL, ViewCompat.LAYOUT_DIRECTION_LTR},
new Object[]{HORIZONTAL, ViewCompat.LAYOUT_DIRECTION_LTR}
);
}
}
private Activity mActivity;
private RecyclerView mRecyclerView;
private View mBefore;
private View mAfter;
private void setup(final int itemCount) throws Throwable {
mActivity = mActivityRule.getActivity();
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mActivity.setContentView(R.layout.focus_search_activity);
ViewCompat.setLayoutDirection(mActivity.getWindow().getDecorView(), mLayoutDir);
LinearLayout linearLayout = (LinearLayout) mActivity.findViewById(R.id.root);
linearLayout.setOrientation(mOrientation);
mRecyclerView = (RecyclerView) mActivity.findViewById(R.id.recycler_view);
ViewCompat.setLayoutDirection(mRecyclerView, mLayoutDir);
LinearLayoutManager layout = new LinearLayoutManager(mActivity.getBaseContext());
layout.setOrientation(mOrientation);
mRecyclerView.setLayoutManager(layout);
mRecyclerView.setAdapter(new FocusSearchAdapter(itemCount, mOrientation));
if (mOrientation == VERTICAL) {
mRecyclerView.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 250));
} else {
mRecyclerView.setLayoutParams(new LinearLayout.LayoutParams(
250, ViewGroup.LayoutParams.MATCH_PARENT));
}
mBefore = mActivity.findViewById(R.id.before);
mAfter = mActivity.findViewById(R.id.after);
}
});
waitForIdleSync();
assertThat("test sanity", mRecyclerView.getLayoutManager().getLayoutDirection(),
is(mLayoutDir));
assertThat("test sanity", ViewCompat.getLayoutDirection(mRecyclerView), is(mLayoutDir));
}
@Test
public void focusSearchForward() throws Throwable {
setup(20);
requestFocus(mBefore);
assertThat(mBefore, hasFocus());
View focused = mBefore;
for (int i = 0; i < 20; i++) {
focusSearchAndGive(focused, View.FOCUS_FORWARD);
RecyclerView.ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(i);
assertThat("vh at " + i, viewHolder, hasFocus());
focused = viewHolder.itemView;
}
focusSearchAndGive(focused, View.FOCUS_FORWARD);
assertThat(mAfter, hasFocus());
focusSearchAndGive(mAfter, View.FOCUS_FORWARD);
assertThat(mBefore, hasFocus());
focusSearchAndGive(mBefore, View.FOCUS_FORWARD);
focused = mActivity.getCurrentFocus();
//noinspection ConstantConditions
assertThat(focused.getParent(), CoreMatchers.<ViewParent>sameInstance(mRecyclerView));
}
@Test
public void focusSearchBackwards() throws Throwable {
setup(20);
requestFocus(mAfter);
assertThat(mAfter, hasFocus());
View focused = mAfter;
RecyclerView.ViewHolder lastViewHolder = null;
int i = 20;
while(lastViewHolder == null) {
lastViewHolder = mRecyclerView.findViewHolderForAdapterPosition(--i);
}
assertThat(lastViewHolder, notNullValue());
while(i >= 0) {
focusSearchAndGive(focused, View.FOCUS_BACKWARD);
RecyclerView.ViewHolder viewHolder = mRecyclerView.findViewHolderForAdapterPosition(i);
assertThat("vh at " + i, viewHolder, hasFocus());
focused = viewHolder.itemView;
i--;
}
focusSearchAndGive(focused, View.FOCUS_BACKWARD);
assertThat(mBefore, hasFocus());
focusSearchAndGive(mBefore, View.FOCUS_BACKWARD);
assertThat(mAfter, hasFocus());
}
private View focusSearchAndGive(final View view, final int focusDir) throws Throwable {
View next = focusSearch(view, focusDir);
if (next != null && next != view) {
requestFocus(next);
return next;
}
return null;
}
private View focusSearch(final View view, final int focusDir) throws Throwable {
final View[] result = new View[1];
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
result[0] = view.focusSearch(focusDir);
}
});
waitForIdleSync();
return result[0];
}
private void waitForIdleSync() throws Throwable {
waitForIdleScroll(mRecyclerView);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
private void requestFocus(final View view) throws Throwable {
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
view.requestFocus();
}
});
waitForIdleSync();
}
public void waitForIdleScroll(final RecyclerView recyclerView) throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
RecyclerView.OnScrollListener listener = new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == SCROLL_STATE_IDLE) {
latch.countDown();
recyclerView.removeOnScrollListener(this);
}
}
};
if (recyclerView.getScrollState() == SCROLL_STATE_IDLE) {
latch.countDown();
} else {
recyclerView.addOnScrollListener(listener);
}
}
});
assertTrue("should go idle in 10 seconds", latch.await(10, TimeUnit.SECONDS));
}
static class FocusSearchAdapter extends RecyclerView.Adapter {
private int mItemCount;
private int mOrientation;
public FocusSearchAdapter(int itemCount, int orientation) {
mItemCount = itemCount;
mOrientation = orientation;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view,
parent, false);
if (mOrientation == VERTICAL) {
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
50));
} else {
view.setLayoutParams(new ViewGroup.LayoutParams(50,
ViewGroup.LayoutParams.MATCH_PARENT));
}
return new RecyclerView.ViewHolder(view) {};
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
holder.itemView.setTag("pos " + position);
}
@Override
public int getItemCount() {
return mItemCount;
}
}
static HasFocusMatcher hasFocus() {
return new HasFocusMatcher();
}
static class HasFocusMatcher extends BaseMatcher<Object> {
@Override
public boolean matches(Object item) {
if (item instanceof RecyclerView.ViewHolder) {
item = ((RecyclerView.ViewHolder) item).itemView;
}
return item instanceof View && ((View) item).hasFocus();
}
@Override
public void describeTo(Description description) {
description.appendText("view has focus");
}
private String objectToLog(Object item) {
if (item instanceof RecyclerView.ViewHolder) {
RecyclerView.ViewHolder vh = (RecyclerView.ViewHolder) item;
return vh.toString();
}
if (item instanceof View) {
final Object tag = ((View) item).getTag();
return tag == null ? item.toString() : tag.toString();
}
final String classLog = item == null ? "null" : item.getClass().getSimpleName();
return classLog;
}
@Override
public void describeMismatch(Object item, Description description) {
String noun = objectToLog(item);
description.appendText(noun + " does not have focus");
Context context = null;
if (item instanceof RecyclerView.ViewHolder) {
context = ((RecyclerView.ViewHolder)item).itemView.getContext();
} else if (item instanceof View) {
context = ((View) item).getContext();
}
if (context instanceof Activity) {
View currentFocus = ((Activity) context).getWindow().getCurrentFocus();
description.appendText(". Current focus is in " + objectToLog(currentFocus));
}
}
}
}