Merge pull request #154 from gskinnerTeam/feature/keyboard-scrolling
Add fullscreen keyboard list scroller component
This commit is contained in:
commit
9045b5d8fd
75
lib/ui/common/fullscreen_keyboard_list_scroller.dart
Normal file
75
lib/ui/common/fullscreen_keyboard_list_scroller.dart
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,17 @@
|
|||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
|
|
||||||
class FullScreenKeyboardListener extends StatefulWidget {
|
class FullscreenKeyboardListener extends StatefulWidget {
|
||||||
const FullScreenKeyboardListener({super.key, required this.child, this.onKeyDown, this.onKeyUp});
|
const FullscreenKeyboardListener({super.key, required this.child, this.onKeyDown, this.onKeyUp, this.onKeyRepeat});
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final bool Function(KeyDownEvent event)? onKeyDown;
|
final bool Function(KeyDownEvent event)? onKeyDown;
|
||||||
final bool Function(KeyUpEvent event)? onKeyUp;
|
final bool Function(KeyUpEvent event)? onKeyUp;
|
||||||
|
final bool Function(KeyRepeatEvent event)? onKeyRepeat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FullScreenKeyboardListener> createState() => _FullScreenKeyboardListenerState();
|
State<FullscreenKeyboardListener> createState() => _FullscreenKeyboardListenerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FullScreenKeyboardListenerState extends State<FullScreenKeyboardListener> {
|
class _FullscreenKeyboardListenerState extends State<FullscreenKeyboardListener> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -31,6 +32,9 @@ class _FullScreenKeyboardListenerState extends State<FullScreenKeyboardListener>
|
|||||||
if (event is KeyUpEvent && widget.onKeyUp != null) {
|
if (event is KeyUpEvent && widget.onKeyUp != null) {
|
||||||
result = widget.onKeyUp!.call(event);
|
result = widget.onKeyUp!.call(event);
|
||||||
}
|
}
|
||||||
|
if (event is KeyRepeatEvent && widget.onKeyRepeat != null) {
|
||||||
|
result = widget.onKeyRepeat!.call(event);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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/compass_divider.dart';
|
||||||
import 'package:wonders/ui/common/controls/app_header.dart';
|
import 'package:wonders/ui/common/controls/app_header.dart';
|
||||||
import 'package:wonders/ui/common/curved_clippers.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/google_maps_marker.dart';
|
||||||
import 'package:wonders/ui/common/gradient_container.dart';
|
import 'package:wonders/ui/common/gradient_container.dart';
|
||||||
import 'package:wonders/ui/common/hidden_collectible.dart';
|
import 'package:wonders/ui/common/hidden_collectible.dart';
|
||||||
@ -111,54 +112,56 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
|
|||||||
padding: widget.contentPadding,
|
padding: widget.contentPadding,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
child: FocusTraversalGroup(
|
child: FocusTraversalGroup(
|
||||||
child: CustomScrollView(
|
child: FullscreenKeyboardListScroller(
|
||||||
primary: false,
|
scrollController: _scroller,
|
||||||
controller: _scroller,
|
child: CustomScrollView(
|
||||||
scrollBehavior: ScrollConfiguration.of(context).copyWith(),
|
controller: _scroller,
|
||||||
key: PageStorageKey('editorial'),
|
scrollBehavior: ScrollConfiguration.of(context).copyWith(),
|
||||||
slivers: [
|
key: PageStorageKey('editorial'),
|
||||||
/// Invisible padding at the top of the list, so the illustration shows through the btm
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
/// Invisible padding at the top of the list, so the illustration shows through the btm
|
||||||
child: SizedBox(height: illustrationHeight),
|
SliverToBoxAdapter(
|
||||||
),
|
child: SizedBox(height: illustrationHeight),
|
||||||
|
|
||||||
/// Text content, animates itself to hide behind the app bar as it scrolls up
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: ValueListenableBuilder<double>(
|
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
/// Collapsing App bar, pins to the top of the list
|
/// Text content, animates itself to hide behind the app bar as it scrolls up
|
||||||
SliverAppBar(
|
SliverToBoxAdapter(
|
||||||
pinned: true,
|
child: ValueListenableBuilder<double>(
|
||||||
collapsedHeight: minAppBarHeight,
|
valueListenable: _scrollPos,
|
||||||
toolbarHeight: minAppBarHeight,
|
builder: (_, value, child) {
|
||||||
expandedHeight: maxAppBarHeight,
|
double offsetAmt = max(0, value * .3);
|
||||||
backgroundColor: Colors.transparent,
|
double opacity = (1 - offsetAmt / 150).clamp(0, 1);
|
||||||
elevation: 0,
|
return Transform.translate(
|
||||||
leading: SizedBox.shrink(),
|
offset: Offset(0, offsetAmt),
|
||||||
flexibleSpace: SizedBox.expand(
|
child: Opacity(opacity: opacity, child: child),
|
||||||
child: _AppBar(
|
);
|
||||||
widget.data.type,
|
},
|
||||||
scrollPos: _scrollPos,
|
child: _TitleText(widget.data, scroller: _scroller),
|
||||||
sectionIndex: _sectionIndex,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
/// Editorial content (text and images)
|
/// Collapsing App bar, pins to the top of the list
|
||||||
_ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex),
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user