add AnimatedSlide widget (#86395)

This commit is contained in:
Max 2021-07-17 20:36:03 +03:00 committed by GitHub
parent b217575c37
commit 34f69ceaa5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 189 additions and 1 deletions

View file

@ -257,6 +257,7 @@ class TextStyleTween extends Tween<TextStyle> {
/// [DefaultTextStyle].
/// * [AnimatedScale], which is an implicitly animated version of [Transform.scale].
/// * [AnimatedRotation], which is an implicitly animated version of [Transform.rotate].
/// * [AnimatedSlide], which implicitly animates the position of a widget relative to its normal position.
/// * [AnimatedOpacity], which is an implicitly animated version of [Opacity].
/// * [AnimatedPadding], which is an implicitly animated version of [Padding].
/// * [AnimatedPhysicalModel], which is an implicitly animated version of
@ -616,7 +617,7 @@ abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> exten
/// transitions its child's position over a given duration whenever the given
/// position changes.
/// * [AnimatedAlign], which automatically transitions its child's
/// position over a given duration whenever the given [alignment] changes.
/// position over a given duration whenever the given [AnimatedAlign.alignment] changes.
/// * [AnimatedSwitcher], which switches out a child for a new one with a customizable transition.
/// * [AnimatedCrossFade], which fades between two children and interpolates their sizes.
class AnimatedContainer extends ImplicitlyAnimatedWidget {
@ -973,6 +974,7 @@ class _AnimatedPaddingState extends AnimatedWidgetBaseState<AnimatedPadding> {
/// * [AnimatedContainer], which can transition more values at once.
/// * [AnimatedPadding], which can animate the padding instead of the
/// alignment.
/// * [AnimatedSlide], which can animate the translation of child by a given offset relative to its size.
/// * [AnimatedPositioned], which, as a child of a [Stack], automatically
/// transitions its child's position over a given duration whenever the given
/// position changes.
@ -1464,6 +1466,7 @@ class _AnimatedPositionedDirectionalState extends AnimatedWidgetBaseState<Animat
/// * [AnimatedRotation], for animating the rotation of a child.
/// * [AnimatedSize], for animating the resize of a child based on changes
/// in layout.
/// * [AnimatedSlide], for animating the translation of a child by a given offset relative to its size.
/// * [ScaleTransition], an explicitly animated version of this widget, where
/// an [Animation] is provided by the caller instead of being built in.
class AnimatedScale extends ImplicitlyAnimatedWidget {
@ -1671,6 +1674,121 @@ class _AnimatedRotationState extends ImplicitlyAnimatedWidgetState<AnimatedRotat
}
}
/// Widget which automatically transitions the child's
/// offset relative to its normal position whenever the given offset changes.
///
/// The translation is expressed as an [Offset] scaled to the child's size. For
/// example, an [Offset] with a `dx` of 0.25 will result in a horizontal
/// translation of one quarter the width of the child.
///
/// {@tool dartpad --template=stateful_widget_scaffold}
///
/// This code defines a widget that uses [AnimatedSlide] to translate a [FlutterLogo]
/// up or down by the amount of it's height with each press of the corresponding button.
///
/// ```dart
/// Offset offset = Offset.zero;
///
/// void _slideUp() {
/// setState(() => offset -= const Offset(0, 1));
/// }
///
/// void _slideDown() {
/// setState(() => offset += const Offset(0, 1));
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Column(
/// mainAxisSize: MainAxisSize.min,
/// children: <Widget>[
/// ElevatedButton(
/// child: const Text('Slide up'),
/// onPressed: _slideUp,
/// ),
/// ElevatedButton(
/// child: const Text('Slide down'),
/// onPressed: _slideDown,
/// ),
/// Padding(
/// padding: const EdgeInsets.all(50),
/// child: AnimatedSlide(
/// offset: offset,
/// duration: const Duration(milliseconds: 500),
/// curve: Curves.easeInOut,
/// child: const FlutterLogo(),
/// ),
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [AnimatedPositioned], which, as a child of a [Stack], automatically
/// transitions its child's position over a given duration whenever the given
/// position changes.
/// * [AnimatedAlign], which automatically transitions its child's
/// position over a given duration whenever the given [AnimatedAlign.alignment] changes.
class AnimatedSlide extends ImplicitlyAnimatedWidget {
/// Creates a widget that animates its offset translation implicitly.
///
/// The [offset] and [duration] arguments must not be null.
const AnimatedSlide({
Key? key,
this.child,
required this.offset,
Curve curve = Curves.linear,
required Duration duration,
VoidCallback? onEnd,
}) : super(key: key, curve: curve, duration: duration, onEnd: onEnd);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget? child;
/// The target offset.
/// The child will be translated horizontally by `width * dx` and vertically by `height * dy`
///
/// The offset must not be null.
final Offset offset;
@override
ImplicitlyAnimatedWidgetState<AnimatedSlide> createState() => _AnimatedSlideState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Offset>('offset', offset));
}
}
class _AnimatedSlideState extends ImplicitlyAnimatedWidgetState<AnimatedSlide> {
Tween<Offset>? _offset;
late Animation<Offset> _offsetAnimation;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_offset = visitor(_offset, widget.offset, (dynamic value) => Tween<Offset>(begin: value as Offset)) as Tween<Offset>?;
}
@override
void didUpdateTweens() {
_offsetAnimation = animation.drive(_offset!);
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _offsetAnimation,
child: widget.child,
);
}
}
/// Animated version of [Opacity] which automatically transitions the child's
/// opacity over a given duration whenever the given opacity changes.
///

View file

@ -171,6 +171,64 @@ void main() {
expect(mockOnEndFunction.called, 1);
});
testWidgets('AnimatedSlide onEnd callback test', (WidgetTester tester) async {
await tester.pumpWidget(wrap(
child: TestAnimatedWidget(
callback: mockOnEndFunction.handler,
switchKey: switchKey,
state: _TestAnimatedSlideWidgetState(),
),
));
final Finder widgetFinder = find.byKey(switchKey);
await tester.tap(widgetFinder);
await tester.pump();
expect(mockOnEndFunction.called, 0);
await tester.pump(animationDuration);
expect(mockOnEndFunction.called, 0);
await tester.pump(additionalDelay);
expect(mockOnEndFunction.called, 1);
});
testWidgets('AnimatedSlide transition test', (WidgetTester tester) async {
await tester.pumpWidget(wrap(
child: TestAnimatedWidget(
switchKey: switchKey,
state: _TestAnimatedSlideWidgetState(),
),
));
final RebuildCountingState<StatefulWidget> state = tester.widget<TestAnimatedWidget>(
find.byType(TestAnimatedWidget)
).rebuildState!;
final Finder switchFinder = find.byKey(switchKey);
final SlideTransition slideWidget = tester.widget<SlideTransition>(
find.ancestor(
of: find.byType(Placeholder),
matching: find.byType(SlideTransition),
).first,
);
expect(state.builds, equals(1));
await tester.tap(switchFinder);
expect(state.builds, equals(1));
await tester.pump();
expect(slideWidget.position.value, equals(Offset.zero));
expect(state.builds, equals(2));
await tester.pump(const Duration(milliseconds: 500));
expect(slideWidget.position.value, equals(const Offset(0.5,0.5)));
expect(state.builds, equals(2));
await tester.pump(const Duration(milliseconds: 250));
expect(slideWidget.position.value, equals(const Offset(0.75,0.75)));
expect(state.builds, equals(2));
await tester.pump(const Duration(milliseconds: 250));
expect(slideWidget.position.value, equals(const Offset(1,1)));
expect(state.builds, equals(2));
});
testWidgets('AnimatedScale onEnd callback test', (WidgetTester tester) async {
await tester.pumpWidget(wrap(
child: TestAnimatedWidget(
@ -660,6 +718,18 @@ class _TestAnimatedPositionedDirectionalWidgetState extends _TestAnimatedWidgetS
}
}
class _TestAnimatedSlideWidgetState extends _TestAnimatedWidgetState {
@override
Widget getAnimatedWidget() {
return AnimatedSlide(
duration: duration,
onEnd: widget.callback,
offset: toggle ? const Offset(1,1) : Offset.zero,
child: child,
);
}
}
class _TestAnimatedScaleWidgetState extends _TestAnimatedWidgetState {
@override
Widget getAnimatedWidget() {