| /* |
| * 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.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.content.Context; |
| import android.os.Build; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.SdkSuppress; |
| import android.support.test.filters.SmallTest; |
| import android.support.test.runner.AndroidJUnit4; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import androidx.annotation.NonNull; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.ArrayList; |
| import java.util.concurrent.TimeUnit; |
| |
| @SmallTest |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) |
| @RunWith(AndroidJUnit4.class) |
| public class MultiRecyclerViewPrefetchTest { |
| private RecyclerView.RecycledViewPool mRecycledViewPool; |
| private ArrayList<RecyclerView> mViews = new ArrayList<>(); |
| |
| private long mMockNanoTime = 0; |
| |
| @Before |
| public void setup() throws Exception { |
| GapWorker gapWorker = GapWorker.sGapWorker.get(); |
| if (gapWorker != null) { |
| assertTrue(gapWorker.mRecyclerViews.isEmpty()); |
| } |
| mMockNanoTime = 0; |
| mRecycledViewPool = new RecyclerView.RecycledViewPool(); |
| } |
| |
| @After |
| public void teardown() { |
| for (RecyclerView rv : mViews) { |
| if (rv.isAttachedToWindow()) { |
| // ensure we detach views, so ThreadLocal GapWorker's list is cleared |
| rv.onDetachedFromWindow(); |
| } |
| } |
| GapWorker gapWorker = GapWorker.sGapWorker.get(); |
| if (gapWorker != null) { |
| assertTrue(gapWorker.mRecyclerViews.isEmpty()); |
| } |
| mViews.clear(); |
| } |
| |
| private RecyclerView createRecyclerView() { |
| RecyclerView rv = new RecyclerView(getContext()) { |
| @Override |
| long getNanoTime() { |
| return mMockNanoTime; |
| } |
| |
| @Override |
| public int getWindowVisibility() { |
| // Pretend to be visible to avoid being filtered out |
| return View.VISIBLE; |
| } |
| }; |
| |
| // shared stats + enable clearing of pool |
| rv.setRecycledViewPool(mRecycledViewPool); |
| |
| // enable GapWorker |
| rv.onAttachedToWindow(); |
| mViews.add(rv); |
| |
| return rv; |
| } |
| |
| public void registerTimePassingMs(long ms) { |
| mMockNanoTime += TimeUnit.MILLISECONDS.toNanos(ms); |
| } |
| |
| private Context getContext() { |
| return InstrumentationRegistry.getContext(); |
| } |
| |
| private void clearCachesAndPool() { |
| for (RecyclerView rv : mViews) { |
| rv.mRecycler.recycleAndClearCachedViews(); |
| } |
| mRecycledViewPool.clear(); |
| } |
| |
| @Test |
| public void prefetchOrdering() throws Throwable { |
| for (int i = 0; i < 3; i++) { |
| RecyclerView rv = createRecyclerView(); |
| |
| // first view 50x100 pixels, rest are 100x100 so second column is offset |
| rv.setAdapter(new RecyclerView.Adapter() { |
| @NonNull |
| @Override |
| public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, |
| int viewType) { |
| registerTimePassingMs(5); |
| return new RecyclerView.ViewHolder(new View(parent.getContext())) {}; |
| } |
| |
| @Override |
| public void onBindViewHolder( |
| @NonNull RecyclerView.ViewHolder holder, int position) { |
| registerTimePassingMs(5); |
| holder.itemView.setMinimumWidth(100); |
| holder.itemView.setMinimumHeight(position == 0 ? 50 : 100); |
| } |
| |
| @Override |
| public int getItemCount() { |
| return 100; |
| } |
| }); |
| rv.setLayoutManager( |
| new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)); |
| |
| // Attach, position 200x200 view at 100 scroll offset, with an empty cache. |
| rv.measure(View.MeasureSpec.AT_MOST | 200, View.MeasureSpec.AT_MOST | 200); |
| rv.layout(0, 0, 200, 200); |
| rv.scrollBy(0, 100); |
| |
| rv.setTranslationX(100 * i); |
| } |
| |
| GapWorker worker = GapWorker.sGapWorker.get(); |
| assertNotNull(worker); |
| |
| /* Each row is 50 pixels: |
| * ------------- * |
| * 0 | 1 * |
| *___2___|___1___* |
| * 2 | 3 * |
| * 4 | 3 * |
| * 4 | 5 * |
| *___6___|___5___* |
| * 6 | 7 * |
| * 8 | 7 * |
| * ... * |
| */ |
| |
| mViews.get(0).mPrefetchRegistry.setPrefetchVector(0, 10); |
| mViews.get(1).mPrefetchRegistry.setPrefetchVector(0, -11); |
| mViews.get(2).mPrefetchRegistry.setPrefetchVector(0, 60); |
| |
| // prefetch with deadline that has passed - only demand-loaded views |
| clearCachesAndPool(); |
| worker.prefetch(0); |
| CacheUtils.verifyCacheContainsPrefetchedPositions(mViews.get(0), 7); |
| CacheUtils.verifyCacheContainsPrefetchedPositions(mViews.get(1), 1); |
| CacheUtils.verifyCacheContainsPrefetchedPositions(mViews.get(2), 7, 8); |
| |
| |
| // prefetch with 54ms - should load demand-loaded views (taking 40ms) + one more |
| clearCachesAndPool(); |
| worker.prefetch(mMockNanoTime + TimeUnit.MILLISECONDS.toNanos(54)); |
| CacheUtils.verifyCacheContainsPrefetchedPositions(mViews.get(0), 7); |
| CacheUtils.verifyCacheContainsPrefetchedPositions(mViews.get(1), 0, 1); |
| CacheUtils.verifyCacheContainsPrefetchedPositions(mViews.get(2), 7, 8); |
| } |
| } |