Skip to content

Commit

Permalink
[flutter_adaptive_scaffold] Adds additional slot animation parameters (
Browse files Browse the repository at this point in the history
…#7411)

Slots can currently be animated in and out of view, but there is not an ability to change the duration or curves of these animations. This PR adds additional parameters that are passed to the underlying AnimatedSwitcher when switching slots in and out of view.

*List which issues are fixed by this PR. You must list at least one issue.*

#6957
  • Loading branch information
martijn00 committed Sep 13, 2024
1 parent 6936868 commit 8f47459
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 12 deletions.
7 changes: 7 additions & 0 deletions packages/flutter_adaptive_scaffold/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.3.0

* Adds `inDuration`, `outDuration`, `inCurve`, and `outCurve` parameters for
configuring additional `SlotLayoutConfig` animation behavior.
* **BREAKING CHANGES**:
* Removes `duration` parameter from `SlotLayoutConfig`.

## 0.2.6

* Add new sample for using AdaptiveScaffold with GoRouter.
Expand Down
38 changes: 32 additions & 6 deletions packages/flutter_adaptive_scaffold/lib/src/slot_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,20 @@ class SlotLayout extends StatefulWidget {
WidgetBuilder? builder,
Widget Function(Widget, Animation<double>)? inAnimation,
Widget Function(Widget, Animation<double>)? outAnimation,
Duration? duration,
Duration? inDuration,
Duration? outDuration,
Curve? inCurve,
Curve? outCurve,
required Key key,
}) =>
SlotLayoutConfig._(
builder: builder,
inAnimation: inAnimation,
outAnimation: outAnimation,
duration: duration,
inDuration: inDuration,
outDuration: outDuration,
inCurve: inCurve,
outCurve: outCurve,
key: key,
);

Expand All @@ -96,7 +102,11 @@ class _SlotLayoutState extends State<SlotLayout>
chosenWidget = SlotLayout.pickWidget(context, widget.config);
bool hasAnimation = false;
return AnimatedSwitcher(
duration: chosenWidget?.duration ?? const Duration(milliseconds: 1000),
duration:
chosenWidget?.inDuration ?? const Duration(milliseconds: 1000),
reverseDuration: chosenWidget?.outDuration,
switchInCurve: chosenWidget?.inCurve ?? Curves.linear,
switchOutCurve: chosenWidget?.outCurve ?? Curves.linear,
layoutBuilder: (Widget? currentChild, List<Widget> previousChildren) {
final Stack elements = Stack(
children: <Widget>[
Expand Down Expand Up @@ -137,7 +147,10 @@ class SlotLayoutConfig extends StatelessWidget {
required this.builder,
this.inAnimation,
this.outAnimation,
this.duration,
this.inDuration,
this.outDuration,
this.inCurve,
this.outCurve,
});

/// The child Widget that [SlotLayout] eventually returns with an animation.
Expand All @@ -161,8 +174,21 @@ class SlotLayoutConfig extends StatelessWidget {
/// as the returned widget.
final Widget Function(Widget, Animation<double>)? outAnimation;

/// The amount of time taken by the execution of the in and out animations.
final Duration? duration;
/// The duration of the transition from the old child to the new one during
/// a switch in [SlotLayout].
final Duration? inDuration;

/// The duration of the transition from the new child to the old one during
/// a switch in [SlotLayout].
final Duration? outDuration;

/// The animation curve to use when transitioning in a new child during a
/// switch in [SlotLayout].
final Curve? inCurve;

/// The animation curve to use when transitioning a previous slot out during
/// a switch in [SlotLayout].
final Curve? outCurve;

/// An empty [SlotLayoutConfig] to be placed in a slot to indicate that the slot
/// should show nothing.
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter_adaptive_scaffold/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_adaptive_scaffold
description: Widgets to easily build adaptive layouts, including navigation elements.
version: 0.2.6
version: 0.3.0
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_adaptive_scaffold%22
repository: https://github.com/flutter/packages/tree/main/packages/flutter_adaptive_scaffold

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -664,35 +664,35 @@ MediaQuery slot(double width, Duration duration, WidgetTester tester) {
TestBreakpoint0(): SlotLayout.from(
inAnimation: leftOutIn,
outAnimation: leftInOut,
duration: duration,
inDuration: duration,
key: const Key('0'),
builder: (_) => const SizedBox(width: 10, height: 10),
),
TestBreakpoint400(): SlotLayout.from(
inAnimation: leftOutIn,
outAnimation: leftInOut,
duration: duration,
inDuration: duration,
key: const Key('400'),
builder: (_) => const SizedBox(width: 10, height: 10),
),
TestBreakpoint800(): SlotLayout.from(
inAnimation: leftOutIn,
outAnimation: leftInOut,
duration: duration,
inDuration: duration,
key: const Key('800'),
builder: (_) => const SizedBox(width: 10, height: 10),
),
TestBreakpoint1200(): SlotLayout.from(
inAnimation: leftOutIn,
outAnimation: leftInOut,
duration: duration,
inDuration: duration,
key: const Key('1200'),
builder: (_) => const SizedBox(width: 10, height: 10),
),
TestBreakpoint1600(): SlotLayout.from(
inAnimation: leftOutIn,
outAnimation: leftInOut,
duration: duration,
inDuration: duration,
key: const Key('1600'),
builder: (_) => const SizedBox(width: 10, height: 10),
),
Expand Down
185 changes: 185 additions & 0 deletions packages/flutter_adaptive_scaffold/test/slot_layout_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_adaptive_scaffold/src/breakpoints.dart';
import 'package:flutter_adaptive_scaffold/src/slot_layout.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets(
'SlotLayout displays correct widget based on screen width',
(WidgetTester tester) async {
MediaQuery slot(double width) {
return MediaQuery(
data: MediaQueryData(size: Size(width, 2000)),
child: Directionality(
textDirection: TextDirection.ltr,
child: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.smallAndUp: SlotLayout.from(
key: const Key('0'), builder: (_) => const Text('Small')),
Breakpoints.mediumAndUp: SlotLayout.from(
key: const Key('400'),
builder: (_) => const Text('Medium')),
Breakpoints.largeAndUp: SlotLayout.from(
key: const Key('800'), builder: (_) => const Text('Large')),
},
),
),
);
}

await tester.pumpWidget(slot(300));
expect(find.text('Small'), findsOneWidget);
expect(find.text('Medium'), findsNothing);
expect(find.text('Large'), findsNothing);

await tester.pumpWidget(slot(600));
expect(find.text('Small'), findsNothing);
expect(find.text('Medium'), findsOneWidget);
expect(find.text('Large'), findsNothing);

await tester.pumpWidget(slot(1200));
expect(find.text('Small'), findsNothing);
expect(find.text('Medium'), findsNothing);
expect(find.text('Large'), findsOneWidget);
},
);

testWidgets(
'SlotLayout handles null configurations gracefully',
(WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: MediaQueryData.fromView(tester.view)
.copyWith(size: const Size(500, 2000)),
child: Directionality(
textDirection: TextDirection.ltr,
child: SlotLayout(
config: <Breakpoint, SlotLayoutConfig?>{
Breakpoints.smallAndUp: SlotLayout.from(
key: const Key('0'),
builder: (BuildContext context) => Container(),
),
Breakpoints.mediumAndUp: null,
Breakpoints.largeAndUp: SlotLayout.from(
key: const Key('800'),
builder: (BuildContext context) => Container(),
),
},
),
),
),
);

expect(find.byKey(const Key('0')), findsOneWidget);
expect(find.byKey(const Key('400')), findsNothing);
expect(find.byKey(const Key('800')), findsNothing);
},
);

testWidgets(
'SlotLayout builder generates widgets correctly',
(WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(size: Size(600, 2000)),
child: Directionality(
textDirection: TextDirection.ltr,
child: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.mediumAndUp: SlotLayout.from(
key: const Key('0'),
builder: (_) => const Text('Builder Test')),
},
),
),
),
);

expect(find.text('Builder Test'), findsOneWidget);
},
);

testWidgets(
'SlotLayout applies inAnimation and outAnimation correctly when changing breakpoints',
(WidgetTester tester) async {
// Define a SlotLayout with custom animations.
Widget buildSlotLayout(double width) {
return MediaQuery(
data: MediaQueryData(size: Size(width, 2000)),
child: Directionality(
textDirection: TextDirection.ltr,
child: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.smallAndUp: SlotLayout.from(
key: const Key('small'),
builder: (_) => const SizedBox(
key: Key('smallBox'), width: 100, height: 100),
inAnimation: (Widget widget, Animation<double> animation) =>
ScaleTransition(
scale: animation,
child: widget,
),
outAnimation: (Widget widget, Animation<double> animation) =>
FadeTransition(
opacity: animation,
child: widget,
),
inDuration: const Duration(seconds: 1),
outDuration: const Duration(seconds: 2),
inCurve: Curves.easeIn,
outCurve: Curves.easeOut,
),
Breakpoints.mediumAndUp: SlotLayout.from(
key: const Key('medium'),
builder: (_) => const SizedBox(
key: Key('mediumBox'), width: 200, height: 200),
inAnimation: (Widget widget, Animation<double> animation) =>
ScaleTransition(
scale: animation,
child: widget,
),
outAnimation: (Widget widget, Animation<double> animation) =>
FadeTransition(
opacity: animation,
child: widget,
),
inDuration: const Duration(seconds: 1),
outDuration: const Duration(seconds: 2),
inCurve: Curves.easeIn,
outCurve: Curves.easeOut,
),
},
),
),
);
}

// Pump the widget with the SlotLayout at small breakpoint.
await tester.pumpWidget(buildSlotLayout(300));
expect(find.byKey(const Key('smallBox')), findsOneWidget);
expect(find.byKey(const Key('mediumBox')), findsNothing);

// Change to medium breakpoint to trigger outAnimation for small and inAnimation for medium.
await tester.pumpWidget(buildSlotLayout(600));
await tester.pump(); // Start the animation.
await tester.pump(const Duration(
milliseconds: 1000)); // Halfway through the outDuration.

// Verify that the outAnimation is in progress for smallBox.
final FadeTransition fadeTransitionMid =
tester.widget(find.byType(FadeTransition));
expect(fadeTransitionMid.opacity.value, lessThan(1.0));
expect(fadeTransitionMid.opacity.value, greaterThan(0.0));

// Complete the animation.
await tester.pumpAndSettle();
expect(find.byKey(const Key('smallBox')), findsNothing);
expect(find.byKey(const Key('mediumBox')), findsOneWidget);
},
);
}

0 comments on commit 8f47459

Please sign in to comment.