Skip to content

Commit b966fc4

Browse files
committed
Refactored Bottom Modal
1 parent 7aa85d0 commit b966fc4

5 files changed

+223
-133
lines changed

lib/action/bottom_modal_action.dart

-111
This file was deleted.

lib/action/bottom_modal_actions.dart

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import 'dart:developer';
2+
3+
import 'package:ensemble/framework/action.dart';
4+
import 'package:ensemble/framework/data_context.dart';
5+
import 'package:ensemble/framework/error_handling.dart';
6+
import 'package:ensemble/framework/event.dart';
7+
import 'package:ensemble/framework/extensions.dart';
8+
import 'package:ensemble/framework/scope.dart';
9+
import 'package:ensemble/framework/view/context_scope_widget.dart';
10+
import 'package:ensemble/framework/view/data_scope_widget.dart';
11+
import 'package:ensemble/screen_controller.dart';
12+
import 'package:ensemble/util/utils.dart';
13+
import 'package:ensemble_ts_interpreter/invokables/invokable.dart';
14+
import 'package:flutter/material.dart';
15+
16+
/// open a Modal Bottom Sheet
17+
class ShowBottomModalAction extends EnsembleAction {
18+
ShowBottomModalAction(
19+
{super.initiator,
20+
super.inputs,
21+
this.body,
22+
this.styles,
23+
this.options,
24+
this.onDismiss});
25+
26+
final dynamic body;
27+
final Map<String, dynamic>? styles;
28+
final Map<String, dynamic>? options;
29+
final EnsembleAction? onDismiss;
30+
31+
// default height is size to content
32+
MainAxisSize getVerticalSize(scopeManager) =>
33+
MainAxisSize.values
34+
.from(scopeManager.dataContext.eval(styles?['verticalSize'])) ??
35+
MainAxisSize.min;
36+
37+
MainAxisAlignment getVerticalAlignment(scopeManager) {
38+
var alignment = scopeManager.dataContext.eval(styles?['verticalAlignment']);
39+
switch (alignment) {
40+
case 'top':
41+
return MainAxisAlignment.start;
42+
case 'bottom':
43+
return MainAxisAlignment.end;
44+
default:
45+
// if verticalSize is min this doesn't matter, but center align if max
46+
return MainAxisAlignment.center;
47+
}
48+
}
49+
50+
// default width is stretching 100%
51+
CrossAxisAlignment getHorizontalSize(scopeManager) {
52+
var size = MainAxisSize.values
53+
.from(scopeManager.dataContext.eval(styles?['horizontalSize'])) ??
54+
MainAxisSize.max;
55+
return size == MainAxisSize.min
56+
? CrossAxisAlignment.center
57+
: CrossAxisAlignment.stretch;
58+
}
59+
60+
Alignment? getHorizontalAlignment(scopeManager) {
61+
var alignment =
62+
scopeManager.dataContext.eval(styles?['horizontalAlignment']);
63+
switch (alignment) {
64+
case 'start':
65+
return Alignment.centerLeft;
66+
case 'center':
67+
return Alignment.center;
68+
case 'end':
69+
return Alignment.centerRight;
70+
default:
71+
return null;
72+
}
73+
}
74+
75+
Color? getBarrierColor(scopeManager) =>
76+
Utils.getColor(scopeManager.dataContext.eval(styles?['barrierColor']));
77+
78+
// default background is the dialog background
79+
Color? getBackgroundColor(scopeManager) =>
80+
Utils.getColor(scopeManager.dataContext.eval(styles?['backgroundColor']));
81+
82+
int? getTopBorderRadius(scopeManager) => Utils.optionalInt(
83+
scopeManager.dataContext.eval(styles?['topBorderRadius']));
84+
85+
bool getEnableDrag(scopeManager) =>
86+
Utils.getBool(scopeManager.dataContext.eval(styles?['enableDrag']),
87+
fallback: true);
88+
89+
factory ShowBottomModalAction.from({Invokable? initiator, Map? payload}) {
90+
dynamic body = payload?['body'] ?? payload?['widget'];
91+
if (payload == null || body == null) {
92+
throw LanguageError(
93+
"${ActionType.showBottomModal.name} requires a body widget.");
94+
}
95+
return ShowBottomModalAction(
96+
initiator: initiator,
97+
inputs: Utils.getMap(payload['inputs']),
98+
body: body,
99+
styles: Utils.getMap(payload['styles']),
100+
options: Utils.getMap(payload['options']),
101+
onDismiss: EnsembleAction.fromYaml(payload['onDismiss']));
102+
}
103+
104+
@override
105+
Future<dynamic> execute(BuildContext context, ScopeManager scopeManager) {
106+
if (body == null) return Future.value(null);
107+
108+
// verticalSize: min | max
109+
// verticalAlignment: top | center | bottom
110+
// horizontalSize: min | max
111+
// horizontalAlignment: start | center | end
112+
113+
// topRadius: 15
114+
// backgroundColor
115+
// barrierColor
116+
117+
var topRadius =
118+
Radius.circular(getTopBorderRadius(scopeManager)?.toDouble() ?? 16);
119+
var horizontalAlignment = getHorizontalAlignment(scopeManager);
120+
var widget = scopeManager.buildWidgetFromDefinition(body);
121+
122+
var bodyWidget = Material(
123+
type: MaterialType.transparency,
124+
elevation: 16,
125+
child: Container(
126+
decoration: BoxDecoration(
127+
color: getBackgroundColor(scopeManager) ??
128+
Theme.of(context).dialogBackgroundColor,
129+
borderRadius:
130+
BorderRadius.only(topLeft: topRadius, topRight: topRadius)),
131+
child: Column(
132+
// vertical
133+
mainAxisSize: getVerticalSize(scopeManager),
134+
mainAxisAlignment: getVerticalAlignment(scopeManager),
135+
136+
// horizontal
137+
crossAxisAlignment: getHorizontalSize(scopeManager),
138+
children: [
139+
// account for the bottom notch
140+
SafeArea(
141+
bottom: false,
142+
child: horizontalAlignment != null
143+
? Align(alignment: horizontalAlignment, child: widget)
144+
: widget)
145+
])),
146+
);
147+
148+
showModalBottomSheet(
149+
context: context,
150+
// disable the default bottom sheet styling since we use our own
151+
backgroundColor: Colors.transparent,
152+
elevation: 16,
153+
154+
barrierColor: getBarrierColor(scopeManager),
155+
isScrollControlled: true,
156+
enableDrag: getEnableDrag(scopeManager),
157+
// padding to account for the keyboard when we have input widgets inside the modal
158+
builder: (modalContext) => Padding(
159+
padding: EdgeInsets.only(
160+
bottom: MediaQuery.of(modalContext).viewInsets.bottom,
161+
),
162+
// have a bottom modal scope widget so we can close the modal
163+
child: BottomModalScopeWidget(
164+
rootContext: modalContext,
165+
// create a new Data Scope since the bottom modal is placed in a different context tree (directly under MaterialApp)
166+
child: DataScopeWidget(
167+
scopeManager: scopeManager.createChildScope(),
168+
child: bodyWidget),
169+
)),
170+
).then((payload) {
171+
if (onDismiss != null) {
172+
return ScreenController().executeActionWithScope(
173+
context, scopeManager, onDismiss!,
174+
event: EnsembleEvent(null, data: payload));
175+
}
176+
});
177+
return Future.value(null);
178+
}
179+
}
180+
181+
/// Dismiss the Bottom Modal (if the context is a descendant, no-op otherwise)
182+
class DismissBottomModalAction extends EnsembleAction {
183+
DismissBottomModalAction({this.payload});
184+
185+
Map? payload;
186+
187+
factory DismissBottomModalAction.from({Map? payload}) =>
188+
DismissBottomModalAction(payload: payload?['payload']);
189+
190+
@override
191+
Future<dynamic> execute(BuildContext context, ScopeManager scopeManager,
192+
{DataContext? dataContext}) {
193+
BuildContext? bottomModalContext =
194+
BottomModalScopeWidget.getRootContext(context);
195+
if (bottomModalContext != null) {
196+
return Navigator.maybePop(
197+
bottomModalContext, scopeManager.dataContext.eval(payload));
198+
}
199+
return Navigator.maybePop(context, scopeManager.dataContext.eval(payload));
200+
}
201+
}
202+
203+
/// a wrapper InheritedWidget for its descendant to look up the root modal context to close it
204+
class BottomModalScopeWidget extends InheritedWidget {
205+
const BottomModalScopeWidget(
206+
{super.key, required super.child, required this.rootContext});
207+
208+
// this is the context root of the modal
209+
final BuildContext rootContext;
210+
211+
@override
212+
bool updateShouldNotify(covariant BottomModalScopeWidget oldWidget) {
213+
return oldWidget.rootContext != rootContext;
214+
}
215+
216+
static BuildContext? getRootContext(BuildContext context) {
217+
BottomModalScopeWidget? wrapperWidget =
218+
context.dependOnInheritedWidgetOfExactType<BottomModalScopeWidget>();
219+
return wrapperWidget?.rootContext;
220+
}
221+
}

lib/framework/action.dart

+1-1
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

+1-1
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';
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,2 @@
11
import 'package:flutter/cupertino.dart';
22

3-
/// a wrapper InheritedWidget to look up the root scope, typically used
4-
/// by deeply nested children to find the root context of (for ex) the
5-
/// BottomSheetModal so it can close the modal as needed
6-
class ContextScopeWidget extends InheritedWidget {
7-
const ContextScopeWidget(
8-
{super.key, required super.child, required this.rootContext});
9-
10-
final BuildContext rootContext;
11-
12-
@override
13-
bool updateShouldNotify(covariant ContextScopeWidget oldWidget) {
14-
return oldWidget.rootContext != rootContext;
15-
}
16-
17-
static BuildContext? getRootContext(BuildContext context) {
18-
ContextScopeWidget? wrapperWidget =
19-
context.dependOnInheritedWidgetOfExactType<ContextScopeWidget>();
20-
return wrapperWidget?.rootContext;
21-
}
22-
}

0 commit comments

Comments
 (0)