Unverified Commit 547b86a9 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

[Android 10] Activity zoom transition (#41935)

* Android 10 zoom transition
parent 4dd50971
......@@ -88,7 +88,7 @@ class TweenSequence<T> extends Animatable<T> {
return _evaluateAt(t, index);
}
// Should be unreachable.
assert(false, 'TweenSequence.evaluate() could not find a interval for $t');
assert(false, 'TweenSequence.evaluate() could not find an interval for $t');
return null;
}
......@@ -96,6 +96,30 @@ class TweenSequence<T> extends Animatable<T> {
String toString() => 'TweenSequence(${_items.length} items)';
}
/// Enables creating a flipped [Animation] whose value is defined by a sequence
/// of [Tween]s.
///
/// This creates a [TweenSequence] that evaluates to a result that flips the
/// tween both horizontally and vertically.
///
/// This tween sequence assumes that the evaluated result has to be a double
/// between 0.0 and 1.0.
class FlippedTweenSequence extends TweenSequence<double> {
/// Creates a flipped [TweenSequence].
///
/// The [items] parameter must be a list of one or more [TweenSequenceItem]s.
///
/// There's a small cost associated with building a `TweenSequence` so it's
/// best to reuse one, rather than rebuilding it on every frame, when that's
/// possible.
FlippedTweenSequence(List<TweenSequenceItem<double>> items)
: assert(items != null),
super(items);
@override
double transform(double t) => 1 - super.transform(1 - t);
}
/// A simple holder for one element of a [TweenSequence].
class TweenSequenceItem<T> {
/// Construct a TweenSequenceItem.
......
......@@ -146,6 +146,168 @@ class _OpenUpwardsPageTransition extends StatelessWidget {
}
}
// Zooms and fades a new page in, zooming out the previous page. This transition
// is designed to match the Android 10 activity transition.
class _ZoomPageTransition extends StatefulWidget {
const _ZoomPageTransition({
Key key,
this.animation,
this.secondaryAnimation,
this.child,
}) : super(key: key);
// The scrim obscures the old page by becoming increasingly opaque.
static final Tween<double> _scrimOpacityTween = Tween<double>(
begin: 0.0,
end: 0.60,
);
// A curve sequence that is similar to the 'fastOutExtraSlowIn' curve used in
// the native transition.
static final List<TweenSequenceItem<double>> fastOutExtraSlowInTweenSequenceItems = <TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.0, end: 0.4)
.chain(CurveTween(curve: const Cubic(0.05, 0.0, 0.133333, 0.06))),
weight: 0.166666,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0.4, end: 1.0)
.chain(CurveTween(curve: const Cubic(0.208333, 0.82, 0.25, 1.0))),
weight: 1.0 - 0.166666,
),
];
static final TweenSequence<double> _scaleCurveSequence = TweenSequence<double>(fastOutExtraSlowInTweenSequenceItems);
static final FlippedTweenSequence _flippedScaleCurveSequence = FlippedTweenSequence(fastOutExtraSlowInTweenSequenceItems);
final Animation<double> animation;
final Animation<double> secondaryAnimation;
final Widget child;
@override
__ZoomPageTransitionState createState() => __ZoomPageTransitionState();
}
class __ZoomPageTransitionState extends State<_ZoomPageTransition> {
AnimationStatus _currentAnimationStatus;
AnimationStatus _lastAnimationStatus;
@override
void initState() {
super.initState();
widget.animation.addStatusListener((AnimationStatus animationStatus) {
_lastAnimationStatus = _currentAnimationStatus;
_currentAnimationStatus = animationStatus;
});
}
// This check ensures that the animation reverses the original animation if
// the transition were interruped midway. This prevents a disjointed
// experience since the reverse animation uses different fade and scaling
// curves.
bool get _transitionWasInterrupted {
bool wasInProgress = false;
bool isInProgress = false;
switch (_currentAnimationStatus) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
isInProgress = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
isInProgress = true;
break;
}
switch (_lastAnimationStatus) {
case AnimationStatus.completed:
case AnimationStatus.dismissed:
wasInProgress = false;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
wasInProgress = true;
break;
}
return wasInProgress && isInProgress;
}
@override
Widget build(BuildContext context) {
final Animation<double> _forwardScrimOpacityAnimation = widget.animation.drive(
_ZoomPageTransition._scrimOpacityTween
.chain(CurveTween(curve: const Interval(0.2075, 0.4175))));
final Animation<double> _forwardEndScreenScaleTransition = widget.animation.drive(
Tween<double>(begin: 0.85, end: 1.00)
.chain(_ZoomPageTransition._scaleCurveSequence));
final Animation<double> _forwardStartScreenScaleTransition = widget.secondaryAnimation.drive(
Tween<double>(begin: 1.00, end: 1.05)
.chain(_ZoomPageTransition._scaleCurveSequence));
final Animation<double> _forwardEndScreenFadeTransition = widget.animation.drive(
Tween<double>(begin: 0.0, end: 1.00)
.chain(CurveTween(curve: const Interval(0.125, 0.250))));
final Animation<double> _reverseEndScreenScaleTransition = widget.secondaryAnimation.drive(
Tween<double>(begin: 1.00, end: 1.10)
.chain(_ZoomPageTransition._flippedScaleCurveSequence));
final Animation<double> _reverseStartScreenScaleTransition = widget.animation.drive(
Tween<double>(begin: 0.9, end: 1.0)
.chain(_ZoomPageTransition._flippedScaleCurveSequence));
final Animation<double> _reverseStartScreenFadeTransition = widget.animation.drive(
Tween<double>(begin: 0.0, end: 1.00)
.chain(CurveTween(curve: const Interval(1 - 0.2075, 1 - 0.0825))));
return AnimatedBuilder(
animation: widget.animation,
builder: (BuildContext context, Widget child) {
if (widget.animation.status == AnimationStatus.forward || _transitionWasInterrupted) {
return Container(
color: Colors.black.withOpacity(_forwardScrimOpacityAnimation.value),
child: FadeTransition(
opacity: _forwardEndScreenFadeTransition,
child: ScaleTransition(
scale: _forwardEndScreenScaleTransition,
child: child,
),
),
);
} else if (widget.animation.status == AnimationStatus.reverse) {
return ScaleTransition(
scale: _reverseStartScreenScaleTransition,
child: FadeTransition(
opacity: _reverseStartScreenFadeTransition,
child: child,
),
);
}
return child;
},
child: AnimatedBuilder(
animation: widget.secondaryAnimation,
builder: (BuildContext context, Widget child) {
if (widget.secondaryAnimation.status == AnimationStatus.forward || _transitionWasInterrupted) {
return ScaleTransition(
scale: _forwardStartScreenScaleTransition,
child: child,
);
} else if (widget.secondaryAnimation.status == AnimationStatus.reverse) {
return ScaleTransition(
scale: _reverseEndScreenScaleTransition,
child: child,
);
}
return child;
},
child: widget.child,
),
);
}
}
/// Used by [PageTransitionsTheme] to define a [MaterialPageRoute] page
/// transition animation.
///
......@@ -158,6 +320,8 @@ class _OpenUpwardsPageTransition extends StatelessWidget {
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// that's similar to the one provided by Android P.
/// * [ZoomPageTransitionsBuilder], which defines a page transition similar
/// to the one provided in Android 10.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
abstract class PageTransitionsBuilder {
......@@ -191,6 +355,8 @@ abstract class PageTransitionsBuilder {
///
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// that's similar to the one provided by Android P.
/// * [ZoomPageTransitionsBuilder], which defines a page transition similar
/// to the one provided in Android 10.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
class FadeUpwardsPageTransitionsBuilder extends PageTransitionsBuilder {
......@@ -216,6 +382,8 @@ class FadeUpwardsPageTransitionsBuilder extends PageTransitionsBuilder {
/// See also:
///
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [ZoomPageTransitionsBuilder], which defines a page transition similar
/// to the one provided in Android 10.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder {
......@@ -238,6 +406,37 @@ class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder {
}
}
/// Used by [PageTransitionsTheme] to define a zooming [MaterialPageRoute] page
/// transition animation that looks like the default page transition used on
/// Android 10.
///
/// See also:
///
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// similar to the one provided by Android P.
/// * [CupertinoPageTransitionsBuilder], which defines a horizontal page
/// transition that matches native iOS page transitions.
class ZoomPageTransitionsBuilder extends PageTransitionsBuilder {
/// Construct a [ZoomPageTransitionsBuilder].
const ZoomPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _ZoomPageTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
}
}
/// Used by [PageTransitionsTheme] to define a horizontal [MaterialPageRoute]
/// page transition animation that matches native iOS page transitions.
///
......@@ -246,6 +445,8 @@ class OpenUpwardsPageTransitionsBuilder extends PageTransitionsBuilder {
/// * [FadeUpwardsPageTransitionsBuilder], which defines a default page transition.
/// * [OpenUpwardsPageTransitionsBuilder], which defines a page transition
/// that's similar to the one provided by Android P.
/// * [ZoomPageTransitionsBuilder], which defines a page transition similar
/// to the one provided in Android 10.
class CupertinoPageTransitionsBuilder extends PageTransitionsBuilder {
/// Construct a [CupertinoPageTransitionsBuilder].
const CupertinoPageTransitionsBuilder();
......
......@@ -117,4 +117,44 @@ void main() {
expect(findOpenUpwardsPageTransition(), findsOneWidget);
});
testWidgets('pageTranstionsTheme override builds a _ZoomPageTransition for android', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => Material(
child: FlatButton(
child: const Text('push'),
onPressed: () { Navigator.of(context).pushNamed('/b'); },
),
),
'/b': (BuildContext context) => const Text('page b'),
};
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
platform: TargetPlatform.android,
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: ZoomPageTransitionsBuilder(), // creates a _ZoomPageTransition
},
),
),
routes: routes,
),
);
Finder findZoomPageTransition() {
return find.descendant(
of: find.byType(MaterialApp),
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_ZoomPageTransition'),
);
}
expect(Theme.of(tester.element(find.text('push'))).platform, TargetPlatform.android);
expect(findZoomPageTransition(), findsOneWidget);
await tester.tap(find.text('push'));
await tester.pumpAndSettle();
expect(find.text('page b'), findsOneWidget);
expect(findZoomPageTransition(), findsOneWidget);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment