Merge branch 'infra_ui/overlay_base' into main

This commit is contained in:
Jaylen Bian 2021-08-09 10:50:10 +08:00
commit 1d7540c954
6 changed files with 360 additions and 111 deletions

View File

@ -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'),
);
}),
],
),
);
}),
));
),
);
}),
),
);
}
}

View File

@ -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);

View File

@ -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';

View File

@ -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';

View File

@ -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,
),
);
}
}

View File

@ -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.');
}
}