mirror of
https://github.com/flutter/flutter
synced 2024-10-12 11:12:54 +00:00
add AnimatedSlide widget (#86395)
This commit is contained in:
parent
b217575c37
commit
34f69ceaa5
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue