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