mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
CupertinoSearchTextField and CupertinoSliverNavigationBar.search more fidelity updates (#169708)
This PR does the following: ## Show large title mid-transition if automaticallyImplyLeading is false (see https://github.com/flutter/flutter/pull/169708#issuecomment-2922792587) ## Correct color for the CupertinoSearchTextField placeholder (see https://github.com/flutter/flutter/issues/163020#issuecomment-2660522169) | Dark mode | Light mode | | --- | --- | | <img width="381" alt="placeholder color dark mode" src="https://github.com/user-attachments/assets/e37e23fa-9f4f-495e-8f02-b9c38a4faffb" /> | <img width="381" alt="placeholder color light mode" src="https://github.com/user-attachments/assets/16e24a61-2528-44e0-9afa-8431487cf5ff" /> | And also: - Removes flaky mid-transition goldens - Fixes a CupertinoTextField crash caused by https://github.com/flutter/flutter/pull/166952 where size > constraints Fixes [Improve fidelity of CupertinoSliverNavigationBar.search and CupertinoSearchTextField](https://github.com/flutter/flutter/issues/163020)
This commit is contained in:
parent
134ca1c17e
commit
b05da524ae
@ -883,12 +883,19 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
|
||||
/// It should be placed at top of the screen and automatically accounts for
|
||||
/// the iOS status bar.
|
||||
///
|
||||
/// This navigation bar is expanded only in portrait orientation. In landscape
|
||||
/// mode, the navigation bar remains permanently collapsed. The navigation bar
|
||||
/// also collapses when scrolling in portrait mode.
|
||||
///
|
||||
/// Minimally, a [largeTitle] widget will appear in the middle of the app bar
|
||||
/// when the sliver is collapsed and transfer to the area below in larger font
|
||||
/// when the sliver is expanded.
|
||||
/// when the sliver is expanded. This expanded view will only trigger in
|
||||
/// portrait orientation, while in landscape mode the bar will stay in its
|
||||
/// collapsed view.
|
||||
///
|
||||
/// For advanced uses, an optional [middle] widget can be supplied to show a
|
||||
/// different widget in the middle of the navigation bar when the sliver is collapsed.
|
||||
/// For advanced uses, an optional [middle] widget
|
||||
/// can be supplied to show a different widget in the middle of the navigation
|
||||
/// bar when the sliver is collapsed.
|
||||
///
|
||||
/// Like [CupertinoNavigationBar], it also supports a [leading] and [trailing]
|
||||
/// widget on the static section on top that remains while scrolling.
|
||||
@ -1085,10 +1092,12 @@ class CupertinoSliverNavigationBar extends StatefulWidget {
|
||||
/// A widget to place in the middle of the static navigation bar instead of
|
||||
/// the [largeTitle].
|
||||
///
|
||||
/// This widget is visible in both collapsed and expanded states if
|
||||
/// [alwaysShowMiddle] is true, otherwise just in collapsed state. The text
|
||||
/// supplied in [largeTitle] will no longer appear in collapsed state if a
|
||||
/// [middle] widget is provided.
|
||||
/// If [alwaysShowMiddle] is true, this widget is visible in both the
|
||||
/// collapsed and expanded states of the navigation bar. Else, it is visible
|
||||
/// only in the collapsed state.
|
||||
///
|
||||
/// If null, [largeTitle] will be displayed in the navigation bar's collapsed
|
||||
/// state.
|
||||
final Widget? middle;
|
||||
|
||||
/// {@macro flutter.cupertino.CupertinoNavigationBar.trailing}
|
||||
@ -3060,7 +3069,6 @@ class _NavigationBarComponentsTransition {
|
||||
final KeyedSubtree? bottomLargeTitle =
|
||||
bottomComponents.largeTitleKey.currentWidget as KeyedSubtree?;
|
||||
final KeyedSubtree? topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree?;
|
||||
final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
|
||||
|
||||
if (bottomLargeTitle == null || !bottomLargeExpanded) {
|
||||
return null;
|
||||
@ -3093,32 +3101,28 @@ class _NavigationBarComponentsTransition {
|
||||
);
|
||||
}
|
||||
|
||||
if (topLeading != null) {
|
||||
// Unlike bottom middle, the bottom large title moves when it can't
|
||||
// transition to the top back label position.
|
||||
final RelativeRect from = positionInTransitionBox(
|
||||
bottomComponents.largeTitleKey,
|
||||
from: bottomNavBarBox,
|
||||
);
|
||||
// Unlike bottom middle, the bottom large title moves when it can't
|
||||
// transition to the top back label position.
|
||||
final RelativeRect from = positionInTransitionBox(
|
||||
bottomComponents.largeTitleKey,
|
||||
from: bottomNavBarBox,
|
||||
);
|
||||
|
||||
final RelativeRectTween positionTween = RelativeRectTween(
|
||||
begin: from,
|
||||
end: from.shift(Offset(forwardDirection * bottomNavBarBox.size.width / 4.0, 0.0)),
|
||||
);
|
||||
final RelativeRectTween positionTween = RelativeRectTween(
|
||||
begin: from,
|
||||
end: from.shift(Offset(forwardDirection * bottomNavBarBox.size.width / 4.0, 0.0)),
|
||||
);
|
||||
|
||||
// Just shift slightly towards the trailing edge instead of moving to the
|
||||
// back label position.
|
||||
return PositionedTransition(
|
||||
rect: animation.drive(positionTween),
|
||||
child: FadeTransition(
|
||||
opacity: fadeOutBy(0.4),
|
||||
// Keep the font when transitioning into a non-back-label leading.
|
||||
child: DefaultTextStyle(style: bottomLargeTitleTextStyle!, child: bottomLargeTitle.child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
// Just shift slightly towards the trailing edge instead of moving to the
|
||||
// back label position.
|
||||
return PositionedTransition(
|
||||
rect: animation.drive(positionTween),
|
||||
child: FadeTransition(
|
||||
opacity: fadeOutBy(0.4),
|
||||
// Keep the font when transitioning into a non-back-label leading.
|
||||
child: DefaultTextStyle(style: bottomLargeTitleTextStyle!, child: bottomLargeTitle.child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get bottomTrailing {
|
||||
|
||||
@ -483,10 +483,17 @@ class _CupertinoSearchTextFieldState extends State<CupertinoSearchTextField> wit
|
||||
Widget build(BuildContext context) {
|
||||
final String placeholder =
|
||||
widget.placeholder ?? CupertinoLocalizations.of(context).searchTextFieldPlaceholderLabel;
|
||||
|
||||
final Color defaultPlaceholderColor = CupertinoDynamicColor.resolve(
|
||||
CupertinoColors.secondaryLabel,
|
||||
context,
|
||||
);
|
||||
final TextStyle placeholderStyle =
|
||||
widget.placeholderStyle ??
|
||||
TextStyle(color: CupertinoColors.systemGrey.withOpacity(1.0 - _fadeExtent));
|
||||
TextStyle(
|
||||
color: defaultPlaceholderColor.withAlpha(
|
||||
(255 * (defaultPlaceholderColor.a * (1 - _fadeExtent))).round(),
|
||||
),
|
||||
);
|
||||
|
||||
// The icon size will be scaled by a factor of the accessibility text scale,
|
||||
// to follow the behavior of `UISearchTextField`.
|
||||
|
||||
@ -1879,6 +1879,6 @@ class _RenderBaselineAlignedStack extends RenderBox
|
||||
width = math.max(width, editableTextSize.width);
|
||||
final Size size = Size(width, height);
|
||||
assert(size.isFinite);
|
||||
return size;
|
||||
return constraints.constrain(size);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2846,14 +2846,6 @@ void main() {
|
||||
// Tap the search field.
|
||||
await tester.tap(find.byType(CupertinoSearchTextField), warnIfMissed: false);
|
||||
await tester.pump();
|
||||
// Pump halfway through the animation.
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
await expectLater(
|
||||
find.byType(CupertinoSliverNavigationBar),
|
||||
matchesGoldenFile('nav_bar.search.transition_forward.png'),
|
||||
);
|
||||
|
||||
// Pump to the end of the animation.
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
@ -2865,14 +2857,6 @@ void main() {
|
||||
// Tap the 'Cancel' button to exit the search view.
|
||||
await tester.tap(find.widgetWithText(CupertinoButton, 'Cancel'));
|
||||
await tester.pump();
|
||||
// Pump halfway through the animation.
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
await expectLater(
|
||||
find.byType(CupertinoSliverNavigationBar),
|
||||
matchesGoldenFile('nav_bar.search.transition_backward.png'),
|
||||
);
|
||||
|
||||
// Pump for the duration of the search field animation.
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
|
||||
@ -1850,6 +1850,33 @@ void main() {
|
||||
expect(find.text('Page 2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Bottom large title is shown mid-transition when top has no leading', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
setWindowToPortrait(tester);
|
||||
await startTransitionBetween(
|
||||
tester,
|
||||
from: const CupertinoSliverNavigationBar(largeTitle: Text('Page 1')),
|
||||
to: const CupertinoSliverNavigationBar(
|
||||
largeTitle: Text('Page 2'),
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
);
|
||||
|
||||
// Go to the next page.
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
|
||||
// Start the gesture at the edge of the screen.
|
||||
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0));
|
||||
// Trigger the swipe.
|
||||
await gesture.moveBy(const Offset(200.0, 0.0));
|
||||
|
||||
// Back gestures should trigger and draw the hero transition in the very same
|
||||
// frame (since the "from" route has already moved to reveal the "to" route).
|
||||
await tester.pump();
|
||||
expect(flying(tester, find.text('Page 1')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Back label is not clipped mid-transition', (WidgetTester tester) async {
|
||||
const String label = 'backbackback';
|
||||
await startTransitionBetween(
|
||||
|
||||
@ -129,7 +129,7 @@ void main() {
|
||||
);
|
||||
|
||||
Text placeholder = tester.widget(find.text('Search'));
|
||||
expect(placeholder.style!.color!.value, CupertinoColors.systemGrey.darkColor.value);
|
||||
expect(placeholder.style!.color!.value, CupertinoColors.secondaryLabel.darkColor.value);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -141,7 +141,7 @@ void main() {
|
||||
);
|
||||
|
||||
placeholder = tester.widget(find.text('Search'));
|
||||
expect(placeholder.style!.color!.value, CupertinoColors.systemGrey.color.value);
|
||||
expect(placeholder.style!.color!.value, CupertinoColors.secondaryLabel.color.value);
|
||||
});
|
||||
|
||||
testWidgets("placeholderStyle modifies placeholder's style and doesn't affect text's style", (
|
||||
@ -623,7 +623,7 @@ void main() {
|
||||
expect(suffixIconFinder, findsOneWidget);
|
||||
expect(placeholderFinder, findsOneWidget);
|
||||
|
||||
// Initially, the icons and placeholder text are fully opaque.
|
||||
// Initially, the icons are fully opaque.
|
||||
expect(
|
||||
tester
|
||||
.widget<Opacity>(find.ancestor(of: prefixIconFinder, matching: find.byType(Opacity)))
|
||||
@ -636,7 +636,8 @@ void main() {
|
||||
.opacity,
|
||||
equals(1.0),
|
||||
);
|
||||
expect(tester.widget<Text>(placeholderFinder).style?.color?.a, equals(1.0));
|
||||
// The default placeholder color is semi-transparent.
|
||||
expect(tester.widget<Text>(placeholderFinder).style?.color?.a, equals(0.6));
|
||||
|
||||
final double searchTextFieldHeight = tester.getSize(searchTextFieldFinder).height;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user