feat: auto scroll to bottom after crating a new field

This commit is contained in:
appflowy 2022-09-07 17:28:22 +08:00
parent 56fdc99990
commit 9a57593690
4 changed files with 78 additions and 33 deletions

View File

@ -13,12 +13,14 @@ import 'field_type_option_editor.dart';
class FieldEditor extends StatefulWidget { class FieldEditor extends StatefulWidget {
final String gridId; final String gridId;
final String fieldName; final String fieldName;
final VoidCallback? onRemoved;
final IFieldTypeOptionLoader typeOptionLoader; final IFieldTypeOptionLoader typeOptionLoader;
const FieldEditor({ const FieldEditor({
required this.gridId, required this.gridId,
required this.fieldName, this.fieldName = "",
required this.typeOptionLoader, required this.typeOptionLoader,
this.onRemoved,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);

View File

@ -33,6 +33,7 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return RoundedInputField( return RoundedInputField(
height: 36, height: 36,
autoFocus: true,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
controller: controller, controller: controller,
normalBorderColor: theme.shader4, normalBorderColor: theme.shader4,
@ -47,7 +48,8 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
@override @override
void didUpdateWidget(covariant FieldNameTextField oldWidget) { void didUpdateWidget(covariant FieldNameTextField oldWidget) {
controller.text = widget.name; controller.text = widget.name;
controller.selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length)); controller.selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }

View File

@ -117,6 +117,7 @@ class _PropertyList extends StatelessWidget {
axis: Axis.vertical, axis: Axis.vertical,
controller: _scrollController, controller: _scrollController,
barSize: GridSize.scrollBarSize, barSize: GridSize.scrollBarSize,
autoHideScrollbar: false,
child: ListView.separated( child: ListView.separated(
controller: _scrollController, controller: _scrollController,
itemCount: state.gridCells.length, itemCount: state.gridCells.length,
@ -132,7 +133,27 @@ class _PropertyList extends StatelessWidget {
), ),
), ),
), ),
_CreateFieldButton(viewId: viewId), _CreateFieldButton(
viewId: viewId,
onClosed: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);
});
},
onOpened: () {
return OverlayContainer(
constraints: BoxConstraints.loose(const Size(240, 200)),
child: FieldEditor(
gridId: viewId,
typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
),
);
},
),
], ],
); );
}, },
@ -142,7 +163,14 @@ class _PropertyList extends StatelessWidget {
class _CreateFieldButton extends StatelessWidget { class _CreateFieldButton extends StatelessWidget {
final String viewId; final String viewId;
const _CreateFieldButton({required this.viewId, Key? key}) : super(key: key); final Widget Function() onOpened;
final VoidCallback onClosed;
const _CreateFieldButton({
required this.viewId,
required this.onOpened,
required this.onClosed,
Key? key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -150,6 +178,8 @@ class _CreateFieldButton extends StatelessWidget {
return Popover( return Popover(
triggerActions: PopoverTriggerActionFlags.click, triggerActions: PopoverTriggerActionFlags.click,
direction: PopoverDirection.bottomWithLeftAligned,
onClose: onClosed,
child: SizedBox( child: SizedBox(
height: 40, height: 40,
child: FlowyButton( child: FlowyButton(
@ -162,16 +192,7 @@ class _CreateFieldButton extends StatelessWidget {
leftIcon: svgWidget("home/add"), leftIcon: svgWidget("home/add"),
), ),
), ),
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) => onOpened(),
return OverlayContainer(
constraints: BoxConstraints.loose(const Size(240, 200)),
child: FieldEditor(
gridId: viewId,
fieldName: "",
typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
),
);
},
); );
} }
} }

View File

@ -14,6 +14,7 @@ class StyledScrollbar extends StatefulWidget {
final ScrollController controller; final ScrollController controller;
final Function(double)? onDrag; final Function(double)? onDrag;
final bool showTrack; final bool showTrack;
final bool autoHideScrollbar;
final Color? handleColor; final Color? handleColor;
final Color? trackColor; final Color? trackColor;
@ -30,6 +31,7 @@ class StyledScrollbar extends StatefulWidget {
this.onDrag, this.onDrag,
this.contentSize, this.contentSize,
this.showTrack = false, this.showTrack = false,
this.autoHideScrollbar = true,
this.handleColor, this.handleColor,
this.trackColor}) this.trackColor})
: super(key: key); : super(key: key);
@ -48,16 +50,13 @@ class ScrollbarState extends State<StyledScrollbar> {
widget.controller.addListener(() => setState(() {})); widget.controller.addListener(() => setState(() {}));
widget.controller.position.isScrollingNotifier.addListener( widget.controller.position.isScrollingNotifier.addListener(
() { () {
if (!mounted) { if (!mounted) return;
return; if (!widget.autoHideScrollbar) return;
}
_hideScrollbarOperation?.cancel(); _hideScrollbarOperation?.cancel();
if (!widget.controller.position.isScrollingNotifier.value) { if (!widget.controller.position.isScrollingNotifier.value) {
// scroll is stopped
_hideScrollbarOperation = CancelableOperation.fromFuture( _hideScrollbarOperation = CancelableOperation.fromFuture(
Future.delayed(const Duration(seconds: 2), () {}), Future.delayed(const Duration(seconds: 2), () {}),
).then((_) { ).then((_) {
// Opti: hide with animation
hideHandler = true; hideHandler = true;
if (mounted) { if (mounted) {
setState(() {}); setState(() {});
@ -103,7 +102,6 @@ class ScrollbarState extends State<StyledScrollbar> {
break; break;
case Axis.horizontal: case Axis.horizontal:
// Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents // Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents
if (contentSize != null && contentSize > 0) { if (contentSize != null && contentSize > 0) {
maxExtent = contentSize - constraints.maxWidth; maxExtent = contentSize - constraints.maxWidth;
} else { } else {
@ -118,7 +116,8 @@ class ScrollbarState extends State<StyledScrollbar> {
// Calculate the alignment for the handle, this is a value between 0 and 1, // Calculate the alignment for the handle, this is a value between 0 and 1,
// it automatically takes the handle size into acct // it automatically takes the handle size into acct
// ignore: omit_local_variable_types // ignore: omit_local_variable_types
double handleAlignment = maxExtent == 0 ? 0 : widget.controller.offset / maxExtent; double handleAlignment =
maxExtent == 0 ? 0 : widget.controller.offset / maxExtent;
// Convert handle alignment from [0, 1] to [-1, 1] // Convert handle alignment from [0, 1] to [-1, 1]
handleAlignment *= 2.0; handleAlignment *= 2.0;
@ -127,19 +126,25 @@ class ScrollbarState extends State<StyledScrollbar> {
// Calculate handleSize by comparing the total content size to our viewport // Calculate handleSize by comparing the total content size to our viewport
var handleExtent = _viewExtent; var handleExtent = _viewExtent;
if (contentExtent > _viewExtent) { if (contentExtent > _viewExtent) {
//Make sure handle is never small than the minSize // Make sure handle is never small than the minSize
handleExtent = max(60, _viewExtent * _viewExtent / contentExtent); handleExtent = max(60, _viewExtent * _viewExtent / contentExtent);
} }
// Hide the handle if content is < the viewExtent // Hide the handle if content is < the viewExtent
var showHandle = contentExtent > _viewExtent && contentExtent > 0; var showHandle = contentExtent > _viewExtent && contentExtent > 0;
if (hideHandler) { if (hideHandler) {
showHandle = false; showHandle = false;
} }
// Handle color // Handle color
var handleColor = widget.handleColor ?? (theme.isDark ? theme.bg2.withOpacity(.2) : theme.bg2); var handleColor = widget.handleColor ??
(theme.isDark ? theme.bg2.withOpacity(.2) : theme.bg2);
// Track color // Track color
var trackColor = widget.trackColor ?? (theme.isDark ? theme.bg2.withOpacity(.1) : theme.bg2.withOpacity(.3)); var trackColor = widget.trackColor ??
(theme.isDark
? theme.bg2.withOpacity(.1)
: theme.bg2.withOpacity(.3));
//Layout the stack, it just contains a child, and //Layout the stack, it just contains a child, and
return Stack(children: <Widget>[ return Stack(children: <Widget>[
@ -149,8 +154,12 @@ class ScrollbarState extends State<StyledScrollbar> {
alignment: const Alignment(1, 1), alignment: const Alignment(1, 1),
child: Container( child: Container(
color: trackColor, color: trackColor,
width: widget.axis == Axis.vertical ? widget.size : double.infinity, width: widget.axis == Axis.vertical
height: widget.axis == Axis.horizontal ? widget.size : double.infinity, ? widget.size
: double.infinity,
height: widget.axis == Axis.horizontal
? widget.size
: double.infinity,
), ),
), ),
@ -167,10 +176,14 @@ class ScrollbarState extends State<StyledScrollbar> {
// HANDLE SHAPE // HANDLE SHAPE
child: MouseHoverBuilder( child: MouseHoverBuilder(
builder: (_, isHovered) => Container( builder: (_, isHovered) => Container(
width: widget.axis == Axis.vertical ? widget.size : handleExtent, width:
height: widget.axis == Axis.horizontal ? widget.size : handleExtent, widget.axis == Axis.vertical ? widget.size : handleExtent,
height: widget.axis == Axis.horizontal
? widget.size
: handleExtent,
decoration: BoxDecoration( decoration: BoxDecoration(
color: handleColor.withOpacity(isHovered ? 1 : .85), borderRadius: Corners.s3Border), color: handleColor.withOpacity(isHovered ? 1 : .85),
borderRadius: Corners.s3Border),
), ),
), ),
), ),
@ -182,15 +195,19 @@ class ScrollbarState extends State<StyledScrollbar> {
void _handleHorizontalDrag(DragUpdateDetails details) { void _handleHorizontalDrag(DragUpdateDetails details) {
var pos = widget.controller.offset; var pos = widget.controller.offset;
var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) / _viewExtent; var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /
widget.controller.jumpTo((pos + details.delta.dx * pxRatio).clamp(0.0, widget.controller.position.maxScrollExtent)); _viewExtent;
widget.controller.jumpTo((pos + details.delta.dx * pxRatio)
.clamp(0.0, widget.controller.position.maxScrollExtent));
widget.onDrag?.call(details.delta.dx); widget.onDrag?.call(details.delta.dx);
} }
void _handleVerticalDrag(DragUpdateDetails details) { void _handleVerticalDrag(DragUpdateDetails details) {
var pos = widget.controller.offset; var pos = widget.controller.offset;
var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) / _viewExtent; var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /
widget.controller.jumpTo((pos + details.delta.dy * pxRatio).clamp(0.0, widget.controller.position.maxScrollExtent)); _viewExtent;
widget.controller.jumpTo((pos + details.delta.dy * pxRatio)
.clamp(0.0, widget.controller.position.maxScrollExtent));
widget.onDrag?.call(details.delta.dy); widget.onDrag?.call(details.delta.dy);
} }
} }
@ -204,6 +221,7 @@ class ScrollbarListStack extends StatelessWidget {
final EdgeInsets? scrollbarPadding; final EdgeInsets? scrollbarPadding;
final Color? handleColor; final Color? handleColor;
final Color? trackColor; final Color? trackColor;
final bool autoHideScrollbar;
const ScrollbarListStack( const ScrollbarListStack(
{Key? key, {Key? key,
@ -214,6 +232,7 @@ class ScrollbarListStack extends StatelessWidget {
this.contentSize, this.contentSize,
this.scrollbarPadding, this.scrollbarPadding,
this.handleColor, this.handleColor,
this.autoHideScrollbar = true,
this.trackColor}) this.trackColor})
: super(key: key); : super(key: key);
@ -238,6 +257,7 @@ class ScrollbarListStack extends StatelessWidget {
contentSize: contentSize, contentSize: contentSize,
trackColor: trackColor, trackColor: trackColor,
handleColor: handleColor, handleColor: handleColor,
autoHideScrollbar: autoHideScrollbar,
), ),
) )
// The animate will be used by the children that using styled_widget. // The animate will be used by the children that using styled_widget.