| /* |
| * Copyright (C) 2015 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 android.support.v7.widget; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| import android.graphics.Rect; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.test.suitebuilder.annotation.LargeTest; |
| import android.util.Log; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.UUID; |
| |
| import static org.junit.Assert.assertEquals; |
| |
| @RunWith(Parameterized.class) |
| @LargeTest |
| public class StaggeredGridLayoutManagerSavedStateTest extends BaseStaggeredGridLayoutManagerTest { |
| private final Config mConfig; |
| private final boolean mWaitForLayout; |
| private final boolean mLoadDataAfterRestore; |
| private final PostLayoutRunnable mPostLayoutOperations; |
| |
| public StaggeredGridLayoutManagerSavedStateTest( |
| Config config, boolean waitForLayout, boolean loadDataAfterRestore, |
| PostLayoutRunnable postLayoutOperations) throws CloneNotSupportedException { |
| this.mConfig = (Config) config.clone(); |
| this.mWaitForLayout = waitForLayout; |
| this.mLoadDataAfterRestore = loadDataAfterRestore; |
| this.mPostLayoutOperations = postLayoutOperations; |
| if (postLayoutOperations != null) { |
| postLayoutOperations.mTest = this; |
| } |
| } |
| |
| @Parameterized.Parameters(name = "config={0} waitForLayout={1} loadDataAfterRestore={2}" |
| + " postLayoutRunnable={3}") |
| public static List<Object[]> getParams() throws CloneNotSupportedException { |
| List<Config> variations = createBaseVariations(); |
| |
| PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{ |
| new PostLayoutRunnable() { |
| @Override |
| public void run() throws Throwable { |
| // do nothing |
| } |
| |
| @Override |
| public String describe() { |
| return "doing nothing"; |
| } |
| }, |
| new PostLayoutRunnable() { |
| @Override |
| public void run() throws Throwable { |
| layoutManager().expectLayouts(1); |
| scrollToPosition(adapter().getItemCount() * 3 / 4); |
| layoutManager().waitForLayout(2); |
| } |
| |
| @Override |
| public String describe() { |
| return "scroll to position item count * 3 / 4"; |
| } |
| }, |
| new PostLayoutRunnable() { |
| @Override |
| public void run() throws Throwable { |
| layoutManager().expectLayouts(1); |
| scrollToPositionWithOffset(adapter().getItemCount() / 3, |
| 50); |
| layoutManager().waitForLayout(2); |
| } |
| |
| @Override |
| public String describe() { |
| return "scroll to position item count / 3 with positive offset"; |
| } |
| }, |
| new PostLayoutRunnable() { |
| @Override |
| public void run() throws Throwable { |
| layoutManager().expectLayouts(1); |
| scrollToPositionWithOffset(adapter().getItemCount() * 2 / 3, |
| -50); |
| layoutManager().waitForLayout(2); |
| } |
| |
| @Override |
| public String describe() { |
| return "scroll to position with negative offset"; |
| } |
| } |
| }; |
| boolean[] waitForLayoutOptions = new boolean[]{false, true}; |
| boolean[] loadDataAfterRestoreOptions = new boolean[]{false, true}; |
| List<Config> testVariations = new ArrayList<Config>(); |
| testVariations.addAll(variations); |
| for (Config config : variations) { |
| if (config.mSpanCount < 2) { |
| continue; |
| } |
| final Config clone = (Config) config.clone(); |
| clone.mItemCount = clone.mSpanCount - 1; |
| testVariations.add(clone); |
| } |
| |
| List<Object[]> params = new ArrayList<>(); |
| for (Config config : testVariations) { |
| for (PostLayoutRunnable runnable : postLayoutOptions) { |
| for (boolean waitForLayout : waitForLayoutOptions) { |
| for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) { |
| params.add(new Object[]{config, waitForLayout, loadDataAfterRestore, |
| runnable}); |
| } |
| } |
| } |
| } |
| return params; |
| } |
| |
| @Test |
| public void savedState() throws Throwable { |
| if (DEBUG) { |
| Log.d(TAG, "testing saved state with wait for layout = " + mWaitForLayout + " config " |
| + mConfig + " post layout action " + mPostLayoutOperations.describe()); |
| } |
| setupByConfig(mConfig); |
| if (mLoadDataAfterRestore) { |
| // We are going to re-create items, force non-random item size. |
| mAdapter.mOnBindCallback = new OnBindCallback() { |
| @Override |
| void onBoundItem(TestViewHolder vh, int position) { |
| } |
| |
| boolean assignRandomSize() { |
| return false; |
| } |
| }; |
| } |
| waitFirstLayout(); |
| if (mWaitForLayout) { |
| mPostLayoutOperations.run(); |
| } |
| getInstrumentation().waitForIdleSync(); |
| final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt(); |
| Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); |
| Parcelable savedState = mRecyclerView.onSaveInstanceState(); |
| // we append a suffix to the parcelable to test out of bounds |
| String parcelSuffix = UUID.randomUUID().toString(); |
| Parcel parcel = Parcel.obtain(); |
| savedState.writeToParcel(parcel, 0); |
| parcel.writeString(parcelSuffix); |
| removeRecyclerView(); |
| // reset for reading |
| parcel.setDataPosition(0); |
| // re-create |
| savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); |
| removeRecyclerView(); |
| |
| final int itemCount = mAdapter.getItemCount(); |
| List<Item> mItems = new ArrayList<>(); |
| if (mLoadDataAfterRestore) { |
| mItems.addAll(mAdapter.mItems); |
| mAdapter.deleteAndNotify(0, itemCount); |
| } |
| |
| RecyclerView restored = new RecyclerView(getActivity()); |
| mLayoutManager = new WrappedLayoutManager(mConfig.mSpanCount, mConfig.mOrientation); |
| mLayoutManager.setGapStrategy(mConfig.mGapStrategy); |
| restored.setLayoutManager(mLayoutManager); |
| // use the same adapter for Rect matching |
| restored.setAdapter(mAdapter); |
| restored.onRestoreInstanceState(savedState); |
| |
| if (mLoadDataAfterRestore) { |
| mAdapter.resetItemsTo(mItems); |
| } |
| |
| assertEquals("Parcel reading should not go out of bounds", parcelSuffix, |
| parcel.readString()); |
| mLayoutManager.expectLayouts(1); |
| setRecyclerView(restored); |
| mLayoutManager.waitForLayout(2); |
| assertEquals(mConfig + " on saved state, reverse layout should be preserved", |
| mConfig.mReverseLayout, mLayoutManager.getReverseLayout()); |
| assertEquals(mConfig + " on saved state, orientation should be preserved", |
| mConfig.mOrientation, mLayoutManager.getOrientation()); |
| assertEquals(mConfig + " on saved state, span count should be preserved", |
| mConfig.mSpanCount, mLayoutManager.getSpanCount()); |
| assertEquals(mConfig + " on saved state, gap strategy should be preserved", |
| mConfig.mGapStrategy, mLayoutManager.getGapStrategy()); |
| assertEquals(mConfig + " on saved state, first completely visible child position should" |
| + " be preserved", firstCompletelyVisiblePosition, |
| mLayoutManager.findFirstVisibleItemPositionInt()); |
| if (mWaitForLayout) { |
| final boolean strictItemEquality = !mLoadDataAfterRestore; |
| assertRectSetsEqual(mConfig + "\npost layout op:" + mPostLayoutOperations.describe() |
| + ": on restore, previous view positions should be preserved", |
| before, mLayoutManager.collectChildCoordinates(), strictItemEquality); |
| } |
| // TODO add tests for changing values after restore before layout |
| } |
| |
| static abstract class PostLayoutRunnable { |
| StaggeredGridLayoutManagerSavedStateTest mTest; |
| public void setup(StaggeredGridLayoutManagerSavedStateTest test) { |
| mTest = test; |
| } |
| |
| public GridTestAdapter adapter() { |
| return mTest.mAdapter; |
| } |
| |
| public WrappedLayoutManager layoutManager() { |
| return mTest.mLayoutManager; |
| } |
| |
| public void scrollToPositionWithOffset(int position, int offset) throws Throwable { |
| mTest.scrollToPositionWithOffset(position, offset); |
| } |
| |
| public void scrollToPosition(int position) throws Throwable { |
| mTest.scrollToPosition(position); |
| } |
| |
| abstract void run() throws Throwable; |
| |
| abstract String describe(); |
| |
| @Override |
| public String toString() { |
| return describe(); |
| } |
| } |
| } |