Files
buzz/mobile/lib/shared/widgets/ios_glass_navigation_button.dart
klopez4212 ffb12d3b05 Fix mobile thread tail and iOS channel header (#6399)
## Summary

- Hide the thread Latest control once the lazy tail is fully reached.
- Polish the mobile channel header and use native iOS liquid glass for
Back.
- Keep the two-line header aligned and safe at larger text sizes.

## Validation

- `just mobile-check`
- `flutter test` (1,553 tests)
- Signed iPhone build installed and launched

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Carl <3c4caeafb646d23867f1c4832e68211d77e2561946171625f75c3ce1a3f2670f@buzz.block.builderlab.xyz>
2026-08-21 15:27:30 +01:00

165 lines
5.4 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show PlatformViewHitTestBehavior;
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../theme/theme.dart';
/// The navigation glyph displayed by [IosGlassNavigationButton].
enum IosGlassNavigationIcon { back, close }
/// Leading width used by iOS channel-style headers.
const iosGlassChannelHeaderLeadingWidth = 58.0;
/// Horizontal center of the native button inside a channel-style leading.
const iosGlassChannelHeaderButtonCenterX = 38.0;
/// Space between the leading region and a channel-style title.
const iosGlassChannelHeaderTitleSpacing = Grid.xs;
/// A native iOS navigation control using the system glass button treatment.
///
/// Callers should only insert this widget on iOS and retain their existing
/// Flutter control on other platforms.
class IosGlassNavigationButton extends HookWidget {
const IosGlassNavigationButton({
super.key,
required this.icon,
required this.semanticLabel,
required this.onPressed,
this.width = 48,
this.height = 48,
this.buttonCenterX,
this.foregroundColor,
this.nativeViewSuppressed,
});
static const viewType = 'buzz/navigation_glass';
final IosGlassNavigationIcon icon;
final String semanticLabel;
final VoidCallback? onPressed;
final double width;
final double height;
final double? buttonCenterX;
final Color? foregroundColor;
final ValueListenable<bool>? nativeViewSuppressed;
@override
Widget build(BuildContext context) {
assert(defaultTargetPlatform == TargetPlatform.iOS);
final nativeChannel = useState<MethodChannel?>(null);
final onPressedRef = useRef(onPressed)..value = onPressed;
final brightness = context.theme.brightness.name;
final effectiveForeground = foregroundColor ?? context.colors.primary;
final foregroundValue = effectiveForeground.toARGB32();
final enabled = onPressed != null;
useEffect(() {
final channel = nativeChannel.value;
if (channel == null) return null;
channel.setMethodCallHandler((call) async {
if (call.method == 'pressed') {
onPressedRef.value?.call();
}
});
return () => channel.setMethodCallHandler(null);
}, [nativeChannel.value]);
useEffect(() {
final channel = nativeChannel.value;
if (channel != null) {
unawaited(
channel.invokeMethod<void>('setAppearance', <String, Object>{
'brightness': brightness,
'foregroundColor': foregroundValue,
'enabled': enabled,
}),
);
}
return null;
}, [nativeChannel.value, brightness, foregroundValue, enabled]);
Widget buildControl({required bool suppressNativeView}) {
if (suppressNativeView) {
final resolvedButtonCenterX = buttonCenterX ?? width / 2;
return Semantics(
container: true,
button: true,
enabled: enabled,
label: semanticLabel,
onTap: onPressed,
child: ExcludeSemantics(
child: Stack(
key: const ValueKey('ios-glass-navigation-flutter-fallback'),
children: [
Positioned(
left: resolvedButtonCenterX - 20,
top: (height - 40) / 2,
width: 40,
height: 40,
child: DecoratedBox(
decoration: BoxDecoration(
color: context.colors.surface.withValues(alpha: 0.72),
shape: BoxShape.circle,
border: Border.all(
color: context.colors.inverseSurface.withValues(
alpha: 0.07,
),
),
),
child: Icon(
icon == IosGlassNavigationIcon.back
? Icons.arrow_back_ios_new_rounded
: Icons.close_rounded,
size: 20,
color: effectiveForeground,
),
),
),
],
),
),
);
}
return UiKitView(
viewType: viewType,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
creationParams: <String, Object>{
'icon': icon.name,
'accessibilityLabel': semanticLabel,
'brightness': brightness,
'foregroundColor': foregroundValue,
'enabled': enabled,
'buttonCenterX': buttonCenterX ?? width / 2,
'hitTargetWidth': width,
'hitTargetHeight': height,
},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: (viewId) {
nativeChannel.value = MethodChannel('$viewType/$viewId');
},
);
}
return Tooltip(
message: semanticLabel,
excludeFromSemantics: true,
child: SizedBox(
width: width,
height: height,
child: nativeViewSuppressed == null
? buildControl(suppressNativeView: false)
: ValueListenableBuilder<bool>(
valueListenable: nativeViewSuppressed!,
builder: (context, suppressNativeView, _) =>
buildControl(suppressNativeView: suppressNativeView),
),
),
);
}
}