diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index a5ed56a7da..51344e6a5c 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -2,7 +2,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; -import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -90,7 +89,9 @@ class _GridPageState extends State { } class FlowyGrid extends StatefulWidget { - const FlowyGrid({Key? key}) : super(key: key); + const FlowyGrid({ + super.key, + }); @override State createState() => _FlowyGridState(); @@ -104,8 +105,8 @@ class _FlowyGridState extends State { @override void initState() { - headerScrollController = _scrollController.linkHorizontalController(); super.initState(); + headerScrollController = _scrollController.linkHorizontalController(); } @override @@ -120,12 +121,12 @@ class _FlowyGridState extends State { buildWhen: (previous, current) => previous.fields != current.fields, builder: (context, state) { final contentWidth = GridLayout.headerWidth(state.fields.value); - final child = _wrapScrollView( - contentWidth, - [ - const _GridRows(), - const _GridFooter(), - ], + final child = _WrapScrollView( + scrollController: _scrollController, + contentWidth: contentWidth, + child: _GridRows( + verticalScrollController: _scrollController.verticalController, + ), ); return Column( @@ -135,45 +136,13 @@ class _FlowyGridState extends State { GridAccessoryMenu(viewId: state.viewId), _gridHeader(context, state.viewId), Flexible(child: child), - const RowCountBadge(), + const _RowCountBadge(), ], ); }, ); } - Widget _wrapScrollView( - double contentWidth, - List slivers, - ) { - final verticalScrollView = ScrollConfiguration( - behavior: const ScrollBehavior().copyWith(scrollbars: false), - child: CustomScrollView( - physics: StyledScrollPhysics(), - controller: _scrollController.verticalController, - slivers: slivers, - ), - ); - - final sizedVerticalScrollView = SizedBox( - width: contentWidth, - child: verticalScrollView, - ); - - final horizontalScrollView = StyledSingleChildScrollView( - controller: _scrollController.horizontalController, - axis: Axis.horizontal, - child: sizedVerticalScrollView, - ); - - return ScrollbarListStack( - axis: Axis.vertical, - controller: _scrollController.verticalController, - barSize: GridSize.scrollBarSize, - child: horizontalScrollView, - ); - } - Widget _gridHeader(BuildContext context, String viewId) { final fieldController = context.read().databaseController.fieldController; @@ -185,91 +154,68 @@ class _FlowyGridState extends State { } } -class _GridRows extends StatefulWidget { - const _GridRows({Key? key}) : super(key: key); +class _GridRows extends StatelessWidget { + const _GridRows({ + required this.verticalScrollController, + }); - @override - State<_GridRows> createState() => _GridRowsState(); -} - -class _GridRowsState extends State<_GridRows> { - final _key = GlobalKey(); + final ScrollController verticalScrollController; @override Widget build(BuildContext context) { - return Builder( - builder: (context) { - final filterState = context.watch().state; - final sortState = context.watch().state; + final filterState = context.watch().state; + final sortState = context.watch().state; - return BlocConsumer( - listenWhen: (previous, current) => previous.reason != current.reason, - listener: (context, state) { - state.reason.whenOrNull( - insert: (item) { - _key.currentState?.insertItem(item.index); - }, - delete: (item) { - _key.currentState?.removeItem( - item.index, - (context, animation) => _renderRow( - context, - item.rowInfo, - animation: animation, - ), + return BlocBuilder( + buildWhen: (previous, current) => current.reason.maybeWhen( + reorderRows: () => true, + reorderSingleRow: (reorderRow, rowInfo) => true, + delete: (item) => true, + insert: (item) => true, + orElse: () => false, + ), + builder: (context, state) { + final rowInfos = state.rowInfos; + final behavior = ScrollConfiguration.of(context).copyWith( + scrollbars: false, + ); + return ScrollConfiguration( + behavior: behavior, + child: ReorderableListView.builder( + /// TODO(Xazin): Resolve inconsistent scrollbar behavior + /// This is a workaround related to + /// https://github.com/flutter/flutter/issues/25652 + cacheExtent: 5000, + scrollController: verticalScrollController, + buildDefaultDragHandles: false, + proxyDecorator: (child, index, animation) => Material( + color: Colors.white.withOpacity(.1), + child: Opacity(opacity: .5, child: child), + ), + onReorder: (fromIndex, newIndex) { + final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; + if (fromIndex == toIndex) { + return; + } + context + .read() + .add(GridEvent.moveRow(fromIndex, toIndex)); + }, + itemCount: rowInfos.length + 1, // the extra item is the footer + itemBuilder: (context, index) { + if (index < rowInfos.length) { + final rowInfo = rowInfos[index]; + return _renderRow( + context, + rowInfo, + index: index, + isSortEnabled: sortState.sortInfos.isNotEmpty, + isFilterEnabled: filterState.filters.isNotEmpty, ); - }, - ); - }, - buildWhen: (previous, current) { - return current.reason.maybeWhen( - reorderRows: () => true, - reorderSingleRow: (reorderRow, rowInfo) => true, - delete: (item) => true, - insert: (item) => true, - orElse: () => false, - ); - }, - builder: (context, state) { - final rowInfos = context.watch().state.rowInfos; - - return SliverFillRemaining( - child: ReorderableListView.builder( - key: _key, - buildDefaultDragHandles: false, - proxyDecorator: (child, index, animation) => Material( - color: Colors.white.withOpacity(.1), - child: Opacity( - opacity: .5, - child: child, - ), - ), - onReorder: (fromIndex, newIndex) { - final toIndex = - newIndex > fromIndex ? newIndex - 1 : newIndex; - - if (fromIndex == toIndex) { - return; - } - - context - .read() - .add(GridEvent.moveRow(fromIndex, toIndex)); - }, - itemCount: rowInfos.length, - itemBuilder: (BuildContext context, int index) { - final RowInfo rowInfo = rowInfos[index]; - return _renderRow( - context, - rowInfo, - index: index, - isSortEnabled: sortState.sortInfos.isNotEmpty, - isFilterEnabled: filterState.filters.isNotEmpty, - ); - }, - ), - ); - }, + } + return const _GridFooter(key: Key('gridFooter')); + }, + ), ); }, ); @@ -352,27 +298,53 @@ class _GridRowsState extends State<_GridRows> { } class _GridFooter extends StatelessWidget { - const _GridFooter({Key? key}) : super(key: key); + const _GridFooter({ + super.key, + }); @override Widget build(BuildContext context) { - return SliverPadding( - padding: const EdgeInsets.only(bottom: 200), - sliver: SliverToBoxAdapter( + return Container( + padding: GridSize.footerContentInsets, + height: GridSize.footerHeight, + margin: const EdgeInsets.only(bottom: 200), + child: const GridAddRowButton(), + ); + } +} + +class _WrapScrollView extends StatelessWidget { + const _WrapScrollView({ + required this.contentWidth, + required this.scrollController, + required this.child, + }); + + final GridScrollController scrollController; + final double contentWidth; + final Widget child; + + @override + Widget build(BuildContext context) { + return ScrollbarListStack( + axis: Axis.vertical, + controller: scrollController.verticalController, + barSize: GridSize.scrollBarSize, + autoHideScrollbar: false, + child: StyledSingleChildScrollView( + controller: scrollController.horizontalController, + axis: Axis.horizontal, child: SizedBox( - height: GridSize.footerHeight, - child: Padding( - padding: GridSize.footerContentInsets, - child: const SizedBox(height: 40, child: GridAddRowButton()), - ), + width: contentWidth, + child: child, ), ), ); } } -class RowCountBadge extends StatelessWidget { - const RowCountBadge({Key? key}) : super(key: key); +class _RowCountBadge extends StatelessWidget { + const _RowCountBadge(); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart index df3237119e..b04440cede 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/grid_header.dart @@ -23,6 +23,7 @@ class GridHeaderSliverAdaptor extends StatefulWidget { final String viewId; final FieldController fieldController; final ScrollController anchorScrollController; + const GridHeaderSliverAdaptor({ required this.viewId, required this.fieldController, @@ -59,12 +60,6 @@ class _GridHeaderSliverAdaptorState extends State { child: _GridHeader(viewId: widget.viewId), ), ); - - // return SliverPersistentHeader( - // delegate: SliverHeaderDelegateImplementation(gridId: gridId, fields: state.fields), - // floating: true, - // pinned: true, - // ); }, ), ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart index 61cc750478..595991b5c4 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart @@ -46,32 +46,12 @@ class ScrollbarState extends State { @override void initState() { - widget.controller.addListener(() => setState(() {})); - widget.controller.position.isScrollingNotifier.addListener( - () { - if (!mounted) return; - if (!widget.autoHideScrollbar) return; - _hideScrollbarOperation?.cancel(); - if (!widget.controller.position.isScrollingNotifier.value) { - _hideScrollbarOperation = CancelableOperation.fromFuture( - Future.delayed(const Duration(seconds: 2), () {}), - ).then((_) { - hideHandler = true; - if (mounted) { - setState(() {}); - } - }); - } else { - hideHandler = false; - } - }, - ); super.initState(); - } + widget.controller.addListener(() => setState(() {})); - @override - void dispose() { - super.dispose(); + widget.controller.position.isScrollingNotifier.addListener( + _hideScrollbarInTime, + ); } @override @@ -86,6 +66,7 @@ class ScrollbarState extends State { builder: (_, BoxConstraints constraints) { double maxExtent; final contentSize = widget.contentSize; + switch (widget.axis) { case Axis.vertical: // Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents @@ -122,7 +103,7 @@ class ScrollbarState extends State { handleAlignment -= 1.0; // Calculate handleSize by comparing the total content size to our viewport - var handleExtent = _viewExtent; + double handleExtent = _viewExtent; if (contentExtent > _viewExtent) { // Make sure handle is never small than the minSize handleExtent = max(60, _viewExtent * _viewExtent / contentExtent); @@ -146,53 +127,76 @@ class ScrollbarState extends State { ? AFThemeExtension.of(context).greyHover.withOpacity(.1) : AFThemeExtension.of(context).greyHover.withOpacity(.3)); - //Layout the stack, it just contains a child, and - return Stack(children: [ - /// TRACK, thin strip, aligned along the end of the parent - if (widget.showTrack) - Align( - alignment: const Alignment(1, 1), - child: Container( - color: trackColor, - width: widget.axis == Axis.vertical - ? widget.size - : double.infinity, - height: widget.axis == Axis.horizontal - ? widget.size - : double.infinity, - ), - ), - - /// HANDLE - Clickable shape that changes scrollController when dragged - Align( - // Use calculated alignment to position handle from -1 to 1, let Alignment do the rest of the work - alignment: Alignment( - widget.axis == Axis.vertical ? 1 : handleAlignment, - widget.axis == Axis.horizontal ? 1 : handleAlignment, - ), - child: GestureDetector( - onVerticalDragUpdate: _handleVerticalDrag, - onHorizontalDragUpdate: _handleHorizontalDrag, - // HANDLE SHAPE - child: MouseHoverBuilder( - builder: (_, isHovered) => Container( - width: - widget.axis == Axis.vertical ? widget.size : handleExtent, + // Layout the stack, it just contains a child, and + return Stack( + children: [ + /// TRACK, thin strip, aligned along the end of the parent + if (widget.showTrack) + Align( + alignment: const Alignment(1, 1), + child: Container( + color: trackColor, + width: widget.axis == Axis.vertical + ? widget.size + : double.infinity, height: widget.axis == Axis.horizontal ? widget.size - : handleExtent, - decoration: BoxDecoration( - color: handleColor.withOpacity(isHovered ? 1 : .85), - borderRadius: Corners.s3Border), + : double.infinity, ), ), - ), - ) - ]).opacity(showHandle ? 1.0 : 0.0, animate: true); + + /// HANDLE - Clickable shape that changes scrollController when dragged + Align( + // Use calculated alignment to position handle from -1 to 1, let Alignment do the rest of the work + alignment: Alignment( + widget.axis == Axis.vertical ? 1 : handleAlignment, + widget.axis == Axis.horizontal ? 1 : handleAlignment, + ), + child: GestureDetector( + onVerticalDragUpdate: _handleVerticalDrag, + onHorizontalDragUpdate: _handleHorizontalDrag, + // HANDLE SHAPE + child: MouseHoverBuilder( + builder: (_, isHovered) => Container( + width: widget.axis == Axis.vertical + ? widget.size + : handleExtent, + height: widget.axis == Axis.horizontal + ? widget.size + : handleExtent, + decoration: BoxDecoration( + color: handleColor.withOpacity(isHovered ? 1 : .85), + borderRadius: Corners.s3Border, + ), + ), + ), + ), + ) + ], + ).opacity(showHandle ? 1.0 : 0.0, animate: true); }, ); } + void _hideScrollbarInTime() { + if (!mounted || !widget.autoHideScrollbar) return; + + _hideScrollbarOperation?.cancel(); + + if (!widget.controller.position.isScrollingNotifier.value) { + _hideScrollbarOperation = CancelableOperation.fromFuture( + Future.delayed(const Duration(seconds: 2), () {}), + ).then((_) { + hideHandler = true; + if (mounted) { + setState(() {}); + } + }); + } else { + hideHandler = false; + } + } + void _handleHorizontalDrag(DragUpdateDetails details) { var pos = widget.controller.offset; var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) / @@ -223,23 +227,23 @@ class ScrollbarListStack extends StatelessWidget { final Color? trackColor; final bool autoHideScrollbar; - const ScrollbarListStack( - {Key? key, - required this.barSize, - required this.axis, - required this.child, - required this.controller, - this.contentSize, - this.scrollbarPadding, - this.handleColor, - this.autoHideScrollbar = true, - this.trackColor}) - : super(key: key); + const ScrollbarListStack({ + super.key, + required this.barSize, + required this.axis, + required this.child, + required this.controller, + this.contentSize, + this.scrollbarPadding, + this.handleColor, + this.autoHideScrollbar = true, + this.trackColor, + }); @override Widget build(BuildContext context) { return Stack( - children: [ + children: [ /// LIST /// Wrap with a bit of padding on the right child.padding(