LinearLayoutManagerTest.java revision 668e774379c036a5d53d07ec69ed9ebee13a1fd9
1/*
2 * Copyright (C) 2014 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 android.support.v7.widget;
18
19import android.content.Context;
20import android.graphics.Rect;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.util.Log;
24import android.util.StringBuilderPrinter;
25import android.view.View;
26
27import java.lang.reflect.Field;
28import java.util.ArrayList;
29import java.util.LinkedHashMap;
30import java.util.List;
31import java.util.Map;
32import java.util.UUID;
33import java.util.concurrent.CountDownLatch;
34import java.util.concurrent.TimeUnit;
35
36/**
37 * Includes tests for {@link LinearLayoutManager}.
38 * <p>
39 * Since most UI tests are not practical, these tests are focused on internal data representation
40 * and stability of LinearLayoutManager in response to different events (state change, scrolling
41 * etc) where it is very hard to do manual testing.
42 */
43public class LinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
44
45    private static final boolean DEBUG = false;
46
47    private static final String TAG = "LinearLayoutManagerTest";
48
49    WrappedLinearLayoutManager mLayoutManager;
50
51    TestAdapter mTestAdapter;
52
53    final List<Config> mBaseVariations = new ArrayList<Config>();
54
55    @Override
56    protected void setUp() throws Exception {
57        super.setUp();
58        for (int orientation : new int[]{LinearLayoutManager.VERTICAL,
59                LinearLayoutManager.HORIZONTAL}) {
60            for (boolean reverseLayout : new boolean[]{false, true}) {
61                for (boolean stackFromBottom : new boolean[]{false, true}) {
62                    mBaseVariations.add(new Config(orientation, reverseLayout, stackFromBottom));
63                }
64            }
65        }
66    }
67
68    protected List<Config> addConfigVariation(List<Config> base, String fieldName,
69            Object... variations)
70            throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
71        List<Config> newConfigs = new ArrayList<Config>();
72        Field field = Config.class.getDeclaredField(fieldName);
73        for (Config config : base) {
74            for (Object variation : variations) {
75                Config newConfig = (Config) config.clone();
76                field.set(newConfig, variation);
77                newConfigs.add(newConfig);
78            }
79        }
80        return newConfigs;
81    }
82
83    void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable {
84        mRecyclerView = new RecyclerView(getActivity());
85        mRecyclerView.setHasFixedSize(true);
86        mTestAdapter = new TestAdapter(config.mItemCount);
87        mRecyclerView.setAdapter(mTestAdapter);
88        mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation,
89                config.mReverseLayout);
90        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
91        mRecyclerView.setLayoutManager(mLayoutManager);
92        if (waitForFirstLayout) {
93            waitForFirstLayout();
94        }
95    }
96
97    private void waitForFirstLayout() throws Throwable {
98        mLayoutManager.expectLayouts(1);
99        setRecyclerView(mRecyclerView);
100        mLayoutManager.waitForLayout(2);
101    }
102
103
104    public void testGetFirstLastChildrenTest() throws Throwable {
105        for (Config config : mBaseVariations) {
106            getFirstLastChildrenTest(config);
107        }
108    }
109
110    public void getFirstLastChildrenTest(final Config config) throws Throwable {
111        setupByConfig(config, true);
112        Runnable viewInBoundsTest = new Runnable() {
113            @Override
114            public void run() {
115                VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
116                final String boundsLog = mLayoutManager.getBoundsLog();
117                assertEquals(config + ":\nfirst visible child should match traversal result\n"
118                                + boundsLog, visibleChildren.firstVisiblePosition,
119                        mLayoutManager.findFirstVisibleItemPosition()
120                );
121                assertEquals(
122                        config + ":\nfirst fully visible child should match traversal result\n"
123                                + boundsLog, visibleChildren.firstFullyVisiblePosition,
124                        mLayoutManager.findFirstCompletelyVisibleItemPosition()
125                );
126
127                assertEquals(config + ":\nlast visible child should match traversal result\n"
128                                + boundsLog, visibleChildren.lastVisiblePosition,
129                        mLayoutManager.findLastVisibleItemPosition()
130                );
131                assertEquals(
132                        config + ":\nlast fully visible child should match traversal result\n"
133                                + boundsLog, visibleChildren.lastFullyVisiblePosition,
134                        mLayoutManager.findLastCompletelyVisibleItemPosition()
135                );
136            }
137        };
138        runTestOnUiThread(viewInBoundsTest);
139        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
140        // case
141        final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount();
142        runTestOnUiThread(new Runnable() {
143            @Override
144            public void run() {
145                mRecyclerView.smoothScrollToPosition(scrollPosition);
146            }
147        });
148        while (mLayoutManager.isSmoothScrolling() ||
149                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
150            runTestOnUiThread(viewInBoundsTest);
151            Thread.sleep(400);
152        }
153        // delete all items
154        mLayoutManager.expectLayouts(2);
155        mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount());
156        mLayoutManager.waitForLayout(2);
157        // test empty case
158        runTestOnUiThread(viewInBoundsTest);
159        // set a new adapter with huge items to test full bounds check
160        mLayoutManager.expectLayouts(1);
161        final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace();
162        final TestAdapter newAdapter = new TestAdapter(100) {
163            @Override
164            public void onBindViewHolder(TestViewHolder holder,
165                    int position) {
166                super.onBindViewHolder(holder, position);
167                if (config.mOrientation == LinearLayoutManager.HORIZONTAL) {
168                    holder.itemView.setMinimumWidth(totalSpace + 5);
169                } else {
170                    holder.itemView.setMinimumHeight(totalSpace + 5);
171                }
172            }
173        };
174        runTestOnUiThread(new Runnable() {
175            @Override
176            public void run() {
177                mRecyclerView.setAdapter(newAdapter);
178            }
179        });
180        mLayoutManager.waitForLayout(2);
181        runTestOnUiThread(viewInBoundsTest);
182    }
183
184    public void testSavedState() throws Throwable {
185        PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
186                new PostLayoutRunnable() {
187                    @Override
188                    public void run() throws Throwable {
189                        // do nothing
190                    }
191
192                    @Override
193                    public String describe() {
194                        return "doing nothing";
195                    }
196                },
197                new PostLayoutRunnable() {
198                    @Override
199                    public void run() throws Throwable {
200                        mLayoutManager.expectLayouts(1);
201                        scrollToPosition(mTestAdapter.getItemCount() * 3 / 4);
202                        mLayoutManager.waitForLayout(2);
203                    }
204
205                    @Override
206                    public String describe() {
207                        return "scroll to position";
208                    }
209                },
210                new PostLayoutRunnable() {
211                    @Override
212                    public void run() throws Throwable {
213                        mLayoutManager.expectLayouts(1);
214                        scrollToPositionWithOffset(mTestAdapter.getItemCount() * 1 / 3,
215                                50);
216                        mLayoutManager.waitForLayout(2);
217                    }
218
219                    @Override
220                    public String describe() {
221                        return "scroll to position with positive offset";
222                    }
223                },
224                new PostLayoutRunnable() {
225                    @Override
226                    public void run() throws Throwable {
227                        mLayoutManager.expectLayouts(1);
228                        scrollToPositionWithOffset(mTestAdapter.getItemCount() * 2 / 3,
229                                -50);
230                        mLayoutManager.waitForLayout(2);
231                    }
232
233                    @Override
234                    public String describe() {
235                        return "scroll to position with negative offset";
236                    }
237                }
238        };
239
240        PostRestoreRunnable[] postRestoreOptions = new PostRestoreRunnable[]{
241                new PostRestoreRunnable() {
242                    @Override
243                    public String describe() {
244                        return "Doing nothing";
245                    }
246                },
247                new PostRestoreRunnable() {
248                    @Override
249                    void onAfterRestore(Config config) throws Throwable {
250                        // update config as well so that restore assertions will work
251                        config.mOrientation = 1 - config.mOrientation;
252                        mLayoutManager.setOrientation(config.mOrientation);
253                    }
254
255                    @Override
256                    boolean shouldLayoutMatch(Config config) {
257                        return config.mItemCount == 0;
258                    }
259
260                    @Override
261                    public String describe() {
262                        return "Changing orientation";
263                    }
264                },
265                new PostRestoreRunnable() {
266                    @Override
267                    void onAfterRestore(Config config) throws Throwable {
268                        config.mStackFromEnd = !config.mStackFromEnd;
269                        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
270                    }
271
272                    @Override
273                    boolean shouldLayoutMatch(Config config) {
274                        return true; //stack from end should not move items on change
275                    }
276
277                    @Override
278                    public String describe() {
279                        return "Changing stack from end";
280                    }
281                },
282                new PostRestoreRunnable() {
283                    @Override
284                    void onAfterRestore(Config config) throws Throwable {
285                        config.mReverseLayout = !config.mReverseLayout;
286                        mLayoutManager.setReverseLayout(config.mReverseLayout);
287                    }
288
289                    @Override
290                    boolean shouldLayoutMatch(Config config) {
291                        return config.mItemCount == 0;
292                    }
293
294                    @Override
295                    public String describe() {
296                        return "Changing reverse layout";
297                    }
298                },
299                new PostRestoreRunnable() {
300                    int position;
301                    @Override
302                    void onAfterRestore(Config config) throws Throwable {
303                        position = mTestAdapter.getItemCount() / 2;
304                        mLayoutManager.scrollToPosition(position);
305                    }
306
307                    @Override
308                    boolean shouldLayoutMatch(Config config) {
309                        return mTestAdapter.getItemCount() == 0;
310                    }
311
312                    @Override
313                    String describe() {
314                        return "Scroll to position " + position ;
315                    }
316
317                    @Override
318                    void onAfterReLayout(Config config) {
319                        if (mTestAdapter.getItemCount() > 0) {
320                            assertEquals(config + ":scrolled view should be last completely visible",
321                                    position,
322                                    config.mStackFromEnd ?
323                                            mLayoutManager.findLastCompletelyVisibleItemPosition()
324                                        : mLayoutManager.findFirstCompletelyVisibleItemPosition());
325                        }
326                    }
327                }
328        };
329        boolean[] waitForLayoutOptions = new boolean[]{true, false};
330        for (Config config : addConfigVariation(mBaseVariations, "mItemCount", 0, 300)) {
331            for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) {
332                for (boolean waitForLayout : waitForLayoutOptions) {
333                    for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) {
334                        savedStateTest((Config) config.clone(), waitForLayout, postLayoutRunnable,
335                                postRestoreRunnable);
336                        removeRecyclerView();
337                    }
338
339                }
340            }
341        }
342    }
343
344    public void savedStateTest(Config config, boolean waitForLayout,
345            PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation)
346            throws Throwable {
347        if (DEBUG) {
348            Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " +
349                    config + " post layout action " + postLayoutOperation.describe() +
350                    "post restore action " + postRestoreOperation.describe());
351        }
352        setupByConfig(config, false);
353        if (waitForLayout) {
354            waitForFirstLayout();
355            postLayoutOperation.run();
356        }
357        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
358        Parcelable savedState = mRecyclerView.onSaveInstanceState();
359        // we append a suffix to the parcelable to test out of bounds
360        String parcelSuffix = UUID.randomUUID().toString();
361        Parcel parcel = Parcel.obtain();
362        savedState.writeToParcel(parcel, 0);
363        parcel.writeString(parcelSuffix);
364        removeRecyclerView();
365        // reset for reading
366        parcel.setDataPosition(0);
367        // re-create
368        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
369        removeRecyclerView();
370
371        RecyclerView restored = new RecyclerView(getActivity());
372        // this config should be no op.
373        mLayoutManager = new WrappedLinearLayoutManager(getActivity(),
374                1 - config.mOrientation, !config.mReverseLayout);
375        mLayoutManager.setStackFromEnd(!config.mStackFromEnd);
376        restored.setLayoutManager(mLayoutManager);
377        // use the same adapter for Rect matching
378        restored.setAdapter(mTestAdapter);
379        restored.onRestoreInstanceState(savedState);
380        postRestoreOperation.onAfterRestore(config);
381        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
382                parcel.readString());
383        mLayoutManager.expectLayouts(1);
384        setRecyclerView(restored);
385        mLayoutManager.waitForLayout(2);
386        // calculate prefix here instead of above to include post restore changes
387        final String logPrefix = config + "\npostLayout:" + postLayoutOperation.describe() +
388                "\npostRestore:" + postRestoreOperation.describe() + "\n";
389        assertEquals(logPrefix + " on saved state, reverse layout should be preserved",
390                config.mReverseLayout, mLayoutManager.getReverseLayout());
391        assertEquals(logPrefix + " on saved state, orientation should be preserved",
392                config.mOrientation, mLayoutManager.getOrientation());
393        assertEquals(logPrefix + " on saved state, stack from end should be preserved",
394                config.mStackFromEnd, mLayoutManager.getStackFromEnd());
395        if (waitForLayout) {
396            if (postRestoreOperation.shouldLayoutMatch(config)) {
397                assertRectSetsEqual(
398                        logPrefix + ": on restore, previous view positions should be preserved",
399                        before, mLayoutManager.collectChildCoordinates());
400            } else {
401                assertRectSetsNotEqual(
402                        logPrefix
403                                + ": on restore with changes, previous view positions should NOT be preserved",
404                        before, mLayoutManager.collectChildCoordinates());
405            }
406            postRestoreOperation.onAfterReLayout(config);
407        }
408    }
409
410    void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
411        runTestOnUiThread(new Runnable() {
412            @Override
413            public void run() {
414                mLayoutManager.scrollToPositionWithOffset(position, offset);
415            }
416        });
417    }
418
419    public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
420            Map<Item, Rect> after) {
421        Throwable throwable = null;
422        try {
423            assertRectSetsEqual("NOT " + message, before, after);
424        } catch (Throwable t) {
425            throwable = t;
426        }
427        assertNotNull(message + "\ntwo layout should be different", throwable);
428    }
429
430    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
431        StringBuilder sb = new StringBuilder();
432        sb.append("checking rectangle equality.");
433         sb.append("before:\n");
434        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
435            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
436        }
437        sb.append("after:\n");
438        for (Map.Entry<Item, Rect> entry : after.entrySet()) {
439            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
440        }
441        message = message + "\n" + sb.toString();
442        assertEquals(message + ":\nitem counts should be equal", before.size()
443                , after.size());
444        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
445            Rect afterRect = after.get(entry.getKey());
446            assertNotNull(message + ":\nSame item should be visible after simple re-layout",
447                    afterRect);
448            assertEquals(message + ":\nItem should be laid out at the same coordinates",
449                    entry.getValue(), afterRect);
450        }
451    }
452
453    static class VisibleChildren {
454
455        int firstVisiblePosition = RecyclerView.NO_POSITION;
456
457        int firstFullyVisiblePosition = RecyclerView.NO_POSITION;
458
459        int lastVisiblePosition = RecyclerView.NO_POSITION;
460
461        int lastFullyVisiblePosition = RecyclerView.NO_POSITION;
462
463        @Override
464        public String toString() {
465            return "VisibleChildren{" +
466                    "firstVisiblePosition=" + firstVisiblePosition +
467                    ", firstFullyVisiblePosition=" + firstFullyVisiblePosition +
468                    ", lastVisiblePosition=" + lastVisiblePosition +
469                    ", lastFullyVisiblePosition=" + lastFullyVisiblePosition +
470                    '}';
471        }
472    }
473
474    abstract private class PostLayoutRunnable {
475
476        abstract void run() throws Throwable;
477
478        abstract String describe();
479    }
480
481    abstract private class PostRestoreRunnable {
482
483        void onAfterRestore(Config config) throws Throwable {
484        }
485
486        abstract String describe();
487
488        boolean shouldLayoutMatch(Config config) {
489            return true;
490        }
491
492        void onAfterReLayout(Config config) {
493
494        };
495    }
496
497    class WrappedLinearLayoutManager extends LinearLayoutManager {
498
499        CountDownLatch layoutLatch;
500
501        OrientationHelper mSecondaryOrientation;
502
503        public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
504            super(context, orientation, reverseLayout);
505        }
506
507        public void expectLayouts(int count) {
508            layoutLatch = new CountDownLatch(count);
509        }
510
511        public void waitForLayout(long timeout) throws InterruptedException {
512            waitForLayout(timeout, TimeUnit.SECONDS);
513        }
514
515        @Override
516        public void setOrientation(int orientation) {
517            super.setOrientation(orientation);
518            mSecondaryOrientation = null;
519        }
520
521        @Override
522        void ensureLayoutState() {
523            super.ensureLayoutState();
524            if (mSecondaryOrientation == null) {
525                mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
526                        1 - getOrientation());
527            }
528        }
529
530        private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
531            layoutLatch.await(timeout, timeUnit);
532            assertEquals("all expected layouts should be executed at the expected time",
533                    0, layoutLatch.getCount());
534        }
535
536        public String getBoundsLog() {
537            StringBuilder sb = new StringBuilder();
538            sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding())
539                    .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding());
540            sb.append("\nchildren bounds\n");
541            final int childCount = getChildCount();
542            for (int i = 0; i < childCount; i++) {
543                View child = getChildAt(i);
544                sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
545                        .append("[").append("start:").append(
546                        mOrientationHelper.getDecoratedStart(child)).append(", end:")
547                        .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n");
548            }
549            return sb.toString();
550        }
551
552        public VisibleChildren traverseAndFindVisibleChildren() {
553            int childCount = getChildCount();
554            final VisibleChildren visibleChildren = new VisibleChildren();
555            final int start = mOrientationHelper.getStartAfterPadding();
556            final int end = mOrientationHelper.getEndAfterPadding();
557            for (int i = 0; i < childCount; i++) {
558                View child = getChildAt(i);
559                final int childStart = mOrientationHelper.getDecoratedStart(child);
560                final int childEnd = mOrientationHelper.getDecoratedEnd(child);
561                final boolean fullyVisible = childStart >= start && childEnd <= end;
562                final boolean hidden = childEnd <= start || childStart >= end;
563                if (hidden) {
564                    continue;
565                }
566                final int position = getPosition(child);
567                if (fullyVisible) {
568                    if (position < visibleChildren.firstFullyVisiblePosition ||
569                            visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) {
570                        visibleChildren.firstFullyVisiblePosition = position;
571                    }
572
573                    if (position > visibleChildren.lastFullyVisiblePosition) {
574                        visibleChildren.lastFullyVisiblePosition = position;
575                    }
576                }
577
578                if (position < visibleChildren.firstVisiblePosition ||
579                        visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) {
580                    visibleChildren.firstVisiblePosition = position;
581                }
582
583                if (position > visibleChildren.lastVisiblePosition) {
584                    visibleChildren.lastVisiblePosition = position;
585                }
586
587            }
588            return visibleChildren;
589        }
590
591        Rect getViewBounds(View view) {
592            if (getOrientation() == HORIZONTAL) {
593                return new Rect(
594                        mOrientationHelper.getDecoratedStart(view),
595                        mSecondaryOrientation.getDecoratedStart(view),
596                        mOrientationHelper.getDecoratedEnd(view),
597                        mSecondaryOrientation.getDecoratedEnd(view));
598            } else {
599                return new Rect(
600                        mSecondaryOrientation.getDecoratedStart(view),
601                        mOrientationHelper.getDecoratedStart(view),
602                        mSecondaryOrientation.getDecoratedEnd(view),
603                        mOrientationHelper.getDecoratedEnd(view));
604            }
605
606        }
607
608        Map<Item, Rect> collectChildCoordinates() throws Throwable {
609            final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
610            runTestOnUiThread(new Runnable() {
611                @Override
612                public void run() {
613                    final int childCount = getChildCount();
614                    for (int i = 0; i < childCount; i++) {
615                        View child = getChildAt(i);
616                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child
617                                .getLayoutParams();
618                        TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
619                        items.put(vh.mBindedItem, getViewBounds(child));
620                    }
621                }
622            });
623            return items;
624        }
625
626        @Override
627        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
628            super.onLayoutChildren(recycler, state);
629            layoutLatch.countDown();
630        }
631    }
632
633    static class Config implements Cloneable {
634
635        private static final int DEFAULT_ITEM_COUNT = 100;
636
637        private boolean mStackFromEnd;
638
639        int mOrientation = LinearLayoutManager.VERTICAL;
640
641        boolean mReverseLayout = false;
642
643        int mItemCount = DEFAULT_ITEM_COUNT;
644
645        Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
646            mOrientation = orientation;
647            mReverseLayout = reverseLayout;
648            mStackFromEnd = stackFromEnd;
649        }
650
651        public Config() {
652
653        }
654
655        Config orientation(int orientation) {
656            mOrientation = orientation;
657            return this;
658        }
659
660        Config stackFromBottom(boolean stackFromBottom) {
661            mStackFromEnd = stackFromBottom;
662            return this;
663        }
664
665        Config reverseLayout(boolean reverseLayout) {
666            mReverseLayout = reverseLayout;
667            return this;
668        }
669
670        public Config itemCount(int itemCount) {
671            mItemCount = itemCount;
672            return this;
673        }
674
675        // required by convention
676        @Override
677        public Object clone() throws CloneNotSupportedException {
678            return super.clone();
679        }
680
681        @Override
682        public String toString() {
683            return "Config{" +
684                    "mStackFromEnd=" + mStackFromEnd +
685                    ", mOrientation=" + mOrientation +
686                    ", mReverseLayout=" + mReverseLayout +
687                    ", mItemCount=" + mItemCount +
688                    '}';
689        }
690    }
691}
692