// 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/material.dart'; /// Animations class to compute animation values for overlay widgets. /// /// Values are loosely based on Material Design specs, which are minimal. class Animations { Animations( this.openController, this.tapController, this.rippleController, this.dismissController, ); final AnimationController openController; final AnimationController tapController; final AnimationController rippleController; final AnimationController dismissController; static const double backgroundMaxOpacity = 0.96; static const double backgroundTapRadius = 20.0; static const double rippleMaxOpacity = 0.75; static const double tapTargetToContentDistance = 20.0; static const double tapTargetMaxRadius = 44.0; static const double tapTargetMinRadius = 20.0; static const double tapTargetRippleRadius = 64.0; Animation backgroundOpacity(FeatureDiscoveryStatus status) { switch (status) { case FeatureDiscoveryStatus.closed: return const AlwaysStoppedAnimation(0); case FeatureDiscoveryStatus.open: return Tween(begin: 0, end: backgroundMaxOpacity).animate( CurvedAnimation( parent: openController, curve: const Interval(0, 0.5, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.tap: return Tween( begin: backgroundMaxOpacity, end: 0, ).animate(CurvedAnimation(parent: tapController, curve: Curves.ease)); case FeatureDiscoveryStatus.dismiss: return Tween(begin: backgroundMaxOpacity, end: 0).animate( CurvedAnimation( parent: dismissController, curve: const Interval(0.2, 1.0, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.ripple: return const AlwaysStoppedAnimation(backgroundMaxOpacity); } } Animation backgroundRadius(FeatureDiscoveryStatus status, double backgroundRadiusMax) { switch (status) { case FeatureDiscoveryStatus.closed: return const AlwaysStoppedAnimation(0); case FeatureDiscoveryStatus.open: return Tween(begin: 0, end: backgroundRadiusMax).animate( CurvedAnimation( parent: openController, curve: const Interval(0, 0.5, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.tap: return Tween( begin: backgroundRadiusMax, end: backgroundRadiusMax + backgroundTapRadius, ).animate(CurvedAnimation(parent: tapController, curve: Curves.ease)); case FeatureDiscoveryStatus.dismiss: return Tween( begin: backgroundRadiusMax, end: 0, ).animate(CurvedAnimation(parent: dismissController, curve: Curves.ease)); case FeatureDiscoveryStatus.ripple: return AlwaysStoppedAnimation(backgroundRadiusMax); } } Animation backgroundCenter(FeatureDiscoveryStatus status, Offset start, Offset end) { switch (status) { case FeatureDiscoveryStatus.closed: return AlwaysStoppedAnimation(start); case FeatureDiscoveryStatus.open: return Tween(begin: start, end: end).animate( CurvedAnimation( parent: openController, curve: const Interval(0, 0.5, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.tap: return Tween( begin: end, end: start, ).animate(CurvedAnimation(parent: tapController, curve: Curves.ease)); case FeatureDiscoveryStatus.dismiss: return Tween( begin: end, end: start, ).animate(CurvedAnimation(parent: dismissController, curve: Curves.ease)); case FeatureDiscoveryStatus.ripple: return AlwaysStoppedAnimation(end); } } Animation contentOpacity(FeatureDiscoveryStatus status) { switch (status) { case FeatureDiscoveryStatus.closed: return const AlwaysStoppedAnimation(0); case FeatureDiscoveryStatus.open: return Tween(begin: 0, end: 1.0).animate( CurvedAnimation( parent: openController, curve: const Interval(0.4, 0.7, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.tap: return Tween(begin: 1.0, end: 0).animate( CurvedAnimation(parent: tapController, curve: const Interval(0, 0.4, curve: Curves.ease)), ); case FeatureDiscoveryStatus.dismiss: return Tween(begin: 1.0, end: 0).animate( CurvedAnimation( parent: dismissController, curve: const Interval(0, 0.4, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.ripple: return const AlwaysStoppedAnimation(1.0); } } Animation rippleOpacity(FeatureDiscoveryStatus status) { switch (status) { case FeatureDiscoveryStatus.ripple: return Tween(begin: rippleMaxOpacity, end: 0).animate( CurvedAnimation( parent: rippleController, curve: const Interval(0.3, 0.8, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.closed: case FeatureDiscoveryStatus.open: case FeatureDiscoveryStatus.tap: case FeatureDiscoveryStatus.dismiss: return const AlwaysStoppedAnimation(0); } } Animation rippleRadius(FeatureDiscoveryStatus status) { switch (status) { case FeatureDiscoveryStatus.ripple: if (rippleController.value >= 0.3 && rippleController.value <= 0.8) { return Tween(begin: tapTargetMaxRadius, end: 79.0).animate( CurvedAnimation( parent: rippleController, curve: const Interval(0.3, 0.8, curve: Curves.ease), ), ); } return const AlwaysStoppedAnimation(tapTargetMaxRadius); case FeatureDiscoveryStatus.closed: case FeatureDiscoveryStatus.open: case FeatureDiscoveryStatus.tap: case FeatureDiscoveryStatus.dismiss: return const AlwaysStoppedAnimation(0); } } Animation tapTargetOpacity(FeatureDiscoveryStatus status) { switch (status) { case FeatureDiscoveryStatus.closed: return const AlwaysStoppedAnimation(0); case FeatureDiscoveryStatus.open: return Tween(begin: 0, end: 1.0).animate( CurvedAnimation( parent: openController, curve: const Interval(0, 0.4, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.tap: return Tween(begin: 1.0, end: 0).animate( CurvedAnimation( parent: tapController, curve: const Interval(0.1, 0.6, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.dismiss: return Tween(begin: 1.0, end: 0).animate( CurvedAnimation( parent: dismissController, curve: const Interval(0.2, 0.8, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.ripple: return const AlwaysStoppedAnimation(1.0); } } Animation tapTargetRadius(FeatureDiscoveryStatus status) { switch (status) { case FeatureDiscoveryStatus.closed: return const AlwaysStoppedAnimation(tapTargetMinRadius); case FeatureDiscoveryStatus.open: return Tween(begin: tapTargetMinRadius, end: tapTargetMaxRadius).animate( CurvedAnimation( parent: openController, curve: const Interval(0, 0.4, curve: Curves.ease), ), ); case FeatureDiscoveryStatus.ripple: if (rippleController.value < 0.3) { return Tween(begin: tapTargetMaxRadius, end: tapTargetRippleRadius).animate( CurvedAnimation( parent: rippleController, curve: const Interval(0, 0.3, curve: Curves.ease), ), ); } else if (rippleController.value < 0.6) { return Tween(begin: tapTargetRippleRadius, end: tapTargetMaxRadius).animate( CurvedAnimation( parent: rippleController, curve: const Interval(0.3, 0.6, curve: Curves.ease), ), ); } return const AlwaysStoppedAnimation(tapTargetMaxRadius); case FeatureDiscoveryStatus.tap: return Tween( begin: tapTargetMaxRadius, end: tapTargetMinRadius, ).animate(CurvedAnimation(parent: tapController, curve: Curves.ease)); case FeatureDiscoveryStatus.dismiss: return Tween( begin: tapTargetMaxRadius, end: tapTargetMinRadius, ).animate(CurvedAnimation(parent: dismissController, curve: Curves.ease)); } } } /// Enum to indicate the current status of a [FeatureDiscovery] widget. enum FeatureDiscoveryStatus { closed, // Overlay is closed. open, // Overlay is opening. ripple, // Overlay is rippling. tap, // Overlay is tapped. dismiss, // Overlay is being dismissed. }