From d710a9718a20912ce4300451a680d325ee3b3459 Mon Sep 17 00:00:00 2001 From: Shawn Date: Mon, 4 Dec 2023 11:32:45 -0700 Subject: [PATCH] Add fullscreen keyboard list scroller component, incorporate into editorial_screen --- .../fullscreen_keyboard_list_scroller.dart | 75 ++++++++++++++++ .../common/fullscreen_keyboard_listener.dart | 12 ++- .../screens/editorial/editorial_screen.dart | 89 ++++++++++--------- 3 files changed, 129 insertions(+), 47 deletions(-) create mode 100644 lib/ui/common/fullscreen_keyboard_list_scroller.dart diff --git a/lib/ui/common/fullscreen_keyboard_list_scroller.dart b/lib/ui/common/fullscreen_keyboard_list_scroller.dart new file mode 100644 index 00000000..2ca190fa --- /dev/null +++ b/lib/ui/common/fullscreen_keyboard_list_scroller.dart @@ -0,0 +1,75 @@ +import 'package:wonders/common_libs.dart'; +import 'package:wonders/logic/common/throttler.dart'; +import 'package:wonders/ui/common/fullscreen_keyboard_listener.dart'; + +class FullscreenKeyboardListScroller extends StatelessWidget { + FullscreenKeyboardListScroller({super.key, required this.child, required this.scrollController}); + + static const int _scrollAmountOnPress = 75; + static const int _scrollAmountOnHold = 30; + static final Duration _keyPressAnimationDuration = $styles.times.fast * .5; + + final Widget child; + final ScrollController scrollController; + final Throttler _throttler = Throttler(32.milliseconds); + + double clampOffset(px) => px.clamp(0, scrollController.position.maxScrollExtent).toDouble(); + + void _handleKeyDown(int px) { + scrollController.animateTo( + clampOffset(scrollController.offset + px), + duration: _keyPressAnimationDuration, + curve: Curves.easeOut, + ); + } + + void _handleKeyRepeat(int px) { + final offset = clampOffset(scrollController.offset + px); + _throttler.call(() => scrollController.jumpTo(offset)); + } + + @override + Widget build(BuildContext context) { + return FullscreenKeyboardListener( + child: child, + onKeyRepeat: (event) { + if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + _handleKeyRepeat(-_scrollAmountOnHold); + return true; + } + if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + _handleKeyRepeat(_scrollAmountOnHold); + return true; + } + return false; + }, + onKeyDown: (event) { + if (event.logicalKey == LogicalKeyboardKey.arrowUp) { + _handleKeyDown(-_scrollAmountOnPress); + return true; + } + if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + _handleKeyDown(_scrollAmountOnPress); + return true; + } + if (event.logicalKey == LogicalKeyboardKey.pageUp) { + _handleKeyDown(-_getViewportSize(context)); + return true; + } + if (event.logicalKey == LogicalKeyboardKey.pageDown) { + _handleKeyDown(_getViewportSize(context)); + return true; + } + return false; + }, + ); + } + + int _getViewportSize(BuildContext context) { + final rb = context.findRenderObject() as RenderBox?; + if (rb != null) { + return rb.size.height.round() - 100; + } + return 0; + } +} diff --git a/lib/ui/common/fullscreen_keyboard_listener.dart b/lib/ui/common/fullscreen_keyboard_listener.dart index e4401af7..55620781 100644 --- a/lib/ui/common/fullscreen_keyboard_listener.dart +++ b/lib/ui/common/fullscreen_keyboard_listener.dart @@ -1,16 +1,17 @@ import 'package:wonders/common_libs.dart'; -class FullScreenKeyboardListener extends StatefulWidget { - const FullScreenKeyboardListener({super.key, required this.child, this.onKeyDown, this.onKeyUp}); +class FullscreenKeyboardListener extends StatefulWidget { + const FullscreenKeyboardListener({super.key, required this.child, this.onKeyDown, this.onKeyUp, this.onKeyRepeat}); final Widget child; final bool Function(KeyDownEvent event)? onKeyDown; final bool Function(KeyUpEvent event)? onKeyUp; + final bool Function(KeyRepeatEvent event)? onKeyRepeat; @override - State createState() => _FullScreenKeyboardListenerState(); + State createState() => _FullscreenKeyboardListenerState(); } -class _FullScreenKeyboardListenerState extends State { +class _FullscreenKeyboardListenerState extends State { @override void initState() { super.initState(); @@ -31,6 +32,9 @@ class _FullScreenKeyboardListenerState extends State if (event is KeyUpEvent && widget.onKeyUp != null) { result = widget.onKeyUp!.call(event); } + if (event is KeyRepeatEvent && widget.onKeyRepeat != null) { + result = widget.onKeyRepeat!.call(event); + } return result; } diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index 4d8da6df..d59a721c 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -14,6 +14,7 @@ import 'package:wonders/ui/common/centered_box.dart'; import 'package:wonders/ui/common/compass_divider.dart'; import 'package:wonders/ui/common/controls/app_header.dart'; import 'package:wonders/ui/common/curved_clippers.dart'; +import 'package:wonders/ui/common/fullscreen_keyboard_list_scroller.dart'; import 'package:wonders/ui/common/google_maps_marker.dart'; import 'package:wonders/ui/common/gradient_container.dart'; import 'package:wonders/ui/common/hidden_collectible.dart'; @@ -111,54 +112,56 @@ class _WonderEditorialScreenState extends State { padding: widget.contentPadding, child: SizedBox( child: FocusTraversalGroup( - child: CustomScrollView( - primary: false, - controller: _scroller, - scrollBehavior: ScrollConfiguration.of(context).copyWith(), - key: PageStorageKey('editorial'), - slivers: [ - /// Invisible padding at the top of the list, so the illustration shows through the btm - SliverToBoxAdapter( - child: SizedBox(height: illustrationHeight), - ), - - /// Text content, animates itself to hide behind the app bar as it scrolls up - SliverToBoxAdapter( - child: ValueListenableBuilder( - valueListenable: _scrollPos, - builder: (_, value, child) { - double offsetAmt = max(0, value * .3); - double opacity = (1 - offsetAmt / 150).clamp(0, 1); - return Transform.translate( - offset: Offset(0, offsetAmt), - child: Opacity(opacity: opacity, child: child), - ); - }, - child: _TitleText(widget.data, scroller: _scroller), + child: FullscreenKeyboardListScroller( + scrollController: _scroller, + child: CustomScrollView( + controller: _scroller, + scrollBehavior: ScrollConfiguration.of(context).copyWith(), + key: PageStorageKey('editorial'), + slivers: [ + /// Invisible padding at the top of the list, so the illustration shows through the btm + SliverToBoxAdapter( + child: SizedBox(height: illustrationHeight), ), - ), - /// Collapsing App bar, pins to the top of the list - SliverAppBar( - pinned: true, - collapsedHeight: minAppBarHeight, - toolbarHeight: minAppBarHeight, - expandedHeight: maxAppBarHeight, - backgroundColor: Colors.transparent, - elevation: 0, - leading: SizedBox.shrink(), - flexibleSpace: SizedBox.expand( - child: _AppBar( - widget.data.type, - scrollPos: _scrollPos, - sectionIndex: _sectionIndex, + /// Text content, animates itself to hide behind the app bar as it scrolls up + SliverToBoxAdapter( + child: ValueListenableBuilder( + valueListenable: _scrollPos, + builder: (_, value, child) { + double offsetAmt = max(0, value * .3); + double opacity = (1 - offsetAmt / 150).clamp(0, 1); + return Transform.translate( + offset: Offset(0, offsetAmt), + child: Opacity(opacity: opacity, child: child), + ); + }, + child: _TitleText(widget.data, scroller: _scroller), ), ), - ), - /// Editorial content (text and images) - _ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex), - ], + /// Collapsing App bar, pins to the top of the list + SliverAppBar( + pinned: true, + collapsedHeight: minAppBarHeight, + toolbarHeight: minAppBarHeight, + expandedHeight: maxAppBarHeight, + backgroundColor: Colors.transparent, + elevation: 0, + leading: SizedBox.shrink(), + flexibleSpace: SizedBox.expand( + child: _AppBar( + widget.data.type, + scrollPos: _scrollPos, + sectionIndex: _sectionIndex, + ), + ), + ), + + /// Editorial content (text and images) + _ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex), + ], + ), ), ), ),