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 20 import static androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory; 21 22 import static org.hamcrest.CoreMatchers.is; 23 import static org.hamcrest.MatcherAssert.assertThat; 24 import static org.junit.Assert.assertEquals; 25 import static org.junit.Assert.assertNotNull; 26 import static org.junit.Assert.assertNull; 27 import static org.junit.Assert.assertTrue; 28 29 import android.content.Context; 30 import android.view.MotionEvent; 31 import android.view.ViewGroup; 32 import android.widget.EdgeEffect; 33 34 import androidx.core.view.InputDeviceCompat; 35 import androidx.test.ext.junit.runners.AndroidJUnit4; 36 import androidx.test.filters.MediumTest; 37 38 import org.jspecify.annotations.NonNull; 39 import org.junit.Before; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 43 /** 44 * Tests custom edge effect are properly applied when scrolling. 45 */ 46 @MediumTest 47 @RunWith(AndroidJUnit4.class) 48 public class CustomEdgeEffectTest extends BaseRecyclerViewInstrumentationTest { 49 50 private static final int NUM_ITEMS = 10; 51 52 private LinearLayoutManager mLayoutManager; 53 private RecyclerView mRecyclerView; 54 55 @Before setup()56 public void setup() throws Throwable { 57 mLayoutManager = new LinearLayoutManager(getActivity()); 58 mLayoutManager.ensureLayoutState(); 59 60 mRecyclerView = new RecyclerView(getActivity()); 61 mRecyclerView.setLayoutManager(mLayoutManager); 62 mRecyclerView.setAdapter(new TestAdapter(NUM_ITEMS) { 63 64 @Override 65 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 66 int viewType) { 67 TestViewHolder holder = super.onCreateViewHolder(parent, viewType); 68 holder.itemView.setMinimumHeight(mRecyclerView.getMeasuredHeight() * 2 / NUM_ITEMS); 69 return holder; 70 } 71 }); 72 setRecyclerView(mRecyclerView); 73 getInstrumentation().waitForIdleSync(); 74 assertThat("Assumption check", mRecyclerView.getChildCount() > 0, is(true)); 75 } 76 77 @Test testEdgeEffectDirections()78 public void testEdgeEffectDirections() throws Throwable { 79 TestEdgeEffectFactory factory = new TestEdgeEffectFactory(); 80 mRecyclerView.setEdgeEffectFactory(factory); 81 scrollToPosition(0); 82 waitForIdleScroll(mRecyclerView); 83 scrollViewBy(3); 84 assertNull(factory.mBottom); 85 assertNotNull(factory.mTop); 86 assertTrue(factory.mTop.mPullDistance > 0); 87 scrollViewBy(-3); 88 89 scrollToPosition(NUM_ITEMS - 1); 90 waitForIdleScroll(mRecyclerView); 91 scrollViewBy(-3); 92 93 assertNotNull(factory.mBottom); 94 assertTrue(factory.mBottom.mPullDistance > 0); 95 } 96 97 @Test testEdgeEffectReplaced()98 public void testEdgeEffectReplaced() throws Throwable { 99 TestEdgeEffectFactory factory1 = new TestEdgeEffectFactory(); 100 mRecyclerView.setEdgeEffectFactory(factory1); 101 scrollToPosition(0); 102 waitForIdleScroll(mRecyclerView); 103 104 scrollViewBy(3); 105 assertNotNull(factory1.mTop); 106 float oldPullDistance = factory1.mTop.mPullDistance; 107 108 waitForIdleScroll(mRecyclerView); 109 TestEdgeEffectFactory factory2 = new TestEdgeEffectFactory(); 110 mRecyclerView.setEdgeEffectFactory(factory2); 111 scrollViewBy(30); 112 assertNotNull(factory2.mTop); 113 114 assertTrue(factory2.mTop.mPullDistance > oldPullDistance); 115 assertEquals(oldPullDistance, factory1.mTop.mPullDistance, 0.1f); 116 } 117 scrollViewBy(final int value)118 private void scrollViewBy(final int value) throws Throwable { 119 mActivityRule.runOnUiThread(new Runnable() { 120 @Override 121 public void run() { 122 TouchUtils.scrollView(MotionEvent.AXIS_VSCROLL, value, 123 InputDeviceCompat.SOURCE_CLASS_POINTER, mRecyclerView); 124 } 125 }); 126 } 127 128 private class TestEdgeEffectFactory extends EdgeEffectFactory { 129 130 TestEdgeEffect mTop, mBottom; 131 132 @Override createEdgeEffect(RecyclerView view, int direction)133 protected @NonNull EdgeEffect createEdgeEffect(RecyclerView view, int direction) { 134 TestEdgeEffect effect = new TestEdgeEffect(view.getContext()); 135 if (direction == EdgeEffectFactory.DIRECTION_TOP) { 136 mTop = effect; 137 } else if (direction == EdgeEffectFactory.DIRECTION_BOTTOM) { 138 mBottom = effect; 139 } 140 return effect; 141 } 142 } 143 144 private class TestEdgeEffect extends EdgeEffect { 145 146 private float mPullDistance; 147 private float mDistance; 148 TestEdgeEffect(Context context)149 TestEdgeEffect(Context context) { 150 super(context); 151 } 152 153 @Override onPull(float deltaDistance, float displacement)154 public void onPull(float deltaDistance, float displacement) { 155 onPull(deltaDistance); 156 } 157 158 @Override onPull(float deltaDistance)159 public void onPull(float deltaDistance) { 160 mPullDistance = deltaDistance; 161 mDistance += deltaDistance; 162 } 163 164 @Override onPullDistance(float deltaDistance, float displacement)165 public float onPullDistance(float deltaDistance, float displacement) { 166 float maxDelta = Math.max(-mDistance, deltaDistance); 167 onPull(maxDelta); 168 return maxDelta; 169 } 170 171 @Override getDistance()172 public float getDistance() { 173 return mDistance; 174 } 175 } 176 } 177