Skip to content

Commit da4794b

Browse files
committed
emoji: Fix bottom padding of emoji picker
Previously, the body of the bottom sheet was wrapped in `SafeArea`. This pads the bottom unconditionally, shifting the bottom of the list view above the device bottom padding. This is undesirable, because the area beneath the bottom padding is still visible, and the list view should extend to the bottom regardless of the bottom inset. By just removing the `SafeArea`, the list view extends to the bottom. However, if the bottom padding is more than 8px, we can't scroll past the last item in the list view. Essentially, we want the behavior of `SilverSafeArea.minimum` — the bottom of the list should always be padded by at least 8px, so that we can scroll past the shadow; and if the bottom padding is more than 8px, use that as the padding instead. Signed-off-by: Zixuan James Li <[email protected]> revision Signed-off-by: Zixuan James Li <[email protected]>
1 parent 604bffb commit da4794b

File tree

3 files changed

+112
-18
lines changed

3 files changed

+112
-18
lines changed

lib/widgets/emoji_reaction.dart

+25-18
Original file line numberDiff line numberDiff line change
@@ -417,20 +417,21 @@ void showEmojiPickerSheet({
417417
// on my iPhone 13 Pro but is marked as "much slower":
418418
// https://api.flutter.dev/flutter/dart-ui/Clip.html
419419
clipBehavior: Clip.antiAlias,
420+
// This removes insets from all sides except bottom.
421+
// Leave it to the descendent [CustomScrollView].
420422
useSafeArea: true,
421423
isScrollControlled: true,
422424
builder: (BuildContext context) {
423-
return SafeArea(
424-
child: Padding(
425-
// By default, when software keyboard is opened, the ListView
426-
// expands behind the software keyboard — resulting in some
427-
// list entries being covered by the keyboard. Add explicit
428-
// bottom padding the size of the keyboard, which fixes this.
429-
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
430-
// For _EmojiPickerItem, and RealmContentNetworkImage used in ImageEmojiWidget.
431-
child: PerAccountStoreWidget(
432-
accountId: store.accountId,
433-
child: EmojiPicker(pageContext: pageContext, message: message))));
425+
return Padding(
426+
// By default, when software keyboard is opened, the ListView
427+
// expands behind the software keyboard — resulting in some
428+
// list entries being covered by the keyboard. Add explicit
429+
// bottom padding the size of the keyboard, which fixes this.
430+
padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom),
431+
// For _EmojiPickerItem, and RealmContentNetworkImage used in ImageEmojiWidget.
432+
child: PerAccountStoreWidget(
433+
accountId: store.accountId,
434+
child: EmojiPicker(pageContext: pageContext, message: message)));
434435
});
435436
}
436437

@@ -531,13 +532,19 @@ class _EmojiPickerState extends State<EmojiPicker> with PerAccountStoreAwareStat
531532
Expanded(child: InsetShadowBox(
532533
top: 8, bottom: 8,
533534
color: designVariables.bgContextMenu,
534-
child: ListView.builder(
535-
padding: const EdgeInsets.symmetric(vertical: 8),
536-
itemCount: _resultsToDisplay.length,
537-
itemBuilder: (context, i) => EmojiPickerListEntry(
538-
pageContext: widget.pageContext,
539-
emoji: _resultsToDisplay[i].candidate,
540-
message: widget.message)))),
535+
child: CustomScrollView(
536+
slivers: [
537+
SliverPadding(
538+
padding: EdgeInsets.only(top: 8),
539+
sliver: SliverSafeArea(
540+
minimum: EdgeInsets.only(bottom: 8),
541+
sliver: SliverList.builder(
542+
itemCount: _resultsToDisplay.length,
543+
itemBuilder: (context, i) => EmojiPickerListEntry(
544+
pageContext: widget.pageContext,
545+
emoji: _resultsToDisplay[i].candidate,
546+
message: widget.message)))),
547+
]))),
541548
]);
542549
}
543550
}

test/flutter_checks.dart

+5
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,8 @@ extension TableChecks on Subject<Table> {
169169
extension IconButtonChecks on Subject<IconButton> {
170170
Subject<bool?> get isSelected => has((x) => x.isSelected, 'isSelected');
171171
}
172+
173+
extension OffsetChecks on Subject<Offset> {
174+
Subject<double> get dx => has((x) => x.dx, 'dx');
175+
Subject<double> get dy => has((x) => x.dy, 'dy');
176+
}

test/widgets/emoji_reaction_test.dart

+82
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,88 @@ void main() {
469469

470470
debugNetworkImageHttpClientProvider = null;
471471
});
472+
473+
group('handle view paddings', () {
474+
const screenHeight = 400.0;
475+
476+
late double scrollViewTop;
477+
late double scrollViewBottom;
478+
final scrollViewFinder = find.descendant(
479+
of: find.byType(BottomSheet), matching: find.bySubtype<ScrollView>());
480+
481+
late Widget firstOption;
482+
late Widget lastOption;
483+
double getScrollChildrenTop(WidgetTester tester) =>
484+
tester.getTopLeft(find.byWidget(firstOption)).dy;
485+
double getScrollChildrenBottom(WidgetTester tester) =>
486+
tester.getBottomLeft(find.byWidget(lastOption)).dy;
487+
488+
Future<void> prepare(WidgetTester tester, {
489+
required FakeViewPadding viewPadding,
490+
}) async {
491+
addTearDown(tester.view.reset);
492+
tester.view.physicalSize = Size(640, screenHeight);
493+
// This makes it easier to convert between device pixels used for
494+
// [FakeViewPadding] and logical pixels used in tests.
495+
tester.view.devicePixelRatio = 1.0;
496+
497+
tester.view.viewPadding = viewPadding;
498+
tester.view.padding = viewPadding;
499+
500+
final message = eg.streamMessage();
501+
await setupEmojiPicker(tester,
502+
message: message, narrow: TopicNarrow.ofMessage(message));
503+
504+
scrollViewTop = tester.getTopLeft(scrollViewFinder).dy;
505+
scrollViewBottom = tester.getBottomLeft(scrollViewFinder).dy;
506+
// The scroll view should expand all the way to the bottom of the
507+
// screen, even if there is device bottom padding.
508+
check(scrollViewBottom).equals(screenHeight);
509+
510+
final options = tester.widgetList(find.byType(EmojiPickerListEntry));
511+
firstOption = options.first;
512+
lastOption = options.last;
513+
}
514+
515+
testWidgets('no view padding', (tester) async {
516+
await prepare(tester, viewPadding: FakeViewPadding.zero);
517+
518+
// The top of the list of children is padded by 8px;
519+
// the bottom of them is out of view.
520+
check(scrollViewTop).equals(getScrollChildrenTop(tester) - 8);
521+
check(scrollViewBottom).isLessThan(getScrollChildrenBottom(tester));
522+
523+
// Scroll to the very bottom of the list with a large offset.
524+
await tester.drag(scrollViewFinder, Offset(0, -500));
525+
await tester.pump();
526+
// The top of the list of children is out of view.
527+
// The bottom is padded by 8px, the minimum offset.
528+
check(scrollViewTop).isGreaterThan(getScrollChildrenTop(tester));
529+
check(scrollViewBottom).equals(getScrollChildrenBottom(tester) + 8);
530+
531+
debugNetworkImageHttpClientProvider = null;
532+
});
533+
534+
testWidgets('with bottom view padding', (tester) async {
535+
await prepare(tester, viewPadding: FakeViewPadding(bottom: 10));
536+
537+
// The top of the list of children is padded by 8px;
538+
// The bottom of them is out of view.
539+
check(scrollViewTop).equals(getScrollChildrenTop(tester) - 8);
540+
check(scrollViewBottom).isLessThan(getScrollChildrenBottom(tester));
541+
542+
// Scroll to the very bottom of the list with a large offset.
543+
await tester.drag(scrollViewFinder, Offset(0, -500));
544+
await tester.pump();
545+
// The top of the list of children is out of view;
546+
// the bottom is padded by `max(8, 10)` px, where the view bottom
547+
// padding is larger.
548+
check(scrollViewTop).isGreaterThan(getScrollChildrenTop(tester));
549+
check(scrollViewBottom).equals(getScrollChildrenBottom(tester) + 10);
550+
551+
debugNetworkImageHttpClientProvider = null;
552+
});
553+
});
472554
});
473555
}
474556

0 commit comments

Comments
 (0)