From 4d2e43e1baa7c64b85d238902eb91c5236d33ca9 Mon Sep 17 00:00:00 2001 From: Naveenkumar S Date: Wed, 4 Dec 2024 18:41:50 +0530 Subject: [PATCH 1/3] Added bottom sheet control changes --- maui/src/BottomSheet/BottomSheetBorder.cs | 56 + maui/src/BottomSheet/Enum.cs | 76 + maui/src/BottomSheet/SfBottomSheet.Windows.cs | 227 ++ maui/src/BottomSheet/SfBottomSheet.cs | 2328 +++++++++++++++++ maui/src/BottomSheet/SfBottomSheet.iOS.cs | 65 + maui/src/BottomSheet/StateChangedEventArgs.cs | 23 + .../Core/Theme/Resources/DefaultTheme.xaml | 9 +- maui/src/Themes/SfBottomSheetStyle.xaml | 12 + maui/src/Themes/SfBottomSheetStyle.xaml.cs | 17 + 9 files changed, 2812 insertions(+), 1 deletion(-) create mode 100644 maui/src/BottomSheet/BottomSheetBorder.cs create mode 100644 maui/src/BottomSheet/Enum.cs create mode 100644 maui/src/BottomSheet/SfBottomSheet.Windows.cs create mode 100644 maui/src/BottomSheet/SfBottomSheet.cs create mode 100644 maui/src/BottomSheet/SfBottomSheet.iOS.cs create mode 100644 maui/src/BottomSheet/StateChangedEventArgs.cs create mode 100644 maui/src/Themes/SfBottomSheetStyle.xaml create mode 100644 maui/src/Themes/SfBottomSheetStyle.xaml.cs diff --git a/maui/src/BottomSheet/BottomSheetBorder.cs b/maui/src/BottomSheet/BottomSheetBorder.cs new file mode 100644 index 0000000..7db3437 --- /dev/null +++ b/maui/src/BottomSheet/BottomSheetBorder.cs @@ -0,0 +1,56 @@ +using Syncfusion.Maui.Toolkit.Helper; +using Syncfusion.Maui.Toolkit.Internals; + +namespace Syncfusion.Maui.Toolkit.BottomSheet +{ + /// + /// Represents the that defines the layout of bottom sheet. + /// + internal class BottomSheetBorder : SfBorder, ITouchListener + { + #region Fields + + // To store the weak reference of bottom sheet instance. + readonly WeakReference? _bottomSheetRef; + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The SfBottomSheet instance. + /// Thrown if bottomSheet is null. + public BottomSheetBorder(SfBottomSheet bottomSheet) + { + if (bottomSheet is not null) + { + _bottomSheetRef = new WeakReference(bottomSheet); + this.AddTouchListener(this); + } + } + + #endregion + + #region Interface Implementation + + /// + /// Method to invoke swiping in bottom sheet. + /// + /// e. + public void OnTouch(Toolkit.Internals.PointerEventArgs e) + { +#if IOS || MACCATALYST || ANDROID + if (e is not null && _bottomSheetRef is not null) + { + if (_bottomSheetRef.TryGetTarget(out var bottomSheet)) + { + bottomSheet.OnHandleTouch(e.Action, e.TouchPoint); + } + } +#endif + } + #endregion + } +} \ No newline at end of file diff --git a/maui/src/BottomSheet/Enum.cs b/maui/src/BottomSheet/Enum.cs new file mode 100644 index 0000000..c0268ef --- /dev/null +++ b/maui/src/BottomSheet/Enum.cs @@ -0,0 +1,76 @@ +namespace Syncfusion.Maui.Toolkit.BottomSheet +{ + /// + /// Specifies the current display state of the SfBottomSheet control. + /// + /// + /// This enum is used to represent the various possible states of the bottom sheet, + /// allowing for precise control and state management of the UI component. + /// + public enum BottomSheetState + { + /// + /// Indicates that the bottom sheet is fully expanded to cover the entire screen. + /// + FullExpanded, + + /// + /// Represents the state where the bottom sheet is expanded to cover approximately half of the screen. + /// + HalfExpanded, + + /// + /// Denotes that the bottom sheet is in its minimized or collapsed state, typically showing only a small portion or header. + /// + Collapsed, + + /// + /// Signifies that the bottom sheet is completely hidden from view. + /// + Hidden + } + + /// + /// Defines the allowable states for the SfBottomSheet control. + /// + /// + /// This enum is used to configure the permitted states of the bottom sheet, + /// enabling developers to restrict or allow specific display modes. + /// + public enum BottomSheetAllowedState + { + /// + /// Configures the bottom sheet to only allow full screen expansion. + /// When set, the bottom sheet can only be fully expanded or hidden. + /// + FullExpanded, + + /// + /// Restricts the bottom sheet to only permit half screen expansion. + /// When set, the bottom sheet can only be half expanded or hidden. + /// + HalfExpanded, + + /// + /// Allows the bottom sheet to be displayed in both full screen and half screen modes. + /// This option provides the most flexibility, permitting all possible states. + /// + All + } + + /// + /// Defines the content width mode for the SfBottomSheet control. + /// + public enum BottomSheetContentWidthMode + { + /// + /// The BottomSheet will span the full width of the parent container. + /// + Full, + + /// + /// The BottomSheet will use a custom width value, centered if not full width. + /// + Custom + } +} diff --git a/maui/src/BottomSheet/SfBottomSheet.Windows.cs b/maui/src/BottomSheet/SfBottomSheet.Windows.cs new file mode 100644 index 0000000..3c9d2ba --- /dev/null +++ b/maui/src/BottomSheet/SfBottomSheet.Windows.cs @@ -0,0 +1,227 @@ +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml; +using Syncfusion.Maui.Toolkit.Internals; + +namespace Syncfusion.Maui.Toolkit.BottomSheet +{ + public partial class SfBottomSheet + { + #region Fields + + /// + /// The native platform view of the bottom sheet, used for handling touch and manipulation events. + /// + FrameworkElement? _bottomSheetNativeView; + + /// + /// A flag indicating whether a manipulation gesture (e.g., drag) has started on the bottom sheet. + /// + bool _isManipulationStarted; + + /// + /// A flag that tracks whether the touch event has been handled, specifically for touch devices. + /// + bool _isTouchHandled; + + #endregion + + #region Override Methods + + /// + /// Raises on handler changing. + /// + /// Handler changing event arguments. + protected override void OnHandlerChanging(HandlerChangingEventArgs args) + { + base.OnHandlerChanging(args); + } + + #endregion + + #region Private Methods + + /// + /// Configures touch events based on the availability of the platform-specific view. + /// + void ConfigureTouch() + { + if (Handler is not null && Handler.PlatformView is not null) + { + WireEvents(); + } + else + { + UnWireEvents(); + } + } + + /// + /// Attaches necessary touch and manipulation event handlers to the platform-specific view. + /// + void WireEvents() + { + if (Handler is not null && Handler.PlatformView is not null && Handler.PlatformView is FrameworkElement) + { + _bottomSheetNativeView = Handler.PlatformView as FrameworkElement; + if (_bottomSheetNativeView is not null) + { + _bottomSheetNativeView.ManipulationMode = ManipulationModes.All; + _bottomSheetNativeView.ManipulationStarted += OnManipulationStarted; + _bottomSheetNativeView.PointerPressed += OnPointerPressed; + _bottomSheetNativeView.ManipulationDelta += OnManipulationDelta; + _bottomSheetNativeView.ManipulationCompleted += OnManipulationCompleted; + _bottomSheetNativeView.PointerReleased += OnPointerReleased; + _bottomSheetNativeView.PointerMoved += OnPointerMoved; + } + } + } + + /// + /// Detaches previously attached touch and manipulation event handlers from the platform-specific view. + /// + void UnWireEvents() + { + if (Handler is not null && Handler.PlatformView is not null) + { + _bottomSheetNativeView = Handler.PlatformView as FrameworkElement; + if (_bottomSheetNativeView is not null) + { + _bottomSheetNativeView.ManipulationMode = ManipulationModes.All; + _bottomSheetNativeView.ManipulationStarted -= OnManipulationStarted; + _bottomSheetNativeView.PointerPressed -= OnPointerPressed; + _bottomSheetNativeView.ManipulationDelta -= OnManipulationDelta; + _bottomSheetNativeView.ManipulationCompleted -= OnManipulationCompleted; + _bottomSheetNativeView.PointerReleased -= OnPointerReleased; + _bottomSheetNativeView.PointerMoved -= OnPointerMoved; + } + } + } + + /// + /// Handles the start of a manipulation gesture on the platform-specific view. + /// + /// The source of the event, typically the platform view. + /// The manipulation started event arguments containing position details. + void OnManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e) + { + if (!_isPointerPressed && _bottomSheet is not null) + { + if (e.Position.Y < _bottomSheet.TranslationY) + { + return; + } + + // Handle the start of the manipulation + OnHandleTouch(PointerActions.Pressed, new Point(e.Position.X, e.Position.Y)); + _isManipulationStarted = true; + } + } + + /// + /// Handles the pointer press action on the platform-specific view. + /// + /// The source of the event, typically the platform view. + /// Pointer routed event arguments containing pointer details. + void OnPointerPressed(object sender, PointerRoutedEventArgs e) + { + if (_bottomSheetNativeView is not null && _bottomSheet is not null) + { + // Get the pointer position relative to the view + var point = e.GetCurrentPoint(_bottomSheetNativeView).Position; + if (point.Y < _bottomSheet.TranslationY) + { + return; + } + + // Handle the pointer press action + OnHandleTouch(PointerActions.Pressed, new Point(point.X, point.Y)); + } + + if (e.Pointer.PointerDeviceType == Microsoft.UI.Input.PointerDeviceType.Touch) + { + _isTouchHandled = true; + } + else + { + _isTouchHandled = false; + } + } + + /// + /// Handles the pointer move action on the platform-specific view. + /// + /// The source of the event, typically the platform view. + /// Pointer routed event arguments containing pointer details. + void OnPointerMoved(object sender, PointerRoutedEventArgs e) + { + if (e.Pointer.PointerDeviceType == Microsoft.UI.Input.PointerDeviceType.Mouse && _isTouchHandled) + { + return; + } + + if (_isPointerPressed && _bottomSheetNativeView is not null) + { + // Get the pointer position relative to the view + var point = e.GetCurrentPoint(_bottomSheetNativeView).Position; + // Handle the pointer move action + OnHandleTouch(PointerActions.Moved, new Point(point.X, point.Y)); + } + } + + /// + /// Handles the pointer release action on the platform-specific view. + /// + /// The source of the event, typically the platform view. + /// Pointer routed event arguments containing pointer details. + void OnPointerReleased(object sender, PointerRoutedEventArgs e) + { + if (_bottomSheetNativeView is not null) + { + var point = e.GetCurrentPoint(_bottomSheetNativeView).Position; + OnHandleTouch(PointerActions.Released, new Point(point.X, point.Y)); + } + + _isManipulationStarted = false; + _isTouchHandled = false; + } + + /// + /// Handles the completion of a manipulation gesture on the platform-specific view. + /// + /// The source of the event, typically the platform view. + /// Manipulation completed event arguments containing details about the gesture. + void OnManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) + { + if (_isPointerPressed && _isManipulationStarted) + { + // Handle the release action at the end of manipulation. + OnHandleTouch(PointerActions.Released, new Point(e.Position.X, e.Position.Y)); + // Reset the manipulation state + _isManipulationStarted = false; + } + } + + /// + /// Handles the ongoing manipulation action (such as drag) as the user moves their pointer. + /// + /// The source of the event, typically the platform view. + /// Manipulation delta event arguments containing details about the ongoing manipulation. + void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) + { + if (_isPointerPressed && _isManipulationStarted) + { + // Handle the intermediate manipulation action + OnHandleTouch(PointerActions.Moved, new Point(e.Position.X, e.Position.Y)); + + // Check for boundary conditions + if (e.Position.Y < 0 || e.Position.Y > Height) + { + // Handle the release action if outside of content width + OnHandleTouch(PointerActions.Released, new Point(e.Position.X, e.Position.Y)); + } + } + } + + #endregion + } +} diff --git a/maui/src/BottomSheet/SfBottomSheet.cs b/maui/src/BottomSheet/SfBottomSheet.cs new file mode 100644 index 0000000..d8e44b8 --- /dev/null +++ b/maui/src/BottomSheet/SfBottomSheet.cs @@ -0,0 +1,2328 @@ +using Microsoft.Maui.Controls.Shapes; +using Syncfusion.Maui.Toolkit.Internals; +using Syncfusion.Maui.Toolkit.Themes; +using Syncfusion.Maui.Toolkit.Helper; +using Syncfusion.Maui.ToolKit.BottomSheet; + +namespace Syncfusion.Maui.Toolkit.BottomSheet +{ + + /// + /// Represents the control that displays from the bottom of the screen. + /// + [ContentProperty(nameof(Content))] + public partial class SfBottomSheet : SfView, IParentThemeElement + { + #region Fields + + // Overlay and content + /// + /// The grid used to create the overlay effect. + /// + SfGrid? _overlayGrid; + + /// + /// The border control representing the main bottom sheet. + /// + BottomSheetBorder? _bottomSheet; + + /// + /// The grid containing the content of the bottom sheet. + /// + SfGrid? _bottomSheetContent; + + /// + /// A border control used to add padding to the bottom sheet content. + /// + SfBorder? _contentBorder; + + // Grabber + /// + /// The border control representing the grabber (drag handle) of the bottom sheet. + /// + SfBorder? _grabber; + + /// + /// The shape used to provide corner radius for the grabber. + /// + RoundRectangle? _grabberStrokeShape; + + // Shape + /// + /// The shape used to provide corner radius for the bottom sheet. + /// + RoundRectangle? _bottomSheetStrokeShape; + + // State + /// + /// Indicates whether the bottom sheet is in a half-expanded state. + /// + bool _isHalfExpanded = true; + + /// + /// Indicates whether the bottom sheet is currently open. + /// + bool _isSheetOpen; + + /// + /// Indicates whether a pointer (touch or mouse) is currently pressed on the bottom sheet. + /// + bool _isPointerPressed; + + // Touch tracking + /// + /// The initial Y-coordinate of a touch event on the bottom sheet. + /// + double _initialTouchY; + + /// + /// The starting Y-coordinate of a swipe gesture on the bottom sheet. + /// + double _startTouchY; + + /// + /// The ending Y-coordinate of a swipe gesture on the bottom sheet. + /// + double _endTouchY; + + // Event args + /// + /// Event arguments used to track state changes in the bottom sheet. + /// + readonly StateChangedEventArgs _stateChangedEventArgs = new StateChangedEventArgs(); + + // Constants + /// + /// The default opacity value for the overlay. + /// + const double DefaultOverlayOpacity = 0.5; + + /// + /// The default height value for the collapsed state of the bottom sheet. + /// + const double MinimizedHeight = 100; + + // Grabber constants + /// + /// The default height of the grabber. + /// + const double DefaultGrabberHeight = 4; + + /// + /// The default width of the grabber. + /// + const double DefaultGrabberWidth = 32; + + /// + /// The default corner radius of the grabber. + /// + const double DefaultGrabberCornerRadius = 12; + + // Ratio constants + /// + /// The default height of the row containing the grabber. + /// + const double DefaultGrabberRowHeight = 30; + + /// + /// The minimum allowed value for the HalfExpandedRatio property. + /// + /// + /// This value represents the smallest fraction of the screen height that the bottom sheet can occupy when half-expanded. + /// + const double MinHalfExpandedRatio = 0.1; + + /// + /// The maximum allowed value for the HalfExpandedRatio property. + /// + /// + /// This value represents the largest fraction of the screen height that the bottom sheet can occupy when half-expanded. + /// + const double MaxHalfExpandedRatio = 0.9; + + /// + /// The minimum allowed value for the FullExpandedRatio property. + /// + /// + /// This value represents the smallest fraction of the screen height that the bottom sheet can occupy when full-expanded. + /// + const double MinFullfExpandedRatio = 0.1; + + /// + /// The maximum allowed value for the FullExpandedRatio property. + /// + /// + /// This value represents the largest fraction of the screen height that the bottom sheet can occupy when full-expanded. + /// + const double MaxFullfExpandedRatio = 1; + + /// + /// The default ratio value for the half-expanded. + /// + const double DefaultHalfExpandedRatio = 0.5; + + /// + /// The default ratio value for the full-expanded. + /// + const double DefaultFullExpandedRatio = 1; + + #endregion + + #region Bindable Properties + + /// + /// Identifies the bindable property. + /// + /// + /// It is mandatory to set the property for the when initializing. + /// + public static readonly BindableProperty ContentProperty = + BindableProperty.Create( + nameof(Content), + typeof(View), + typeof(SfBottomSheet), + null, + BindingMode.Default, + null, + propertyChanged: OnContentChanged); + + // Content and State + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty BottomSheetContentProperty = BindableProperty.Create( + nameof(BottomSheetContent), + typeof(View), + typeof(SfBottomSheet), + null, + BindingMode.Default, + propertyChanged: OnContentPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty StateProperty = BindableProperty.Create( + nameof(State), + typeof(BottomSheetState), + typeof(SfBottomSheet), + BottomSheetState.Hidden, + BindingMode.Default, + propertyChanged: OnStatePropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty HalfExpandedRatioProperty = BindableProperty.Create( + nameof(HalfExpandedRatio), + typeof(double), + typeof(SfBottomSheet), + DefaultHalfExpandedRatio, + BindingMode.Default, + propertyChanged: OnHalfExpandedRatioPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty FullExpandedRatioProperty = BindableProperty.Create( + nameof(FullExpandedRatio), + typeof(double), + typeof(SfBottomSheet), + DefaultFullExpandedRatio, + BindingMode.Default, + propertyChanged: OnFullExpandedRatioPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty CollapsedHeightProperty = BindableProperty.Create( + nameof(CollapsedHeight), + typeof(double), + typeof(SfBottomSheet), + MinimizedHeight, + BindingMode.Default, + propertyChanged: OnCollapsedHeightPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty BottomSheetContentWidthProperty = BindableProperty.Create( + nameof(BottomSheetContentWidth), + typeof(double), + typeof(SfBottomSheet), + 300.0, + BindingMode.Default, + propertyChanged: OnBottomSheetContentWidthPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty ContentWidthModeProperty = BindableProperty.Create( + nameof(ContentWidthMode), + typeof(BottomSheetContentWidthMode), + typeof(SfBottomSheet), + BottomSheetContentWidthMode.Full, + BindingMode.Default, + propertyChanged: OnContentWidthModePropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty AllowedStateProperty = BindableProperty.Create( + nameof(AllowedState), + typeof(BottomSheetAllowedState), + typeof(SfBottomSheet), + BottomSheetAllowedState.All, + BindingMode.Default, + propertyChanged: OnAllowedStatePropertyChanged); + + // Appearance + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty IsModalProperty = BindableProperty.Create( + nameof(IsModal), + typeof(bool), + typeof(SfBottomSheet), + true, + BindingMode.Default, + propertyChanged: OnIsModalPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty ShowGrabberProperty = BindableProperty.Create( + nameof(ShowGrabber), + typeof(bool), + typeof(SfBottomSheet), + true, + BindingMode.Default, + propertyChanged: OnShowGrabberPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty IsOpenProperty = BindableProperty.Create( + nameof(IsOpen), + typeof(bool), + typeof(SfBottomSheet), + false, + BindingMode.Default, + propertyChanged: OnIsOpenPropertyChanged); + + // Appearance (continued) + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty GrabberBackgroundProperty = BindableProperty.Create( + nameof(GrabberBackground), + typeof(Brush), + typeof(SfBottomSheet), + new SolidColorBrush(Color.FromArgb("#CAC4D0")), + BindingMode.Default, + propertyChanged: OnGrabberBackgroundPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly new BindableProperty BackgroundProperty = BindableProperty.Create( + nameof(Background), + typeof(Brush), + typeof(SfBottomSheet), + new SolidColorBrush(Color.FromArgb("#F7F2FB")), + BindingMode.Default, + propertyChanged: OnBackgroundPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create( + nameof(CornerRadius), + typeof(CornerRadius), + typeof(SfBottomSheet), + new CornerRadius(0), + BindingMode.Default, + propertyChanged: OnCornerRadiusPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty ContentPaddingProperty = BindableProperty.Create( + nameof(ContentPadding), + typeof(Thickness), + typeof(SfBottomSheet), + new Thickness(5), + BindingMode.Default, + propertyChanged: OnContentPaddingPropertyChanged); + + // Behavior + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty EnableSwipingProperty = BindableProperty.Create( + nameof(EnableSwiping), + typeof(bool), + typeof(SfBottomSheet), + true, + BindingMode.Default); + + // Grabber customization + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty GrabberHeightProperty = BindableProperty.Create( + nameof(GrabberHeight), + typeof(double), + typeof(SfBottomSheet), + DefaultGrabberHeight, + BindingMode.Default, + propertyChanged: OnGrabberHeightPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty GrabberWidthProperty = BindableProperty.Create( + nameof(GrabberWidth), + typeof(double), + typeof(SfBottomSheet), + DefaultGrabberWidth, + BindingMode.Default, + propertyChanged: OnGrabberWidthPropertyChanged); + + /// + /// Identifies the bindable property. + /// + /// + /// The identifier for bindable property. + /// + public static readonly BindableProperty GrabberCornerRadiusProperty = BindableProperty.Create( + nameof(GrabberCornerRadius), + typeof(CornerRadius), + typeof(SfBottomSheet), + new CornerRadius(DefaultGrabberCornerRadius), + BindingMode.Default, + propertyChanged: OnGrabberCornerRadiusPropertyChanged); + + #endregion + + #region Internal Bindable Properties + + /// + /// Identifies the bindable property. + /// + /// + /// This property determines the background color of the overlay that appears behind the bottom sheet. + /// The default value is a semi-transparent black color (#80000000). + /// + internal static readonly BindableProperty OverlayBackgroundColorProperty = BindableProperty.Create( + nameof(OverlayBackgroundColor), + typeof(Color), + typeof(SfBottomSheet), + Color.FromArgb("#80000000"), + BindingMode.Default, + propertyChanged: OnOverlayBackgroundColorChanged); + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + public SfBottomSheet() + { + ThemeElement.InitializeThemeResources(this, "SfBottomSheetTheme"); + InitializeLayout(); + ApplyThemeResources(); + } + + #endregion + + #region Properties + + /// + /// Gets or sets the view that can be used to customize the main content of the SfBottomSheet. + /// + /// + /// A that represents the content view of the bottom sheet. + /// + public View Content + { + get { return (View)GetValue(ContentProperty); } + set { SetValue(ContentProperty, value); } + } + + /// + /// Gets or sets the content of the SfBottomSheet control. + /// + /// + /// A that represents the content of the bottom sheet. + /// + public View BottomSheetContent + { + get => (View)GetValue(BottomSheetContentProperty); + set => SetValue(BottomSheetContentProperty, value); + } + + /// + /// Gets or sets the expanded state of the SfBottomSheet control. + /// + /// + /// A value. The default is . + /// Possible values are: + /// - + /// - + /// - + /// - + /// + public BottomSheetState State + { + get => (BottomSheetState)GetValue(StateProperty); + set => SetValue(StateProperty, value); + } + + /// + /// Gets or sets the height ratio of the bottom sheet in half-expanded state. + /// + /// + /// A value between 0 and 1. The default value is 0.5. + /// + /// + /// This value represents the fraction of the screen height that the bottom sheet will occupy when half-expanded. + /// For example, 0.5 means the bottom sheet will cover half of the screen. + /// + public double HalfExpandedRatio + { + get => (double)GetValue(HalfExpandedRatioProperty); + set => SetValue(HalfExpandedRatioProperty, value); + } + + /// + /// Gets or sets the height ratio of the bottom sheet in full-expanded state. + /// + /// + /// A value between 0 and 1. The default value is 1. + /// + /// + /// This value represents the fraction of the screen height that the bottom sheet will occupy when full-expanded. + /// For example, 0.75 means the bottom sheet will cover 3/4th of the screen. + /// + public double FullExpandedRatio + { + get => (double)GetValue(FullExpandedRatioProperty); + set => SetValue(FullExpandedRatioProperty, value); + } + + /// + /// Gets or sets the height of the bottom sheet in collapsed state. + /// + /// + /// A value representing the height in device-independent units. The default value is 100. + /// + /// + /// This value represents the height bottom sheet will occupy when collapsed. + /// + public double CollapsedHeight + { + get => (double)GetValue(CollapsedHeightProperty); + set => SetValue(CollapsedHeightProperty, value); + } + + /// + /// Specifies the custom width value (in pixels) for the BottomSheet when ContentWidthMode is set to Custom. + /// + public double BottomSheetContentWidth + { + get => (double)GetValue(BottomSheetContentWidthProperty); + set => SetValue(BottomSheetContentWidthProperty, value); + } + + /// + /// Gets or sets a value that customizes the content width for the bottom sheet. + /// + /// + /// A value. The default is . + /// Possible values are: + /// - + /// - + /// + public BottomSheetContentWidthMode ContentWidthMode + { + get => (BottomSheetContentWidthMode)GetValue(ContentWidthModeProperty); + set => SetValue(ContentWidthModeProperty, value); + } + + /// + /// Gets or sets a value that indicates the allowed states for the bottom sheet. + /// + /// + /// A value. The default is . + /// Possible values are: + /// - + /// - + /// - + /// + public BottomSheetAllowedState AllowedState + { + get => (BottomSheetAllowedState)GetValue(AllowedStateProperty); + set => SetValue(AllowedStateProperty, value); + } + + /// + /// Gets or sets a value that indicates whether the SfBottomSheet acts as a modal dialog. + /// + /// + /// A value. The default value is true. + /// + /// + /// When set to true, the bottom sheet will behave like a modal dialog, preventing interaction with underlying content. + /// + public bool IsModal + { + get => (bool)GetValue(IsModalProperty); + set => SetValue(IsModalProperty, value); + } + + /// + /// Gets or sets a value indicating whether to show the drag handle (grabber) in the SfBottomSheet. + /// + /// + /// A value. The default value is true. + /// + public bool ShowGrabber + { + get => (bool)GetValue(ShowGrabberProperty); + set => SetValue(ShowGrabberProperty, value); + } + + /// + /// Gets or sets a value that indicates whether the SfBottomSheet is opened or not. + /// + /// + /// A value. The default value is false. + /// + /// + /// When set to true, the bottom sheet will be opened. + /// + public bool IsOpen + { + get => (bool)GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); + } + + /// + /// Gets or sets the background of the SfBottomSheet. + /// + /// + /// A value representing the background. + /// + /// + /// This property overrides the background property from the base class. + /// + public new Brush Background + { + get => (Brush)GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + /// + /// Gets or sets the corner radius of the SfBottomSheet. + /// + /// + /// A value. The default value is 0. + /// + /// + /// This property allows you to round the corners of the bottom sheet. + /// Set all values to 0 for sharp corners, or provide individual values for each corner. + /// + public CornerRadius CornerRadius + { + get => (CornerRadius)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + /// + /// Gets or sets the padding of the content in SfBottomSheet. + /// + /// + /// A value representing the padding. The default value is 5 on all sides. + /// + /// + /// Use this property to add space between the content and the edges of the bottom sheet. + /// + public Thickness ContentPadding + { + get => (Thickness)GetValue(ContentPaddingProperty); + set => SetValue(ContentPaddingProperty, value); + } + + /// + /// Gets or sets the background color of the grabber in SfBottomSheet. + /// + /// + /// A value representing the grabber's background color. + /// + public Brush GrabberBackground + { + get => (Brush)GetValue(GrabberBackgroundProperty); + set => SetValue(GrabberBackgroundProperty, value); + } + + /// + /// Gets or sets a value indicating whether swiping is enabled in the SfBottomSheet control. + /// + /// + /// A value. The default value is true. + /// + /// + /// When set to true, users can swipe the bottom sheet to change its state. + /// When set to false, swiping gestures are disabled. + /// + public bool EnableSwiping + { + get => (bool)GetValue(EnableSwipingProperty); + set => SetValue(EnableSwipingProperty, value); + } + + /// + /// Gets or sets the height of the grabber in SfBottomSheet. + /// + /// + /// A value representing the height in device-independent units. The default value is 4. + /// + /// + /// This property allows you to customize the size of the grabber handle. + /// + public double GrabberHeight + { + get => (double)GetValue(GrabberHeightProperty); + set => SetValue(GrabberHeightProperty, value); + } + + /// + /// Gets or sets the width of the grabber in SfBottomSheet. + /// + /// + /// A value representing the width in device-independent units. The default value is 32. + /// + /// + /// This property allows you to customize the size of the grabber handle. + /// + public double GrabberWidth + { + get => (double)GetValue(GrabberWidthProperty); + set => SetValue(GrabberWidthProperty, value); + } + + /// + /// Gets or sets the corner radius of the grabber in SfBottomSheet. + /// + /// + /// A value. The default value is 0 for all corners. + /// + /// + /// Use this property to round the corners of the grabber handle. + /// + public CornerRadius GrabberCornerRadius + { + get => (CornerRadius)GetValue(GrabberCornerRadiusProperty); + set => SetValue(GrabberCornerRadiusProperty, value); + } + + #endregion + + #region Internal Properties + + /// + /// Gets or sets the background color of the overlay grid. + /// + /// + /// A value representing the background color of the overlay. + /// The default value is a semi-transparent black color. + /// + internal Color OverlayBackgroundColor + { + get => (Color)GetValue(OverlayBackgroundColorProperty); + set => SetValue(OverlayBackgroundColorProperty, value); + } + + #endregion + + #region Public Methods + + /// + /// Shows the SfBottomSheet. + /// + /// + /// Thrown when the bottom sheet or overlay grid is not initialized. + /// + public void Show() + { + if (_bottomSheet is null || _overlayGrid is null) + { + return; + } + + _bottomSheet.IsVisible = true; + if (!IsValidHeight()) + { + RegisterSizeChangedEvent(); + return; + } + + SetupBottomSheetForShow(); + _isSheetOpen = true; + AnimateBottomSheet(GetTargetPosition()); + IsOpen = true; + } + + /// + /// Closes the SfBottomSheet. + /// + /// + /// Thrown when the bottom sheet or overlay grid is not initialized. + /// + public void Close() + { + if(_bottomSheet is null || _overlayGrid is null) + { + return; + } + + AnimateBottomSheet(Height, onFinish: () => + { + _bottomSheet.IsVisible = false; + _overlayGrid.IsVisible = false; + }); + + if (_isSheetOpen) + { + _isSheetOpen = false; + IsOpen = false; + State = BottomSheetState.Hidden; + } + } + + #endregion + + #region Internal Methods + + /// + /// Raises the StateChanged event when the sheet's state changes. + /// + /// The state changed event arguments. + internal virtual void OnStateChanged(StateChangedEventArgs args) + { + StateChanged?.Invoke(this, args); + } + + /// + /// Handles touch-related logic for the bottom sheet. + /// + /// The pointer action. + /// The touch point. + internal void OnHandleTouch(PointerActions action, Point point) + { + if (!EnableSwiping || !_isSheetOpen || _bottomSheet is null) + { + return; + } + + double touchY = GetPlatformAdjustedTouchY(point); + + switch (action) + { + case PointerActions.Pressed: + HandleTouchPressed(touchY); + return; + case PointerActions.Moved: + HandleTouchMoved(touchY); + return; + case PointerActions.Released: + HandleTouchReleased(touchY); + return; + default: + // Log or handle unexpected action + return; + } + } + + #endregion + + #region Private Methods + + /// + /// Initializes the layout of the SfBottomSheet. + /// + void InitializeLayout() + { + InitializeOverlayGrid(); + InitializeGrabber(); + InitializeBottomSheetContent(); + InitializeBottomSheetBorder(); + InitializeContentBorder(); + + if (_bottomSheet is not null && _overlayGrid is not null) + { + Children.Add(_overlayGrid); + Children.Add(_bottomSheet); + _bottomSheet.IsVisible = false; + } + } + + /// + /// This method is used to clear and update the children to the SfBottomSheet. + /// + void UpdateContentView() + { + Children.Clear(); + UpdateAllChild(); + } + + /// + /// The method used to update all children of the SfBottomSheet. + /// + void UpdateAllChild() + { + AddChild(Content); + AddChild(_overlayGrid); + AddChild(_bottomSheet); + } + + /// + /// The method used to add child of the SfBottomSheet. + /// + /// The view to be set as child of the bottom sheet. + void AddChild(View? child) + { + if (child is not null) + { + Children.Add(child); + } + } + + /// + /// Initializes the overlay grid of the bottom sheet. + /// + void InitializeOverlayGrid() + { + _overlayGrid = new SfGrid() + { + BackgroundColor = OverlayBackgroundColor, + Opacity = DefaultOverlayOpacity, + IsVisible = false + }; + + var tapGestureRecognizer = new TapGestureRecognizer(); + tapGestureRecognizer.Tapped += OnOverlayGridTapped; + _overlayGrid.GestureRecognizers.Add(tapGestureRecognizer); + } + + /// + /// Initializes the grabber (drag handle) of the bottom sheet. + /// + void InitializeGrabber() + { + _grabberStrokeShape = new RoundRectangle() { CornerRadius = DefaultGrabberCornerRadius }; + + _grabber = new SfBorder() + { + Background = GrabberBackground, + Stroke = Colors.Transparent, + HeightRequest = DefaultGrabberHeight, + WidthRequest = DefaultGrabberWidth, + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + StrokeShape = _grabberStrokeShape + }; + } + + /// + /// Initializes the content of the bottom sheet. + /// + void InitializeBottomSheetContent() + { + _bottomSheetContent = new SfGrid() + { + Background = Background, + RowDefinitions = new RowDefinitionCollection + { + new RowDefinition { Height = new GridLength(DefaultGrabberRowHeight) }, + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) } + } + }; + + // Add grabber to the first row if it has been initialized + if (_grabber is not null) + { + _bottomSheetContent.Children.Add(_grabber); + SfGrid.SetRow(_grabber, 0); + } + } + + /// + /// Initializes the border of the bottom sheet. + /// + void InitializeBottomSheetBorder() + { + _bottomSheetStrokeShape = new RoundRectangle() { CornerRadius = CornerRadius }; + + _bottomSheet = new BottomSheetBorder(this) + { + Background = Background, + StrokeThickness = 0, + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Fill, + HeightRequest = CalculateInitialHeight(), + IsVisible = false, + StrokeShape = _bottomSheetStrokeShape, + Content = _bottomSheetContent ?? throw new InvalidOperationException("Bottom sheet content is not initialized.") + }; + } + + /// + /// Calculates the initial height for the half-expanded state. + /// + /// The calculated initial height. + double CalculateInitialHeight() + { + return Height > 0 ? Height * HalfExpandedRatio : 0; + } + + /// + /// Initializes the content border of the bottom sheet. + /// + void InitializeContentBorder() + { + _contentBorder = new SfBorder() + { + StrokeThickness = 0, + Padding = ContentPadding + }; + } + + /// + /// Applies theme resources to the control. + /// + void ApplyThemeResources() + { + SetDynamicResource(OverlayBackgroundColorProperty, "SfBottomSheetOverlayBackgroundColor"); + } + + /// + /// Updates the HalfExpandedRatio property with the given value. + /// + /// The new value for HalfExpandedRatio. + void UpdateHalfExpandedRatioProperty(double newValue) + { + double clampedValue = Math.Clamp(newValue, MinHalfExpandedRatio, MaxHalfExpandedRatio); + HalfExpandedRatio = clampedValue; + if (State is BottomSheetState.HalfExpanded) + { + OnSizeAllocated(Width, Height); + } + } + + /// + /// Updates the FullExpandedRatio property with the given value. + /// + /// The new value for FullExpandedRatio. + void UpdateFullExpandedRatioProperty(double newValue) + { + double clampedValue = Math.Clamp(newValue, MinFullfExpandedRatio, MaxFullfExpandedRatio); + FullExpandedRatio = clampedValue; + if (State is BottomSheetState.FullExpanded) + { + OnSizeAllocated(Width, Height); + } + } + + /// + /// Updates the CollapsedHeight property with the given value. + /// + /// The new value for CollapsedHeight. + void UpdateCollapsedHeightProperty(double newValue) + { + if(CollapsedHeight<=0) + { + return; + } + + if (State is BottomSheetState.Collapsed) + { + OnSizeAllocated(Width, Height); + } + } + + /// + /// Updates the BottomSheetContentWidth property with the given value. + /// + /// The new value for BottomSheetContentWidth. + void UpdateContentWidthProperty(double newValue) + { + if (BottomSheetContentWidth <= 0 || ContentWidthMode == BottomSheetContentWidthMode.Full) + { + return; + } + + if(_bottomSheet is not null) + { + _bottomSheet.WidthRequest = newValue; + } + } + + + /// + /// Updates the ContentWidthMode property with the given value. + /// + /// The new value for ContentWidthMode. + void UpdateContentWidthModeProperty(BottomSheetContentWidthMode newValue) + { + if (_bottomSheet is not null) + { + if (newValue is BottomSheetContentWidthMode.Full) + { + _bottomSheet.WidthRequest = Width; + } + else if (BottomSheetContentWidth > 0) + { + _bottomSheet.WidthRequest = BottomSheetContentWidth; + } + } + } + + /// + /// Sets the content of the bottom sheet. + /// + /// The view to be set as the content of the bottom sheet. + /// Thrown when the content is null. + /// + /// Thrown when either the bottom sheet content or content border is not initialized. + /// + void SetBottomSheetContent(View? content) + { + if (content is null || _bottomSheetContent is null) + { + return; + } + + // Remove existing content if any + if (_bottomSheetContent.Children.Count > 1) + { + _bottomSheetContent.Children.RemoveAt(1); + } + + // Set new content + if (_contentBorder is not null) + { + _contentBorder.Content = content; + } + + // Add content border to bottom sheet + _bottomSheetContent.Children.Add(_contentBorder); + SfGrid.SetRow(_contentBorder, 1); + } + + + /// + /// Updates the height of the first row in the bottom sheet content based on the visibility of the grabber. + /// + void UpdateRowHeight() + { + const int GrabberRowIndex = 0; + + if (_bottomSheetContent is not null) + { + _bottomSheetContent.RowDefinitions[GrabberRowIndex].Height = + ShowGrabber ? new GridLength(DefaultGrabberRowHeight) : new GridLength(0); + } + } + + + /// + /// Handles the tapped event on the overlay grid. + /// This method is called when the user taps on the overlay area of the bottom sheet. + /// + /// The object that raised the event. + /// The event arguments. + void OnOverlayGridTapped(object? sender, EventArgs e) + { + if (_isSheetOpen) + { + Close(); + } + } + + /// + /// Updates the state of the bottom sheet based on the current AllowedState and open status. + /// + void UpdateState() + { + var (newState, newIsHalfExpanded) = AllowedState switch + { + BottomSheetAllowedState.HalfExpanded => (_isSheetOpen ? BottomSheetState.HalfExpanded : State, true), + BottomSheetAllowedState.FullExpanded => (_isSheetOpen ? BottomSheetState.FullExpanded : State, false), + BottomSheetAllowedState.All => (State, _isHalfExpanded), + _ => (!_isSheetOpen ? BottomSheetState.Hidden : State, true) + }; + + if (!newState.Equals(State)) + { + SetValue(StateProperty, newState); + } + + _isHalfExpanded = newIsHalfExpanded; + } + + /// + /// Updates the background of the bottom sheet and its content. + /// + /// The brush to be set as the background. If null, the default background is used. + void UpdateBottomSheetBackground(Brush brush) + { + if (_bottomSheet is null || _bottomSheetContent is null) + { + return; + } + + var background = brush ?? (Brush)SfBottomSheet.BackgroundProperty.DefaultValue; + _bottomSheet.Background = background; + _bottomSheetContent.Background = background; + } + + + /// + /// Updates the background of the grabber. + /// + /// The brush to be set as the grabber's background. If null, the default background is used. + void UpdateGrabberBackground(Brush brush) + { + if (_grabber is null) + { + return; + } + + _grabber.Background = brush ?? (Brush)SfBottomSheet.GrabberBackgroundProperty.DefaultValue; + } + + + /// + /// Updates the corner radius of the bottom sheet. + /// + /// The new corner radius to be applied. + void UpdateCornerRadius(CornerRadius cornerRadius) + { + if (_bottomSheet?.StrokeShape is null || _bottomSheetStrokeShape is null) + { + return; + } + + cornerRadius = EnsureValidCornerRadius(cornerRadius); + + if (!_bottomSheetStrokeShape.CornerRadius.Equals(cornerRadius)) + { + _bottomSheetStrokeShape.CornerRadius = cornerRadius; + _bottomSheet.StrokeShape = _bottomSheetStrokeShape; + OnPropertyChanged(nameof(CornerRadius)); + } + } + + /// + /// Ensures that the provided corner radius has valid (non-negative) values. + /// + /// The corner radius to check. + /// A valid corner radius. If the input was invalid, returns the default value. + CornerRadius EnsureValidCornerRadius(CornerRadius cornerRadius) + { + return (cornerRadius.TopLeft < 0 || cornerRadius.TopRight < 0 || cornerRadius.BottomLeft < 0 || cornerRadius.BottomRight < 0) + ? (CornerRadius)SfBottomSheet.CornerRadiusProperty.DefaultValue + : cornerRadius; + } + + + /// + /// Updates the padding of the content border if it has changed. + /// + /// The new padding to be applied. + void UpdatePadding(Thickness padding) + { + if (_contentBorder is not null && !_contentBorder.Padding.Equals(padding)) + { + _contentBorder.Padding = padding; + OnPropertyChanged(nameof(ContentPadding)); + } + } + + /// + /// Updates the height of the grabber, ensuring it's not negative. + /// + /// The new height to be set for the grabber. + void UpdateGrabberHeightProperty(double newValue) + { + if (_grabber is null) + { + return; + } + + _grabber.HeightRequest = (newValue<0) ? (double)(GrabberHeightProperty.DefaultValue) : newValue; + } + + /// + /// Updates the width of the grabber, ensuring it's not negative. + /// + /// The new width to be set for the grabber. + void UpdateGrabberWidthProperty(double newValue) + { + if (_grabber is null) + { + return; + } + + _grabber.WidthRequest = (newValue<0) ? (double)GrabberWidthProperty.DefaultValue : newValue; + } + + + /// + /// Updates the corner radius of the grabber. + /// + /// The new corner radius to be applied to the grabber. + void UpdateGrabberCornerRadius(CornerRadius cornerRadius) + { + if (_grabber?.StrokeShape is null || _grabberStrokeShape is null) + { + return; + } + + cornerRadius = EnsureValidCornerRadius(cornerRadius, SfBottomSheet.GrabberCornerRadiusProperty.DefaultValue); + + if (!_grabberStrokeShape.CornerRadius.Equals(cornerRadius)) + { + _grabberStrokeShape.CornerRadius = cornerRadius; + _grabber.StrokeShape = _grabberStrokeShape; + OnPropertyChanged(nameof(GrabberCornerRadius)); + } + } + + /// + /// Ensures that the provided corner radius has valid (non-negative) values. + /// + /// The corner radius to check. + /// The default value to use if the corner radius is invalid. + /// A valid corner radius. If the input was invalid, returns the default value. + CornerRadius EnsureValidCornerRadius(CornerRadius cornerRadius, object defaultValue) + { + return (cornerRadius.TopLeft < 0 || cornerRadius.TopRight < 0 || cornerRadius.BottomLeft < 0 || cornerRadius.BottomRight < 0) + ? (CornerRadius)defaultValue + : cornerRadius; + } + + + /// + /// Handles the SizeChanged event of the control. + /// This method is called when the size of the control changes, and it shows the bottom sheet. + /// + /// The object that raised the event. + /// The event arguments. + void OnSizeChanged(object? sender, EventArgs e) + { + if (_bottomSheet is not null && _bottomSheet.IsVisible) + { + Show(); + } + } + + + /// + /// Updates the position of the bottom sheet based on swipe gestures, transitioning between states. + /// + void UpdatePosition() + { + const double SwipeThreshold = 100; + const double DoubleSwipeThreshold = SwipeThreshold * 2; + double swipeDistance = _endTouchY - _startTouchY; + + switch (State) + { + case BottomSheetState.FullExpanded: + HandleFullExpandedState(swipeDistance, SwipeThreshold, DoubleSwipeThreshold); + break; + case BottomSheetState.HalfExpanded: + HandleHalfExpandedState(swipeDistance, SwipeThreshold); + break; + case BottomSheetState.Collapsed: + HandleCollapsedState(swipeDistance, SwipeThreshold); + break; + } + } + + /// + /// Handles the state transition of the bottom sheet when in a full-expanded state. + /// + /// The distance swiped by the user. + /// The threshold required to transition to a half-expanded state. + /// The threshold required to transition to a collapsed state. + void HandleFullExpandedState(double swipeDistance, double swipeThreshold, double doubleSwipeThreshold) + { + if (swipeDistance > swipeThreshold && AllowedState is not BottomSheetAllowedState.FullExpanded) + { + UpdateStateBasedOnNearestPoint(); + } + else if (swipeDistance > doubleSwipeThreshold) + { + State = BottomSheetState.Collapsed; + } + else + { + Show(); + } + } + + /// + /// Handles the state transition of the bottom sheet when in a half-expanded state. + /// + /// The distance swiped by the user. + /// The threshold required to transition to a different state. + void HandleHalfExpandedState(double swipeDistance, double swipeThreshold) + { + if (-swipeDistance > swipeThreshold && AllowedState is not BottomSheetAllowedState.HalfExpanded) + { + State = BottomSheetState.FullExpanded; + } + else if (swipeDistance > swipeThreshold) + { + State = BottomSheetState.Collapsed; + } + else + { + Show(); + } + } + + /// + /// Handles the state transition of the bottom sheet when in a collapsed state. + /// + /// The distance swiped by the user. + /// The threshold required to transition to a different state. + void HandleCollapsedState(double swipeDistance, double swipeThreshold) + { + if (-swipeDistance > swipeThreshold) + { + if(AllowedState == BottomSheetAllowedState.HalfExpanded) + { + State = BottomSheetState.HalfExpanded; + } + else if (AllowedState == BottomSheetAllowedState.FullExpanded) + { + State = BottomSheetState.FullExpanded; + } + else + { + UpdateStateBasedOnNearestPoint(); + } + } + else + { + Show(); + } + } + + /// + /// Methods to update the bottom sheet's state based on the nearest point. + /// + void UpdateStateBasedOnNearestPoint() + { + double fullExpandedHeight = Height * (1 - FullExpandedRatio); + double halfExpandedHeight = Height * (1 - HalfExpandedRatio); + double collapsedHeight = Height - CollapsedHeight; + List predefinedPoints = new List + { + fullExpandedHeight, halfExpandedHeight, collapsedHeight + }; + + double nearestPoint = predefinedPoints.OrderBy(p => Math.Abs(p - _endTouchY)).First(); + + if (nearestPoint == fullExpandedHeight) + { + State = BottomSheetState.FullExpanded; + } + else if (nearestPoint == halfExpandedHeight) + { + State = BottomSheetState.HalfExpanded; + } + else + { + State = BottomSheetState.Collapsed; + } + } + + + /// + /// Updates the state and raises the StateChanged event when the state changes. + /// + /// The previous state of the bottom sheet. + /// The new state of the bottom sheet. + void UpdateStateChanged(BottomSheetState oldState, BottomSheetState newState) + { + if (!oldState.Equals(newState)) + { + _stateChangedEventArgs.OldState = oldState; + _stateChangedEventArgs.NewState = newState; + if (_overlayGrid is not null) + { + _overlayGrid.IsVisible = (State is BottomSheetState.Collapsed) ? false : IsModal; + } + + OnStateChanged(_stateChangedEventArgs); + } + } + + /// + /// Checks if the current height is valid for bottom sheet operations. + /// + /// True if the height is greater than 0 and not positive infinity; otherwise, false. + bool IsValidHeight() + { + return Height > 0 && Height is not double.PositiveInfinity; + } + + /// + /// Registers the OnSizeChanged event handler, ensuring it's only registered once. + /// + void RegisterSizeChangedEvent() + { + SizeChanged -= OnSizeChanged; + SizeChanged += OnSizeChanged; + } + + + /// + /// Sets up the initial state of the bottom sheet and overlay grid for display. + /// + void SetupBottomSheetForShow() + { + if (_isSheetOpen || _bottomSheet is null || _overlayGrid is null) + { + return; + } + + // Position the bottom sheet just below the visible area + _bottomSheet.TranslationY = Height; + _bottomSheet.IsVisible = true; + _overlayGrid.IsVisible = IsModal; + _overlayGrid.Opacity = 0; + } + + + /// + /// Calculates the target position for the bottom sheet based on its current state. + /// + /// The target Y position for the bottom sheet. + double GetTargetPosition() + { + if (State is BottomSheetState.FullExpanded || !_isHalfExpanded) + { + return GetFullExpandedPosition(); + } + + if (State is BottomSheetState.Collapsed) + { + return GetCollapsedPosition(); + } + + return GetHalfExpandedPosition(); + } + + /// + /// Calculates and sets the position for the bottom sheet when fully expanded. + /// + /// The calculated position for the fully expanded state. Currently, it always returns 0. + double GetFullExpandedPosition() + { + if (State is BottomSheetState.Hidden) + { + State = BottomSheetState.FullExpanded; + } + + double targetPosition = Math.Abs(Height * (1 - FullExpandedRatio)); + if (_bottomSheet is not null) + { + _bottomSheet.HeightRequest = Height * FullExpandedRatio; + } + + return targetPosition; + } + + /// + /// Calculates the position of the bottom sheet when in the collapsed state. + /// + /// The calculated position for the collapsed state. + double GetCollapsedPosition() + { + double targetPosition = Height - CollapsedHeight; + if (_overlayGrid is not null) + { + _overlayGrid.IsVisible = false; + } + + return targetPosition; + } + + /// + /// Calculates the position of the bottom sheet when in the half-expanded state. + /// + /// The calculated target position for the half-expanded state. + double GetHalfExpandedPosition() + { + double targetPosition = Height * (1 - HalfExpandedRatio); + if (_bottomSheet is not null) + { + if (!_isSheetOpen || _bottomSheet.TranslationY > targetPosition) + { + State = BottomSheetState.HalfExpanded; + _bottomSheet.HeightRequest = Height * HalfExpandedRatio; + } + } + + return targetPosition; + } + + /// + /// Animates the bottom sheet and overlay grid to their target positions. + /// + /// The target Y position for the bottom sheet. + /// Optional action to be executed when the animation finishes. + void AnimateBottomSheet(double targetPosition, Action? onFinish = null) + { + const int AnimationDuration = 150; + const int topPadding = 2; + + if (_bottomSheet is not null) + { + var bottomSheetAnimation = new Animation(d => _bottomSheet.TranslationY = d, _bottomSheet.TranslationY, targetPosition + topPadding); + _bottomSheet?.Animate("bottomSheetAnimation", bottomSheetAnimation, length: AnimationDuration, easing: Easing.Linear, finished: (v, e) => + { + UpdateBottomSheetHeight(); + onFinish?.Invoke(); + }); + } + + if (_overlayGrid is not null) + { + var overlayGridAnimation = new Animation(d => _overlayGrid.Opacity = d, _overlayGrid.Opacity, _isSheetOpen ? DefaultOverlayOpacity : 0); + _overlayGrid?.Animate("overlayGridAnimation", overlayGridAnimation, length: AnimationDuration, easing: Easing.Linear); + } + } + + /// + /// Updates the visibility of the bottom sheet based on IsOpen. + /// + void UpdateBottomSheetVisibility() + { + if(IsOpen && !_isSheetOpen) + { + Show(); + } + else if(!IsOpen && _isSheetOpen) + { + Close(); + } + } + + /// + /// Updates the height of the bottom sheet based on its current state. + /// + void UpdateBottomSheetHeight() + { + if (!_isSheetOpen || _bottomSheet is null) + { + return; + } + + if (State is BottomSheetState.HalfExpanded) + { + _bottomSheet.HeightRequest = Height * HalfExpandedRatio; + } + else if (State is BottomSheetState.Collapsed) + { + _bottomSheet.HeightRequest = CollapsedHeight; + } + else if(State is BottomSheetState.FullExpanded) + { + _bottomSheet.HeightRequest = Height * FullExpandedRatio; + } + } + + + /// + /// Adjusts the Y coordinate of a touch point based on the platform. + /// + /// The original touch point. + /// The adjusted Y coordinate. + double GetPlatformAdjustedTouchY(Point point) + { +#if IOS || MACCATALYST || ANDROID + return point.Y + Height - _bottomSheet?.HeightRequest ?? 0; +#else + return point.Y; +#endif + } + + /// + /// Handles the initial touch press event on the bottom sheet. + /// + /// The Y coordinate of the touch point. + void HandleTouchPressed(double touchY) + { + _initialTouchY = touchY; + _isPointerPressed = true; + _startTouchY = _initialTouchY; + } + + + /// + /// Handles touch movement on the bottom sheet. + /// + /// The current Y coordinate of the touch point. + void HandleTouchMoved(double touchY) + { + if (!_isPointerPressed || _bottomSheet == null) + { + return; + } + + double diffY = touchY - _initialTouchY; + double newTranslationY = Math.Max(0, _bottomSheet.TranslationY + diffY); + + if (ShouldRestrictMovement(newTranslationY, diffY)) + { + return; + } + + UpdateBottomSheetPosition(newTranslationY, touchY); + } + + + /// + /// Determines if the movement of the bottom sheet should be restricted. + /// + /// The new translation Y value. + /// The difference in Y position from the last touch point. + /// True if movement should be restricted, false otherwise. + bool ShouldRestrictMovement(double newTranslationY, double diffY) + { + if (_bottomSheet is null) + { + return false; + } + + bool isHalfExpandedAndRestricted = State is BottomSheetState.HalfExpanded && + AllowedState is BottomSheetAllowedState.HalfExpanded && + _bottomSheet.TranslationY > newTranslationY; + + bool isCollapsedAndMovingDown = State is BottomSheetState.Collapsed && diffY > 0; + + bool isFullExpandedRestricted = State is BottomSheetState.FullExpanded && + _bottomSheet.TranslationY > newTranslationY; + + bool isBehind = (newTranslationY > Height - CollapsedHeight) || (newTranslationY < Height * (1 - FullExpandedRatio)); + + return isHalfExpandedAndRestricted || isCollapsedAndMovingDown || isFullExpandedRestricted || isBehind; + } + + + /// + /// Updates the position of the bottom sheet based on touch input. + /// + /// The new translation Y value for the bottom sheet. + /// The current Y coordinate of the touch point. + void UpdateBottomSheetPosition(double newTranslationY, double touchY) + { + if (_bottomSheet is null) + { + return; + } + + _bottomSheet.TranslationY = newTranslationY; + _initialTouchY = touchY; + _bottomSheet.HeightRequest = Height - newTranslationY; + } + + /// + /// Handles the touch release event on the bottom sheet. + /// + /// The Y coordinate of the touch point when released. + void HandleTouchReleased(double touchY) + { + _endTouchY = _initialTouchY; + _initialTouchY = 0; + _isPointerPressed = false; + + UpdatePosition(); + } + + + /// + /// Updates the height and position of the bottom sheet when it's in a half-expanded state. + /// + /// The total height available for the bottom sheet. + void UpdateHalfExpandedHeight(double height) + { + if (_bottomSheet is { } sheet) + { + sheet.TranslationY = height * (1 - HalfExpandedRatio); + sheet.HeightRequest = height * HalfExpandedRatio; + } + } + + /// + /// Updates the height and position of the bottom sheet when it's in a collapsed state. + /// + /// The total height available for the bottom sheet. + void UpdateCollapsedHeight(double height) + { + if (_bottomSheet is not null) + { + _bottomSheet.TranslationY = height - CollapsedHeight; + _bottomSheet.HeightRequest = CollapsedHeight; + } + } + + /// + /// Updates the height of the bottom sheet based on its current state. + /// + /// The height to be applied to the bottom sheet. + void UpdateBottomSheetHeight(double height) + { + switch (State) + { + case BottomSheetState.Hidden: + SetBottomSheetHidden(); + break; + case BottomSheetState.Collapsed: + UpdateCollapsedHeight(height); + break; + case BottomSheetState.HalfExpanded: + UpdateHalfExpandedHeight(height); + break; + case BottomSheetState.FullExpanded: + SetBottomSheetFullyExpanded(height); + break; + } + } + + /// + /// Sets the bottom sheet to a hidden state by adjusting its height to zero. + /// + void SetBottomSheetHidden() + { + if (_bottomSheet is not null) + { + _bottomSheet.HeightRequest = 0; + } + } + + /// + /// Sets the bottom sheet to a fully expanded state by adjusting its height to the specified value. + /// + /// The height to set for the bottom sheet in its fully expanded state. + void SetBottomSheetFullyExpanded(double height) + { + if (_bottomSheet is not null) + { + _bottomSheet.TranslationY = Math.Abs(height * (1 - FullExpandedRatio)); + _bottomSheet.HeightRequest = height * FullExpandedRatio; + } + } + + /// + /// Configures platform-specific behavior for the bottom sheet. + /// + void ConfigurePlatformSpecificBehavior() + { +#if IOS || MACCATALYST || WINDOWS + ConfigureTouch(); +#endif + } + + #endregion + + #region Override Methods + + /// + /// Handles size allocation for the bottom sheet, adjusting its height based on the current state. + /// + /// The allocated width. + /// The allocated height. + protected override void OnSizeAllocated(double width, double height) + { + base.OnSizeAllocated(width, height); + + if (_bottomSheet is null || height <= 0 || height is double.PositiveInfinity) + { + return; + } + + UpdateBottomSheetHeight(height); + } + + /// + /// Called when the handler for the control changes. Configures platform-specific behavior. + /// + protected override void OnHandlerChanged() + { + base.OnHandlerChanged(); + ConfigurePlatformSpecificBehavior(); + } + + #endregion + + #region Property Changed Methods + + /// + /// Handles changes to the property. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the IsModal property. + /// The new value of the IsModal property. + static void OnIsModalPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (sheet._overlayGrid is not null) + { + sheet._overlayGrid.IsVisible = sheet.IsModal; + } + } + } + + + /// + /// Handles changes to the property. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the ShowGrabber property. + /// The new value of the ShowGrabber property. + static void OnShowGrabberPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (sheet._grabber is not null) + { + sheet._grabber.IsVisible = sheet.ShowGrabber; + sheet.UpdateRowHeight(); + } + } + } + + /// + /// Handles changes to the property. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the IsOpen property. + /// The new value of the IsOpen property. + static void OnIsOpenPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue)) + { + sheet.UpdateBottomSheetVisibility(); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the Content property. + /// The new value of the Content property. + static void OnContentPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (newValue is View newContent) + { + sheet.SetBottomSheetContent(newContent); + } + } + } + + /// + /// Invoked whenever the is set. + /// + /// The bindable. + /// The old value. + /// The new value. + static void OnContentChanged(BindableObject bindable, object oldValue, object newValue) + { + (bindable as SfBottomSheet)?.UpdateContentView(); + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the state. + /// The new value of the state. + static void OnStatePropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is not SfBottomSheet sheet) + return; + + var newState = (BottomSheetState)newValue; + + if ((sheet.AllowedState == BottomSheetAllowedState.HalfExpanded && newState == BottomSheetState.FullExpanded) || + (sheet.AllowedState == BottomSheetAllowedState.FullExpanded && newState == BottomSheetState.HalfExpanded)) + { + sheet.State = sheet._isSheetOpen + ? (sheet.AllowedState == BottomSheetAllowedState.HalfExpanded ? BottomSheetState.HalfExpanded : BottomSheetState.FullExpanded) + : BottomSheetState.Hidden; + return; + } + + if (newState == BottomSheetState.Hidden) + { + sheet._isHalfExpanded = true; + if (sheet._isSheetOpen) + { + sheet._isSheetOpen = false; + sheet.Close(); + } + } + else if(newState == BottomSheetState.Collapsed) + { + if(sheet._isSheetOpen) + { + sheet._isHalfExpanded = true; + sheet.Show(); + } + } + else + { + sheet._isHalfExpanded = newState == BottomSheetState.HalfExpanded; + if (sheet._isSheetOpen) + { + sheet.Show(); + } + } + + sheet.UpdateStateChanged((BottomSheetState)oldValue, newState); + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the half-expanded ratio. + /// The new value of the half-expanded ratio. + static void OnHalfExpandedRatioPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue) && newValue is not null) + { + sheet.UpdateHalfExpandedRatioProperty((double)newValue); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the full-expanded ratio. + /// The new value of the full-expanded ratio. + static void OnFullExpandedRatioPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue) && newValue is not null) + { + sheet.UpdateFullExpandedRatioProperty((double)newValue); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the collapsed height. + /// The new value of the collapsed height. + static void OnCollapsedHeightPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue) && newValue is not null) + { + sheet.UpdateCollapsedHeightProperty((double)newValue); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the content width. + /// The new value of the content width. + static void OnBottomSheetContentWidthPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue) && newValue is not null) + { + sheet.UpdateContentWidthProperty((double)newValue); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the content width mode. + /// The new value of the content width mode. + static void OnContentWidthModePropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue) && newValue is not null) + { + sheet.UpdateContentWidthModeProperty((BottomSheetContentWidthMode)newValue); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the allowed state. + /// The new value of the allowed state. + static void OnAllowedStatePropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + sheet.UpdateState(); + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the background. + /// The new value of the background. + static void OnBackgroundPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + sheet.UpdateBottomSheetBackground((Brush)newValue); + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the corner radius. + /// The new value of the corner radius. + static void OnCornerRadiusPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + sheet.UpdateCornerRadius((CornerRadius)newValue); + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the content padding. + /// The new value of the content padding. + static void OnContentPaddingPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + sheet.UpdatePadding((Thickness)newValue); + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the grabber background. + /// The new value of the grabber background. + static void OnGrabberBackgroundPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + sheet.UpdateGrabberBackground((Brush)newValue); + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the overlay background color. + /// The new value of the overlay background color. + static void OnOverlayBackgroundColorChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet && sheet._overlayGrid is not null) + { + sheet._overlayGrid.Background = sheet.OverlayBackgroundColor; + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the grabber height. + /// The new value of the grabber height. + static void OnGrabberHeightPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue) && newValue is not null) + { + sheet.UpdateGrabberHeightProperty((double)newValue); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the grabber width. + /// The new value of the grabber width. + static void OnGrabberWidthPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + if (!oldValue.Equals(newValue) && newValue is not null) + { + sheet.UpdateGrabberWidthProperty((double)newValue); + } + } + } + + /// + /// Handles changes to the property of the bottom sheet. + /// + /// The bindable object (should be SfBottomSheet). + /// The old value of the grabber corner radius. + /// The new value of the grabber corner radius. + static void OnGrabberCornerRadiusPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is SfBottomSheet sheet) + { + sheet.UpdateGrabberCornerRadius((CornerRadius)newValue); + } + } + + #endregion + + #region Interface Implementation + + /// + /// Returns the theme dictionary for the SfBottomSheet control. + /// + /// A ResourceDictionary containing the theme styles for the control. + public ResourceDictionary GetThemeDictionary() + { + return new SfBottomSheetStyle(); + } + + /// + /// Handles changes to the control-specific theme. + /// + /// The previous theme. + /// The new theme. + public void OnControlThemeChanged(string oldTheme, string newTheme) + { + // TODO: Implement control-specific theme change logic + } + + /// + /// Handles changes to the common theme. + /// + /// The previous theme. + /// The new theme. + public void OnCommonThemeChanged(string oldTheme, string newTheme) + { + // TODO: Implement common theme change logic + } + + #endregion + + #region Events + + /// + /// Invoke the event when the state of is changed. + /// + public event EventHandler? StateChanged; + + #endregion + } +} + diff --git a/maui/src/BottomSheet/SfBottomSheet.iOS.cs b/maui/src/BottomSheet/SfBottomSheet.iOS.cs new file mode 100644 index 0000000..4ea2bfd --- /dev/null +++ b/maui/src/BottomSheet/SfBottomSheet.iOS.cs @@ -0,0 +1,65 @@ +using Microsoft.Maui.Controls; +using Syncfusion.Maui.Toolkit.Platform; + +namespace Syncfusion.Maui.Toolkit.BottomSheet +{ + public partial class SfBottomSheet + { + #region Fields + + /// + /// Gets the native view. + /// + LayoutViewExt? _layoutViewExt; + + #endregion + + #region Private Methods + + void ConfigureTouch() + { + if (Handler?.PlatformView is null) + { + UnwireEvents(); + } + else + { + WireEvents(); + } + } + + void WireEvents() + { + if (Handler?.PlatformView is LayoutViewExt nativeView) + { + _layoutViewExt = nativeView; + _layoutViewExt.UserInteractionEnabled = false; + } + } + + void UnwireEvents() + { + _layoutViewExt = null; + } + + #endregion + + #region Override Methods + + /// + /// Raises on handler changing. + /// + /// Relevant . + protected override void OnHandlerChanging(HandlerChangingEventArgs args) + { + if (args.OldHandler is not null) + { + UnwireEvents(); + } + + base.OnHandlerChanging(args); + } + + #endregion + } +} diff --git a/maui/src/BottomSheet/StateChangedEventArgs.cs b/maui/src/BottomSheet/StateChangedEventArgs.cs new file mode 100644 index 0000000..280c279 --- /dev/null +++ b/maui/src/BottomSheet/StateChangedEventArgs.cs @@ -0,0 +1,23 @@ +namespace Syncfusion.Maui.Toolkit.BottomSheet +{ + using System; + + /// + /// Represents BottomSheet’s state changed event arguments. + /// + /// + /// It contains information like old value and new value. + /// + public class StateChangedEventArgs : EventArgs + { + /// + /// Gets or sets a old value when the BottomSheet’s state is changed. + /// + public BottomSheetState OldState { get; internal set; } + + /// + /// Gets or sets an new value when the BottomSheet’s state is changed. + /// + public BottomSheetState NewState { get; internal set; } + } +} diff --git a/maui/src/Core/Theme/Resources/DefaultTheme.xaml b/maui/src/Core/Theme/Resources/DefaultTheme.xaml index f82fa21..d9a8d8d 100644 --- a/maui/src/Core/Theme/Resources/DefaultTheme.xaml +++ b/maui/src/Core/Theme/Resources/DefaultTheme.xaml @@ -308,5 +308,12 @@ 12 - + + + CommonTheme + + + + + diff --git a/maui/src/Themes/SfBottomSheetStyle.xaml b/maui/src/Themes/SfBottomSheetStyle.xaml new file mode 100644 index 0000000..1e6925b --- /dev/null +++ b/maui/src/Themes/SfBottomSheetStyle.xaml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/maui/src/Themes/SfBottomSheetStyle.xaml.cs b/maui/src/Themes/SfBottomSheetStyle.xaml.cs new file mode 100644 index 0000000..1484551 --- /dev/null +++ b/maui/src/Themes/SfBottomSheetStyle.xaml.cs @@ -0,0 +1,17 @@ +using Microsoft.Maui.Controls; + +namespace Syncfusion.Maui.ToolKit.BottomSheet; + +/// +/// SfBottomSheetStyle provides a set of predefined styles for SfBottomSheet control. +/// +public partial class SfBottomSheetStyle : ResourceDictionary +{ + /// + /// Initializes a new instance of the class. + /// + public SfBottomSheetStyle() + { + InitializeComponent(); + } +} \ No newline at end of file From b8b6f4c3dc0d8effaaa5ae364f7c01f9ffd4e610 Mon Sep 17 00:00:00 2001 From: Naveenkumar S Date: Wed, 4 Dec 2024 18:51:45 +0530 Subject: [PATCH 2/3] Updated the AssemblyInfo.cs --- maui/src/AssemblyInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/maui/src/AssemblyInfo.cs b/maui/src/AssemblyInfo.cs index f56feb1..61c21f7 100644 --- a/maui/src/AssemblyInfo.cs +++ b/maui/src/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Syncfusion.Maui.Toolkit.UnitTest")] +[assembly: XmlnsDefinition("http://schemas.syncfusion.com/maui/toolkit", "Syncfusion.Maui.Toolkit.BottomSheet")] [assembly: XmlnsDefinition("http://schemas.syncfusion.com/maui/toolkit", "Syncfusion.Maui.Toolkit.Carousel")] [assembly: XmlnsDefinition("http://schemas.syncfusion.com/maui/toolkit", "Syncfusion.Maui.Toolkit.Charts")] [assembly: XmlnsDefinition("http://schemas.syncfusion.com/maui/toolkit", "Syncfusion.Maui.Toolkit.Chips")] From 8ad27e28d67073bd593bf11a5638e72d29a2261c Mon Sep 17 00:00:00 2001 From: Naveenkumar S Date: Wed, 4 Dec 2024 18:58:21 +0530 Subject: [PATCH 3/3] Moved scroll fix changes in the iOS platform for bottom sheet --- maui/src/BottomSheet/BottomSheetBorder.cs | 102 ++++++++++++++++++++-- 1 file changed, 96 insertions(+), 6 deletions(-) diff --git a/maui/src/BottomSheet/BottomSheetBorder.cs b/maui/src/BottomSheet/BottomSheetBorder.cs index 7db3437..a2720cd 100644 --- a/maui/src/BottomSheet/BottomSheetBorder.cs +++ b/maui/src/BottomSheet/BottomSheetBorder.cs @@ -13,16 +13,23 @@ internal class BottomSheetBorder : SfBorder, ITouchListener // To store the weak reference of bottom sheet instance. readonly WeakReference? _bottomSheetRef; - #endregion +#if IOS + + // To store the child count of bottom sheet + double _childLoopCount; + +#endif + +#endregion #region Constructor /// - /// Initializes a new instance of the class. - /// - /// The SfBottomSheet instance. - /// Thrown if bottomSheet is null. - public BottomSheetBorder(SfBottomSheet bottomSheet) + /// Initializes a new instance of the class. + /// + /// The SfBottomSheet instance. + /// Thrown if bottomSheet is null. + public BottomSheetBorder(SfBottomSheet bottomSheet) { if (bottomSheet is not null) { @@ -33,6 +40,72 @@ public BottomSheetBorder(SfBottomSheet bottomSheet) #endregion + #region Private Methods + +#if IOS + /// + /// Gets the X and Y coordinates of the specified element based on the screen. + /// + /// The current element for which coordinates are requested. + /// The current element for which coordinates are requested. + bool IsChildElementScrolled(IVisualTreeElement? element, Point touchPoint) + { + if (element is null) + { + return false; + } + + var view = element as View; + if (view is null || view.Handler is null || view.Handler.PlatformView is null) + { + return false; + } + + if (view is ScrollView || view is ListView || view is CollectionView) + { + return true; + } + + foreach (var childView in element.GetVisualChildren().OfType()) + { + if (childView is null || childView.Handler is null || childView.Handler.PlatformView is null) + { + return false; + } + + var childNativeView = childView.Handler.PlatformView; + + // Here items X and Y position converts based on screen. + Point locationOnScreen = ChildLocationToScreen(childNativeView); + var bottom = locationOnScreen.Y + childView.Bounds.Height; + var right = locationOnScreen.X + childView.Bounds.Width; + + // We loop through child only 10 times. + if (touchPoint.Y >= locationOnScreen.Y && touchPoint.Y <= bottom && touchPoint.X >= locationOnScreen.X && touchPoint.X <= right && _childLoopCount <= 10) + { + _childLoopCount++; + return IsChildElementScrolled(childView, touchPoint); + } + } + + _childLoopCount = 0; + return false; + } + + Point ChildLocationToScreen(object child) + { + if (child is UIKit.UIView view && this.Handler is not null) + { + var point = view.ConvertPointToView(view.Bounds.Location, Handler.PlatformView as UIKit.UIView); + return new Microsoft.Maui.Graphics.Point(point.X, point.Y); + } + + return new Microsoft.Maui.Graphics.Point(0, 0); + } +#endif + + #endregion + #region Interface Implementation /// @@ -42,6 +115,20 @@ public BottomSheetBorder(SfBottomSheet bottomSheet) public void OnTouch(Toolkit.Internals.PointerEventArgs e) { #if IOS || MACCATALYST || ANDROID + +#if IOS + if (Content is null) + { + return; + } + + var firstDescendant = Content.GetVisualTreeDescendants().FirstOrDefault(); + if (firstDescendant is not null && IsChildElementScrolled(firstDescendant, e.TouchPoint)) + { + return; + } + +#endif if (e is not null && _bottomSheetRef is not null) { if (_bottomSheetRef.TryGetTarget(out var bottomSheet)) @@ -49,8 +136,11 @@ public void OnTouch(Toolkit.Internals.PointerEventArgs e) bottomSheet.OnHandleTouch(e.Action, e.TouchPoint); } } + #endif } + #endregion + } } \ No newline at end of file