diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart index 2f66dc8f7..2c23c6959 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart @@ -8,6 +8,7 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/html/html_interaction.dart'; import 'package:core/utils/html/html_template.dart'; import 'package:core/utils/html/html_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:universal_html/html.dart' as html; @@ -87,6 +88,7 @@ class _HtmlContentViewerOnWebState extends State static const String iframeOnLoadMessage = 'iframeHasBeenLoaded'; static const String onClickHyperLinkName = 'onClickHyperLink'; static const String onScrollChangedEvent = 'onScrollChanged'; + static const String onScrollEndEvent = 'onScrollEnd'; @override void initState() { @@ -106,9 +108,12 @@ class _HtmlContentViewerOnWebState extends State if (viewId != _createdViewId) return; final type = data['type']; - if (_isScrollChangedEventTriggered(type)) { + if (_isScrollingIsAvailable && _isScrollChangedEventTriggered(type)) { _handleIframeOnScrollChangedListener(data, widget.scrollController!); return; + } else if (_isScrollingIsAvailable && _isScrollEndEventTriggered(type)) { + _handleIframeOnScrollEndListener(data, widget.scrollController!); + return; } if (data['message'] == iframeOnLoadMessage) { @@ -130,10 +135,17 @@ class _HtmlContentViewerOnWebState extends State } } - bool _isScrollChangedEventTriggered(String? type) { + bool get _isScrollingIsAvailable { return widget.scrollController != null && - widget.scrollController?.hasClients == true && - type?.contains('toDart: $onScrollChangedEvent') == true; + widget.scrollController?.hasClients == true; + } + + bool _isScrollChangedEventTriggered(String? type) { + return type?.contains('toDart: $onScrollChangedEvent') == true; + } + + bool _isScrollEndEventTriggered(String? type) { + return type?.contains('toDart: $onScrollEndEvent') == true; } void _handleIframeOnScrollChangedListener( @@ -141,17 +153,51 @@ class _HtmlContentViewerOnWebState extends State ScrollController controller, ) { final deltaY = data['deltaY'] ?? 0.0; - final newOffset = controller.offset + deltaY; - log('_HtmlContentViewerOnWebState::_handleIframeOnScrollChangedListener:deltaY = $deltaY | newOffset = $newOffset'); - if (newOffset < controller.position.minScrollExtent) { - controller.jumpTo(controller.position.minScrollExtent); - } else if (newOffset > controller.position.maxScrollExtent) { - controller.jumpTo(controller.position.maxScrollExtent); + final target = controller.offset + deltaY; + + if (PlatformInfo.isWebTouchDevice) { + final newOffset = target.clamp( + controller.position.minScrollExtent, + controller.position.maxScrollExtent, + ); + + controller.animateTo( + newOffset, + duration: const Duration(milliseconds: 50), + curve: Curves.linear, + ); } else { - controller.jumpTo(newOffset); + if (target < controller.position.minScrollExtent) { + controller.jumpTo(controller.position.minScrollExtent); + } else if (target > controller.position.maxScrollExtent) { + controller.jumpTo(controller.position.maxScrollExtent); + } else { + controller.jumpTo(target); + } } } + void _handleIframeOnScrollEndListener( + dynamic data, + ScrollController controller, + ) { + final velocity = data['velocity'] ?? 0.0; + + final distance = velocity * 800; + final target = controller.offset + distance; + + final newOffset = target.clamp( + controller.position.minScrollExtent, + controller.position.maxScrollExtent, + ); + + controller.animateTo( + newOffset, + duration: const Duration(milliseconds: 600), + curve: Curves.decelerate, + ); + } + bool _isHtmlContentHeightEventTriggered(String? type) => type?.contains('toDart: htmlHeight') == true; @@ -319,17 +365,6 @@ class _HtmlContentViewerOnWebState extends State ${!widget.autoAdjustHeight ? 'resizeObserver.observe(document.body);' : ''} } - - ${widget.scrollController != null ? ''' - window.addEventListener('wheel', function (event) { - const deltaY = event.deltaY; - window.parent.postMessage(JSON.stringify({ - "view": "$_createdViewId", - "type": "toDart: $onScrollChangedEvent", - "deltaY": deltaY - }), "*"); - }); - ''' : ''} '''; @@ -349,6 +384,17 @@ class _HtmlContentViewerOnWebState extends State HtmlInteraction.scriptsHandleLazyLoadingBackgroundImage, HtmlInteraction.generateNormalizeImageScript(widget.widthContent), if (widget.enableQuoteToggle) HtmlUtils.quoteToggleScript, + if (widget.scrollController != null) + PlatformInfo.isWebTouchDevice + ? HtmlInteraction.scriptsTouchEventListener( + viewId: _createdViewId, + onScrollChangedEvent: onScrollChangedEvent, + onScrollEndEvent: onScrollEndEvent, + ) + : HtmlInteraction.scriptsWheelEventListener( + viewId: _createdViewId, + onScrollChangedEvent: onScrollChangedEvent, + ), ].join(); final htmlTemplate = HtmlUtils.generateHtmlDocument( diff --git a/core/lib/utils/html/html_interaction.dart b/core/lib/utils/html/html_interaction.dart index 1936bffd7..b3141baf1 100644 --- a/core/lib/utils/html/html_interaction.dart +++ b/core/lib/utils/html/html_interaction.dart @@ -298,4 +298,84 @@ class HtmlInteraction { }); '''; + + static String scriptsWheelEventListener({ + required String viewId, + required String onScrollChangedEvent, + }) => ''' + + '''; + + static String scriptsTouchEventListener({ + required String viewId, + required String onScrollChangedEvent, + required String onScrollEndEvent, + }) => ''' + + + '''; } \ No newline at end of file diff --git a/core/lib/utils/platform_info.dart b/core/lib/utils/platform_info.dart index b9eaec8ea..fd0427de2 100644 --- a/core/lib/utils/platform_info.dart +++ b/core/lib/utils/platform_info.dart @@ -1,6 +1,7 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/web_renderer/canvas_kit.dart'; import 'package:flutter/foundation.dart'; +import 'package:universal_html/html.dart' as universal_html; abstract class PlatformInfo { @visibleForTesting @@ -44,4 +45,25 @@ abstract class PlatformInfo { log('PlatformInfo::platformNameOS(): $platformName'); return platformName; } + + static String get userAgent => + isWeb ? universal_html.window.navigator.userAgent.toLowerCase() : ''; + + /// Web browser on a mobile phone + static bool get isWebMobile => + isWeb && (userAgent.contains('iphone') || + (userAgent.contains('android') && userAgent.contains('mobile'))); + + /// Web browser on a tablet device + static bool get isWebTablet => + isWeb && (userAgent.contains('ipad') || + (userAgent.contains('android') && !userAgent.contains('mobile'))); + + /// Web browser on a desktop (PC/Mac) + static bool get isWebDesktop => + isWeb && !isWebMobile && !isWebTablet; + + /// Touch devices + static bool get isWebTouchDevice => + isWeb && (isWebMobile || isWebTablet); }