Files
buzz/mobile/lib/shared/widgets/tappable_flapping_bee.dart
klopez4212andGitHub 871a3b3772 Refine mobile community switching and discovery (#2967)
## Summary

- Redesign community switching with relay-provided avatars,
selected-state checks, edit-to-remove controls, and confirmation.
- Bring desktop-inspired pairing onboarding to mobile with QR scanning
as the primary action, an expandable pairing-code flow, and a tappable
flapping bee.
- Polish Search and Activity headers, filter chips, typography, keyboard
stability, and Android adaptive-icon sizing.

## Validation

- `just mobile-check`
- `just mobile-test` — 661 passed, 1 skipped
- Pixel 10 snapshot review
2026-07-26 15:05:57 -07:00

150 lines
4.0 KiB
Dart

import 'dart:math' show cos, min, pi;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
/// The Buzz mark with wings that flutter twice when the user taps it.
///
/// The geometry and wing tuck match the desktop loading bee. When reduced
/// motion is enabled, the mark stays static.
class TappableFlappingBee extends HookConsumerWidget {
/// The rendered width of the complete bee mark.
final double width;
/// The color used for the bee silhouette.
final Color color;
const TappableFlappingBee({
required this.width,
required this.color,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final animation = useAnimationController(
duration: const Duration(milliseconds: 480),
);
final reducedMotion = MediaQuery.disableAnimationsOf(context);
void flutterWings() {
if (reducedMotion) return;
animation.forward(from: 0);
}
return Semantics(
button: true,
label: 'Buzz bee',
hint: 'Tap to make its wings flutter',
onTap: flutterWings,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
excludeFromSemantics: true,
onTap: flutterWings,
child: RepaintBoundary(
child: AnimatedBuilder(
animation: animation,
builder: (context, _) {
final flapAmount = 0.5 - (0.5 * cos(animation.value * 4 * pi));
return CustomPaint(
size: Size(width, width * 309 / 466),
painter: _FlappingBeePainter(
color: color,
flapAmount: flapAmount,
),
);
},
),
),
),
);
}
}
class _FlappingBeePainter extends CustomPainter {
final Color color;
final double flapAmount;
const _FlappingBeePainter({required this.color, required this.flapAmount});
@override
void paint(Canvas canvas, Size size) {
final scale = min(size.width / 466, size.height / 309);
final renderedWidth = 466 * scale;
final renderedHeight = 309 * scale;
canvas
..save()
..translate(
(size.width - renderedWidth) / 2,
(size.height - renderedHeight) / 2,
)
..scale(scale);
final wingRadiusX = 91.7 * (1 - (0.38 * flapAmount));
final wingTranslation = 30 * flapAmount;
final leftWing = Path()
..addOval(
Rect.fromCenter(
center: Offset(91.7 + wingTranslation, 154.5),
width: wingRadiusX * 2,
height: 183.4,
),
);
final rightWing = Path()
..addOval(
Rect.fromCenter(
center: Offset(374.3 - wingTranslation, 154.5),
width: wingRadiusX * 2,
height: 183.4,
),
);
final body = Path()
..addRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(128, 0, 210, 309),
const Radius.circular(34),
),
);
final cutouts = Path()
..addOval(
Rect.fromCenter(
center: const Offset(193.3, 84.4),
width: 54,
height: 54,
),
)
..addOval(
Rect.fromCenter(center: const Offset(276, 84.4), width: 54, height: 54),
)
..addRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(166.3, 157.2, 136.9, 38.3),
const Radius.circular(5),
),
)
..addRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(166.9, 235.1, 136.2, 37.6),
const Radius.circular(5),
),
);
final wings = Path.combine(PathOperation.union, leftWing, rightWing);
final silhouette = Path.combine(PathOperation.union, wings, body);
final finishedMark = Path.combine(
PathOperation.difference,
silhouette,
cutouts,
);
canvas
..drawPath(finishedMark, Paint()..color = color)
..restore();
}
@override
bool shouldRepaint(_FlappingBeePainter oldDelegate) =>
color != oldDelegate.color || flapAmount != oldDelegate.flapAmount;
}