Home | History | Annotate | Download | only in widget
      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 
     17 package androidx.recyclerview.widget;
     18 
     19 import static androidx.recyclerview.widget.LayoutState.LAYOUT_END;
     20 import static androidx.recyclerview.widget.LayoutState.LAYOUT_START;
     21 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
     22 
     23 import static org.hamcrest.CoreMatchers.hasItem;
     24 import static org.hamcrest.CoreMatchers.is;
     25 import static org.hamcrest.CoreMatchers.not;
     26 import static org.hamcrest.CoreMatchers.sameInstance;
     27 import static org.junit.Assert.assertEquals;
     28 import static org.junit.Assert.assertNotNull;
     29 import static org.junit.Assert.assertThat;
     30 
     31 import android.graphics.Rect;
     32 import android.support.test.filters.LargeTest;
     33 import android.util.Log;
     34 import android.view.View;
     35 import android.view.ViewParent;
     36 
     37 import androidx.annotation.NonNull;
     38 
     39 import org.junit.Test;
     40 import org.junit.runner.RunWith;
     41 import org.junit.runners.Parameterized;
     42 
     43 import java.util.ArrayList;
     44 import java.util.List;
     45 import java.util.Map;
     46 
     47 /**
     48  * Tests that rely on the basic configuration and does not do any additions / removals
     49  */
     50 @RunWith(Parameterized.class)
     51 @LargeTest
     52 public class LinearLayoutManagerBaseConfigSetTest extends BaseLinearLayoutManagerTest {
     53 
     54     private final Config mConfig;
     55 
     56     public LinearLayoutManagerBaseConfigSetTest(Config config) {
     57         mConfig = config;
     58     }
     59 
     60 
     61     @Parameterized.Parameters(name = "{0}")
     62     public static List<Config> configs() throws CloneNotSupportedException {
     63         List<Config> result = new ArrayList<>();
     64         for (Config config : createBaseVariations()) {
     65             result.add(config);
     66         }
     67         return result;
     68     }
     69 
     70     @Test
     71     public void scrollToPositionWithOffsetTest() throws Throwable {
     72         Config config = ((Config) mConfig.clone()).itemCount(300);
     73         setupByConfig(config, true);
     74         OrientationHelper orientationHelper = OrientationHelper
     75                 .createOrientationHelper(mLayoutManager, config.mOrientation);
     76         Rect layoutBounds = getDecoratedRecyclerViewBounds();
     77         // try scrolling towards head, should not affect anything
     78         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
     79         if (config.mStackFromEnd) {
     80             scrollToPositionWithOffset(mTestAdapter.getItemCount() - 1,
     81                     mLayoutManager.mOrientationHelper.getEnd() - 500);
     82         } else {
     83             scrollToPositionWithOffset(0, 20);
     84         }
     85         assertRectSetsEqual(config + " trying to over scroll with offset should be no-op",
     86                 before, mLayoutManager.collectChildCoordinates());
     87         // try offsetting some visible children
     88         int testCount = 10;
     89         while (testCount-- > 0) {
     90             // get middle child
     91             final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
     92             final int position = mRecyclerView.getChildLayoutPosition(child);
     93             final int startOffset = config.mReverseLayout ?
     94                     orientationHelper.getEndAfterPadding() - orientationHelper
     95                             .getDecoratedEnd(child)
     96                     : orientationHelper.getDecoratedStart(child) - orientationHelper
     97                             .getStartAfterPadding();
     98             final int scrollOffset = config.mStackFromEnd ? startOffset + startOffset / 2
     99                     : startOffset / 2;
    100             mLayoutManager.expectLayouts(1);
    101             scrollToPositionWithOffset(position, scrollOffset);
    102             mLayoutManager.waitForLayout(2);
    103             final int finalOffset = config.mReverseLayout ?
    104                     orientationHelper.getEndAfterPadding() - orientationHelper
    105                             .getDecoratedEnd(child)
    106                     : orientationHelper.getDecoratedStart(child) - orientationHelper
    107                             .getStartAfterPadding();
    108             assertEquals(config + " scroll with offset on a visible child should work fine " +
    109                             " offset:" + finalOffset + " , existing offset:" + startOffset + ", "
    110                             + "child " + position,
    111                     scrollOffset, finalOffset);
    112         }
    113 
    114         // try scrolling to invisible children
    115         testCount = 10;
    116         // we test above and below, one by one
    117         int offsetMultiplier = -1;
    118         while (testCount-- > 0) {
    119             final TargetTuple target = findInvisibleTarget(config);
    120             final String logPrefix = config + " " + target;
    121             mLayoutManager.expectLayouts(1);
    122             final int offset = offsetMultiplier
    123                     * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
    124             scrollToPositionWithOffset(target.mPosition, offset);
    125             mLayoutManager.waitForLayout(2);
    126             final View child = mLayoutManager.findViewByPosition(target.mPosition);
    127             assertNotNull(logPrefix + " scrolling to a mPosition with offset " + offset
    128                     + " should layout it", child);
    129             final Rect bounds = mLayoutManager.getViewBounds(child);
    130             if (DEBUG) {
    131                 Log.d(TAG, logPrefix + " post scroll to invisible mPosition " + bounds + " in "
    132                         + layoutBounds + " with offset " + offset);
    133             }
    134 
    135             if (config.mReverseLayout) {
    136                 assertEquals(logPrefix + " when scrolling with offset to an invisible in reverse "
    137                                 + "layout, its end should align with recycler view's end - offset",
    138                         orientationHelper.getEndAfterPadding() - offset,
    139                         orientationHelper.getDecoratedEnd(child)
    140                 );
    141             } else {
    142                 assertEquals(
    143                         logPrefix + " when scrolling with offset to an invisible child in normal"
    144                                 + " layout its start should align with recycler view's start + "
    145                                 + "offset",
    146                         orientationHelper.getStartAfterPadding() + offset,
    147                         orientationHelper.getDecoratedStart(child)
    148                 );
    149             }
    150             offsetMultiplier *= -1;
    151         }
    152     }
    153 
    154     @Test
    155     public void getFirstLastChildrenTest() throws Throwable {
    156         final Config config = ((Config) mConfig.clone()).itemCount(300);
    157         setupByConfig(config, true);
    158         Runnable viewInBoundsTest = new Runnable() {
    159             @Override
    160             public void run() {
    161                 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
    162                 final String boundsLog = mLayoutManager.getBoundsLog();
    163                 assertEquals(config + ":\nfirst visible child should match traversal result\n"
    164                                 + boundsLog, visibleChildren.firstVisiblePosition,
    165                         mLayoutManager.findFirstVisibleItemPosition()
    166                 );
    167                 assertEquals(
    168                         config + ":\nfirst fully visible child should match traversal result\n"
    169                                 + boundsLog, visibleChildren.firstFullyVisiblePosition,
    170                         mLayoutManager.findFirstCompletelyVisibleItemPosition()
    171                 );
    172 
    173                 assertEquals(config + ":\nlast visible child should match traversal result\n"
    174                                 + boundsLog, visibleChildren.lastVisiblePosition,
    175                         mLayoutManager.findLastVisibleItemPosition()
    176                 );
    177                 assertEquals(
    178                         config + ":\nlast fully visible child should match traversal result\n"
    179                                 + boundsLog, visibleChildren.lastFullyVisiblePosition,
    180                         mLayoutManager.findLastCompletelyVisibleItemPosition()
    181                 );
    182             }
    183         };
    184         mActivityRule.runOnUiThread(viewInBoundsTest);
    185         // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
    186         // case
    187         final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount();
    188         mActivityRule.runOnUiThread(new Runnable() {
    189             @Override
    190             public void run() {
    191                 mRecyclerView.smoothScrollToPosition(scrollPosition);
    192             }
    193         });
    194         while (mLayoutManager.isSmoothScrolling() ||
    195                 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
    196             mActivityRule.runOnUiThread(viewInBoundsTest);
    197             Thread.sleep(400);
    198         }
    199         // delete all items
    200         mLayoutManager.expectLayouts(2);
    201         mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount());
    202         mLayoutManager.waitForLayout(2);
    203         // test empty case
    204         mActivityRule.runOnUiThread(viewInBoundsTest);
    205         // set a new adapter with huge items to test full bounds check
    206         mLayoutManager.expectLayouts(1);
    207         final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace();
    208         final TestAdapter newAdapter = new TestAdapter(100) {
    209             @Override
    210             public void onBindViewHolder(@NonNull TestViewHolder holder,
    211                     int position) {
    212                 super.onBindViewHolder(holder, position);
    213                 if (config.mOrientation == HORIZONTAL) {
    214                     holder.itemView.setMinimumWidth(totalSpace + 5);
    215                 } else {
    216                     holder.itemView.setMinimumHeight(totalSpace + 5);
    217                 }
    218             }
    219         };
    220         mActivityRule.runOnUiThread(new Runnable() {
    221             @Override
    222             public void run() {
    223                 mRecyclerView.setAdapter(newAdapter);
    224             }
    225         });
    226         mLayoutManager.waitForLayout(2);
    227         mActivityRule.runOnUiThread(viewInBoundsTest);
    228     }
    229 
    230     @Test
    231     public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable {
    232         final Config config = ((Config) mConfig.clone()).itemCount(1000);
    233         setupByConfig(config, true);
    234         mLayoutManager.expectLayouts(1);
    235         scrollToPosition(500);
    236         mLayoutManager.waitForLayout(2);
    237         final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(500);
    238         OrientationHelper helper = mLayoutManager.mOrientationHelper;
    239         int gap = helper.getDecoratedStart(vh.itemView);
    240         scrollBy(gap);
    241         gap = helper.getDecoratedStart(vh.itemView);
    242         assertThat("test sanity", gap, is(0));
    243 
    244         final int size = helper.getDecoratedMeasurement(vh.itemView);
    245         AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
    246         mActivityRule.runOnUiThread(new Runnable() {
    247             @Override
    248             public void run() {
    249                 if (mConfig.mOrientation == HORIZONTAL) {
    250                     vh.itemView.setTranslationX(size * 2);
    251                 } else {
    252                     vh.itemView.setTranslationY(size * 2);
    253                 }
    254             }
    255         });
    256         scrollBy(size * 2);
    257         assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
    258         assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
    259         assertThat(vh.getAdapterPosition(), is(500));
    260         scrollBy(size * 2);
    261         assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
    262     }
    263 
    264     @Test
    265     public void dontRecycleViewsTranslatedOutOfBoundsFromEnd() throws Throwable {
    266         final Config config = ((Config) mConfig.clone()).itemCount(1000);
    267         setupByConfig(config, true);
    268         mLayoutManager.expectLayouts(1);
    269         scrollToPosition(500);
    270         mLayoutManager.waitForLayout(2);
    271         final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(500);
    272         OrientationHelper helper = mLayoutManager.mOrientationHelper;
    273         int gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
    274         scrollBy(-gap);
    275         gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
    276         assertThat("test sanity", gap, is(0));
    277 
    278         final int size = helper.getDecoratedMeasurement(vh.itemView);
    279         AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
    280         mActivityRule.runOnUiThread(new Runnable() {
    281             @Override
    282             public void run() {
    283                 if (mConfig.mOrientation == HORIZONTAL) {
    284                     vh.itemView.setTranslationX(-size * 2);
    285                 } else {
    286                     vh.itemView.setTranslationY(-size * 2);
    287                 }
    288             }
    289         });
    290         scrollBy(-size * 2);
    291         assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
    292         assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
    293         assertThat(vh.getAdapterPosition(), is(500));
    294         scrollBy(-size * 2);
    295         assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
    296     }
    297 
    298     private TargetTuple findInvisibleTarget(Config config) {
    299         int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
    300         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
    301             View child = mLayoutManager.getChildAt(i);
    302             int position = mRecyclerView.getChildLayoutPosition(child);
    303             if (position < minPosition) {
    304                 minPosition = position;
    305             }
    306             if (position > maxPosition) {
    307                 maxPosition = position;
    308             }
    309         }
    310         final int tailTarget = maxPosition +
    311                 (mRecyclerView.getAdapter().getItemCount() - maxPosition) / 2;
    312         final int headTarget = minPosition / 2;
    313         final int target;
    314         // where will the child come from ?
    315         final int itemLayoutDirection;
    316         if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
    317             target = tailTarget;
    318             itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
    319         } else {
    320             target = headTarget;
    321             itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
    322         }
    323         if (DEBUG) {
    324             Log.d(TAG,
    325                     config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
    326         }
    327         return new TargetTuple(target, itemLayoutDirection);
    328     }
    329 }
    330