Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 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 package android.support.v7.widget;
     17 
     18 import static org.hamcrest.CoreMatchers.is;
     19 import static org.hamcrest.MatcherAssert.assertThat;
     20 import static org.junit.Assert.assertEquals;
     21 import static org.junit.Assert.assertNull;
     22 import static org.junit.Assert.assertTrue;
     23 
     24 import android.app.Activity;
     25 import android.graphics.Color;
     26 import android.graphics.Rect;
     27 import android.support.annotation.Nullable;
     28 import android.support.v4.util.LongSparseArray;
     29 import android.support.v7.widget.TestedFrameLayout.FullControlLayoutParams;
     30 import android.util.Log;
     31 import android.view.Gravity;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.widget.TextView;
     35 
     36 import org.hamcrest.CoreMatchers;
     37 
     38 import java.util.ArrayList;
     39 import java.util.Collections;
     40 import java.util.List;
     41 
     42 /**
     43  * Class to test any generic wrap content behavior.
     44  * It does so by running the same view scenario twice. Once with match parent setup to record all
     45  * dimensions and once with wrap_content setup. Then compares all child locations & ids +
     46  * RecyclerView size.
     47  */
     48 abstract public class BaseWrapContentTest extends BaseRecyclerViewInstrumentationTest {
     49 
     50     static final boolean DEBUG = false;
     51     static final String TAG = "WrapContentTest";
     52     RecyclerView.LayoutManager mLayoutManager;
     53 
     54     TestAdapter mTestAdapter;
     55 
     56     LoggingItemAnimator mLoggingItemAnimator;
     57 
     58     boolean mIsWrapContent;
     59 
     60     protected final WrapContentConfig mWrapContentConfig;
     61 
     62     public BaseWrapContentTest(WrapContentConfig config) {
     63         mWrapContentConfig = config;
     64     }
     65 
     66     abstract RecyclerView.LayoutManager createLayoutManager();
     67 
     68     void unspecifiedWithHintTest(boolean horizontal) throws Throwable {
     69         final int itemHeight = 20;
     70         final int itemWidth = 15;
     71         RecyclerView.LayoutManager layoutManager = createLayoutManager();
     72         WrappedRecyclerView rv = createRecyclerView(getActivity());
     73         TestAdapter testAdapter = new TestAdapter(20) {
     74             @Override
     75             public void onBindViewHolder(TestViewHolder holder,
     76                     int position) {
     77                 super.onBindViewHolder(holder, position);
     78                 holder.itemView.setLayoutParams(new ViewGroup.LayoutParams(itemWidth, itemHeight));
     79             }
     80         };
     81         rv.setLayoutManager(layoutManager);
     82         rv.setAdapter(testAdapter);
     83         TestedFrameLayout.FullControlLayoutParams lp =
     84                 new TestedFrameLayout.FullControlLayoutParams(0, 0);
     85         if (horizontal) {
     86             lp.wSpec = View.MeasureSpec.makeMeasureSpec(25, View.MeasureSpec.UNSPECIFIED);
     87             lp.hSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.AT_MOST);
     88         } else {
     89             lp.hSpec = View.MeasureSpec.makeMeasureSpec(25, View.MeasureSpec.UNSPECIFIED);
     90             lp.wSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.AT_MOST);
     91         }
     92         rv.setLayoutParams(lp);
     93         setRecyclerView(rv);
     94         rv.waitUntilLayout();
     95 
     96         // we don't assert against the given size hint because LM will still ask for more if it
     97         // lays out more children. This is the correct behavior because the spec is not AT_MOST,
     98         // it is UNSPECIFIED.
     99         if (horizontal) {
    100             int expectedWidth = rv.getPaddingLeft() + rv.getPaddingRight() + itemWidth;
    101             while (expectedWidth < 25) {
    102                 expectedWidth += itemWidth;
    103             }
    104             assertThat(rv.getWidth(), CoreMatchers.is(expectedWidth));
    105         } else {
    106             int expectedHeight = rv.getPaddingTop() + rv.getPaddingBottom() + itemHeight;
    107             while (expectedHeight < 25) {
    108                 expectedHeight += itemHeight;
    109             }
    110             assertThat(rv.getHeight(), CoreMatchers.is(expectedHeight));
    111         }
    112     }
    113 
    114     protected void testScenerio(Scenario scenario) throws Throwable {
    115         FullControlLayoutParams matchParent = new FullControlLayoutParams(
    116                 ViewGroup.LayoutParams.MATCH_PARENT,
    117                 ViewGroup.LayoutParams.MATCH_PARENT);
    118         FullControlLayoutParams wrapContent = new FullControlLayoutParams(
    119                 ViewGroup.LayoutParams.WRAP_CONTENT,
    120                 ViewGroup.LayoutParams.WRAP_CONTENT);
    121         if (mWrapContentConfig.isUnlimitedHeight()) {
    122             wrapContent.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    123         }
    124         if (mWrapContentConfig.isUnlimitedWidth()) {
    125             wrapContent.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    126         }
    127 
    128         mIsWrapContent = false;
    129         List<Snapshot> s1 = runScenario(scenario, matchParent, null);
    130         mIsWrapContent = true;
    131 
    132         List<Snapshot> s2 = runScenario(scenario, wrapContent, s1);
    133         assertEquals("test sanity", s1.size(), s2.size());
    134 
    135         for (int i = 0; i < s1.size(); i++) {
    136             Snapshot step1 = s1.get(i);
    137             Snapshot step2 = s2.get(i);
    138             step1.assertSame(step2, i);
    139         }
    140     }
    141 
    142     public List<Snapshot> runScenario(Scenario scenario, ViewGroup.LayoutParams lp,
    143             @Nullable List<Snapshot> compareWith)
    144             throws Throwable {
    145         removeRecyclerView();
    146         Item.idCounter.set(0);
    147         List<Snapshot> result = new ArrayList<>();
    148         RecyclerView.LayoutManager layoutManager = scenario.createLayoutManager();
    149         WrappedRecyclerView recyclerView = new WrappedRecyclerView(getActivity());
    150         recyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
    151         recyclerView.setLayoutManager(layoutManager);
    152         recyclerView.setLayoutParams(lp);
    153         mLayoutManager = layoutManager;
    154         mTestAdapter = new TestAdapter(scenario.getSeedAdapterSize());
    155         recyclerView.setAdapter(mTestAdapter);
    156         mLoggingItemAnimator = new LoggingItemAnimator();
    157         recyclerView.setItemAnimator(mLoggingItemAnimator);
    158         setRecyclerView(recyclerView);
    159         recyclerView.waitUntilLayout();
    160         int stepIndex = 0;
    161         for (Step step : scenario.mStepList) {
    162             mLoggingItemAnimator.reset();
    163             step.onRun();
    164             recyclerView.waitUntilLayout();
    165             recyclerView.waitUntilAnimations();
    166             Snapshot snapshot = takeSnapshot();
    167             if (mIsWrapContent) {
    168                 snapshot.assertRvSize();
    169             }
    170             result.add(snapshot);
    171             if (compareWith != null) {
    172                 compareWith.get(stepIndex).assertSame(snapshot, stepIndex);
    173             }
    174             stepIndex++;
    175         }
    176         recyclerView.waitUntilLayout();
    177         recyclerView.waitUntilAnimations();
    178         Snapshot snapshot = takeSnapshot();
    179         if (mIsWrapContent) {
    180             snapshot.assertRvSize();
    181         }
    182         result.add(snapshot);
    183         if (compareWith != null) {
    184             compareWith.get(stepIndex).assertSame(snapshot, stepIndex);
    185         }
    186         return result;
    187     }
    188 
    189     protected WrappedRecyclerView createRecyclerView(Activity activity) {
    190         return new WrappedRecyclerView(getActivity());
    191     }
    192 
    193     void layoutAndCheck(TestedFrameLayout.FullControlLayoutParams lp,
    194             BaseWrapContentWithAspectRatioTest.WrapContentAdapter adapter, Rect[] expected,
    195             int width, int height) throws Throwable {
    196         WrappedRecyclerView recyclerView = createRecyclerView(getActivity());
    197         recyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
    198         recyclerView.setLayoutManager(createLayoutManager());
    199         recyclerView.setAdapter(adapter);
    200         recyclerView.setLayoutParams(lp);
    201         Rect padding = mWrapContentConfig.padding;
    202         recyclerView.setPadding(padding.left, padding.top, padding.right, padding.bottom);
    203         setRecyclerView(recyclerView);
    204         recyclerView.waitUntilLayout();
    205         Snapshot snapshot = takeSnapshot();
    206         int index = 0;
    207         Rect tmp = new Rect();
    208         for (BaseWrapContentWithAspectRatioTest.MeasureBehavior behavior : adapter.behaviors) {
    209             tmp.set(expected[index]);
    210             tmp.offset(padding.left, padding.top);
    211             assertThat("behavior " + index, snapshot.mChildCoordinates.get(behavior.getId()),
    212                     is(tmp));
    213             index ++;
    214         }
    215         Rect boundingBox = new Rect(0, 0, 0, 0);
    216         for (Rect rect : expected) {
    217             boundingBox.union(rect);
    218         }
    219         assertThat(recyclerView.getWidth(), is(width + padding.left + padding.right));
    220         assertThat(recyclerView.getHeight(), is(height + padding.top + padding.bottom));
    221     }
    222 
    223 
    224     abstract protected int getVerticalGravity(RecyclerView.LayoutManager layoutManager);
    225 
    226     abstract protected int getHorizontalGravity(RecyclerView.LayoutManager layoutManager);
    227 
    228     protected Snapshot takeSnapshot() throws Throwable {
    229         Snapshot snapshot = new Snapshot(mRecyclerView, mLoggingItemAnimator,
    230                 getHorizontalGravity(mLayoutManager), getVerticalGravity(mLayoutManager));
    231         return snapshot;
    232     }
    233 
    234     abstract class Scenario {
    235 
    236         ArrayList<Step> mStepList = new ArrayList<>();
    237 
    238         public Scenario(Step... steps) {
    239             Collections.addAll(mStepList, steps);
    240         }
    241 
    242         public int getSeedAdapterSize() {
    243             return 10;
    244         }
    245 
    246         public RecyclerView.LayoutManager createLayoutManager() {
    247             return BaseWrapContentTest.this.createLayoutManager();
    248         }
    249     }
    250 
    251     abstract static class Step {
    252 
    253         abstract void onRun() throws Throwable;
    254     }
    255 
    256     class Snapshot {
    257 
    258         Rect mRawChildrenBox = new Rect();
    259 
    260         Rect mRvSize = new Rect();
    261 
    262         Rect mRvPadding = new Rect();
    263 
    264         Rect mRvParentSize = new Rect();
    265 
    266         LongSparseArray<Rect> mChildCoordinates = new LongSparseArray<>();
    267 
    268         LongSparseArray<String> mAppear = new LongSparseArray<>();
    269 
    270         LongSparseArray<String> mDisappear = new LongSparseArray<>();
    271 
    272         LongSparseArray<String> mPersistent = new LongSparseArray<>();
    273 
    274         LongSparseArray<String> mChanged = new LongSparseArray<>();
    275 
    276         int mVerticalGravity;
    277 
    278         int mHorizontalGravity;
    279 
    280         int mOffsetX, mOffsetY;// how much we should offset children
    281 
    282         public Snapshot(RecyclerView recyclerView, LoggingItemAnimator loggingItemAnimator,
    283                 int horizontalGravity, int verticalGravity)
    284                 throws Throwable {
    285             mRvSize = getViewBounds(recyclerView);
    286             mRvParentSize = getViewBounds((View) recyclerView.getParent());
    287             mRvPadding = new Rect(recyclerView.getPaddingLeft(), recyclerView.getPaddingTop(),
    288                     recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
    289             mVerticalGravity = verticalGravity;
    290             mHorizontalGravity = horizontalGravity;
    291             if (mVerticalGravity == Gravity.TOP) {
    292                 mOffsetY = 0;
    293             } else {
    294                 mOffsetY = mRvParentSize.bottom - mRvSize.bottom;
    295             }
    296 
    297             if (mHorizontalGravity == Gravity.LEFT) {
    298                 mOffsetX = 0;
    299             } else {
    300                 mOffsetX = mRvParentSize.right - mRvSize.right;
    301             }
    302             collectChildCoordinates(recyclerView);
    303             if (loggingItemAnimator != null) {
    304                 collectInto(mAppear, loggingItemAnimator.mAnimateAppearanceList);
    305                 collectInto(mDisappear, loggingItemAnimator.mAnimateDisappearanceList);
    306                 collectInto(mPersistent, loggingItemAnimator.mAnimatePersistenceList);
    307                 collectInto(mChanged, loggingItemAnimator.mAnimateChangeList);
    308             }
    309         }
    310 
    311         public boolean doesChildrenFitVertically() {
    312             return mRawChildrenBox.top >= mRvPadding.top
    313                     && mRawChildrenBox.bottom <= mRvSize.bottom - mRvPadding.bottom;
    314         }
    315 
    316         public boolean doesChildrenFitHorizontally() {
    317             return mRawChildrenBox.left >= mRvPadding.left
    318                     && mRawChildrenBox.right <= mRvSize.right - mRvPadding.right;
    319         }
    320 
    321         public void assertSame(Snapshot other, int step) {
    322             if (mWrapContentConfig.isUnlimitedHeight() &&
    323                     (!doesChildrenFitVertically() || !other.doesChildrenFitVertically())) {
    324                 if (DEBUG) {
    325                     Log.d(TAG, "cannot assert coordinates because it does not fit vertically");
    326                 }
    327                 return;
    328             }
    329             if (mWrapContentConfig.isUnlimitedWidth() &&
    330                     (!doesChildrenFitHorizontally() || !other.doesChildrenFitHorizontally())) {
    331                 if (DEBUG) {
    332                     Log.d(TAG, "cannot assert coordinates because it does not fit horizontally");
    333                 }
    334                 return;
    335             }
    336             assertMap("child coordinates. step:" + step, mChildCoordinates,
    337                     other.mChildCoordinates);
    338             if (mWrapContentConfig.isUnlimitedHeight() || mWrapContentConfig.isUnlimitedWidth()) {
    339                 return;//cannot assert animatinos in unlimited size
    340             }
    341             assertMap("appearing step:" + step, mAppear, other.mAppear);
    342             assertMap("disappearing step:" + step, mDisappear, other.mDisappear);
    343             assertMap("persistent step:" + step, mPersistent, other.mPersistent);
    344             assertMap("changed step:" + step, mChanged, other.mChanged);
    345         }
    346 
    347         private void assertMap(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2) {
    348             StringBuilder logBuilder = new StringBuilder();
    349             logBuilder.append(prefix).append("\n");
    350             logBuilder.append("map1").append("\n");
    351             logInto(map1, logBuilder);
    352             logBuilder.append("map2").append("\n");
    353             logInto(map2, logBuilder);
    354             final String log = logBuilder.toString();
    355             assertEquals(log + " same size", map1.size(), map2.size());
    356             for (int i = 0; i < map1.size(); i++) {
    357                 assertAtIndex(log, map1, map2, i);
    358             }
    359         }
    360 
    361         private void assertAtIndex(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2,
    362                 int index) {
    363             long key1 = map1.keyAt(index);
    364             long key2 = map2.keyAt(index);
    365             assertEquals(prefix + "key mismatch at index " + index, key1, key2);
    366             Object value1 = map1.valueAt(index);
    367             Object value2 = map2.valueAt(index);
    368             assertEquals(prefix + " value mismatch at index " + index, value1, value2);
    369         }
    370 
    371         private void logInto(LongSparseArray<?> map, StringBuilder sb) {
    372             for (int i = 0; i < map.size(); i++) {
    373                 long key = map.keyAt(i);
    374                 Object value = map.valueAt(i);
    375                 sb.append(key).append(" : ").append(value).append("\n");
    376             }
    377         }
    378 
    379         @Override
    380         public String toString() {
    381             StringBuilder sb = new StringBuilder("Snapshot{\n");
    382             sb.append("child coordinates:\n");
    383             logInto(mChildCoordinates, sb);
    384             sb.append("appear animations:\n");
    385             logInto(mAppear, sb);
    386             sb.append("disappear animations:\n");
    387             logInto(mDisappear, sb);
    388             sb.append("change animations:\n");
    389             logInto(mChanged, sb);
    390             sb.append("persistent animations:\n");
    391             logInto(mPersistent, sb);
    392             sb.append("}");
    393             return sb.toString();
    394         }
    395 
    396         @Override
    397         public int hashCode() {
    398             int result = mChildCoordinates.hashCode();
    399             result = 31 * result + mAppear.hashCode();
    400             result = 31 * result + mDisappear.hashCode();
    401             result = 31 * result + mPersistent.hashCode();
    402             result = 31 * result + mChanged.hashCode();
    403             return result;
    404         }
    405 
    406         private void collectInto(
    407                 LongSparseArray<String> target,
    408                 List<? extends BaseRecyclerViewAnimationsTest.AnimateLogBase> list) {
    409             for (BaseRecyclerViewAnimationsTest.AnimateLogBase base : list) {
    410                 long id = getItemId(base.viewHolder);
    411                 assertNull(target.get(id));
    412                 target.put(id, log(base));
    413             }
    414         }
    415 
    416         private String log(BaseRecyclerViewAnimationsTest.AnimateLogBase base) {
    417             return base.getClass().getSimpleName() +
    418                     ((TextView) base.viewHolder.itemView).getText() + ": " +
    419                     "[pre:" + log(base.postInfo) +
    420                     ", post:" + log(base.postInfo) + "]";
    421         }
    422 
    423         private String log(BaseRecyclerViewAnimationsTest.LoggingInfo postInfo) {
    424             if (postInfo == null) {
    425                 return "?";
    426             }
    427             return "PI[flags: " + postInfo.changeFlags
    428                     + ",l:" + (postInfo.left + mOffsetX)
    429                     + ",t:" + (postInfo.top + mOffsetY)
    430                     + ",r:" + (postInfo.right + mOffsetX)
    431                     + ",b:" + (postInfo.bottom + mOffsetY) + "]";
    432         }
    433 
    434         void collectChildCoordinates(RecyclerView recyclerView) throws Throwable {
    435             mRawChildrenBox = new Rect(0, 0, 0, 0);
    436             final int childCount = recyclerView.getChildCount();
    437             for (int i = 0; i < childCount; i++) {
    438                 View child = recyclerView.getChildAt(i);
    439                 Rect childBounds = getChildBounds(recyclerView, child, true);
    440                 mRawChildrenBox.union(getChildBounds(recyclerView, child, false));
    441                 RecyclerView.ViewHolder childViewHolder = recyclerView.getChildViewHolder(child);
    442                 mChildCoordinates.put(getItemId(childViewHolder), childBounds);
    443             }
    444         }
    445 
    446         private Rect getViewBounds(View view) {
    447             return new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
    448         }
    449 
    450         private Rect getChildBounds(RecyclerView recyclerView, View child, boolean offset) {
    451             RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
    452             RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
    453             Rect rect = new Rect(layoutManager.getDecoratedLeft(child) - lp.leftMargin,
    454                     layoutManager.getDecoratedTop(child) - lp.topMargin,
    455                     layoutManager.getDecoratedRight(child) + lp.rightMargin,
    456                     layoutManager.getDecoratedBottom(child) + lp.bottomMargin);
    457             if (offset) {
    458                 rect.offset(mOffsetX, mOffsetY);
    459             }
    460             return rect;
    461         }
    462 
    463         private long getItemId(RecyclerView.ViewHolder vh) {
    464             if (vh instanceof TestViewHolder) {
    465                 return ((TestViewHolder) vh).mBoundItem.mId;
    466             } else if (vh instanceof BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) {
    467                 BaseWrapContentWithAspectRatioTest.WrapContentViewHolder casted =
    468                         (BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) vh;
    469                 return casted.mView.mBehavior.getId();
    470             } else {
    471                 throw new IllegalArgumentException("i don't support any VH");
    472             }
    473         }
    474 
    475         public void assertRvSize() {
    476             if (shouldWrapContentHorizontally()) {
    477                 int expectedW = mRawChildrenBox.width() + mRvPadding.left + mRvPadding.right;
    478                 assertTrue(mRvSize.width() + " <= " + expectedW, mRvSize.width() <= expectedW);
    479             }
    480             if (shouldWrapContentVertically()) {
    481                 int expectedH = mRawChildrenBox.height() + mRvPadding.top + mRvPadding.bottom;
    482                 assertTrue(mRvSize.height() + "<=" + expectedH, mRvSize.height() <= expectedH);
    483             }
    484         }
    485     }
    486 
    487     protected boolean shouldWrapContentHorizontally() {
    488         return true;
    489     }
    490 
    491     protected boolean shouldWrapContentVertically() {
    492         return true;
    493     }
    494 
    495     static class WrapContentConfig {
    496 
    497         public boolean unlimitedWidth;
    498         public boolean unlimitedHeight;
    499         public Rect padding = new Rect(0, 0, 0, 0);
    500 
    501         public WrapContentConfig(boolean unlimitedWidth, boolean unlimitedHeight) {
    502             this.unlimitedWidth = unlimitedWidth;
    503             this.unlimitedHeight = unlimitedHeight;
    504         }
    505 
    506         public WrapContentConfig(boolean unlimitedWidth, boolean unlimitedHeight, Rect padding) {
    507             this.unlimitedWidth = unlimitedWidth;
    508             this.unlimitedHeight = unlimitedHeight;
    509             this.padding.set(padding);
    510         }
    511 
    512         public boolean isUnlimitedWidth() {
    513             return unlimitedWidth;
    514         }
    515 
    516         public WrapContentConfig setUnlimitedWidth(boolean unlimitedWidth) {
    517             this.unlimitedWidth = unlimitedWidth;
    518             return this;
    519         }
    520 
    521         public boolean isUnlimitedHeight() {
    522             return unlimitedHeight;
    523         }
    524 
    525         public WrapContentConfig setUnlimitedHeight(boolean unlimitedHeight) {
    526             this.unlimitedHeight = unlimitedHeight;
    527             return this;
    528         }
    529 
    530         @Override
    531         public String toString() {
    532             return "WrapContentConfig{"
    533                     + "unlimitedWidth=" + unlimitedWidth
    534                     + ",unlimitedHeight=" + unlimitedHeight
    535                     + ",padding=" + padding
    536                     + '}';
    537         }
    538 
    539         public TestedFrameLayout.FullControlLayoutParams toLayoutParams(int wDim, int hDim) {
    540             TestedFrameLayout.FullControlLayoutParams
    541                     lp = new TestedFrameLayout.FullControlLayoutParams(
    542                     wDim, hDim);
    543             if (unlimitedWidth) {
    544                 lp.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    545             }
    546             if (unlimitedHeight) {
    547                 lp.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
    548             }
    549             return lp;
    550         }
    551     }
    552 }
    553