1+ package com .fastcomments .sdk ;
2+
3+ import android .content .Context ;
4+ import android .util .AttributeSet ;
5+ import android .view .MotionEvent ;
6+ import android .view .View ;
7+ import android .view .ViewConfiguration ;
8+ import android .widget .FrameLayout ;
9+
10+ import androidx .annotation .NonNull ;
11+ import androidx .annotation .Nullable ;
12+ import androidx .viewpager2 .widget .ViewPager2 ;
13+
14+ /**
15+ * Layout that prevents horizontal touch event conflicts when nesting a horizontal ViewPager2
16+ * inside another horizontal ViewPager2. This specifically handles the case where image galleries
17+ * need to work properly when the feed is embedded in a parent ViewPager2.
18+ *
19+ * Key behaviors:
20+ * - Only handles horizontal scroll conflicts (preserves vertical scrolling)
21+ * - Allows child ViewPager2 to consume horizontal swipes when it can scroll
22+ * - Delegates to parent when child reaches horizontal bounds
23+ * - Never interferes with vertical scrolling of the feed
24+ *
25+ * Based on Google's NestedScrollableHost but optimized for horizontal-only conflicts.
26+ */
27+ public class NestedScrollableHost extends FrameLayout {
28+ private int touchSlop ;
29+ private float initialX = 0f ;
30+ private float initialY = 0f ;
31+
32+ public NestedScrollableHost (@ NonNull Context context ) {
33+ super (context );
34+ init ();
35+ }
36+
37+ public NestedScrollableHost (@ NonNull Context context , @ Nullable AttributeSet attrs ) {
38+ super (context , attrs );
39+ init ();
40+ }
41+
42+ public NestedScrollableHost (@ NonNull Context context , @ Nullable AttributeSet attrs , int defStyleAttr ) {
43+ super (context , attrs , defStyleAttr );
44+ init ();
45+ }
46+
47+ private void init () {
48+ touchSlop = ViewConfiguration .get (getContext ()).getScaledTouchSlop ();
49+ }
50+
51+ @ Override
52+ public boolean onInterceptTouchEvent (MotionEvent ev ) {
53+ handleInterceptTouchEvent (ev );
54+ return super .onInterceptTouchEvent (ev );
55+ }
56+
57+ private void handleInterceptTouchEvent (MotionEvent ev ) {
58+ switch (ev .getAction ()) {
59+ case MotionEvent .ACTION_DOWN :
60+ initialX = ev .getX ();
61+ initialY = ev .getY ();
62+ // Don't block parent initially - let them know we might want to handle horizontals
63+ break ;
64+
65+ case MotionEvent .ACTION_MOVE :
66+ float deltaX = Math .abs (ev .getX () - initialX );
67+ float deltaY = Math .abs (ev .getY () - initialY );
68+
69+ // Only handle horizontal scroll conflicts when parent is horizontal ViewPager2
70+ if (isParentHorizontalViewPager () && deltaX > touchSlop && deltaX > deltaY ) {
71+ // This is primarily a horizontal swipe
72+ boolean swipeLeft = (ev .getX () - initialX ) < 0 ;
73+
74+ // Check if our child ViewPager2 can scroll in the swipe direction
75+ if (canChildScrollHorizontally (swipeLeft )) {
76+ // Child can handle this horizontal scroll, block parent
77+ getParent ().requestDisallowInterceptTouchEvent (true );
78+ } else {
79+ // Child can't scroll further horizontally, let parent handle
80+ getParent ().requestDisallowInterceptTouchEvent (false );
81+ }
82+ } else if (deltaY > touchSlop && deltaY > deltaX ) {
83+ // This is primarily a vertical swipe - always let parent handle
84+ // (for feed scrolling)
85+ getParent ().requestDisallowInterceptTouchEvent (false );
86+ }
87+ break ;
88+
89+ case MotionEvent .ACTION_UP :
90+ case MotionEvent .ACTION_CANCEL :
91+ // Reset state
92+ getParent ().requestDisallowInterceptTouchEvent (false );
93+ break ;
94+ }
95+ }
96+
97+ /**
98+ * Check if the child view can scroll horizontally in the specified direction
99+ * @param swipeLeft true if swiping left, false if swiping right
100+ * @return true if the child can scroll in the specified direction
101+ */
102+ private boolean canChildScrollHorizontally (boolean swipeLeft ) {
103+ View child = getChildAt (0 );
104+ if (child == null ) {
105+ return false ;
106+ }
107+
108+ if (swipeLeft ) {
109+ // Swiping left means scrolling to show next item (positive direction)
110+ return child .canScrollHorizontally (1 );
111+ } else {
112+ // Swiping right means scrolling to show previous item (negative direction)
113+ return child .canScrollHorizontally (-1 );
114+ }
115+ }
116+
117+ /**
118+ * Check if there's a parent ViewPager2 that's oriented horizontally
119+ * We only need to handle conflicts with horizontal parent ViewPagers
120+ */
121+ private boolean isParentHorizontalViewPager () {
122+ ViewPager2 parentVp = findParentViewPager2 ();
123+ if (parentVp != null ) {
124+ return parentVp .getOrientation () == ViewPager2 .ORIENTATION_HORIZONTAL ;
125+ }
126+ // If no parent ViewPager2, no conflict to handle
127+ return false ;
128+ }
129+
130+ /**
131+ * Find the parent ViewPager2 in the view hierarchy
132+ */
133+ private ViewPager2 findParentViewPager2 () {
134+ View parent = (View ) getParent ();
135+ while (parent != null ) {
136+ if (parent instanceof ViewPager2 ) {
137+ return (ViewPager2 ) parent ;
138+ }
139+ if (parent .getParent () instanceof View ) {
140+ parent = (View ) parent .getParent ();
141+ } else {
142+ break ;
143+ }
144+ }
145+ return null ;
146+ }
147+ }
0 commit comments