mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'infra_ui/overlay_base' into main
This commit is contained in:
commit
1d7540c954
@ -1,3 +1,4 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -48,109 +49,31 @@ class OverlayScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Overlay Demo'),
|
||||
),
|
||||
body: ChangeNotifierProvider(
|
||||
create: (context) => OverlayDemoConfiguration(AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch),
|
||||
child: Builder(builder: (providerContext) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(width: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 48.0),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
FlowyOverlay.of(context).insertCustom(
|
||||
widget: Positioned(
|
||||
left: windowSize.width / 2.0 - 100,
|
||||
top: 200,
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
height: 100,
|
||||
child: Card(
|
||||
color: Colors.green[200],
|
||||
child: GestureDetector(
|
||||
// ignore: avoid_print
|
||||
onTapDown: (_) => print('Hello Flutter'),
|
||||
child: const Center(child: FlutterLogo(size: 100)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
identifier: 'overlay_flutter_logo',
|
||||
delegate: null,
|
||||
);
|
||||
},
|
||||
child: const Text('Show Overlay'),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
DropdownButton<AnchorDirection>(
|
||||
value: providerContext.watch<OverlayDemoConfiguration>().anchorDirection,
|
||||
onChanged: (AnchorDirection? newValue) {
|
||||
if (newValue != null) {
|
||||
providerContext.read<OverlayDemoConfiguration>().anchorDirection = newValue;
|
||||
}
|
||||
},
|
||||
items: AnchorDirection.values.map((AnchorDirection classType) {
|
||||
return DropdownMenuItem<AnchorDirection>(value: classType, child: Text(classType.toString()));
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
DropdownButton<OverlapBehaviour>(
|
||||
value: providerContext.watch<OverlayDemoConfiguration>().overlapBehaviour,
|
||||
onChanged: (OverlapBehaviour? newValue) {
|
||||
if (newValue != null) {
|
||||
providerContext.read<OverlayDemoConfiguration>().overlapBehaviour = newValue;
|
||||
}
|
||||
},
|
||||
items: OverlapBehaviour.values.map((OverlapBehaviour classType) {
|
||||
return DropdownMenuItem<OverlapBehaviour>(value: classType, child: Text(classType.toString()));
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Builder(builder: (buttonContext) {
|
||||
return SizedBox(
|
||||
height: 100,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: SizedBox(
|
||||
width: 300,
|
||||
height: 50,
|
||||
child: Card(
|
||||
color: Colors.grey[200],
|
||||
child: GestureDetector(
|
||||
// ignore: avoid_print
|
||||
onTapDown: (_) => print('Hello Flutter'),
|
||||
child: const Center(child: FlutterLogo(size: 50)),
|
||||
),
|
||||
),
|
||||
),
|
||||
identifier: 'overlay_anchored_card',
|
||||
delegate: null,
|
||||
anchorContext: buttonContext,
|
||||
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
|
||||
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
|
||||
);
|
||||
},
|
||||
child: const Text('Show Anchored Overlay'),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 24.0),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
FlowyOverlay.of(context).insertWithRect(
|
||||
widget: SizedBox(
|
||||
appBar: AppBar(
|
||||
title: const Text('Overlay Demo'),
|
||||
),
|
||||
body: ChangeNotifierProvider(
|
||||
create: (context) => OverlayDemoConfiguration(AnchorDirection.rightWithTopAligned, OverlapBehaviour.stretch),
|
||||
child: Builder(builder: (providerContext) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(width: 500),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 48.0),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
FlowyOverlay.of(context).insertCustom(
|
||||
widget: Positioned(
|
||||
left: windowSize.width / 2.0 - 100,
|
||||
top: 200,
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
height: 100,
|
||||
child: Card(
|
||||
color: Colors.orange[200],
|
||||
color: Colors.green[200],
|
||||
child: GestureDetector(
|
||||
// ignore: avoid_print
|
||||
onTapDown: (_) => print('Hello Flutter'),
|
||||
@ -158,21 +81,144 @@ class OverlayScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
identifier: 'overlay_positioned_card',
|
||||
delegate: null,
|
||||
anchorPosition: Offset(0, windowSize.height - 200),
|
||||
anchorSize: Size.zero,
|
||||
),
|
||||
identifier: 'overlay_flutter_logo',
|
||||
delegate: null,
|
||||
);
|
||||
},
|
||||
child: const Text('Show Overlay'),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
DropdownButton<AnchorDirection>(
|
||||
value: providerContext.watch<OverlayDemoConfiguration>().anchorDirection,
|
||||
onChanged: (AnchorDirection? newValue) {
|
||||
if (newValue != null) {
|
||||
providerContext.read<OverlayDemoConfiguration>().anchorDirection = newValue;
|
||||
}
|
||||
},
|
||||
items: AnchorDirection.values.map((AnchorDirection classType) {
|
||||
return DropdownMenuItem<AnchorDirection>(value: classType, child: Text(classType.toString()));
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
DropdownButton<OverlapBehaviour>(
|
||||
value: providerContext.watch<OverlayDemoConfiguration>().overlapBehaviour,
|
||||
onChanged: (OverlapBehaviour? newValue) {
|
||||
if (newValue != null) {
|
||||
providerContext.read<OverlayDemoConfiguration>().overlapBehaviour = newValue;
|
||||
}
|
||||
},
|
||||
items: OverlapBehaviour.values.map((OverlapBehaviour classType) {
|
||||
return DropdownMenuItem<OverlapBehaviour>(value: classType, child: Text(classType.toString()));
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Builder(builder: (buttonContext) {
|
||||
return SizedBox(
|
||||
height: 100,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: SizedBox(
|
||||
width: 300,
|
||||
height: 50,
|
||||
child: Card(
|
||||
color: Colors.grey[200],
|
||||
child: GestureDetector(
|
||||
// ignore: avoid_print
|
||||
onTapDown: (_) => print('Hello Flutter'),
|
||||
child: const Center(child: FlutterLogo(size: 50)),
|
||||
),
|
||||
),
|
||||
),
|
||||
identifier: 'overlay_anchored_card',
|
||||
delegate: null,
|
||||
anchorContext: buttonContext,
|
||||
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
|
||||
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
|
||||
);
|
||||
},
|
||||
child: const Text('Show Anchored Overlay'),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 24.0),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final windowSize = MediaQuery.of(context).size;
|
||||
FlowyOverlay.of(context).insertWithRect(
|
||||
widget: SizedBox(
|
||||
width: 200,
|
||||
height: 100,
|
||||
child: Card(
|
||||
color: Colors.orange[200],
|
||||
child: GestureDetector(
|
||||
// ignore: avoid_print
|
||||
onTapDown: (_) => print('Hello Flutter'),
|
||||
child: const Center(child: FlutterLogo(size: 100)),
|
||||
),
|
||||
),
|
||||
),
|
||||
identifier: 'overlay_positioned_card',
|
||||
delegate: null,
|
||||
anchorPosition: Offset(0, windowSize.height - 200),
|
||||
anchorSize: Size.zero,
|
||||
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
|
||||
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
|
||||
);
|
||||
},
|
||||
child: const Text('Show Positioned Overlay'),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Builder(builder: (buttonContext) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
ListOverlay.showWithAnchor(
|
||||
context,
|
||||
itemBuilder: (_, index) => Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
|
||||
elevation: 0,
|
||||
child: Text(
|
||||
'Option $index',
|
||||
style: const TextStyle(fontSize: 20.0, color: Colors.black),
|
||||
),
|
||||
),
|
||||
itemCount: 10,
|
||||
identifier: 'overlay_list_menu',
|
||||
anchorContext: buttonContext,
|
||||
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
|
||||
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
|
||||
maxWidth: 200.0,
|
||||
maxHeight: 200.0,
|
||||
);
|
||||
},
|
||||
child: const Text('Show List Overlay'),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 24.0),
|
||||
Builder(builder: (buttonContext) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
OptionOverlay.showWithAnchor(
|
||||
context,
|
||||
items: <String>['Alpha', 'Beta', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel'],
|
||||
onHover: (value, index) => print('Did hover option $index, value $value'),
|
||||
onTap: (value, index) => print('Did tap option $index, value $value'),
|
||||
identifier: 'overlay_options',
|
||||
anchorContext: buttonContext,
|
||||
anchorDirection: providerContext.read<OverlayDemoConfiguration>().anchorDirection,
|
||||
overlapBehaviour: providerContext.read<OverlayDemoConfiguration>().overlapBehaviour,
|
||||
);
|
||||
},
|
||||
child: const Text('Show Positioned Overlay'),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text('Show Options Overlay'),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
));
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,3 +3,6 @@ import 'package:flutter/material.dart';
|
||||
// MARK: - Shared Builder
|
||||
|
||||
typedef WidgetBuilder = Widget Function();
|
||||
|
||||
typedef IndexedCallback = void Function(int index);
|
||||
typedef IndexedValueCallback<T> = void Function(T value, int index);
|
||||
|
@ -6,3 +6,5 @@ export 'src/keyboard/keyboard_visibility_detector.dart';
|
||||
|
||||
// Overlay
|
||||
export 'src/flowy_overlay/flowy_overlay.dart';
|
||||
export 'src/flowy_overlay/list_overlay.dart';
|
||||
export 'src/flowy_overlay/option_overlay.dart';
|
||||
|
@ -6,3 +6,5 @@ export 'src/keyboard/keyboard_visibility_detector.dart';
|
||||
|
||||
// Overlay
|
||||
export 'src/flowy_overlay/flowy_overlay.dart';
|
||||
export 'src/flowy_overlay/list_overlay.dart';
|
||||
export 'src/flowy_overlay/option_overlay.dart';
|
||||
|
@ -0,0 +1,99 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ListOverlay extends StatelessWidget {
|
||||
const ListOverlay({
|
||||
Key? key,
|
||||
required this.itemBuilder,
|
||||
this.itemCount,
|
||||
this.controller,
|
||||
this.maxWidth = double.infinity,
|
||||
this.maxHeight = double.infinity,
|
||||
}) : super(key: key);
|
||||
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
final int? itemCount;
|
||||
final ScrollController? controller;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
|
||||
static void showWithAnchor(
|
||||
BuildContext context, {
|
||||
required String identifier,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
int? itemCount,
|
||||
ScrollController? controller,
|
||||
double maxWidth = double.infinity,
|
||||
double maxHeight = double.infinity,
|
||||
required BuildContext anchorContext,
|
||||
AnchorDirection? anchorDirection,
|
||||
FlowyOverlayDelegate? delegate,
|
||||
OverlapBehaviour? overlapBehaviour,
|
||||
}) {
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: ListOverlay(
|
||||
itemBuilder: itemBuilder,
|
||||
itemCount: itemCount,
|
||||
controller: controller,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
identifier: identifier,
|
||||
anchorContext: anchorContext,
|
||||
anchorDirection: anchorDirection,
|
||||
delegate: delegate,
|
||||
overlapBehaviour: overlapBehaviour,
|
||||
);
|
||||
}
|
||||
|
||||
static void showWithRect(
|
||||
BuildContext context, {
|
||||
required BuildContext anchorContext,
|
||||
required String identifier,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
int? itemCount,
|
||||
ScrollController? controller,
|
||||
double maxWidth = double.infinity,
|
||||
double maxHeight = double.infinity,
|
||||
required Offset anchorPosition,
|
||||
required Size anchorSize,
|
||||
AnchorDirection? anchorDirection,
|
||||
FlowyOverlayDelegate? delegate,
|
||||
OverlapBehaviour? overlapBehaviour,
|
||||
}) {
|
||||
FlowyOverlay.of(context).insertWithRect(
|
||||
widget: ListOverlay(
|
||||
itemBuilder: itemBuilder,
|
||||
itemCount: itemCount,
|
||||
controller: controller,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
),
|
||||
identifier: identifier,
|
||||
anchorPosition: anchorPosition,
|
||||
anchorSize: anchorSize,
|
||||
anchorDirection: anchorDirection,
|
||||
delegate: delegate,
|
||||
overlapBehaviour: overlapBehaviour,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints.tight(Size(maxWidth, maxHeight)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withOpacity(0.1), spreadRadius: 1, blurRadius: 20.0),
|
||||
],
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: itemBuilder,
|
||||
itemCount: itemCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OptionItem {
|
||||
const OptionItem(this.icon, this.title);
|
||||
|
||||
final Icon? icon;
|
||||
final String title;
|
||||
}
|
||||
|
||||
class OptionOverlay<T> extends StatelessWidget {
|
||||
const OptionOverlay({
|
||||
Key? key,
|
||||
required this.items,
|
||||
this.onHover,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<T> items;
|
||||
final IndexedValueCallback<T>? onHover;
|
||||
final IndexedValueCallback<T>? onTap;
|
||||
|
||||
static void showWithAnchor<T>(
|
||||
BuildContext context, {
|
||||
required String identifier,
|
||||
required List<T> items,
|
||||
IndexedValueCallback<T>? onHover,
|
||||
IndexedValueCallback<T>? onTap,
|
||||
required BuildContext anchorContext,
|
||||
AnchorDirection? anchorDirection,
|
||||
FlowyOverlayDelegate? delegate,
|
||||
OverlapBehaviour? overlapBehaviour,
|
||||
}) {
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OptionOverlay(
|
||||
items: items,
|
||||
onHover: onHover,
|
||||
onTap: onTap,
|
||||
),
|
||||
identifier: identifier,
|
||||
anchorContext: anchorContext,
|
||||
anchorDirection: anchorDirection,
|
||||
delegate: delegate,
|
||||
overlapBehaviour: overlapBehaviour,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<_OptionListItem> listItems = items.map((e) => _OptionListItem(e)).toList();
|
||||
return ListOverlay(
|
||||
itemBuilder: (context, index) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onHover: onHover != null ? (_) => onHover!(items[index], index) : null,
|
||||
child: GestureDetector(
|
||||
onTap: onTap != null ? () => onTap!(items[index], index) : null,
|
||||
child: listItems[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: listItems.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionListItem<T> extends StatelessWidget {
|
||||
const _OptionListItem(
|
||||
this.value, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final T value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (T == String || T == OptionItem) {
|
||||
var children = <Widget>[];
|
||||
if (value is String) {
|
||||
children = [
|
||||
Text(value as String),
|
||||
];
|
||||
} else if (value is OptionItem) {
|
||||
final optionItem = value as OptionItem;
|
||||
children = [
|
||||
if (optionItem.icon != null) optionItem.icon!,
|
||||
Text(optionItem.title),
|
||||
];
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
throw UnimplementedError('The type $T is not supported by option list.');
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user