20 | * Created by yuyang on 2023/12/4.
21 | */
22 | public class MainActivity extends AppCompatActivity {
23 |
24 | private ParentRecyclerView mParentRecyclerView;
25 |
26 | private ParentAdapter mParentAdapter;
27 |
28 | @Override
29 | protected void onCreate(@Nullable Bundle savedInstanceState) {
30 | super.onCreate(savedInstanceState);
31 |
32 | setContentView(R.layout.activity_main);
33 |
34 | mParentRecyclerView = findViewById(R.id.parent);
35 | mParentRecyclerView.setLayoutManager(new LinearLayoutManager(this));
36 | mParentRecyclerView.setAdapter(mParentAdapter = new ParentAdapter());
37 |
38 | mParentAdapter.setDataList(Arrays.asList(R.mipmap.p1, R.mipmap.p2, R.mipmap.p3));
39 | }
40 |
41 | @Override
42 | public boolean onCreateOptionsMenu(Menu menu) {
43 | MenuInflater inflater = getMenuInflater();
44 | inflater.inflate(R.menu.menu_main, menu);
45 | return true;
46 | }
47 |
48 | @Override
49 | public boolean onOptionsItemSelected(MenuItem item) {
50 | if (item.getItemId() == R.id.action_tab) {
51 | Intent intent = new Intent(this, MainActivityWithoutTab.class);
52 | startActivity(intent);
53 | return true;
54 | }
55 | return super.onOptionsItemSelected(item);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yuyang/library/ParentAdapter.java:
--------------------------------------------------------------------------------
1 | package com.yuyang.library;
2 |
3 | import android.graphics.drawable.Drawable;
4 | import android.support.annotation.NonNull;
5 | import android.support.v4.content.ContextCompat;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.widget.ImageView;
11 | import android.widget.Toast;
12 |
13 | import com.yuyang.library.nestedrv.ChildRecyclerView;
14 | import com.yuyang.library.nestedrv.INestedParentAdapter;
15 |
16 | import java.util.ArrayList;
17 | import java.util.Arrays;
18 | import java.util.List;
19 |
20 | /**
21 | * Created by yuyang on 2023/12/4.
22 | */
23 | public class ParentAdapter extends RecyclerView.Adapter
16 | * Created by yuyang on 2023/12/4.
17 | */
18 | public class MainActivityWithoutTab extends AppCompatActivity {
19 |
20 | private ParentRecyclerView mParentRecyclerView;
21 |
22 | private ParentAdapterWithoutTab mParentAdapterWithoutTab;
23 |
24 | @Override
25 | protected void onCreate(@Nullable Bundle savedInstanceState) {
26 | super.onCreate(savedInstanceState);
27 |
28 | setContentView(R.layout.activity_main);
29 |
30 | mParentRecyclerView = findViewById(R.id.parent);
31 | mParentRecyclerView.setLayoutManager(new LinearLayoutManager(this));
32 | mParentRecyclerView.setAdapter(mParentAdapterWithoutTab = new ParentAdapterWithoutTab());
33 |
34 | mParentAdapterWithoutTab.setDataList(Arrays.asList(R.mipmap.p1, R.mipmap.p2, R.mipmap.p3));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yuyang/library/nested/ParentAdapterWithoutTab.java:
--------------------------------------------------------------------------------
1 | package com.yuyang.library.nested;
2 |
3 | import android.graphics.Rect;
4 | import android.graphics.drawable.Drawable;
5 | import android.support.annotation.NonNull;
6 | import android.support.v4.content.ContextCompat;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.support.v7.widget.StaggeredGridLayoutManager;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.ImageView;
12 | import android.widget.Toast;
13 |
14 | import com.yuyang.library.ChildAdapter;
15 | import com.yuyang.library.nestedrv.ChildRecyclerView;
16 | import com.yuyang.library.nestedrv.INestedParentAdapter;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | /**
22 | * Created by yuyang on 2023/12/4.
23 | */
24 | public class ParentAdapterWithoutTab extends RecyclerView.Adapter
14 | * Created by yuyang on 2023/12/4.
15 | */
16 | public class ChildRecyclerView extends RecyclerView {
17 |
18 | private ParentRecyclerView mParentRecyclerView = null;
19 |
20 | /**
21 | * fling时的加速度
22 | */
23 | private int mVelocity = 0;
24 |
25 | private int mLastInterceptX;
26 |
27 | private int mLastInterceptY;
28 |
29 | public ChildRecyclerView(@NonNull Context context) {
30 | this(context, null);
31 | }
32 |
33 | public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
34 | this(context, attrs, 0);
35 | }
36 |
37 | public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
38 | super(context, attrs, defStyle);
39 | init();
40 | }
41 |
42 | private void init() {
43 | setOverScrollMode(OVER_SCROLL_NEVER);
44 |
45 | addOnScrollListener(new OnScrollListener() {
46 | @Override
47 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
48 | super.onScrollStateChanged(recyclerView, newState);
49 | if (newState == SCROLL_STATE_IDLE) {
50 | dispatchParentFling();
51 | }
52 | }
53 | });
54 | }
55 |
56 | private void dispatchParentFling() {
57 | ensureParentRecyclerView();
58 | // 子容器滚动到顶部,如果还有剩余加速度,就交给父容器处理
59 | if (mParentRecyclerView != null && isScrollToTop() && mVelocity != 0) {
60 | // 尽量让速度传递更加平滑
61 | float velocityY = NestedOverScroller.invokeCurrentVelocity(this);
62 | if (Math.abs(velocityY) <= 2.0E-5F) {
63 | velocityY = (float) this.mVelocity * 0.5F;
64 | } else {
65 | velocityY *= 0.65F;
66 | }
67 | mParentRecyclerView.fling(0, (int) velocityY);
68 | mVelocity = 0;
69 | }
70 | }
71 |
72 | @Override
73 | public boolean dispatchTouchEvent(MotionEvent ev) {
74 | if (ev.getAction() == MotionEvent.ACTION_DOWN) {
75 | mVelocity = 0;
76 | }
77 |
78 | int x = (int) ev.getRawX();
79 | int y = (int) ev.getRawY();
80 | if (ev.getAction() != MotionEvent.ACTION_MOVE) {
81 | mLastInterceptX = x;
82 | mLastInterceptY = y;
83 | }
84 |
85 | int deltaX = x - mLastInterceptX;
86 | int deltaY = y - mLastInterceptY;
87 |
88 | if (isScrollToTop() && Math.abs(deltaX) <= Math.abs(deltaY) && getParent() != null) {
89 | // 子容器滚动到顶部,继续向上滑动,此时父容器需要继续拦截事件。与父容器 onInterceptTouchEvent 对应
90 | getParent().requestDisallowInterceptTouchEvent(false);
91 | }
92 | return super.dispatchTouchEvent(ev);
93 | }
94 |
95 | @Override
96 | public boolean fling(int velocityX, int velocityY) {
97 | if (!isAttachedToWindow()) return false;
98 | boolean fling = super.fling(velocityX, velocityY);
99 | if (!fling || velocityY >= 0) {
100 | mVelocity = 0;
101 | } else {
102 | mVelocity = velocityY;
103 | }
104 | return fling;
105 | }
106 |
107 | public boolean isScrollToTop() {
108 | return !canScrollVertically(-1);
109 | }
110 |
111 | public boolean isScrollToBottom() {
112 | return !canScrollVertically(1);
113 | }
114 |
115 | private void ensureParentRecyclerView() {
116 | if (mParentRecyclerView == null) {
117 | ViewParent parentView = getParent();
118 | while (!(parentView instanceof ParentRecyclerView)) {
119 | parentView = parentView.getParent();
120 | }
121 | mParentRecyclerView = (ParentRecyclerView) parentView;
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/library/src/main/java/com/yuyang/library/nestedrv/INestedParentAdapter.java:
--------------------------------------------------------------------------------
1 | package com.yuyang.library.nestedrv;
2 |
3 | /**
4 | * ParentAdapter 需实现此接口
5 | *
6 | * Created by yuyang on 2023/12/4.
7 | */
8 | public interface INestedParentAdapter {
9 |
10 | /**
11 | * 获取当前需要联动的子RecyclerView
12 | *
13 | * @return
14 | */
15 | ChildRecyclerView getCurrentChildRecyclerView();
16 | }
17 |
--------------------------------------------------------------------------------
/library/src/main/java/com/yuyang/library/nestedrv/NestedOverScroller.java:
--------------------------------------------------------------------------------
1 | package com.yuyang.library.nestedrv;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.support.v7.widget.RecyclerView;
5 |
6 | import java.lang.reflect.Field;
7 | import java.lang.reflect.Method;
8 |
9 | /**
10 | * 平滑滚动效果的辅助类
11 | *
12 | *
13 | * Created by yuyang on 2023/12/4.
14 | */
15 | public class NestedOverScroller {
16 |
17 | public static float invokeCurrentVelocity(@NonNull RecyclerView rv) {
18 | try {
19 | Field viewFlinger = null;
20 | for (Class> superClass = rv.getClass().getSuperclass(); superClass != null; superClass = superClass.getSuperclass()) {
21 | try {
22 | viewFlinger = superClass.getDeclaredField("mViewFlinger");
23 | break;
24 | } catch (Throwable ignored) {
25 | }
26 | }
27 |
28 | if (viewFlinger == null) {
29 | return 0.0F;
30 | } else {
31 | viewFlinger.setAccessible(true);
32 | Object viewFlingerValue = viewFlinger.get(rv);
33 | Field scroller = viewFlingerValue.getClass().getDeclaredField("mScroller");
34 | scroller.setAccessible(true);
35 | Object scrollerValue = scroller.get(viewFlingerValue);
36 | Field scrollerY = scrollerValue.getClass().getDeclaredField("mScrollerY");
37 | scrollerY.setAccessible(true);
38 | Object scrollerYValue = scrollerY.get(scrollerValue);
39 | Field currVelocity = scrollerYValue.getClass().getDeclaredField("mCurrVelocity");
40 | currVelocity.setAccessible(true);
41 | return (Float) currVelocity.get(scrollerYValue);
42 | }
43 | } catch (Throwable ignored) {
44 | return 0.0F;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/library/src/main/java/com/yuyang/library/nestedrv/ParentRecyclerView.java:
--------------------------------------------------------------------------------
1 | package com.yuyang.library.nestedrv;
2 |
3 | import android.content.Context;
4 | import android.support.annotation.NonNull;
5 | import android.support.annotation.Nullable;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.util.AttributeSet;
8 | import android.view.MotionEvent;
9 | import android.view.VelocityTracker;
10 | import android.view.ViewConfiguration;
11 |
12 | /**
13 | * 父RecyclerView
14 | *
15 | * Created by yuyang on 2023/12/4.
16 | */
17 | public class ParentRecyclerView extends RecyclerView {
18 |
19 | private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
20 |
21 | /**
22 | * fling时的加速度
23 | */
24 | private int mVelocity = 0;
25 |
26 | private float mLastTouchY = 0f;
27 |
28 | private int mLastInterceptX;
29 | private int mLastInterceptY;
30 |
31 | /**
32 | * 用于向子容器传递 fling 速度
33 | */
34 | private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
35 | private int mMaximumFlingVelocity;
36 | private int mMinimumFlingVelocity;
37 |
38 | /**
39 | * 子容器是否消耗了滑动事件
40 | */
41 | private boolean childConsumeTouch = false;
42 | /**
43 | * 子容器消耗的滑动距离
44 | */
45 | private int childConsumeDistance = 0;
46 |
47 | public ParentRecyclerView(@NonNull Context context) {
48 | this(context, null);
49 | }
50 |
51 | public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
52 | this(context, attrs, 0);
53 | }
54 |
55 | public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
56 | super(context, attrs, defStyle);
57 | init();
58 | }
59 |
60 | private void init() {
61 | ViewConfiguration configuration = ViewConfiguration.get(getContext());
62 | mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
63 | mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
64 |
65 | addOnScrollListener(new OnScrollListener() {
66 | @Override
67 | public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
68 | super.onScrollStateChanged(recyclerView, newState);
69 | if (newState == SCROLL_STATE_IDLE) {
70 | dispatchChildFling();
71 | }
72 | }
73 | });
74 | }
75 |
76 | @Override
77 | public boolean dispatchTouchEvent(MotionEvent ev) {
78 | switch (ev.getAction()) {
79 | case MotionEvent.ACTION_DOWN:
80 | mVelocity = 0;
81 | mLastTouchY = ev.getRawY();
82 | childConsumeTouch = false;
83 | childConsumeDistance = 0;
84 |
85 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
86 | if (isScrollToBottom() && (childRecyclerView != null && !childRecyclerView.isScrollToTop())) {
87 | stopScroll();
88 | }
89 | break;
90 | case MotionEvent.ACTION_UP:
91 | case MotionEvent.ACTION_CANCEL:
92 | childConsumeTouch = false;
93 | childConsumeDistance = 0;
94 | break;
95 | default:
96 | break;
97 | }
98 |
99 | try {
100 | return super.dispatchTouchEvent(ev);
101 | } catch (Exception e) {
102 | e.printStackTrace();
103 | return false;
104 | }
105 | }
106 |
107 | @Override
108 | public boolean onInterceptTouchEvent(MotionEvent event) {
109 | if (isChildConsumeTouch(event)) {
110 | // 子容器如果消费了触摸事件,后续父容器就无法再拦截事件
111 | // 在必要的时候,子容器需调用 requestDisallowInterceptTouchEvent(false) 来允许父容器继续拦截事件
112 | return false;
113 | }
114 | // 子容器不消费触摸事件,父容器按正常流程处理
115 | return super.onInterceptTouchEvent(event);
116 | }
117 |
118 | /**
119 | * 子容器是否消费触摸事件
120 | */
121 | private boolean isChildConsumeTouch(MotionEvent event) {
122 | int x = (int) event.getRawX();
123 | int y = (int) event.getRawY();
124 | if (event.getAction() != MotionEvent.ACTION_MOVE) {
125 | mLastInterceptX = x;
126 | mLastInterceptY = y;
127 | return false;
128 | }
129 | int deltaX = x - mLastInterceptX;
130 | int deltaY = y - mLastInterceptY;
131 | if (Math.abs(deltaX) > Math.abs(deltaY) || Math.abs(deltaY) <= mTouchSlop) {
132 | return false;
133 | }
134 |
135 | return shouldChildScroll(deltaY);
136 | }
137 |
138 | /**
139 | * 子容器是否需要消费滚动事件
140 | */
141 | private boolean shouldChildScroll(int deltaY) {
142 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
143 | if (childRecyclerView == null) {
144 | return false;
145 | }
146 | if (isScrollToBottom()) {
147 | // 父容器已经滚动到底部 且 向下滑动 且 子容器还没滚动到底部
148 | return deltaY < 0 && !childRecyclerView.isScrollToBottom();
149 | } else {
150 | // 父容器还没滚动到底部 且 向上滑动 且 子容器已经滚动到顶部
151 | return deltaY > 0 && !childRecyclerView.isScrollToTop();
152 | }
153 | }
154 |
155 | @Override
156 | public boolean onTouchEvent(MotionEvent e) {
157 | if (isScrollToBottom()) {
158 | // 如果父容器已经滚动到底部,且向上滑动,且子容器还没滚动到顶部,事件传递给子容器
159 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
160 | if (childRecyclerView != null) {
161 | int deltaY = (int) (mLastTouchY - e.getRawY());
162 | if (deltaY >= 0 || !childRecyclerView.isScrollToTop()) {
163 | mVelocityTracker.addMovement(e);
164 | if (e.getAction() == MotionEvent.ACTION_UP) {
165 | // 传递剩余 fling 速度
166 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
167 | float velocityY = mVelocityTracker.getYVelocity();
168 | if (Math.abs(velocityY) > mMinimumFlingVelocity) {
169 | childRecyclerView.fling(0, -(int) velocityY);
170 | }
171 | mVelocityTracker.clear();
172 | } else {
173 | // 传递滑动事件
174 | childRecyclerView.scrollBy(0, deltaY);
175 | }
176 |
177 | childConsumeDistance += deltaY;
178 | mLastTouchY = e.getRawY();
179 | childConsumeTouch = true;
180 | return true;
181 | }
182 | }
183 | }
184 |
185 | mLastTouchY = e.getRawY();
186 |
187 | if (childConsumeTouch) {
188 | // 在同一个事件序列中,子容器消耗了部分滑动距离,需要扣除掉
189 | MotionEvent adjustedEvent = MotionEvent.obtain(
190 | e.getDownTime(),
191 | e.getEventTime(),
192 | e.getAction(),
193 | e.getX(),
194 | e.getY() + childConsumeDistance, // 更新Y坐标
195 | e.getMetaState()
196 | );
197 |
198 | boolean handled = super.onTouchEvent(adjustedEvent);
199 | adjustedEvent.recycle();
200 | return handled;
201 | }
202 |
203 | if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) {
204 | mVelocityTracker.clear();
205 | }
206 |
207 | try {
208 | return super.onTouchEvent(e);
209 | } catch (Exception ex) {
210 | ex.printStackTrace();
211 | return false;
212 | }
213 | }
214 |
215 | @Override
216 | public boolean fling(int velX, int velY) {
217 | boolean fling = super.fling(velX, velY);
218 | if (!fling || velY <= 0) {
219 | mVelocity = 0;
220 | } else {
221 | mVelocity = velY;
222 | }
223 | return fling;
224 | }
225 |
226 | private void dispatchChildFling() {
227 | // 父容器滚动到底部后,如果还有剩余加速度,传递给子容器
228 | if (isScrollToBottom() && mVelocity != 0) {
229 | // 尽量让速度传递更加平滑
230 | float mVelocity = NestedOverScroller.invokeCurrentVelocity(this);
231 | if (Math.abs(mVelocity) <= 2.0E-5F) {
232 | mVelocity = (float) this.mVelocity * 0.5F;
233 | } else {
234 | mVelocity *= 0.46F;
235 | }
236 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
237 | if (childRecyclerView != null) {
238 | childRecyclerView.fling(0, (int) mVelocity);
239 | }
240 | }
241 | mVelocity = 0;
242 | }
243 |
244 | public ChildRecyclerView findNestedScrollingChildRecyclerView() {
245 | if (getAdapter() instanceof INestedParentAdapter) {
246 | return ((INestedParentAdapter) getAdapter()).getCurrentChildRecyclerView();
247 | }
248 | return null;
249 | }
250 |
251 | public boolean isScrollToBottom() {
252 | return !canScrollVertically(1);
253 | }
254 |
255 | public boolean isScrollToTop() {
256 | return !canScrollVertically(-1);
257 | }
258 |
259 | @Override
260 | public void scrollToPosition(final int position) {
261 | checkChildNeedScrollToTop(position);
262 |
263 | super.scrollToPosition(position);
264 | }
265 |
266 | @Override
267 | public void smoothScrollToPosition(int position) {
268 | checkChildNeedScrollToTop(position);
269 |
270 | super.smoothScrollToPosition(position);
271 | }
272 |
273 | private void checkChildNeedScrollToTop(int position) {
274 | if (position == 0) {
275 | // 父容器滚动到顶部,从交互上来说子容器也需要滚动到顶部
276 | ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
277 | if (childRecyclerView != null) {
278 | childRecyclerView.scrollToPosition(0);
279 | }
280 | }
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/record.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smuyyh/NestedRecyclerView/5354e1768032af9904f97d666a55ad5658c2840e/record.gif
--------------------------------------------------------------------------------
/record.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smuyyh/NestedRecyclerView/5354e1768032af9904f97d666a55ad5658c2840e/record.mp4
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smuyyh/NestedRecyclerView/5354e1768032af9904f97d666a55ad5658c2840e/screenshot.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 |
16 | rootProject.name = "NestedRecyclerView"
17 |
18 | include ':app'
19 | include ':library'
20 |
--------------------------------------------------------------------------------