Refine iOS search glass and use standard modal motion

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-10-15 08:30:17 -04:00
parent 8ba536060d
commit 32febb8dd5
6 changed files with 103 additions and 7 deletions

View File

@@ -154,7 +154,7 @@ final class ConcentricSheetSurfacePlatformView: NSObject, FlutterPlatformView {
glassView.effect = effect
}
glassView.backgroundColor = .clear
glassView.contentView.backgroundColor = surfaceColor.withAlphaComponent(0.12)
glassView.contentView.backgroundColor = surfaceColor.withAlphaComponent(min(surfaceColor.cgColor.alpha, 0.12))
} else {
surfaceView.backgroundColor = surfaceColor
}

View File

@@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -110,13 +111,24 @@ class HomePage extends HookConsumerWidget {
allowSnapshotting: false,
transitionDuration: reducedMotion
? Duration.zero
: defaultTargetPlatform == TargetPlatform.iOS
? CupertinoRouteTransitionMixin.kTransitionDuration
: const Duration(milliseconds: 260),
reverseTransitionDuration: reducedMotion
? Duration.zero
: defaultTargetPlatform == TargetPlatform.iOS
? CupertinoRouteTransitionMixin.kTransitionDuration
: const Duration(milliseconds: 220),
transitionsBuilder: (_, animation, secondaryAnimation, child) =>
reducedMotion
? child
: defaultTargetPlatform == TargetPlatform.iOS
? CupertinoFullscreenDialogTransition(
primaryRouteAnimation: animation,
secondaryRouteAnimation: secondaryAnimation,
linearTransition: false,
child: child,
)
: SlideTransition(
position: animation.drive(
Tween(

View File

@@ -86,7 +86,13 @@ class SearchPage extends HookConsumerWidget {
final searchPrimaryColor = navigationPrimaryForeground(context);
final searchPlaceholderColor = navigationSecondaryForeground(context);
final fieldHeight = _idleSearchFieldHeight(context);
final filtersHeight = _searchHeaderFiltersHeight(context);
final filtersHeight = nativeIos
? (MediaQuery.textScalerOf(context).scale(16) * 1.4 + 16).clamp(
48.0,
double.infinity,
) +
Grid.xxs * 2
: _searchHeaderFiltersHeight(context);
void activateSearch() => focusNode.requestFocus();
@@ -208,6 +214,7 @@ class SearchPage extends HookConsumerWidget {
key: const ValueKey('search-header-filters'),
height: filtersHeight,
child: FilterChipBar<_SearchFilter>(
useNativeGlass: nativeIos,
expandItems: true,
visualDensity: const VisualDensity(horizontal: -2),
chipVerticalPadding: _searchFilterChipVerticalPadding,

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'ios_glass_navigation_button.dart';
import '../theme/theme.dart';
@@ -37,6 +39,9 @@ class FilterChipBar<T> extends StatelessWidget {
/// When enabled, every chip shares the width rather than scrolling.
final bool expandItems;
/// Uses native glass capsules on iOS for navigation-style filters.
final bool useNativeGlass;
/// Material density applied to each chip.
final VisualDensity visualDensity;
@@ -52,6 +57,7 @@ class FilterChipBar<T> extends StatelessWidget {
required this.selected,
required this.onSelected,
this.expandItems = false,
this.useNativeGlass = false,
this.visualDensity = VisualDensity.compact,
this.chipVerticalPadding = Grid.quarter,
this.barVerticalPadding = Grid.xxs,
@@ -59,6 +65,60 @@ class FilterChipBar<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (useNativeGlass && defaultTargetPlatform == TargetPlatform.iOS) {
final textStyle = filterChipTextStyle.copyWith(
fontWeight: FontWeight.w600,
);
final height = (MediaQuery.textScalerOf(context).scale(16) * 1.4 + 16)
.clamp(48.0, double.infinity);
final widths = [
for (final item in items)
(TextPainter(
text: TextSpan(text: item.label, style: textStyle),
textScaler: MediaQuery.textScalerOf(context),
textDirection: Directionality.of(context),
)..layout()).width +
32,
];
return LayoutBuilder(
builder: (context, constraints) {
final available = constraints.maxWidth - Grid.gutter * 2;
final required =
widths.fold(0.0, (sum, width) => sum + width) +
Grid.xxs * (items.length - 1);
final extra = expandItems && available > required
? (available - required) / items.length
: 0.0;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
clipBehavior: Clip.none,
padding: EdgeInsets.symmetric(
horizontal: Grid.gutter,
vertical: barVerticalPadding,
),
child: Row(
children: [
for (var index = 0; index < items.length; index++) ...[
if (index > 0) const SizedBox(width: Grid.xxs),
IosGlassNavigationButton(
icon: IosGlassNavigationIcon.more,
label: items[index].label,
semanticLabel: items[index].label,
onPressed: () => onSelected(items[index].id),
isSelected: selected == items[index].id,
width: widths[index] + extra,
height: height,
controlSize: height - 8,
fillWidth: true,
foregroundColor: context.colors.primary,
),
],
],
),
);
},
);
}
final chips = [
for (var index = 0; index < items.length; index++) ...[
if (index > 0) const SizedBox(width: Grid.xxs),

View File

@@ -148,10 +148,10 @@ void main() {
final searchRoute = ModalRoute.of(tester.element(find.byType(SearchPage)))!;
expect(searchRoute.allowSnapshotting, isFalse);
expect(searchRoute.opaque, isTrue);
expect(searchRoute.transitionDuration, const Duration(milliseconds: 260));
expect(searchRoute.transitionDuration, const Duration(milliseconds: 500));
expect(
searchRoute.reverseTransitionDuration,
const Duration(milliseconds: 220),
const Duration(milliseconds: 500),
);
Navigator.of(tester.element(find.byType(SearchPage))).pop();
await tester.pumpAndSettle();

View File

@@ -63,7 +63,12 @@ void main() {
expect(tester.getRect(close).left, greaterThan(fieldRect.right));
expect(tester.getSize(close), const Size(48, 48));
for (final label in ['All', 'Messages', 'Channels', 'People']) {
expect(find.text(label).hitTestable(), findsOneWidget);
expect(
platform == TargetPlatform.iOS
? find.byTooltip(label)
: find.text(label).hitTestable(),
findsOneWidget,
);
}
final input = tester.element(find.byType(TextField));
await tester.tap(find.byType(TextField));
@@ -76,7 +81,19 @@ void main() {
isTrue,
);
await tester.enterText(find.byType(TextField), 'design');
await tester.tap(find.text('Channels'));
if (platform == TargetPlatform.iOS) {
tester
.widget<IosGlassNavigationButton>(
find.byWidgetPredicate(
(widget) =>
widget is IosGlassNavigationButton &&
widget.label == 'Channels',
),
)
.onPressed!();
} else {
await tester.tap(find.text('Channels'));
}
await tester.pumpAndSettle();
expect(tester.getRect(field), fieldRect);
expect(tester.getRect(filters), filtersRect);
@@ -90,7 +107,7 @@ void main() {
);
expect(
find.byType(IosGlassNavigationButton),
platform == TargetPlatform.iOS ? findsOneWidget : findsNothing,
platform == TargetPlatform.iOS ? findsNWidgets(5) : findsNothing,
);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox.shrink());