@@ -39,6 +39,8 @@ public class MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource> : Recycler
3939 EmptyViewAdapter _emptyViewAdapter ;
4040 readonly DataChangeObserver _emptyCollectionObserver ;
4141 readonly DataChangeObserver _itemsUpdateScrollObserver ;
42+ readonly Func < MotionEvent , bool > _dispatchTouchEventToRecyclerView ;
43+ ParentScrollGestureDispatcher _parentScrollGestureDispatcher ;
4244
4345 ScrollBarVisibility _defaultHorizontalScrollVisibility = ScrollBarVisibility . Default ;
4446 ScrollBarVisibility _defaultVerticalScrollVisibility = ScrollBarVisibility . Default ;
@@ -60,6 +62,8 @@ public MauiRecyclerView(Context context, Func<IItemsLayout> getItemsLayout, Func
6062
6163 _emptyCollectionObserver = new DataChangeObserver ( UpdateEmptyViewVisibility ) ;
6264 _itemsUpdateScrollObserver = new DataChangeObserver ( AdjustScrollForItemUpdate ) ;
65+ _dispatchTouchEventToRecyclerView = DispatchTouchEventToRecyclerView ;
66+ _parentScrollGestureDispatcher = new ParentScrollGestureDispatcher ( this ) ;
6367 }
6468
6569 protected override void OnAttachedToWindow ( )
@@ -624,6 +628,23 @@ public override bool OnTouchEvent(MotionEvent e)
624628 return base . OnTouchEvent ( e ) ;
625629 }
626630
631+ bool DispatchTouchEventToRecyclerView ( MotionEvent e ) => base . DispatchTouchEvent ( e ) ;
632+
633+ public override bool DispatchTouchEvent ( MotionEvent e )
634+ {
635+ if ( ItemsView ? . IsEnabled == false && ! ItemsView . IsExplicitlyEnabled )
636+ {
637+ return base . DispatchTouchEvent ( e ) ;
638+ }
639+
640+ if ( _parentScrollGestureDispatcher ? . TryDispatchToParent ( e , _dispatchTouchEventToRecyclerView , out var handled ) == true )
641+ {
642+ return handled ;
643+ }
644+
645+ return base . DispatchTouchEvent ( e ) ;
646+ }
647+
627648 public override bool OnInterceptTouchEvent ( MotionEvent e )
628649 {
629650 // If ItemsView is disabled, intercept all touch events to prevent interactions.
@@ -653,6 +674,12 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b)
653674
654675 protected override void Dispose ( bool disposing )
655676 {
677+ if ( disposing )
678+ {
679+ _parentScrollGestureDispatcher ? . Dispose ( ) ;
680+ _parentScrollGestureDispatcher = null ;
681+ }
682+
656683 base . Dispose ( disposing ) ;
657684 if ( disposing )
658685 {
@@ -662,6 +689,236 @@ protected override void Dispose(bool disposing)
662689
663690 internal ScrollHelper ScrollHelper => _scrollHelper ??= new ScrollHelper ( this ) ;
664691
692+ bool CanHandleOwnScrollDirection => this is not MauiCarouselRecyclerView carouselRecyclerView || carouselRecyclerView . IsSwipeEnabled ;
693+
694+ class ParentScrollGestureDispatcher : IDisposable
695+ {
696+ readonly MauiRecyclerView < TItemsView , TAdapter , TItemsViewSource > _owner ;
697+ readonly int [ ] _targetLocation = new int [ 2 ] ;
698+ MotionEvent _downEvent ;
699+ AView _parentScrollTarget ;
700+ float _touchStartX ;
701+ float _touchStartY ;
702+ int ? _scaledTouchSlop ;
703+ GestureOwner _gestureOwner ;
704+
705+ public ParentScrollGestureDispatcher ( MauiRecyclerView < TItemsView , TAdapter , TItemsViewSource > owner )
706+ {
707+ _owner = owner ;
708+ }
709+
710+ public bool TryDispatchToParent ( MotionEvent e , Func < MotionEvent , bool > dispatchToRecyclerView , out bool handled )
711+ {
712+ handled = false ;
713+
714+ if ( _gestureOwner == GestureOwner . Parent )
715+ {
716+ ForwardToParent ( e ) ;
717+
718+ if ( IsTouchEnd ( e ) )
719+ {
720+ Reset ( ) ;
721+ }
722+
723+ handled = true ;
724+ return true ;
725+ }
726+
727+ if ( _gestureOwner == GestureOwner . RecyclerView )
728+ {
729+ if ( IsTouchEnd ( e ) )
730+ {
731+ Reset ( ) ;
732+ }
733+
734+ return false ;
735+ }
736+
737+ switch ( e . ActionMasked )
738+ {
739+ case MotionEventActions . Down :
740+ TrackDown ( e ) ;
741+ return false ;
742+ case MotionEventActions . Move :
743+ return TryStartForwardingToParent ( e , dispatchToRecyclerView , out handled ) ;
744+ case MotionEventActions . Up :
745+ case MotionEventActions . Cancel :
746+ Reset ( ) ;
747+ return false ;
748+ }
749+
750+ return false ;
751+ }
752+
753+ public void Dispose ( )
754+ {
755+ Reset ( ) ;
756+ }
757+
758+ void TrackDown ( MotionEvent e )
759+ {
760+ Reset ( ) ;
761+ _touchStartX = e . RawX ;
762+ _touchStartY = e . RawY ;
763+ _downEvent = MotionEvent . Obtain ( e ) ;
764+ _owner . Parent ? . RequestDisallowInterceptTouchEvent ( false ) ;
765+ }
766+
767+ bool TryStartForwardingToParent ( MotionEvent e , Func < MotionEvent , bool > dispatchToRecyclerView , out bool handled )
768+ {
769+ handled = false ;
770+
771+ var layoutManager = _owner . GetLayoutManager ( ) ;
772+
773+ if ( layoutManager is null )
774+ {
775+ return false ;
776+ }
777+
778+ var canScrollHorizontally = layoutManager . CanScrollHorizontally ( ) ;
779+ var canScrollVertically = layoutManager . CanScrollVertically ( ) ;
780+
781+ if ( canScrollHorizontally == canScrollVertically )
782+ {
783+ return false ;
784+ }
785+
786+ var deltaX = Math . Abs ( e . RawX - _touchStartX ) ;
787+ var deltaY = Math . Abs ( e . RawY - _touchStartY ) ;
788+
789+ if ( deltaX < ScaledTouchSlop && deltaY < ScaledTouchSlop )
790+ {
791+ return false ;
792+ }
793+
794+ var movesInOwnScrollDirection = canScrollHorizontally
795+ ? deltaX >= deltaY
796+ : deltaY >= deltaX ;
797+
798+ if ( movesInOwnScrollDirection )
799+ {
800+ _gestureOwner = GestureOwner . RecyclerView ;
801+ _owner . Parent ? . RequestDisallowInterceptTouchEvent ( _owner . CanHandleOwnScrollDirection ) ;
802+ return false ;
803+ }
804+
805+ var target = FindParentScrollTarget ( e , canScrollHorizontally ) ;
806+
807+ if ( target is null )
808+ {
809+ return false ;
810+ }
811+
812+ _parentScrollTarget = target ;
813+ _gestureOwner = GestureOwner . Parent ;
814+ _owner . Parent ? . RequestDisallowInterceptTouchEvent ( false ) ;
815+ CancelRecyclerViewGesture ( e , dispatchToRecyclerView ) ;
816+
817+ if ( _downEvent is not null )
818+ {
819+ ForwardToParent ( _downEvent ) ;
820+ }
821+
822+ ForwardToParent ( e ) ;
823+ handled = true ;
824+ return true ;
825+ }
826+
827+ AView FindParentScrollTarget ( MotionEvent e , bool recyclerViewScrollsHorizontally )
828+ {
829+ var scrollDirection = recyclerViewScrollsHorizontally
830+ ? Math . Sign ( _touchStartY - e . RawY )
831+ : Math . Sign ( _touchStartX - e . RawX ) ;
832+
833+ if ( scrollDirection == 0 )
834+ {
835+ return null ;
836+ }
837+
838+ var parent = _owner . Parent ;
839+
840+ while ( parent is not null )
841+ {
842+ if ( parent is AView view )
843+ {
844+ var canScroll = recyclerViewScrollsHorizontally
845+ ? view . CanScrollVertically ( scrollDirection )
846+ : view . CanScrollHorizontally ( scrollDirection ) ;
847+
848+ if ( canScroll )
849+ {
850+ return view ;
851+ }
852+ }
853+
854+ parent = parent . GetParent ( ) ;
855+ }
856+
857+ return null ;
858+ }
859+
860+ void CancelRecyclerViewGesture ( MotionEvent e , Func < MotionEvent , bool > dispatchToRecyclerView )
861+ {
862+ var cancelEvent = MotionEvent . Obtain ( e ) ;
863+ cancelEvent . Action = MotionEventActions . Cancel ;
864+
865+ try
866+ {
867+ dispatchToRecyclerView ( cancelEvent ) ;
868+ }
869+ finally
870+ {
871+ cancelEvent . Recycle ( ) ;
872+ }
873+ }
874+
875+ void ForwardToParent ( MotionEvent source )
876+ {
877+ if ( _parentScrollTarget is null )
878+ {
879+ return ;
880+ }
881+
882+ var targetEvent = MotionEvent . Obtain ( source ) ;
883+ _parentScrollTarget . GetLocationOnScreen ( _targetLocation ) ;
884+ targetEvent . SetLocation ( source . RawX - _targetLocation [ 0 ] , source . RawY - _targetLocation [ 1 ] ) ;
885+
886+ try
887+ {
888+ _parentScrollTarget . OnTouchEvent ( targetEvent ) ;
889+ }
890+ finally
891+ {
892+ targetEvent . Recycle ( ) ;
893+ }
894+ }
895+
896+ void Reset ( )
897+ {
898+ _owner . Parent ? . RequestDisallowInterceptTouchEvent ( false ) ;
899+ _parentScrollTarget = null ;
900+ _gestureOwner = GestureOwner . Undecided ;
901+
902+ if ( _downEvent is not null )
903+ {
904+ _downEvent . Recycle ( ) ;
905+ _downEvent = null ;
906+ }
907+ }
908+
909+ int ScaledTouchSlop => _scaledTouchSlop ??= ViewConfiguration . Get ( _owner . Context ) . ScaledTouchSlop ;
910+
911+ static bool IsTouchEnd ( MotionEvent e ) =>
912+ e . ActionMasked == MotionEventActions . Up || e . ActionMasked == MotionEventActions . Cancel ;
913+
914+ enum GestureOwner
915+ {
916+ Undecided ,
917+ RecyclerView ,
918+ Parent
919+ }
920+ }
921+
665922 internal void UpdateEmptyViewVisibility ( )
666923 {
667924 if ( ItemsViewAdapter == null )
0 commit comments