refactor(ui): use portal

This commit is contained in:
Salvatore Giordano
2021-09-21 09:46:46 +02:00
parent 829c7042cc
commit 3da495a872
5 changed files with 31 additions and 252 deletions
@@ -12,10 +12,6 @@
- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls.
✅ Added
- Added `MessageInput.customOverlays` property to add custom overlays to the `MessageInput`.
## 2.2.1
⚠️ Deprecated
@@ -1,206 +0,0 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// Options for building an [Overlay]
class OverlayOption {
/// Constructor for creating an [OverlayOption].
OverlayOption({
required this.showOverlay,
required this.overlayBuilder,
});
/// Show or hide overlay
final bool showOverlay;
/// Builder for creating overlay widget
final Widget Function(BuildContext, Offset anchor) overlayBuilder;
}
/// Class to create an overlay anchored to a child widget
class AnchoredOverlay extends StatelessWidget {
/// Constructor for creating an [AnchoredOverlay]
const AnchoredOverlay({
Key? key,
required this.overlayOptions,
required this.child,
}) : super(key: key);
/// List of overlay options
/// The first one which showOverlay is true will be used
final List<OverlayOption> overlayOptions;
/// Child to which overlay is anchored
final Widget child;
@override
Widget build(BuildContext context) {
final overlay =
overlayOptions.firstWhereOrNull((option) => option.showOverlay);
return SizedBox(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) =>
OverlayBuilder(
showOverlay: overlay != null,
overlayBuilder: (BuildContext overlayContext) {
final box = context.findRenderObject() as RenderBox?;
if (box != null) {
final center =
box.size.center(box.localToGlobal(const Offset(0, 0)));
return overlay?.overlayBuilder(overlayContext, center) ??
const Offstage();
} else {
return const Offstage();
}
},
child: child,
),
),
);
}
}
/// Class for creating a declarative-ish overlay
class OverlayBuilder extends StatefulWidget {
/// Constructor for creating an [OverlayBuilder]
const OverlayBuilder({
Key? key,
this.showOverlay = false,
required this.overlayBuilder,
required this.child,
}) : super(key: key);
/// Show or hide overlay
final bool showOverlay;
/// Builder for creating overlay widget
final Widget Function(BuildContext) overlayBuilder;
/// Child to which overlay is anchored
final Widget child;
@override
_OverlayBuilderState createState() => _OverlayBuilderState();
}
class _OverlayBuilderState extends State<OverlayBuilder>
with WidgetsBindingObserver {
OverlayEntry? overlayEntry;
@override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
if (widget.showOverlay) {
WidgetsBinding.instance?.addPostFrameCallback((_) => showOverlay());
}
}
EdgeInsets? _previousInsets;
@override
void didChangeMetrics() {
super.didChangeMetrics();
final newInsets = MediaQuery.of(context).viewPadding;
if (newInsets.bottom != _previousInsets?.bottom) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
hideOverlay();
syncWidgetAndOverlay();
});
_previousInsets = newInsets;
}
}
@override
void didUpdateWidget(OverlayBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance
?.addPostFrameCallback((_) => syncWidgetAndOverlay());
}
@override
void reassemble() {
super.reassemble();
WidgetsBinding.instance
?.addPostFrameCallback((_) => syncWidgetAndOverlay());
}
@override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
if (isShowingOverlay()) {
hideOverlay();
}
super.dispose();
}
bool isShowingOverlay() => overlayEntry != null;
void showOverlay() {
overlayEntry = OverlayEntry(
builder: widget.overlayBuilder,
);
if (overlayEntry != null) {
addToOverlay(overlayEntry!);
}
}
void addToOverlay(OverlayEntry entry) async {
Overlay.of(context)?.insert(entry);
}
void hideOverlay() {
overlayEntry?.remove();
overlayEntry = null;
}
void syncWidgetAndOverlay() {
if (isShowingOverlay() && !widget.showOverlay) {
hideOverlay();
} else if (!isShowingOverlay() && widget.showOverlay) {
showOverlay();
}
}
@override
Widget build(BuildContext context) => widget.child;
}
/// Class to position Overlay
class CenterAbout extends StatelessWidget {
/// Constructor for creating an [CenterAbout]
const CenterAbout({
Key? key,
required this.position,
required this.child,
}) : super(key: key);
/// Position of child
final Offset position;
/// Child widget to which overlay is attached
final Widget child;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Positioned(
bottom: size.height - position.dy,
left: position.dx,
child: FractionalTranslation(
translation: const Offset(-0.5, 0),
child: Padding(
padding: const EdgeInsets.only(bottom: 32),
child: child,
),
),
);
}
}
@@ -83,7 +83,7 @@ class MentionsOverlay extends StatelessWidget {
},
child: mentionsTileBuilder != null
? mentionsTileBuilder!(context, m)
: MentionTile(m),
: MentionTile(m, key: ValueKey(m.user?.id)),
),
),
)
@@ -6,11 +6,11 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/anchored_overlay.dart';
import 'package:stream_chat_flutter/src/commands_overlay.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
import 'package:stream_chat_flutter/src/emoji_overlay.dart';
@@ -192,7 +192,6 @@ class MessageInput extends StatefulWidget {
this.onAttachmentLimitExceed,
this.attachmentButtonBuilder,
this.commandButtonBuilder,
this.customOverlays = const [],
}) : assert(
initialMessage == null || editMessage == null,
"Can't provide both `initialMessage` and `editMessage`",
@@ -223,9 +222,6 @@ class MessageInput extends StatefulWidget {
/// Use this to transform the message
final FutureOr<Message> Function(Message)? preMessageSending;
/// List of custom overlays
final List<OverlayOption> customOverlays;
/// Parent message in case of a thread
final Message? parentMessage;
@@ -466,40 +462,32 @@ class MessageInputState extends State<MessageInput> {
);
}
return AnchoredOverlay(
overlayOptions: [
OverlayOption(
overlayBuilder: (context, offset) => CenterAbout(
position: offset,
child: _buildMentionsOverlayEntry(),
),
showOverlay: _showMentionsOverlay,
return PortalEntry(
portal: _buildCommandsOverlayEntry(),
visible: _showCommandsOverlay,
childAnchor: Alignment.topCenter,
portalAnchor: Alignment.bottomCenter,
child: PortalEntry(
portal: _buildEmojiOverlay(),
visible: _focusNode.hasFocus &&
textEditingController.text.isNotEmpty &&
textEditingController.selection.baseOffset > 0 &&
textEditingController.text
.substring(
0,
textEditingController.selection.baseOffset,
)
.contains(':'),
childAnchor: Alignment.topCenter,
portalAnchor: Alignment.bottomCenter,
child: PortalEntry(
portal: _buildMentionsOverlayEntry(),
visible: _showMentionsOverlay,
childAnchor: Alignment.topCenter,
portalAnchor: Alignment.bottomCenter,
child: child,
),
OverlayOption(
showOverlay: _showCommandsOverlay,
overlayBuilder: (context, offset) => CenterAbout(
position: offset,
child: _buildCommandsOverlayEntry(),
),
),
OverlayOption(
showOverlay: _focusNode.hasFocus &&
textEditingController.text.isNotEmpty &&
textEditingController.selection.baseOffset > 0 &&
textEditingController.text
.substring(
0,
textEditingController.selection.baseOffset,
)
.contains(':'),
overlayBuilder: (context, offset) => CenterAbout(
position: offset,
child: _buildEmojiOverlay(),
),
),
...widget.customOverlays,
],
child: child,
),
);
}
@@ -946,8 +934,10 @@ class MessageInputState extends State<MessageInput> {
Widget _buildCommandsOverlayEntry() {
final text = textEditingController.text.trimLeft();
// ignore: cast_nullable_to_non_nullable
final renderObject = context.findRenderObject() as RenderBox;
final renderObject = context.findRenderObject() as RenderBox?;
if (renderObject == null) {
return const Offstage();
}
return CommandsOverlay(
channel: StreamChannel.of(context).channel,
size: Size(renderObject.size.width - 16, 400),
@@ -1,7 +1,6 @@
export 'package:jiffy/jiffy.dart';
export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
export 'src/anchored_overlay.dart';
export 'src/attachment/attachment.dart';
export 'src/back_button.dart';
export 'src/channel_avatar.dart';