From 40940de636173cb4cb0ef2c9543aab9ecae18002 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Fri, 29 Jul 2022 17:41:04 -0700 Subject: [PATCH] [framework] create animation from value listenable (#108644) --- .../flutter/lib/src/animation/animation.dart | 91 +++++++++++++++++++ .../animation_from_listener_test.dart | 51 +++++++++++ 2 files changed, 142 insertions(+) create mode 100644 packages/flutter/test/animation/animation_from_listener_test.dart diff --git a/packages/flutter/lib/src/animation/animation.dart b/packages/flutter/lib/src/animation/animation.dart index a4003e1861e..b48f107dfec 100644 --- a/packages/flutter/lib/src/animation/animation.dart +++ b/packages/flutter/lib/src/animation/animation.dart @@ -13,6 +13,7 @@ export 'tween.dart' show Animatable; // Examples can assume: // late AnimationController _controller; +// late ValueNotifier _scrollPosition; /// The status of an animation. enum AnimationStatus { @@ -32,6 +33,9 @@ enum AnimationStatus { /// Signature for listeners attached using [Animation.addStatusListener]. typedef AnimationStatusListener = void Function(AnimationStatus status); +/// Signature for method used to transform values in [Animation.fromValueListenable]. +typedef ValueListenableTransformer = T Function(T); + /// An animation with a value of type `T`. /// /// An animation consists of a value (of type `T`) together with a status. The @@ -56,6 +60,58 @@ abstract class Animation extends Listenable implements ValueListenable { /// const constructors so that they can be used in const expressions. const Animation(); + /// Create a new animation from a [ValueListenable]. + /// + /// The returned animation will always have an animations status of + /// [AnimationStatus.forward]. The value of the provided listenable can + /// be optionally transformed using the [transformer] function. + /// + /// {@tool snippet} + /// + /// This constructor can be used to replace instances of [ValueListenableBuilder] + /// widgets with a corresponding animated widget, like a [FadeTransition]. + /// + /// Before: + /// + /// ```dart + /// Widget build(BuildContext context) { + /// return ValueListenableBuilder( + /// valueListenable: _scrollPosition, + /// builder: (BuildContext context, double value, Widget? child) { + /// final double opacity = (value / 1000).clamp(0, 1); + /// return Opacity(opacity: opacity, child: child); + /// }, + /// child: Container( + /// color: Colors.red, + /// child: const Text('Hello, Animation'), + /// ), + /// ); + /// } + /// ``` + /// + /// {@end-tool} + /// {@tool snippet} + /// + /// After: + /// + /// ```dart + /// Widget build2(BuildContext context) { + /// return FadeTransition( + /// opacity: Animation.fromValueListenable(_scrollPosition, transformer: (double value) { + /// return (value / 1000).clamp(0, 1); + /// }), + /// child: Container( + /// color: Colors.red, + /// child: const Text('Hello, Animation'), + /// ), + /// ); + /// } + /// ``` + /// {@end-tool} + factory Animation.fromValueListenable(ValueListenable listenable, { + ValueListenableTransformer? transformer, + }) = _ValueListenableDelegateAnimation; + // keep these next five dartdocs in sync with the dartdocs in AnimationWithParentMixin /// Calls the listener every time the value of the animation changes. @@ -207,3 +263,38 @@ abstract class Animation extends Listenable implements ValueListenable { } } } + +// An implementation of an animation that delegates to a value listenable with a fixed direction. +class _ValueListenableDelegateAnimation extends Animation { + _ValueListenableDelegateAnimation(this._listenable, { ValueListenableTransformer? transformer }) + : _transformer = transformer; + + final ValueListenable _listenable; + final ValueListenableTransformer? _transformer; + + @override + void addListener(VoidCallback listener) { + _listenable.addListener(listener); + } + + @override + void addStatusListener(AnimationStatusListener listener) { + // status will never change. + } + + @override + void removeListener(VoidCallback listener) { + _listenable.removeListener(listener); + } + + @override + void removeStatusListener(AnimationStatusListener listener) { + // status will never change. + } + + @override + AnimationStatus get status => AnimationStatus.forward; + + @override + T get value => _transformer?.call(_listenable.value) ?? _listenable.value; +} diff --git a/packages/flutter/test/animation/animation_from_listener_test.dart b/packages/flutter/test/animation/animation_from_listener_test.dart new file mode 100644 index 00000000000..fbd748ea046 --- /dev/null +++ b/packages/flutter/test/animation/animation_from_listener_test.dart @@ -0,0 +1,51 @@ +// Copyright 2014 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/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Animation created from ValueListenable', () { + final ValueNotifier listenable = ValueNotifier(0.0); + final Animation animation = Animation.fromValueListenable(listenable); + + expect(animation.status, AnimationStatus.forward); + expect(animation.value, 0.0); + + listenable.value = 1.0; + + expect(animation.value, 1.0); + + bool listenerCalled = false; + void listener() { + listenerCalled = true; + } + + animation.addListener(listener); + + listenable.value = 0.5; + + expect(listenerCalled, true); + listenerCalled = false; + + animation.removeListener(listener); + + listenable.value = 0.2; + expect(listenerCalled, false); + }); + + test('Animation created from ValueListenable can transform value', () { + final ValueNotifier listenable = ValueNotifier(0.0); + final Animation animation = Animation.fromValueListenable(listenable, transformer: (double input) { + return input / 10; + }); + + expect(animation.status, AnimationStatus.forward); + expect(animation.value, 0.0); + + listenable.value = 10.0; + + expect(animation.value, 1.0); + }); +}