mirror of
https://github.com/block/buzz.git
synced 2026-11-06 01:11:33 +00:00
## Summary - Add a shared footer fade behind the floating tabs on Home, Activity, and Search. - Use a shared anchored popover for Activity filters and section actions, with working section move controls. - Polish message grouping/press states and remove the initial Search back button. <img width="630" height="1368" alt="Screenshot 2026-07-29 at 08 49 37" src="https://github.com/user-attachments/assets/9e787adf-0bb3-49c6-8224-5819e8cfb1ad" /> ### Testing - `flutter analyze` - `flutter test` - Release build installed and checked on a connected iPhone ### Screenshots A real-device Activity baseline showing the original solid footer is attached in a PR comment. The updated review build was checked on the connected iPhone. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com>
246 lines
7.4 KiB
Dart
246 lines
7.4 KiB
Dart
import 'dart:math' as math;
|
|
import 'dart:ui' show SemanticsRole;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/theme.dart';
|
|
|
|
const _popoverEnterDuration = Duration(milliseconds: 150);
|
|
const _popoverExitDuration = Duration(milliseconds: 110);
|
|
const _popoverStartScale = 0.96;
|
|
|
|
/// The horizontal edge a popover aligns to on its triggering control.
|
|
enum AnchoredPopoverAlignment {
|
|
/// Aligns the popover's leading edge with the trigger's leading edge.
|
|
start,
|
|
|
|
/// Aligns the popover's trailing edge with the trigger's trailing edge.
|
|
end,
|
|
}
|
|
|
|
/// Shows an anchored, cross-platform popup menu with the Activity controls'
|
|
/// sizing, motion, and safe-area placement.
|
|
Future<T?> showAnchoredPopover<T>({
|
|
required BuildContext context,
|
|
required List<PopupMenuEntry<T>> items,
|
|
required double width,
|
|
required AnchoredPopoverAlignment alignment,
|
|
required Color color,
|
|
required ShapeBorder shape,
|
|
required double elevation,
|
|
required Color shadowColor,
|
|
Offset offset = Offset.zero,
|
|
EdgeInsetsGeometry menuPadding = EdgeInsets.zero,
|
|
Clip clipBehavior = Clip.antiAlias,
|
|
Key? surfaceKey,
|
|
}) {
|
|
final navigator = Navigator.of(context);
|
|
final overlay = navigator.overlay;
|
|
final triggerRenderObject = context.findRenderObject();
|
|
final overlayRenderObject = overlay?.context.findRenderObject();
|
|
if (triggerRenderObject is! RenderBox || overlayRenderObject is! RenderBox) {
|
|
return Future<T?>.value();
|
|
}
|
|
|
|
final triggerRect = MatrixUtils.transformRect(
|
|
triggerRenderObject.getTransformTo(overlayRenderObject),
|
|
Offset.zero & triggerRenderObject.size,
|
|
);
|
|
final overlayRect = Offset.zero & overlayRenderObject.size;
|
|
final mediaQuery = MediaQuery.of(context);
|
|
|
|
return navigator.push<T>(
|
|
_AnchoredPopoverRoute<T>(
|
|
position: RelativeRect.fromRect(triggerRect, overlayRect),
|
|
items: items,
|
|
width: width,
|
|
alignment: alignment,
|
|
offset: offset,
|
|
color: color,
|
|
shape: shape,
|
|
elevation: elevation,
|
|
shadowColor: shadowColor,
|
|
menuPadding: menuPadding,
|
|
clipBehavior: clipBehavior,
|
|
surfaceKey: surfaceKey,
|
|
screenPadding: EdgeInsets.fromLTRB(
|
|
math.max(Grid.xxs, mediaQuery.padding.left),
|
|
math.max(Grid.xxs, mediaQuery.padding.top),
|
|
math.max(Grid.xxs, mediaQuery.padding.right),
|
|
math.max(Grid.xxs, mediaQuery.padding.bottom),
|
|
),
|
|
reducedMotion: mediaQuery.disableAnimations,
|
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
|
),
|
|
);
|
|
}
|
|
|
|
class _AnchoredPopoverRoute<T> extends PopupRoute<T> {
|
|
final RelativeRect position;
|
|
final List<PopupMenuEntry<T>> items;
|
|
final double width;
|
|
final AnchoredPopoverAlignment alignment;
|
|
final Offset offset;
|
|
final Color color;
|
|
final ShapeBorder shape;
|
|
final double elevation;
|
|
final Color shadowColor;
|
|
final EdgeInsetsGeometry menuPadding;
|
|
final Clip clipBehavior;
|
|
final Key? surfaceKey;
|
|
final EdgeInsets screenPadding;
|
|
final bool reducedMotion;
|
|
final String _barrierLabel;
|
|
|
|
_AnchoredPopoverRoute({
|
|
required this.position,
|
|
required this.items,
|
|
required this.width,
|
|
required this.alignment,
|
|
required this.offset,
|
|
required this.color,
|
|
required this.shape,
|
|
required this.elevation,
|
|
required this.shadowColor,
|
|
required this.menuPadding,
|
|
required this.clipBehavior,
|
|
required this.surfaceKey,
|
|
required this.screenPadding,
|
|
required this.reducedMotion,
|
|
required String barrierLabel,
|
|
}) : _barrierLabel = barrierLabel;
|
|
|
|
@override
|
|
Color? get barrierColor => null;
|
|
|
|
@override
|
|
bool get barrierDismissible => true;
|
|
|
|
@override
|
|
String? get barrierLabel => _barrierLabel;
|
|
|
|
@override
|
|
Duration get transitionDuration =>
|
|
reducedMotion ? Duration.zero : _popoverEnterDuration;
|
|
|
|
@override
|
|
Duration get reverseTransitionDuration =>
|
|
reducedMotion ? Duration.zero : _popoverExitDuration;
|
|
|
|
@override
|
|
Widget buildPage(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
) {
|
|
final curvedAnimation = animation.drive(
|
|
CurveTween(curve: Curves.easeOutCubic),
|
|
);
|
|
final scaleAnimation = Tween<double>(
|
|
begin: _popoverStartScale,
|
|
end: 1,
|
|
).animate(curvedAnimation);
|
|
final transformOrigin = switch (alignment) {
|
|
AnchoredPopoverAlignment.start => Alignment.topLeft,
|
|
AnchoredPopoverAlignment.end => Alignment.topRight,
|
|
};
|
|
|
|
return CustomSingleChildLayout(
|
|
delegate: _AnchoredPopoverLayoutDelegate(
|
|
position: position,
|
|
alignment: alignment,
|
|
offset: offset,
|
|
screenPadding: screenPadding,
|
|
),
|
|
child: FadeTransition(
|
|
key: const ValueKey('activity-popover-fade'),
|
|
opacity: curvedAnimation,
|
|
child: ScaleTransition(
|
|
key: const ValueKey('activity-popover-scale'),
|
|
scale: scaleAnimation,
|
|
alignment: transformOrigin,
|
|
child: Material(
|
|
key: surfaceKey,
|
|
type: MaterialType.card,
|
|
color: color,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: elevation,
|
|
shadowColor: shadowColor,
|
|
shape: shape,
|
|
clipBehavior: clipBehavior,
|
|
child: SizedBox(
|
|
width: width,
|
|
child: Semantics(
|
|
role: SemanticsRole.menu,
|
|
scopesRoute: true,
|
|
namesRoute: true,
|
|
explicitChildNodes: true,
|
|
child: SingleChildScrollView(
|
|
padding: menuPadding,
|
|
child: ListBody(children: items),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AnchoredPopoverLayoutDelegate extends SingleChildLayoutDelegate {
|
|
final RelativeRect position;
|
|
final AnchoredPopoverAlignment alignment;
|
|
final Offset offset;
|
|
final EdgeInsets screenPadding;
|
|
|
|
const _AnchoredPopoverLayoutDelegate({
|
|
required this.position,
|
|
required this.alignment,
|
|
required this.offset,
|
|
required this.screenPadding,
|
|
});
|
|
|
|
@override
|
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
|
return BoxConstraints.loose(
|
|
Size(
|
|
constraints.maxWidth - screenPadding.horizontal,
|
|
constraints.maxHeight - screenPadding.vertical,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Offset getPositionForChild(Size size, Size childSize) {
|
|
final anchorBottom = size.height - position.bottom;
|
|
final desiredX = switch (alignment) {
|
|
AnchoredPopoverAlignment.start => position.left + offset.dx,
|
|
AnchoredPopoverAlignment.end =>
|
|
size.width - position.right - childSize.width + offset.dx,
|
|
};
|
|
final minX = screenPadding.left;
|
|
final maxX = size.width - screenPadding.right - childSize.width;
|
|
final x = desiredX.clamp(minX, maxX).toDouble();
|
|
|
|
final belowY = anchorBottom + offset.dy;
|
|
final aboveY = position.top - childSize.height - offset.dy;
|
|
final maxY = size.height - screenPadding.bottom - childSize.height;
|
|
final desiredY =
|
|
belowY + childSize.height <= size.height - screenPadding.bottom
|
|
? belowY
|
|
: aboveY;
|
|
final y = desiredY.clamp(screenPadding.top, maxY).toDouble();
|
|
|
|
return Offset(x, y);
|
|
}
|
|
|
|
@override
|
|
bool shouldRelayout(_AnchoredPopoverLayoutDelegate oldDelegate) {
|
|
return position != oldDelegate.position ||
|
|
alignment != oldDelegate.alignment ||
|
|
offset != oldDelegate.offset ||
|
|
screenPadding != oldDelegate.screenPadding;
|
|
}
|
|
}
|