Skip to content

Commit 7121674

Browse files
authored
Merge pull request #1383 from EnsembleUI/bottom_modal
Fixed bottom modal scope and refactored
2 parents 7aa85d0 + 72e6ae9 commit 7121674

File tree

5 files changed

+240
-136
lines changed

5 files changed

+240
-136
lines changed

lib/action/bottom_modal_action.dart

Lines changed: 0 additions & 111 deletions
This file was deleted.

lib/action/bottom_modal_actions.dart

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import 'package:ensemble/framework/action.dart';
2+
import 'package:ensemble/framework/data_context.dart';
3+
import 'package:ensemble/framework/error_handling.dart';
4+
import 'package:ensemble/framework/event.dart';
5+
import 'package:ensemble/framework/model.dart';
6+
import 'package:ensemble/framework/scope.dart';
7+
import 'package:ensemble/framework/view/data_scope_widget.dart';
8+
import 'package:ensemble/screen_controller.dart';
9+
import 'package:ensemble/util/utils.dart';
10+
import 'package:ensemble_ts_interpreter/invokables/invokable.dart';
11+
import 'package:flutter/material.dart';
12+
13+
/// open a Modal Bottom Sheet
14+
class ShowBottomModalAction extends EnsembleAction {
15+
ShowBottomModalAction({
16+
super.initiator,
17+
super.inputs,
18+
required this.body,
19+
required this.payload,
20+
this.onDismiss,
21+
});
22+
23+
static const defaultTopBorderRadius = Radius.circular(16);
24+
25+
final Map payload;
26+
final dynamic body;
27+
final EnsembleAction? onDismiss;
28+
29+
factory ShowBottomModalAction.from({Invokable? initiator, Map? payload}) {
30+
dynamic body = payload?['body'] ?? payload?['widget'];
31+
if (payload == null || body == null) {
32+
throw LanguageError(
33+
"${ActionType.showBottomModal.name} requires a body widget.");
34+
}
35+
return ShowBottomModalAction(
36+
initiator: initiator,
37+
inputs: Utils.getMap(payload['inputs']),
38+
body: body,
39+
onDismiss: EnsembleAction.fromYaml(payload['onDismiss']),
40+
payload: payload);
41+
}
42+
43+
EdgeInsets? margin(scopeManager) =>
44+
Utils.optionalInsets(eval(payload["styles"]?["margin"], scopeManager));
45+
46+
EdgeInsets? padding(scopeManager) =>
47+
Utils.optionalInsets(eval(payload["styles"]?["padding"], scopeManager));
48+
49+
EBorderRadius? borderRadius(scopeManager) => Utils.getBorderRadius(
50+
eval(payload["styles"]?["borderRadius"], scopeManager));
51+
52+
bool useSafeArea(scopeManager) =>
53+
Utils.getBool(eval(payload["styles"]?["useSafeArea"], scopeManager),
54+
fallback: false);
55+
56+
Color? getBarrierColor(scopeManager) =>
57+
Utils.getColor(eval(payload["styles"]?['barrierColor'], scopeManager));
58+
59+
Color? getBackgroundColor(scopeManager) =>
60+
Utils.getColor(eval(payload["styles"]?['backgroundColor'], scopeManager));
61+
62+
bool showDragHandle(scopeManager) =>
63+
Utils.getBool(eval(payload["styles"]?["showDragHandle"], scopeManager),
64+
fallback: true);
65+
66+
Color? dragHandleColor(scopeManager) =>
67+
Utils.getColor(eval(payload["styles"]?["dragHandleColor"], scopeManager));
68+
69+
bool? isScrollable(scopeManager) =>
70+
Utils.optionalBool(eval(payload["scrollable"], scopeManager));
71+
72+
// scroll options
73+
double? _initialViewport(scopeManager) => Utils.optionalDouble(
74+
eval(payload["scrollOptions"]?["initialViewport"], scopeManager),
75+
min: 0,
76+
max: 1);
77+
78+
double? _minViewport(scopeManager) => Utils.optionalDouble(
79+
eval(payload["scrollOptions"]?["minViewport"], scopeManager),
80+
min: 0,
81+
max: 1);
82+
83+
double? _maxViewport(scopeManager) => Utils.optionalDouble(
84+
eval(payload["scrollOptions"]?["maxViewport"], scopeManager),
85+
min: 0,
86+
max: 1);
87+
88+
@override
89+
Future<dynamic> execute(BuildContext context, ScopeManager scopeManager) {
90+
if (body != null) {
91+
showModalBottomSheet(
92+
context: context,
93+
// disable the default bottom sheet styling since we use our own
94+
backgroundColor: Colors.transparent,
95+
elevation: 0,
96+
showDragHandle: false,
97+
98+
barrierColor: getBarrierColor(scopeManager),
99+
isScrollControlled: true,
100+
enableDrag: true,
101+
// padding to account for the keyboard when we have input widgets inside the modal
102+
builder: (modalContext) => Padding(
103+
padding: EdgeInsets.only(
104+
bottom: MediaQuery.of(modalContext).viewInsets.bottom,
105+
),
106+
// have a bottom modal scope widget so we can close the modal
107+
child: BottomModalScopeWidget(
108+
rootContext: modalContext,
109+
// create a new Data Scope since the bottom modal is placed in a different context tree (directly under MaterialApp)
110+
child: DataScopeWidget(
111+
scopeManager: scopeManager.createChildScope(),
112+
child: getBodyWidget(scopeManager, context)),
113+
)),
114+
).then((payload) {
115+
if (onDismiss != null) {
116+
return ScreenController().executeActionWithScope(
117+
context, scopeManager, onDismiss!,
118+
event: EnsembleEvent(null, data: payload));
119+
}
120+
});
121+
}
122+
return Future.value(null);
123+
}
124+
125+
Widget getBodyWidget(ScopeManager scopeManager, BuildContext context) {
126+
var widget = scopeManager.buildWidgetFromDefinition(body);
127+
if (isScrollable(scopeManager) == true) {
128+
// fix the viewport numbers if used incorrectly
129+
double minViewport = _minViewport(scopeManager) ?? 0.25;
130+
double maxViewport = _maxViewport(scopeManager) ?? 1;
131+
if (minViewport > maxViewport) {
132+
// reset
133+
minViewport = 0.25;
134+
maxViewport = 1;
135+
}
136+
double initialViewport = _initialViewport(scopeManager) ?? 0.5;
137+
if (initialViewport < minViewport || initialViewport > maxViewport) {
138+
// to middle
139+
initialViewport = (minViewport + maxViewport) / 2.0;
140+
}
141+
142+
return DraggableScrollableSheet(
143+
expand: false,
144+
minChildSize: minViewport,
145+
maxChildSize: maxViewport,
146+
initialChildSize: initialViewport,
147+
builder: (context, scrollController) =>
148+
buildRootContainer(scopeManager, context,
149+
child: SingleChildScrollView(
150+
controller: scrollController,
151+
child: widget,
152+
)));
153+
}
154+
return buildRootContainer(scopeManager, context, child: widget);
155+
}
156+
157+
// This is the root container where all the root styling happen
158+
Widget buildRootContainer(ScopeManager scopeManager, BuildContext context,
159+
{required Widget child}) {
160+
Widget rootWidget = Container(
161+
margin: margin(scopeManager),
162+
padding: padding(scopeManager),
163+
decoration: BoxDecoration(
164+
color: getBackgroundColor(scopeManager) ??
165+
Theme.of(context).dialogBackgroundColor,
166+
borderRadius: borderRadius(scopeManager)?.getValue() ??
167+
const BorderRadius.only(
168+
topLeft: defaultTopBorderRadius,
169+
topRight: defaultTopBorderRadius)),
170+
clipBehavior: Clip.antiAlias,
171+
width: double.infinity,
172+
// stretch width 100%
173+
child: useSafeArea(scopeManager) ? SafeArea(child: child) : child);
174+
if (showDragHandle(scopeManager)) {
175+
rootWidget = Stack(
176+
alignment: Alignment.topCenter,
177+
children: [rootWidget, _buildDragHandle(scopeManager)],
178+
);
179+
}
180+
return rootWidget;
181+
}
182+
183+
Widget _buildDragHandle(ScopeManager scopeManager) {
184+
return Container(
185+
margin: const EdgeInsets.only(top: 10),
186+
width: 32,
187+
height: 3,
188+
decoration: BoxDecoration(
189+
color: dragHandleColor(scopeManager) ?? Colors.grey[500],
190+
borderRadius: BorderRadius.circular(12),
191+
),
192+
);
193+
}
194+
}
195+
196+
/// Dismiss the Bottom Modal (if the context is a descendant, no-op otherwise)
197+
class DismissBottomModalAction extends EnsembleAction {
198+
DismissBottomModalAction({this.payload});
199+
200+
Map? payload;
201+
202+
factory DismissBottomModalAction.from({Map? payload}) =>
203+
DismissBottomModalAction(payload: payload?['payload']);
204+
205+
@override
206+
Future<dynamic> execute(BuildContext context, ScopeManager scopeManager,
207+
{DataContext? dataContext}) {
208+
BuildContext? bottomModalContext =
209+
BottomModalScopeWidget.getRootContext(context);
210+
if (bottomModalContext != null) {
211+
return Navigator.maybePop(
212+
bottomModalContext, scopeManager.dataContext.eval(payload));
213+
}
214+
return Navigator.maybePop(context, scopeManager.dataContext.eval(payload));
215+
}
216+
}
217+
218+
/// a wrapper InheritedWidget for its descendant to look up the root modal context to close it
219+
class BottomModalScopeWidget extends InheritedWidget {
220+
const BottomModalScopeWidget(
221+
{super.key, required super.child, required this.rootContext});
222+
223+
// this is the context root of the modal
224+
final BuildContext rootContext;
225+
226+
@override
227+
bool updateShouldNotify(covariant BottomModalScopeWidget oldWidget) {
228+
return oldWidget.rootContext != rootContext;
229+
}
230+
231+
static BuildContext? getRootContext(BuildContext context) {
232+
BottomModalScopeWidget? wrapperWidget =
233+
context.dependOnInheritedWidgetOfExactType<BottomModalScopeWidget>();
234+
return wrapperWidget?.rootContext;
235+
}
236+
}

lib/framework/action.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'package:app_settings/app_settings.dart';
22
import 'package:ensemble/action/audio_player.dart';
33
import 'package:ensemble/action/Log_event_action.dart';
44
import 'package:ensemble/action/badge_action.dart';
5-
import 'package:ensemble/action/bottom_modal_action.dart';
5+
import 'package:ensemble/action/bottom_modal_actions.dart';
66
import 'package:ensemble/action/deep_link_action.dart';
77
import 'package:ensemble/action/call_external_method.dart';
88
import 'package:ensemble/action/haptic_action.dart';

lib/framework/data_context.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import 'dart:ui';
55
import 'package:ensemble/action/Log_event_action.dart';
66
import 'package:ensemble/action/action_invokable.dart';
77
import 'package:ensemble/action/audio_player.dart';
8-
import 'package:ensemble/action/bottom_modal_action.dart';
8+
import 'package:ensemble/action/bottom_modal_actions.dart';
99
import 'package:ensemble/action/haptic_action.dart';
1010
import 'package:ensemble/action/invoke_api_action.dart';
1111
import 'package:ensemble/action/misc_action.dart';
@@ -59,7 +59,8 @@ class DataContext implements Context {
5959
final Map<String, dynamic> _contextMap = {};
6060

6161
get contextMap => _contextMap;
62-
final BuildContext buildContext;
62+
@Deprecated("do not use")
63+
BuildContext buildContext;
6364

6465
DataContext(
6566
{required this.buildContext,

lib/framework/view/context_scope_widget.dart

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)