blob: 7a28fa7b2192fb024f7fdde6ba670abdabeffaaa [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 org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import android.support.test.filters.MediumTest;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.test.R;
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.atomic.AtomicLong;
/**
* This class only tests the RV's focus recovery logic as focus moves between two views that
* represent the same item in the adapter. Keeping a focused view visible is up-to-the
* LayoutManager and all FW LayoutManagers already have tests for it.
*/
@MediumTest
@RunWith(Parameterized.class)
public class RecyclerViewFocusRecoveryTest extends BaseRecyclerViewInstrumentationTest {
TestLayoutManager mLayoutManager;
TestAdapter mAdapter;
int mChildCount = 10;
// Parameter indicating whether RV's children are simple views (false) or ViewGroups (true).
private final boolean mFocusOnChild;
// Parameter indicating whether RV recovers focus after layout is finished.
private final boolean mDisableRecovery;
// Parameter indicating whether animation is enabled for the ViewHolder items.
private final boolean mDisableAnimation;
@Parameterized.Parameters(name = "focusSubChild:{0},disableRecovery:{1},"
+ "disableAnimation:{2}")
public static List<Object[]> getParams() {
return Arrays.asList(
new Object[]{false, false, true},
new Object[]{true, false, true},
new Object[]{false, true, true},
new Object[]{true, true, true},
new Object[]{false, false, false},
new Object[]{true, false, false},
new Object[]{false, true, false},
new Object[]{true, true, false}
);
}
public RecyclerViewFocusRecoveryTest(boolean focusOnChild, boolean disableRecovery,
boolean disableAnimation) {
super(false);
mFocusOnChild = focusOnChild;
mDisableRecovery = disableRecovery;
mDisableAnimation = disableAnimation;
}
void setupBasic() throws Throwable {
setupBasic(false);
}
void setupBasic(boolean hasStableIds) throws Throwable {
TestAdapter adapter = new FocusTestAdapter(mChildCount);
adapter.setHasStableIds(hasStableIds);
setupBasic(adapter, null);
}
void setupBasic(TestLayoutManager layoutManager) throws Throwable {
setupBasic(null, layoutManager);
}
void setupBasic(TestAdapter adapter) throws Throwable {
setupBasic(adapter, null);
}
void setupBasic(@Nullable TestAdapter adapter, @Nullable TestLayoutManager layoutManager)
throws Throwable {
RecyclerView recyclerView = new RecyclerView(getActivity());
if (layoutManager == null) {
layoutManager = new FocusLayoutManager();
}
if (adapter == null) {
adapter = new FocusTestAdapter(mChildCount);
}
mLayoutManager = layoutManager;
mAdapter = adapter;
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery);
if (mDisableAnimation) {
recyclerView.setItemAnimator(null);
}
mLayoutManager.expectLayouts(1);
setRecyclerView(recyclerView);
mLayoutManager.waitForLayout(1);
}
@Test
public void testFocusRecoveryInChange() throws Throwable {
setupBasic();
mLayoutManager.setSupportsPredictive(true);
final RecyclerView.ViewHolder oldVh = focusVh(3);
mLayoutManager.expectLayouts(mDisableAnimation ? 1 : 2);
mAdapter.changeAndNotify(3, 1);
mLayoutManager.waitForLayout(2);
if (!mDisableAnimation) {
// waiting for RV's ItemAnimator to finish the animation of the removed item
waitForAnimations(2);
}
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3);
assertFocusTransition(oldVh, newVh, false);
}
});
}
@Test
public void testFocusRecoveryAfterRemovingFocusedChild() throws Throwable {
setupBasic(true);
FocusViewHolder fvh = cast(focusVh(4));
assertThat("test sanity", fvh, notNullValue());
assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
assertThat("RV should pass the focus down to its children",
mRecyclerView.isFocused(), is(false));
assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
is(true));
assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
is(true));
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
// removing focused child
mAdapter.mItems.remove(4);
mAdapter.notifyItemRemoved(4);
}
});
mLayoutManager.waitForLayout(1);
if (!mDisableAnimation) {
// waiting for RV's ItemAnimator to finish the animation of the removed item
waitForAnimations(2);
}
assertThat("RV should have " + (mChildCount - 1) + " instead of "
+ mRecyclerView.getChildCount() + " children",
mChildCount - 1, is(mRecyclerView.getChildCount()));
assertFocusAfterLayout(4, 0);
}
@Test
public void testFocusRecoveryAfterMovingFocusedChild() throws Throwable {
setupBasic(true);
FocusViewHolder fvh = cast(focusVh(3));
assertThat("test sanity", fvh, notNullValue());
assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
assertThat("RV should pass the focus down to its children",
mRecyclerView.isFocused(), is(false));
assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
is(true));
assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
is(true));
mLayoutManager.expectLayouts(1);
mAdapter.moveAndNotify(3, 1);
mLayoutManager.waitForLayout(1);
if (!mDisableAnimation) {
// waiting for RV's ItemAnimator to finish the animation of the removed item
waitForAnimations(2);
}
assertFocusAfterLayout(1, 1);
}
@Test
public void testFocusRecoveryAfterRemovingLastChild() throws Throwable {
mChildCount = 1;
setupBasic(true);
FocusViewHolder fvh = cast(focusVh(0));
assertThat("test sanity", fvh, notNullValue());
assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
assertThat("RV should pass the focus down to its children",
mRecyclerView.isFocused(), is(false));
assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
is(true));
assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
is(true));
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
// removing focused child
mAdapter.mItems.remove(0);
mAdapter.notifyDataSetChanged();
}
});
mLayoutManager.waitForLayout(1);
if (!mDisableAnimation) {
// waiting for RV's ItemAnimator to finish the animation of the removed item
waitForAnimations(2);
}
assertThat("RV should have " + (mChildCount - 1) + " instead of "
+ mRecyclerView.getChildCount() + " children",
mChildCount - 1, is(mRecyclerView.getChildCount()));
assertFocusAfterLayout(-1, -1);
}
@Test
public void testFocusRecoveryAfterAddingFirstChild() throws Throwable {
mChildCount = 0;
setupBasic(true);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
requestFocusOnRV();
}
});
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
// adding first child
mAdapter.mItems.add(0, new Item(0, TestAdapter.DEFAULT_ITEM_PREFIX));
mAdapter.notifyDataSetChanged();
}
});
mLayoutManager.waitForLayout(1);
if (!mDisableAnimation) {
// waiting for RV's ItemAnimator to finish the animation of the removed item
waitForAnimations(2);
}
assertFocusAfterLayout(0, -1);
}
@Test
public void testFocusRecoveryAfterChangingFocusableFlag() throws Throwable {
setupBasic(true);
FocusViewHolder fvh = cast(focusVh(6));
assertThat("test sanity", fvh, notNullValue());
assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true));
assertThat("RV should pass the focus down to its children",
mRecyclerView.isFocused(), is(false));
assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(),
is(true));
assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(),
is(true));
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
Item item = mAdapter.mItems.get(6);
item.setFocusable(false);
mAdapter.notifyItemChanged(6);
}
});
mLayoutManager.waitForLayout(1);
if (!mDisableAnimation) {
waitForAnimations(2);
}
FocusViewHolder newVh = cast(mRecyclerView.findViewHolderForAdapterPosition(6));
assertThat("VH should no longer be focusable", newVh.getViewToFocus().isFocusable(),
is(false));
assertFocusAfterLayout(7, 0);
}
@Test
public void testFocusRecoveryBeforeLayoutWithFocusBefore() throws Throwable {
testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
}
@Test
public void testFocusRecoveryBeforeLayoutWithFocusAfter() throws Throwable {
testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_AFTER_DESCENDANTS);
}
@Test
public void testFocusRecoveryBeforeLayoutWithFocusBlocked() throws Throwable {
testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
@Test
public void testFocusRecoveryDuringLayoutWithFocusBefore() throws Throwable {
testFocusRecoveryDuringLayout(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
}
@Test
public void testFocusRecoveryDuringLayoutWithFocusAfter() throws Throwable {
testFocusRecoveryDuringLayout(ViewGroup.FOCUS_AFTER_DESCENDANTS);
}
@Test
public void testFocusRecoveryDuringLayoutWithFocusBlocked() throws Throwable {
testFocusRecoveryDuringLayout(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
/**
* Tests whether the focus is correctly recovered when requestFocus on RV is called before
* laying out the children.
* @throws Throwable
*/
private void testFocusRecoveryBeforeLayout(int descendantFocusability) throws Throwable {
RecyclerView recyclerView = new RecyclerView(getActivity());
recyclerView.setDescendantFocusability(descendantFocusability);
mLayoutManager = new FocusLayoutManager();
mAdapter = new FocusTestAdapter(10);
recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery);
if (mDisableAnimation) {
recyclerView.setItemAnimator(null);
}
setRecyclerView(recyclerView);
assertThat("RV should always be focusable", mRecyclerView.isFocusable(), is(true));
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
requestFocusOnRV();
mRecyclerView.setAdapter(mAdapter);
}
});
mLayoutManager.waitForLayout(1);
assertFocusAfterLayout(0, -1);
}
/**
* Tests whether the focus is correctly recovered when requestFocus on RV is called during
* laying out the children.
* @throws Throwable
*/
private void testFocusRecoveryDuringLayout(int descendantFocusability) throws Throwable {
RecyclerView recyclerView = new RecyclerView(getActivity());
recyclerView.setDescendantFocusability(descendantFocusability);
mLayoutManager = new FocusLayoutManager() {
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
requestFocusOnRV();
}
};
mAdapter = new FocusTestAdapter(10);
recyclerView.setAdapter(mAdapter);
recyclerView.setLayoutManager(mLayoutManager);
if (mDisableAnimation) {
recyclerView.setItemAnimator(null);
}
recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery);
mLayoutManager.expectLayouts(1);
setRecyclerView(recyclerView);
mLayoutManager.waitForLayout(1);
assertFocusAfterLayout(0, -1);
}
private void requestFocusOnRV() {
assertThat("RV initially has no focus", mRecyclerView.hasFocus(), is(false));
assertThat("RV initially is not focused", mRecyclerView.isFocused(), is(false));
mRecyclerView.requestFocus();
String msg = !mRecyclerView.isComputingLayout() ? " before laying out the children"
: " during laying out the children";
assertThat("RV should have focus after calling requestFocus()" + msg,
mRecyclerView.hasFocus(), is(true));
assertThat("RV after calling requestFocus() should become focused" + msg,
mRecyclerView.isFocused(), is(true));
}
/**
* Asserts whether RV and one of its children have the correct focus flags after the layout is
* complete. This is normally called once the RV layout is complete after initiating
* notifyItemChanged.
* @param focusedChildIndexWhenRecoveryEnabled
* This index is relevant when mDisableRecovery is false. In that case, it refers to the index
* of the child that should have focus if the ancestors allow passing down the focus. -1
* indicates none of the children can receive focus even if the ancestors don't block focus, in
* which case RV holds and becomes focused.
* @param focusedChildIndexWhenRecoveryDisabled
* This index is relevant when mDisableRecovery is true. In that case, it refers to the index
* of the child that should have focus if the ancestors allow passing down the focus. -1
* indicates none of the children can receive focus even if the ancestors don't block focus, in
* which case RV holds and becomes focused.
*/
private void assertFocusAfterLayout(int focusedChildIndexWhenRecoveryEnabled,
int focusedChildIndexWhenRecoveryDisabled) {
if (mDisableAnimation && mDisableRecovery) {
// This case is not quite handled properly at the moment. For now, RV may become focused
// without re-delivering the focus down to the children. Skip the checks for now.
return;
}
if (mRecyclerView.getChildCount() == 0) {
assertThat("RV should have focus when it has no children",
mRecyclerView.hasFocus(), is(true));
assertThat("RV should be focused when it has no children",
mRecyclerView.isFocused(), is(true));
return;
}
assertThat("RV should still have focus after layout", mRecyclerView.hasFocus(), is(true));
if ((mDisableRecovery && focusedChildIndexWhenRecoveryDisabled == -1)
|| (!mDisableRecovery && focusedChildIndexWhenRecoveryEnabled == -1)
|| mRecyclerView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
|| mRecyclerView.getDescendantFocusability()
== ViewGroup.FOCUS_BEFORE_DESCENDANTS) {
FocusViewHolder fvh = cast(mRecyclerView.findViewHolderForAdapterPosition(0));
String msg1 = " when focus recovery is disabled";
String msg2 = " when descendant focusability is FOCUS_BLOCK_DESCENDANTS";
String msg3 = " when descendant focusability is FOCUS_BEFORE_DESCENDANTS";
assertThat("RV should not pass the focus down to its children"
+ (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability()
== ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)),
mRecyclerView.isFocused(), is(true));
assertThat("RV's first child should not have focus"
+ (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability()
== ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)),
fvh.itemView.hasFocus(), is(false));
assertThat("RV's first child should not be focused"
+ (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability()
== ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)),
fvh.getViewToFocus().isFocused(), is(false));
} else {
FocusViewHolder fvh = mDisableRecovery
? cast(mRecyclerView.findViewHolderForAdapterPosition(
focusedChildIndexWhenRecoveryDisabled)) :
(focusedChildIndexWhenRecoveryEnabled != -1
? cast(mRecyclerView.findViewHolderForAdapterPosition(
focusedChildIndexWhenRecoveryEnabled)) :
cast(mRecyclerView.findViewHolderForAdapterPosition(0)));
assertThat("test sanity", fvh, notNullValue());
assertThat("RV's first child should be focusable", fvh.getViewToFocus().isFocusable(),
is(true));
String msg = " when descendant focusability is FOCUS_AFTER_DESCENDANTS";
assertThat("RV should pass the focus down to its children after layout" + msg,
mRecyclerView.isFocused(), is(false));
assertThat("RV's child #" + focusedChildIndexWhenRecoveryEnabled + " should have focus"
+ " after layout" + msg,
fvh.itemView.hasFocus(), is(true));
if (mFocusOnChild) {
assertThat("Either the ViewGroup or the TextView within the first child of RV"
+ "should be focused after layout" + msg,
fvh.itemView.isFocused() || fvh.getViewToFocus().isFocused(), is(true));
} else {
assertThat("RV's first child should be focused after layout" + msg,
fvh.getViewToFocus().isFocused(), is(true));
}
}
}
private void assertFocusTransition(RecyclerView.ViewHolder oldVh,
RecyclerView.ViewHolder newVh, boolean typeChanged) {
if (mDisableRecovery) {
if (mDisableAnimation) {
return;
}
assertFocus(newVh, false);
return;
}
assertThat("test sanity", newVh, notNullValue());
if (!typeChanged && mDisableAnimation) {
assertThat(oldVh, sameInstance(newVh));
} else {
assertThat(oldVh, not(sameInstance(newVh)));
assertFocus(oldVh, false);
}
assertFocus(newVh, true);
}
@Test
public void testFocusRecoveryInTypeChangeWithPredictive() throws Throwable {
testFocusRecoveryInTypeChange(true);
}
@Test
public void testFocusRecoveryInTypeChangeWithoutPredictive() throws Throwable {
testFocusRecoveryInTypeChange(false);
}
private void testFocusRecoveryInTypeChange(boolean withAnimation) throws Throwable {
setupBasic();
if (!mDisableAnimation) {
((SimpleItemAnimator) (mRecyclerView.getItemAnimator()))
.setSupportsChangeAnimations(true);
}
mLayoutManager.setSupportsPredictive(withAnimation);
final RecyclerView.ViewHolder oldVh = focusVh(3);
mLayoutManager.expectLayouts(!mDisableAnimation && withAnimation ? 2 : 1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
Item item = mAdapter.mItems.get(3);
item.mType += 2;
mAdapter.notifyItemChanged(3);
}
});
mLayoutManager.waitForLayout(2);
RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3);
assertFocusTransition(oldVh, newVh, true);
assertThat("test sanity", oldVh.getItemViewType(), not(newVh.getItemViewType()));
}
@Test
public void testRecoverAdapterChangeViaStableIdOnDataSetChanged() throws Throwable {
recoverAdapterChangeViaStableId(false, false);
}
@Test
public void testRecoverAdapterChangeViaStableIdOnSwap() throws Throwable {
recoverAdapterChangeViaStableId(true, false);
}
@Test
public void testRecoverAdapterChangeViaStableIdOnDataSetChangedWithTypeChange()
throws Throwable {
recoverAdapterChangeViaStableId(false, true);
}
@Test
public void testRecoverAdapterChangeViaStableIdOnSwapWithTypeChange() throws Throwable {
recoverAdapterChangeViaStableId(true, true);
}
private void recoverAdapterChangeViaStableId(final boolean swap, final boolean changeType)
throws Throwable {
setupBasic(true);
RecyclerView.ViewHolder oldVh = focusVh(4);
long itemId = oldVh.getItemId();
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
Item item = mAdapter.mItems.get(4);
if (changeType) {
item.mType += 2;
}
if (swap) {
mAdapter = new FocusTestAdapter(8);
mAdapter.setHasStableIds(true);
mAdapter.mItems.add(2, item);
mRecyclerView.swapAdapter(mAdapter, false);
} else {
mAdapter.mItems.remove(0);
mAdapter.mItems.remove(0);
mAdapter.notifyDataSetChanged();
}
}
});
mLayoutManager.waitForLayout(1);
if (!mDisableAnimation) {
waitForAnimations(2);
}
RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForItemId(itemId);
if (changeType) {
assertFocusTransition(oldVh, newVh, true);
} else {
// in this case we should use the same VH because we have stable ids
assertThat(oldVh, sameInstance(newVh));
assertFocus(newVh, true);
}
}
@Test
public void testDoNotRecoverViaPositionOnSetAdapter() throws Throwable {
testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
mRecyclerView.setAdapter(new FocusTestAdapter(10));
}
});
}
@Test
public void testDoNotRecoverViaPositionOnSwapAdapterWithRecycle() throws Throwable {
testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
mRecyclerView.swapAdapter(new FocusTestAdapter(10), true);
}
});
}
@Test
public void testDoNotRecoverViaPositionOnSwapAdapterWithoutRecycle() throws Throwable {
testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() {
@Override
public void run(TestAdapter adapter) throws Throwable {
mRecyclerView.swapAdapter(new FocusTestAdapter(10), false);
}
});
}
public void testDoNotRecoverViaPositionOnNewDataSet(
final RecyclerViewLayoutTest.AdapterRunnable runnable) throws Throwable {
setupBasic(false);
assertThat("test sanity", mAdapter.hasStableIds(), is(false));
focusVh(4);
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
runnable.run(mAdapter);
} catch (Throwable throwable) {
postExceptionToInstrumentation(throwable);
}
}
});
mLayoutManager.waitForLayout(1);
RecyclerView.ViewHolder otherVh = mRecyclerView.findViewHolderForAdapterPosition(4);
checkForMainThreadException();
// even if the VH is re-used, it will be removed-reAdded so focus will go away from it.
assertFocus("should not recover focus if data set is badly invalid", otherVh, false);
}
@Test
public void testDoNotRecoverIfReplacementIsNotFocusable() throws Throwable {
final int TYPE_NO_FOCUS = 1001;
TestAdapter adapter = new FocusTestAdapter(10) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (holder.getItemViewType() == TYPE_NO_FOCUS) {
cast(holder).setFocusable(false);
}
}
};
adapter.setHasStableIds(true);
setupBasic(adapter);
RecyclerView.ViewHolder oldVh = focusVh(3);
final long itemId = oldVh.getItemId();
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.mItems.get(3).mType = TYPE_NO_FOCUS;
mAdapter.notifyDataSetChanged();
}
});
mLayoutManager.waitForLayout(2);
if (!mDisableAnimation) {
waitForAnimations(2);
}
RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForItemId(itemId);
assertFocus(newVh, false);
}
@NonNull
private RecyclerView.ViewHolder focusVh(int pos) throws Throwable {
final RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForAdapterPosition(pos);
assertThat("test sanity", oldVh, notNullValue());
requestFocus(oldVh);
assertFocus("test sanity", oldVh, true);
getInstrumentation().waitForIdleSync();
return oldVh;
}
@Test
public void testDoNotOverrideAdapterRequestedFocus() throws Throwable {
final AtomicLong toFocusId = new AtomicLong(-1);
FocusTestAdapter adapter = new FocusTestAdapter(10) {
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder,
int position) {
super.onBindViewHolder(holder, position);
if (holder.getItemId() == toFocusId.get()) {
try {
requestFocus(holder);
} catch (Throwable throwable) {
postExceptionToInstrumentation(throwable);
}
}
}
};
adapter.setHasStableIds(true);
toFocusId.set(adapter.mItems.get(3).mId);
long firstFocusId = toFocusId.get();
setupBasic(adapter);
RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForItemId(toFocusId.get());
assertFocus(oldVh, true);
toFocusId.set(mAdapter.mItems.get(5).mId);
mLayoutManager.expectLayouts(1);
mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mAdapter.mItems.get(3).mType += 2;
mAdapter.mItems.get(5).mType += 2;
mAdapter.notifyDataSetChanged();
}
});
mLayoutManager.waitForLayout(2);
if (!mDisableAnimation) {
waitForAnimations(2);
}
RecyclerView.ViewHolder requested = mRecyclerView.findViewHolderForItemId(toFocusId.get());
assertFocus(oldVh, false);
assertFocus(requested, true);
RecyclerView.ViewHolder oldReplacement = mRecyclerView
.findViewHolderForItemId(firstFocusId);
assertFocus(oldReplacement, false);
checkForMainThreadException();
}
@Test
public void testDoNotOverrideLayoutManagerRequestedFocus() throws Throwable {
final AtomicLong toFocusId = new AtomicLong(-1);
FocusTestAdapter adapter = new FocusTestAdapter(10);
adapter.setHasStableIds(true);
FocusLayoutManager lm = new FocusLayoutManager() {
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
layoutRange(recycler, 0, state.getItemCount());
RecyclerView.ViewHolder toFocus = mRecyclerView
.findViewHolderForItemId(toFocusId.get());
if (toFocus != null) {
try {
requestFocus(toFocus);
} catch (Throwable throwable) {
postExceptionToInstrumentation(throwable);
}
}
layoutLatch.countDown();
}
};
toFocusId.set(adapter.mItems.get(3).mId);
long firstFocusId = toFocusId.get();
setupBasic(adapter, lm);
RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForItemId(toFocusId.get());
assertFocus(oldVh, true);
toFocusId.set(mAdapter.mItems.get(5).mId);
mLayoutManager.expectLayouts(1);
requestLayoutOnUIThread(mRecyclerView);
mLayoutManager.waitForLayout(2);
RecyclerView.ViewHolder requested = mRecyclerView.findViewHolderForItemId(toFocusId.get());
assertFocus(oldVh, false);
assertFocus(requested, true);
RecyclerView.ViewHolder oldReplacement = mRecyclerView
.findViewHolderForItemId(firstFocusId);
assertFocus(oldReplacement, false);
checkForMainThreadException();
}
private void requestFocus(RecyclerView.ViewHolder viewHolder) throws Throwable {
FocusViewHolder fvh = cast(viewHolder);
requestFocus(fvh.getViewToFocus(), false);
}
private void assertFocus(RecyclerView.ViewHolder viewHolder, boolean hasFocus) {
assertFocus("", viewHolder, hasFocus);
}
private void assertFocus(String msg, RecyclerView.ViewHolder vh, boolean hasFocus) {
FocusViewHolder fvh = cast(vh);
assertThat(msg, fvh.getViewToFocus().hasFocus(), is(hasFocus));
}
private <T extends FocusViewHolder> T cast(RecyclerView.ViewHolder vh) {
assertThat(vh, instanceOf(FocusViewHolder.class));
//noinspection unchecked
return (T) vh;
}
private class FocusTestAdapter extends TestAdapter {
public FocusTestAdapter(int count) {
super(count);
}
@Override
public FocusViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
final FocusViewHolder fvh;
if (mFocusOnChild) {
fvh = new FocusViewHolderWithChildren(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.focus_test_item_view, parent, false));
} else {
fvh = new SimpleFocusViewHolder(new TextView(parent.getContext()));
}
fvh.setFocusable(true);
return fvh;
}
@Override
public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
cast(holder).bindTo(mItems.get(position));
}
}
private class FocusLayoutManager extends TestLayoutManager {
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
layoutRange(recycler, 0, state.getItemCount());
layoutLatch.countDown();
}
}
private class FocusViewHolderWithChildren extends FocusViewHolder {
public final ViewGroup root;
public final ViewGroup parent1;
public final ViewGroup parent2;
public final TextView textView;
public FocusViewHolderWithChildren(View view) {
super(view);
root = (ViewGroup) view;
parent1 = (ViewGroup) root.findViewById(R.id.parent1);
parent2 = (ViewGroup) root.findViewById(R.id.parent2);
textView = (TextView) root.findViewById(R.id.text_view);
}
@Override
void setFocusable(boolean focusable) {
parent1.setFocusableInTouchMode(focusable);
parent2.setFocusableInTouchMode(focusable);
textView.setFocusableInTouchMode(focusable);
root.setFocusableInTouchMode(focusable);
parent1.setFocusable(focusable);
parent2.setFocusable(focusable);
textView.setFocusable(focusable);
root.setFocusable(focusable);
}
@Override
void onBind(Item item) {
textView.setText(getText(item));
}
@Override
View getViewToFocus() {
return textView;
}
}
private class SimpleFocusViewHolder extends FocusViewHolder {
public SimpleFocusViewHolder(View itemView) {
super(itemView);
}
@Override
void setFocusable(boolean focusable) {
itemView.setFocusableInTouchMode(focusable);
itemView.setFocusable(focusable);
}
@Override
View getViewToFocus() {
return itemView;
}
@Override
void onBind(Item item) {
((TextView) (itemView)).setText(getText(item));
}
}
private abstract class FocusViewHolder extends TestViewHolder {
public FocusViewHolder(View itemView) {
super(itemView);
}
protected String getText(Item item) {
return item.mText + "(" + item.mId + ")";
}
abstract void setFocusable(boolean focusable);
abstract View getViewToFocus();
abstract void onBind(Item item);
final void bindTo(Item item) {
mBoundItem = item;
setFocusable(item.isFocusable());
onBind(item);
}
}
}