Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expandable Bottom Sheet 2 Electric Boogaloo #149

Merged
merged 22 commits into from
Dec 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 53 additions & 40 deletions feedback/example/lib/custom_feedback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ enum FeedbackRating {
/// The submit button is disabled until the user provides the feedback type. All
/// other fields are optional.
class CustomFeedbackForm extends StatefulWidget {
const CustomFeedbackForm({Key? key, required this.onSubmit})
: super(key: key);
const CustomFeedbackForm({
Key? key,
required this.onSubmit,
required this.scrollController,
}) : super(key: key);

final OnSubmit onSubmit;
final ScrollController? scrollController;

@override
_CustomFeedbackFormState createState() => _CustomFeedbackFormState();
Expand All @@ -67,50 +71,59 @@ class _CustomFeedbackFormState extends State<CustomFeedbackForm> {
return Column(
children: [
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Stack(
children: [
const Text('What kind of feedback do you want to give?'),
Row(
mainAxisAlignment: MainAxisAlignment.start,
if (widget.scrollController != null)
const FeedbackSheetDragHandle(),
ListView(
controller: widget.scrollController,
// Pad the top by 20 to match the corner radius if drag enabled.
padding: EdgeInsets.fromLTRB(
16, widget.scrollController != null ? 20 : 16, 16, 0),
children: [
const Padding(
padding: EdgeInsets.only(right: 8),
child: Text('*'),
const Text('What kind of feedback do you want to give?'),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(right: 8),
child: Text('*'),
),
Flexible(
child: DropdownButton<FeedbackType>(
value: _customFeedback.feedbackType,
items: FeedbackType.values
.map(
(type) => DropdownMenuItem<FeedbackType>(
child: Text(type
.toString()
.split('.')
.last
.replaceAll('_', ' ')),
value: type,
),
)
.toList(),
onChanged: (feedbackType) => setState(() =>
_customFeedback.feedbackType = feedbackType),
),
),
],
),
Flexible(
child: DropdownButton<FeedbackType>(
value: _customFeedback.feedbackType,
items: FeedbackType.values
.map(
(type) => DropdownMenuItem<FeedbackType>(
child: Text(type
.toString()
.split('.')
.last
.replaceAll('_', ' ')),
value: type,
),
)
.toList(),
onChanged: (feedbackType) => setState(
() => _customFeedback.feedbackType = feedbackType),
),
const SizedBox(height: 16),
const Text('What is your feedback?'),
TextField(
onChanged: (newFeedback) =>
_customFeedback.feedbackText = newFeedback,
),
const SizedBox(height: 16),
const Text('How does this make you feel?'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: FeedbackRating.values.map(_ratingToIcon).toList(),
),
],
),
const SizedBox(height: 16),
const Text('What is your feedback?'),
TextField(
onChanged: (newFeedback) =>
_customFeedback.feedbackText = newFeedback,
),
const SizedBox(height: 16),
const Text('How does this make you feel?'),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: FeedbackRating.values.map(_ratingToIcon).toList(),
),
],
),
),
Expand Down
5 changes: 4 additions & 1 deletion feedback/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ class _MyAppState extends State<MyApp> {
// If custom feedback is not enabled, supply null and the default text
// feedback form will be used.
feedbackBuilder: _useCustomFeedback
? (context, onSubmit) => CustomFeedbackForm(onSubmit: onSubmit)
? (context, onSubmit, scrollController) => CustomFeedbackForm(
onSubmit: onSubmit,
scrollController: scrollController,
)
: null,
theme: FeedbackThemeData(
background: Colors.grey,
Expand Down
59 changes: 52 additions & 7 deletions feedback/lib/src/better_feedback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,56 @@ typedef OnSubmit = void Function(

/// A function that returns a Widget that prompts the user for feedback and
/// calls [OnSubmit] when the user wants to submit their feedback.
typedef FeedbackBuilder = Widget Function(BuildContext, OnSubmit);
///
/// A non-null controller is provided if the sheet is set to draggable in the
/// feedback theme.
/// If the sheet is draggable, the non null controller should be passed into a
/// scrollable widget to make the feedback sheet expand when the widget is
/// scrolled. Typically, this will be a `ListView` or `SingleChildScrollView`
/// wrapping the feedback sheet's content.
/// See: [FeedbackThemeData.sheetIsDraggable] and [DraggableScrollableSheet].
typedef FeedbackBuilder = Widget Function(
BuildContext,
OnSubmit,
ScrollController?,
);

/// A drag handle to be placed at the top of a draggable feedback sheet.
///
/// This is a purely visual element that communicates to users that the sheet
/// can be dragged to expand it.
///
/// It should be placed in a stack over the sheet's scrollable element so that
/// users can click and drag on it-the drag handle ignores pointers so the drag
/// will pass through to the scrollable beneath.
// TODO(caseycrogers): Replace this with a pre-built drag handle above the
// builder function once `DraggableScrollableController` is available in
// production.
// See: https://github.com/flutter/flutter/pull/92440.
class FeedbackSheetDragHandle extends StatelessWidget {
/// Create a drag handle.
const FeedbackSheetDragHandle({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Container(
height: 20,
padding: const EdgeInsets.symmetric(vertical: 7.5),
alignment: Alignment.center,
color: FeedbackTheme.of(context).feedbackSheetColor,
child: Container(
height: 5,
width: 30,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(5),
),
),
),
);
}
}

/// Function which gets called when the user submits his feedback.
/// [feedback] is the user generated feedback. A string, by default.
Expand Down Expand Up @@ -132,8 +181,6 @@ class BetterFeedback extends StatefulWidget {
class _BetterFeedbackState extends State<BetterFeedback> {
FeedbackController controller = FeedbackController();

bool feedbackVisible = false;

@override
void initState() {
super.initState();
Expand All @@ -160,7 +207,7 @@ class _BetterFeedbackState extends State<BetterFeedback> {
assert(debugCheckHasFeedbackLocalizations(context));
return FeedbackWidget(
child: widget.child,
isFeedbackVisible: feedbackVisible,
isFeedbackVisible: controller.isVisible,
drawColors: FeedbackTheme.of(context).drawColors,
mode: widget.mode,
pixelRatio: widget.pixelRatio,
Expand All @@ -175,8 +222,6 @@ class _BetterFeedbackState extends State<BetterFeedback> {
}

void onUpdateOfController() {
setState(() {
feedbackVisible = controller.isVisible;
});
setState(() {});
}
}
8 changes: 4 additions & 4 deletions feedback/lib/src/controls_column.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@ class ControlsColumn extends StatelessWidget {
Widget build(BuildContext context) {
final isNavigatingActive = FeedbackMode.navigate == mode;
return Card(
margin: EdgeInsets.zero,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(24),
),
),
clipBehavior: Clip.antiAlias,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
child: Wrap(
direction: Axis.vertical,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
IconButton(
key: const ValueKey<String>('close_controls_column'),
Expand Down
105 changes: 95 additions & 10 deletions feedback/lib/src/feedback_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import 'package:feedback/src/better_feedback.dart';
import 'package:feedback/src/theme/feedback_theme.dart';

import 'package:flutter/material.dart';

/// Shows the text input in which the user can describe his feedback.
Expand All @@ -10,26 +11,110 @@ class FeedbackBottomSheet extends StatelessWidget {
Key? key,
required this.feedbackBuilder,
required this.onSubmit,
required this.sheetProgress,
}) : super(key: key);

final FeedbackBuilder feedbackBuilder;
final OnSubmit onSubmit;
final ValueNotifier<double> sheetProgress;

@override
Widget build(BuildContext context) {
// We need to supply a navigator so that the contents of the bottom sheet
// have access to an overlay (overlays are used by many material widgets
// such as `TextField` and `DropDownButton`.
// Typically, the navigator would be provided by a `MaterialApp`, but
// `BetterFeedback` is used above `MaterialApp` in the widget tree so that
// the nested navigation in navigate mode works properly.
return Navigator(
onGenerateRoute: (_) => MaterialPageRoute<void>(
builder: (context) => Material(
if (FeedbackTheme.of(context).sheetIsDraggable) {
return _DraggableFeedbackSheet(
feedbackBuilder: feedbackBuilder,
onSubmit: onSubmit,
sheetProgress: sheetProgress,
);
}
return Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: MediaQuery.of(context).size.height *
FeedbackTheme.of(context).feedbackSheetHeight,
child: Material(
color: FeedbackTheme.of(context).feedbackSheetColor,
child: feedbackBuilder(context, onSubmit),
// Pass a null scroll controller because the sheet is not drag
// enabled.
child: feedbackBuilder(context, onSubmit, null),
),
),
);
}
}

class _DraggableFeedbackSheet extends StatefulWidget {
const _DraggableFeedbackSheet({
Key? key,
required this.feedbackBuilder,
required this.onSubmit,
required this.sheetProgress,
}) : super(key: key);

final FeedbackBuilder feedbackBuilder;
final OnSubmit onSubmit;
final ValueNotifier<double> sheetProgress;

@override
State<_DraggableFeedbackSheet> createState() => _DraggableFeedbackSheetState();
}

class _DraggableFeedbackSheetState extends State<_DraggableFeedbackSheet> {
@override
Widget build(BuildContext context) {
final FeedbackThemeData feedbackTheme = FeedbackTheme.of(context);
final MediaQueryData query = MediaQuery.of(context);
// We need to recalculate the collapsed height to account for the safe area
// at the top and the keyboard (if present).
final double collapsedHeight = feedbackTheme.feedbackSheetHeight *
query.size.height /
(query.size.height - query.padding.top - query.viewInsets.bottom);
return Column(
children: [
ValueListenableBuilder<void>(
valueListenable: widget.sheetProgress,
child: Container(
height: MediaQuery.of(context).padding.top,
color: FeedbackTheme.of(context).feedbackSheetColor,
),
builder: (context, _, child) {
return Opacity(
// Use the curved progress value
opacity: widget.sheetProgress.value,
child: child,
);
},
),
Expanded(
child: DraggableScrollableSheet(
snap: true,
minChildSize: collapsedHeight,
initialChildSize: collapsedHeight,
builder: (context, scrollController) {
return ValueListenableBuilder<void>(
valueListenable: widget.sheetProgress,
builder: (context, _, child) {
return ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20 * (1 - widget.sheetProgress.value)),
),
child: child,
);
},
child: Material(
color: FeedbackTheme.of(context).feedbackSheetColor,
// A `ListView` makes the content here disappear.
child: widget.feedbackBuilder(
context,
widget.onSubmit,
scrollController,
),
),
);
},
),
),
],
);
}
}
Loading