From 30c7cd6393b8a2103aef6e1e4155f889c9b64b67 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 13 Mar 2024 17:16:58 -0700 Subject: [PATCH 1/4] A WIP OpenContainer transition that also responds to back gestures, albeit ugly --- .../animations/lib/src/open_container.dart | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart index 438251eb8be8..26c2d8f1f54f 100644 --- a/packages/animations/lib/src/open_container.dart +++ b/packages/animations/lib/src/open_container.dart @@ -2,7 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math' show min; +import 'dart:ui' show lerpDouble; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/scheduler.dart'; /// Signature for `action` callback function provided to [OpenContainer.openBuilder]. @@ -720,12 +724,192 @@ class _OpenContainerRoute extends ModalRoute { Navigator.of(subtreeContext!).pop(returnValue); } + // TODO(justinmc): Make it transition from the pback transition into the opencontainer transition. + @override + Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + /* + return AndroidBackGestureDetector( + backGestureController: + TransitionRoute.createDefaultGestureTransitionController(this), + enabledCallback: () => popGestureEnabled, + builder: (BuildContext context, AndroidBackEvent? startBackEvent, AndroidBackEvent? currentBackEvent) { + final bool linearTransition = popGestureInProgress; + if (linearTransition) { + return defaultBackGestureTransitionBuilder( + this, + context, + animation, + secondaryAnimation, + child, + ); + } + + return child; + }, + ); + */ + return AndroidBackGestureDetector( + backGestureController: TransitionRoute.createDefaultGestureTransitionController(this), + enabledCallback: () => popGestureEnabled, + builder: (BuildContext context, AndroidBackEvent? startBackEvent, AndroidBackEvent? currentBackEvent) { + // TODO(justinmc): Name? And should be in buildPage or no? + final Widget backlessChild = Align( + alignment: Alignment.topLeft, + child: AnimatedBuilder( + animation: animation, + child: child, + builder: (BuildContext context, Widget? child) { + if (animation.isCompleted) { + return child!; + } + + final Animation curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.fastOutSlowIn, + reverseCurve: + _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped, + ); + TweenSequence? colorTween; + TweenSequence? closedOpacityTween, openOpacityTween; + Animatable? scrimTween; + switch (animation.status) { + case AnimationStatus.dismissed: + case AnimationStatus.forward: + closedOpacityTween = _closedOpacityTween; + openOpacityTween = _openOpacityTween; + colorTween = _colorTween; + scrimTween = _scrimFadeInTween; + case AnimationStatus.reverse: + if (_transitionWasInterrupted) { + closedOpacityTween = _closedOpacityTween; + openOpacityTween = _openOpacityTween; + colorTween = _colorTween; + scrimTween = _scrimFadeInTween; + break; + } + closedOpacityTween = _closedOpacityTween.flipped; + openOpacityTween = _openOpacityTween.flipped; + colorTween = _colorTween.flipped; + scrimTween = _scrimFadeOutTween; + case AnimationStatus.completed: + assert(false); // Unreachable. + } + assert(colorTween != null); + assert(closedOpacityTween != null); + assert(openOpacityTween != null); + assert(scrimTween != null); + + final Rect rect = _rectTween.evaluate(curvedAnimation)!; + return SizedBox.expand( + child: Container( + color: scrimTween!.evaluate(curvedAnimation), + child: Align( + alignment: Alignment.topLeft, + child: Transform.translate( + offset: Offset(rect.left, rect.top), + child: SizedBox( + width: rect.width, + height: rect.height, + child: Material( + clipBehavior: Clip.antiAlias, + animationDuration: Duration.zero, + color: colorTween!.evaluate(animation), + shape: _shapeTween.evaluate(curvedAnimation), + elevation: _elevationTween.evaluate(curvedAnimation), + child: Stack( + fit: StackFit.passthrough, + children: [ + // Closed child fading out. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.begin!.width, + height: _rectTween.begin!.height, + child: (hideableKey.currentState?.isInTree ?? + false) + ? null + : FadeTransition( + opacity: closedOpacityTween! + .animate(animation), + child: Builder( + key: closedBuilderKey, + builder: (BuildContext context) { + // Use dummy "open container" callback + // since we are in the process of opening. + return closedBuilder(context, () {}); + }, + ), + ), + ), + ), + + // Open child fading in. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.end!.width, + height: _rectTween.end!.height, + child: FadeTransition( + opacity: openOpacityTween!.animate(animation), + child: Builder( + key: _openBuilderKey, + builder: (BuildContext context) { + return openBuilder(context, closeContainer); + }, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + }, + ), + ); + + if (popGestureInProgress) { + // TODO(justinmc): This should be a combination of AndroidBackGestureTransition + // and backlessChild's transition from above. + return _OpenContainerAndroidBackGestureTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + getIsCurrent: () => isCurrent, + child: child, + ); + } + + return backlessChild; + }, + ); + } + @override Widget buildPage( BuildContext context, Animation animation, Animation secondaryAnimation, ) { + return SizedBox.expand( + child: Material( + color: openColor, + elevation: openElevation, + shape: openShape, + child: Builder( + key: _openBuilderKey, + builder: (BuildContext context) { + return openBuilder(context, closeContainer); + }, + ), + ), + ); + /* return Align( alignment: Alignment.topLeft, child: AnimatedBuilder( @@ -857,6 +1041,7 @@ class _OpenContainerRoute extends ModalRoute { }, ), ); + */ } @override @@ -875,6 +1060,176 @@ class _OpenContainerRoute extends ModalRoute { String? get barrierLabel => null; } +class _OpenContainerAndroidBackGestureTransition extends StatelessWidget { + const _OpenContainerAndroidBackGestureTransition({ + required this.animation, + required this.secondaryAnimation, + required this.getIsCurrent, + required this.child, + }); + + final Animation animation; + final Animation secondaryAnimation; + final ValueGetter getIsCurrent; + final Widget child; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: secondaryAnimation, + builder: _secondaryAnimatedBuilder, + child: AnimatedBuilder( + animation: animation, + builder: _primaryAnimatedBuilder, + child: child, + ), + ); + } + + Widget _secondaryAnimatedBuilder(BuildContext context, Widget? child) { + final Size size = MediaQuery.sizeOf(context); + final double screenWidth = size.width; + final double xShift = (screenWidth / 20) - 8; + + final bool isCurrent = getIsCurrent(); + final Tween xShiftTween = isCurrent + ? ConstantTween(0) + : Tween(begin: xShift, end: 0); + final Animatable scaleTween = isCurrent + ? ConstantTween(1) + : TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 0.95, end: 1), weight: 65.0), + TweenSequenceItem( + tween: Tween(begin: 1, end: 1), weight: 35.0), + ]); + final Animatable fadeTween = isCurrent + ? ConstantTween(1) + : TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 0.8), weight: 65.0), + TweenSequenceItem( + tween: Tween(begin: 1, end: 1), weight: 35.0), + ]); + + return Transform.translate( + offset: Offset(xShiftTween.animate(secondaryAnimation).value, 0), + child: Transform.scale( + scale: scaleTween.animate(secondaryAnimation).value, + child: Opacity( + opacity: fadeTween.animate(secondaryAnimation).value, + child: child, + ), + ), + ); + } + + Widget _primaryAnimatedBuilder(BuildContext context, Widget? child) { + final Size size = MediaQuery.sizeOf(context); + final double screenWidth = size.width; + final double xShift = (screenWidth / 20) - 8; + + final Animatable xShiftTween = + TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 0.0), weight: 65.0), + TweenSequenceItem( + tween: Tween(begin: xShift, end: 0.0), weight: 35.0), + ]); + final Animatable scaleTween = + TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 1.0, end: 1.0), weight: 65.0), + TweenSequenceItem( + tween: Tween(begin: 0.95, end: 1.0), weight: 35.0), + ]); + final Animatable fadeTween = + TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 0.0, end: 0.0), weight: 65.0), + TweenSequenceItem( + tween: Tween(begin: 0.95, end: 1.0), weight: 35.0), + ]); + + return Transform.translate( + offset: Offset(xShiftTween.animate(animation).value, 0), + child: Transform.scale( + scale: scaleTween.animate(animation).value, + child: Opacity( + opacity: fadeTween.animate(animation).value, + child: child, + ), + ), + ); + } +} + +// TODO(justinmc): Actually probably don't need this, need a custom AndroidBackGestureTransition instead. +class _OpenContainerBackGestureTransitionController extends GestureTransitionController { + _OpenContainerBackGestureTransitionController({ + required this.controller, + required this.navigator, + }); + + final NavigatorState navigator; + final AnimationController controller; + + @override + void dragStart({double progress = 0}) { + controller.value = progress; + navigator.didStartUserGesture(); + } + + @override + void dragUpdate({required double progress}) { + controller.value = progress; + } + + @override + void dragEnd({required bool animateForward}) { + if (animateForward) { + // The closer the panel is to dismissing, the shorter the animation is. + // We want to cap the animation time, but we want to use a linear curve + // to determine it. + final int droppedPageForwardAnimationTime = min( + lerpDouble(800, 0, controller.value)!.floor(), + 300, + ); + controller.animateTo( + 1.0, + duration: Duration(milliseconds: droppedPageForwardAnimationTime), + curve: Curves.fastLinearToSlowEaseIn, + ); + } else { + // This route is destined to pop at this point. Reuse navigator's pop. + navigator.pop(); + + // The popping may have finished inline if already at the target destination. + if (controller.isAnimating) { + // Otherwise, use a custom popping animation duration and curve. + final int droppedPageBackAnimationTime = + lerpDouble(0, 800, controller.value)!.floor(); + controller.animateBack(0.0, + duration: Duration(milliseconds: droppedPageBackAnimationTime), + curve: Curves.fastLinearToSlowEaseIn); + } + } + + if (controller.isAnimating) { + // Keep the userGestureInProgress in true state since AndroidBackGesturePageTransitionsBuilder + // depends on userGestureInProgress + late AnimationStatusListener animationStatusCallback; + animationStatusCallback = (AnimationStatus status) { + navigator.didStopUserGesture(); + controller.removeStatusListener(animationStatusCallback); + }; + controller.addStatusListener(animationStatusCallback); + } else { + navigator.didStopUserGesture(); + } + } +} + class _FlippableTweenSequence extends TweenSequence { _FlippableTweenSequence(this._items) : super(_items); From 276d1209add8d0774be2b56df79e6f7560151d5a Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 27 Mar 2024 13:39:46 -0700 Subject: [PATCH 2/4] Cleanup and update to match latest predictive-back-route branch --- .../animations/lib/src/open_container.dart | 551 +++++++++--------- 1 file changed, 265 insertions(+), 286 deletions(-) diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart index 26c2d8f1f54f..d943311540b1 100644 --- a/packages/animations/lib/src/open_container.dart +++ b/packages/animations/lib/src/open_container.dart @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math' show min; -import 'dart:ui' show lerpDouble; - import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; /// Signature for `action` callback function provided to [OpenContainer.openBuilder]. /// @@ -724,160 +721,155 @@ class _OpenContainerRoute extends ModalRoute { Navigator.of(subtreeContext!).pop(returnValue); } - // TODO(justinmc): Make it transition from the pback transition into the opencontainer transition. - @override - Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { - /* - return AndroidBackGestureDetector( - backGestureController: - TransitionRoute.createDefaultGestureTransitionController(this), - enabledCallback: () => popGestureEnabled, - builder: (BuildContext context, AndroidBackEvent? startBackEvent, AndroidBackEvent? currentBackEvent) { - final bool linearTransition = popGestureInProgress; - if (linearTransition) { - return defaultBackGestureTransitionBuilder( - this, - context, - animation, - secondaryAnimation, - child, - ); - } - - return child; - }, - ); - */ - return AndroidBackGestureDetector( - backGestureController: TransitionRoute.createDefaultGestureTransitionController(this), - enabledCallback: () => popGestureEnabled, - builder: (BuildContext context, AndroidBackEvent? startBackEvent, AndroidBackEvent? currentBackEvent) { - // TODO(justinmc): Name? And should be in buildPage or no? - final Widget backlessChild = Align( - alignment: Alignment.topLeft, - child: AnimatedBuilder( - animation: animation, - child: child, - builder: (BuildContext context, Widget? child) { - if (animation.isCompleted) { - return child!; - } + Widget _defaultTransition(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return Align( + alignment: Alignment.topLeft, + child: AnimatedBuilder( + animation: animation, + child: child, + builder: (BuildContext context, Widget? child) { + if (animation.isCompleted) { + return child!; + } - final Animation curvedAnimation = CurvedAnimation( - parent: animation, - curve: Curves.fastOutSlowIn, - reverseCurve: - _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped, - ); - TweenSequence? colorTween; - TweenSequence? closedOpacityTween, openOpacityTween; - Animatable? scrimTween; - switch (animation.status) { - case AnimationStatus.dismissed: - case AnimationStatus.forward: - closedOpacityTween = _closedOpacityTween; - openOpacityTween = _openOpacityTween; - colorTween = _colorTween; - scrimTween = _scrimFadeInTween; - case AnimationStatus.reverse: - if (_transitionWasInterrupted) { - closedOpacityTween = _closedOpacityTween; - openOpacityTween = _openOpacityTween; - colorTween = _colorTween; - scrimTween = _scrimFadeInTween; - break; - } - closedOpacityTween = _closedOpacityTween.flipped; - openOpacityTween = _openOpacityTween.flipped; - colorTween = _colorTween.flipped; - scrimTween = _scrimFadeOutTween; - case AnimationStatus.completed: - assert(false); // Unreachable. + final Animation curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.fastOutSlowIn, + reverseCurve: + _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped, + ); + TweenSequence? colorTween; + TweenSequence? closedOpacityTween, openOpacityTween; + Animatable? scrimTween; + switch (animation.status) { + case AnimationStatus.dismissed: + case AnimationStatus.forward: + closedOpacityTween = _closedOpacityTween; + openOpacityTween = _openOpacityTween; + colorTween = _colorTween; + scrimTween = _scrimFadeInTween; + case AnimationStatus.reverse: + if (_transitionWasInterrupted) { + closedOpacityTween = _closedOpacityTween; + openOpacityTween = _openOpacityTween; + colorTween = _colorTween; + scrimTween = _scrimFadeInTween; + break; } - assert(colorTween != null); - assert(closedOpacityTween != null); - assert(openOpacityTween != null); - assert(scrimTween != null); - - final Rect rect = _rectTween.evaluate(curvedAnimation)!; - return SizedBox.expand( - child: Container( - color: scrimTween!.evaluate(curvedAnimation), - child: Align( - alignment: Alignment.topLeft, - child: Transform.translate( - offset: Offset(rect.left, rect.top), - child: SizedBox( - width: rect.width, - height: rect.height, - child: Material( - clipBehavior: Clip.antiAlias, - animationDuration: Duration.zero, - color: colorTween!.evaluate(animation), - shape: _shapeTween.evaluate(curvedAnimation), - elevation: _elevationTween.evaluate(curvedAnimation), - child: Stack( - fit: StackFit.passthrough, - children: [ - // Closed child fading out. - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.topLeft, - child: SizedBox( - width: _rectTween.begin!.width, - height: _rectTween.begin!.height, - child: (hideableKey.currentState?.isInTree ?? - false) - ? null - : FadeTransition( - opacity: closedOpacityTween! - .animate(animation), - child: Builder( - key: closedBuilderKey, - builder: (BuildContext context) { - // Use dummy "open container" callback - // since we are in the process of opening. - return closedBuilder(context, () {}); - }, - ), - ), - ), - ), + closedOpacityTween = _closedOpacityTween.flipped; + openOpacityTween = _openOpacityTween.flipped; + colorTween = _colorTween.flipped; + scrimTween = _scrimFadeOutTween; + case AnimationStatus.completed: + assert(false); // Unreachable. + } + assert(colorTween != null); + assert(closedOpacityTween != null); + assert(openOpacityTween != null); + assert(scrimTween != null); - // Open child fading in. - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.topLeft, - child: SizedBox( - width: _rectTween.end!.width, - height: _rectTween.end!.height, - child: FadeTransition( - opacity: openOpacityTween!.animate(animation), - child: Builder( - key: _openBuilderKey, - builder: (BuildContext context) { - return openBuilder(context, closeContainer); - }, + final Rect rect = _rectTween.evaluate(curvedAnimation)!; + return SizedBox.expand( + child: Container( + color: scrimTween!.evaluate(curvedAnimation), + child: Align( + alignment: Alignment.topLeft, + child: Transform.translate( + offset: Offset(rect.left, rect.top), + child: SizedBox( + width: rect.width, + height: rect.height, + child: Material( + clipBehavior: Clip.antiAlias, + animationDuration: Duration.zero, + color: colorTween!.evaluate(animation), + shape: _shapeTween.evaluate(curvedAnimation), + elevation: _elevationTween.evaluate(curvedAnimation), + child: Stack( + fit: StackFit.passthrough, + children: [ + // Closed child fading out. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.begin!.width, + height: _rectTween.begin!.height, + child: (hideableKey.currentState?.isInTree ?? + false) + ? null + : FadeTransition( + opacity: closedOpacityTween! + .animate(animation), + child: Builder( + key: closedBuilderKey, + builder: (BuildContext context) { + // Use dummy "open container" callback + // since we are in the process of opening. + return closedBuilder(context, () {}); + }, + ), ), - ), + ), + ), + + // Open child fading in. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.end!.width, + height: _rectTween.end!.height, + child: FadeTransition( + opacity: openOpacityTween!.animate(animation), + child: Builder( + key: _openBuilderKey, + builder: (BuildContext context) { + return openBuilder(context, closeContainer); + }, ), ), - ], + ), ), - ), + ], ), ), ), ), - ); - }, - ), + ), + ), + ); + }, + ), + ); + } + + // TODO(justinmc): Make it transition from the pback transition into the opencontainer transition. + @override + Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + // TODO(justinmc): This does the normal open container transition. + /* + return _defaultTransition( + context, + animation, + secondaryAnimation, + child, + ); + */ + return _PredictiveBackGestureDetector( + route: this, + builder: (BuildContext context) { + // TODO(justinmc): Name? And should be in buildPage or no? + final Widget backlessChild = _defaultTransition( + context, + animation, + secondaryAnimation, + child, ); - if (popGestureInProgress) { - // TODO(justinmc): This should be a combination of AndroidBackGestureTransition - // and backlessChild's transition from above. - return _OpenContainerAndroidBackGestureTransition( + print('justin buildTransitions popInProgress? $popGestureInProgress. isAnimating? ${animation.status} ${secondaryAnimation.status}'); + if (popGestureInProgress && animation.status == AnimationStatus.forward) { + return _PredictiveBackOpenContainerPageTransition( animation: animation, secondaryAnimation: secondaryAnimation, getIsCurrent: () => isCurrent, @@ -1060,8 +1052,125 @@ class _OpenContainerRoute extends ModalRoute { String? get barrierLabel => null; } -class _OpenContainerAndroidBackGestureTransition extends StatelessWidget { - const _OpenContainerAndroidBackGestureTransition({ +// TODO(justinmc): This is copied from the framework, but should be made public. +// Definitely think through the API before making it public, though. +class _PredictiveBackGestureDetector extends StatefulWidget { + const _PredictiveBackGestureDetector({ + required this.route, + required this.builder, + }); + + final WidgetBuilder builder; + final PredictiveBackRoute route; + + @override + State<_PredictiveBackGestureDetector> createState() => + _PredictiveBackGestureDetectorState(); +} + +class _PredictiveBackGestureDetectorState extends State<_PredictiveBackGestureDetector> + with WidgetsBindingObserver { + bool _gestureInProgress = false; + + /// True when the predictive back gesture is enabled. + bool get _isEnabled { + return widget.route.isCurrent + && widget.route.popGestureEnabled; + } + + /// The back event when the gesture first started. + PredictiveBackEvent? get startBackEvent => _startBackEvent; + PredictiveBackEvent? _startBackEvent; + set startBackEvent(PredictiveBackEvent? startBackEvent) { + if (_startBackEvent != startBackEvent && mounted) { + setState(() { + _startBackEvent = startBackEvent; + }); + } + } + + /// The most recent back event during the gesture. + PredictiveBackEvent? get currentBackEvent => _currentBackEvent; + PredictiveBackEvent? _currentBackEvent; + set currentBackEvent(PredictiveBackEvent? currentBackEvent) { + if (_currentBackEvent != currentBackEvent && mounted) { + setState(() { + _currentBackEvent = currentBackEvent; + }); + } + } + + // Begin WidgetsBindingObserver. + + @override + bool handleStartBackGesture(PredictiveBackEvent backEvent) { + _gestureInProgress = !backEvent.isButtonEvent && _isEnabled; + if (!_gestureInProgress) { + return false; + } + + widget.route.handleStartBackGesture(progress: 1 - backEvent.progress); + startBackEvent = currentBackEvent = backEvent; + return true; + } + + @override + bool handleUpdateBackGestureProgress(PredictiveBackEvent backEvent) { + if (!_gestureInProgress) { + return false; + } + + widget.route.handleUpdateBackGestureProgress(progress: 1 - backEvent.progress); + currentBackEvent = backEvent; + return true; + } + + @override + bool handleCancelBackGesture() { + if (!_gestureInProgress) { + return false; + } + + widget.route.handleCancelBackGesture(); + _gestureInProgress = false; + startBackEvent = currentBackEvent = null; + return true; + } + + @override + bool handleCommitBackGesture() { + if (!_gestureInProgress) { + return false; + } + + widget.route.handleCommitBackGesture(); + _gestureInProgress = false; + startBackEvent = currentBackEvent = null; + return true; + } + + // End WidgetsBindingObserver. + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context); + } +} + +class _PredictiveBackOpenContainerPageTransition extends StatelessWidget { + const _PredictiveBackOpenContainerPageTransition({ required this.animation, required this.secondaryAnimation, required this.getIsCurrent, @@ -1073,160 +1182,30 @@ class _OpenContainerAndroidBackGestureTransition extends StatelessWidget { final ValueGetter getIsCurrent; final Widget child; - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: secondaryAnimation, - builder: _secondaryAnimatedBuilder, - child: AnimatedBuilder( - animation: animation, - builder: _primaryAnimatedBuilder, - child: child, - ), - ); - } - - Widget _secondaryAnimatedBuilder(BuildContext context, Widget? child) { - final Size size = MediaQuery.sizeOf(context); - final double screenWidth = size.width; - final double xShift = (screenWidth / 20) - 8; - - final bool isCurrent = getIsCurrent(); - final Tween xShiftTween = isCurrent - ? ConstantTween(0) - : Tween(begin: xShift, end: 0); - final Animatable scaleTween = isCurrent - ? ConstantTween(1) - : TweenSequence(>[ - TweenSequenceItem( - tween: Tween(begin: 0.95, end: 1), weight: 65.0), - TweenSequenceItem( - tween: Tween(begin: 1, end: 1), weight: 35.0), - ]); - final Animatable fadeTween = isCurrent - ? ConstantTween(1) - : TweenSequence(>[ - TweenSequenceItem( - tween: Tween(begin: 1.0, end: 0.8), weight: 65.0), - TweenSequenceItem( - tween: Tween(begin: 1, end: 1), weight: 35.0), - ]); - - return Transform.translate( - offset: Offset(xShiftTween.animate(secondaryAnimation).value, 0), - child: Transform.scale( - scale: scaleTween.animate(secondaryAnimation).value, - child: Opacity( - opacity: fadeTween.animate(secondaryAnimation).value, - child: child, - ), - ), - ); - } - Widget _primaryAnimatedBuilder(BuildContext context, Widget? child) { final Size size = MediaQuery.sizeOf(context); final double screenWidth = size.width; final double xShift = (screenWidth / 20) - 8; - final Animatable xShiftTween = - TweenSequence(>[ - TweenSequenceItem( - tween: Tween(begin: 0.0, end: 0.0), weight: 65.0), - TweenSequenceItem( - tween: Tween(begin: xShift, end: 0.0), weight: 35.0), - ]); - final Animatable scaleTween = - TweenSequence(>[ - TweenSequenceItem( - tween: Tween(begin: 1.0, end: 1.0), weight: 65.0), - TweenSequenceItem( - tween: Tween(begin: 0.95, end: 1.0), weight: 35.0), - ]); - final Animatable fadeTween = - TweenSequence(>[ - TweenSequenceItem( - tween: Tween(begin: 0.0, end: 0.0), weight: 65.0), - TweenSequenceItem( - tween: Tween(begin: 0.95, end: 1.0), weight: 35.0), - ]); + final Animatable xShiftTween = Tween(begin: xShift, end: 0.0); + final Animatable scaleTween = Tween(begin: 0.95, end: 1.0); return Transform.translate( offset: Offset(xShiftTween.animate(animation).value, 0), child: Transform.scale( scale: scaleTween.animate(animation).value, - child: Opacity( - opacity: fadeTween.animate(animation).value, - child: child, - ), + child: child, ), ); } -} - -// TODO(justinmc): Actually probably don't need this, need a custom AndroidBackGestureTransition instead. -class _OpenContainerBackGestureTransitionController extends GestureTransitionController { - _OpenContainerBackGestureTransitionController({ - required this.controller, - required this.navigator, - }); - - final NavigatorState navigator; - final AnimationController controller; @override - void dragStart({double progress = 0}) { - controller.value = progress; - navigator.didStartUserGesture(); - } - - @override - void dragUpdate({required double progress}) { - controller.value = progress; - } - - @override - void dragEnd({required bool animateForward}) { - if (animateForward) { - // The closer the panel is to dismissing, the shorter the animation is. - // We want to cap the animation time, but we want to use a linear curve - // to determine it. - final int droppedPageForwardAnimationTime = min( - lerpDouble(800, 0, controller.value)!.floor(), - 300, - ); - controller.animateTo( - 1.0, - duration: Duration(milliseconds: droppedPageForwardAnimationTime), - curve: Curves.fastLinearToSlowEaseIn, - ); - } else { - // This route is destined to pop at this point. Reuse navigator's pop. - navigator.pop(); - - // The popping may have finished inline if already at the target destination. - if (controller.isAnimating) { - // Otherwise, use a custom popping animation duration and curve. - final int droppedPageBackAnimationTime = - lerpDouble(0, 800, controller.value)!.floor(); - controller.animateBack(0.0, - duration: Duration(milliseconds: droppedPageBackAnimationTime), - curve: Curves.fastLinearToSlowEaseIn); - } - } - - if (controller.isAnimating) { - // Keep the userGestureInProgress in true state since AndroidBackGesturePageTransitionsBuilder - // depends on userGestureInProgress - late AnimationStatusListener animationStatusCallback; - animationStatusCallback = (AnimationStatus status) { - navigator.didStopUserGesture(); - controller.removeStatusListener(animationStatusCallback); - }; - controller.addStatusListener(animationStatusCallback); - } else { - navigator.didStopUserGesture(); - } + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animation, + builder: _primaryAnimatedBuilder, + child: child, + ); } } From 4de134fdb33379d041247a5ccbe83f219d1ea58a Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 4 Sep 2024 14:22:54 -0700 Subject: [PATCH 3/4] Proper separation of buildPage and buildTransitions --- .../animations/lib/src/open_container.dart | 164 +----------------- 1 file changed, 7 insertions(+), 157 deletions(-) diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart index d943311540b1..7f85b3bf173a 100644 --- a/packages/animations/lib/src/open_container.dart +++ b/packages/animations/lib/src/open_container.dart @@ -822,12 +822,7 @@ class _OpenContainerRoute extends ModalRoute { height: _rectTween.end!.height, child: FadeTransition( opacity: openOpacityTween!.animate(animation), - child: Builder( - key: _openBuilderKey, - builder: (BuildContext context) { - return openBuilder(context, closeContainer); - }, - ), + child: child, ), ), ), @@ -848,26 +843,9 @@ class _OpenContainerRoute extends ModalRoute { @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { // TODO(justinmc): This does the normal open container transition. - /* - return _defaultTransition( - context, - animation, - secondaryAnimation, - child, - ); - */ return _PredictiveBackGestureDetector( route: this, builder: (BuildContext context) { - // TODO(justinmc): Name? And should be in buildPage or no? - final Widget backlessChild = _defaultTransition( - context, - animation, - secondaryAnimation, - child, - ); - - print('justin buildTransitions popInProgress? $popGestureInProgress. isAnimating? ${animation.status} ${secondaryAnimation.status}'); if (popGestureInProgress && animation.status == AnimationStatus.forward) { return _PredictiveBackOpenContainerPageTransition( animation: animation, @@ -877,7 +855,12 @@ class _OpenContainerRoute extends ModalRoute { ); } - return backlessChild; + return _defaultTransition( + context, + animation, + secondaryAnimation, + child, + ); }, ); } @@ -901,139 +884,6 @@ class _OpenContainerRoute extends ModalRoute { ), ), ); - /* - return Align( - alignment: Alignment.topLeft, - child: AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget? child) { - if (animation.isCompleted) { - return SizedBox.expand( - child: Material( - color: openColor, - elevation: openElevation, - shape: openShape, - child: Builder( - key: _openBuilderKey, - builder: (BuildContext context) { - return openBuilder(context, closeContainer); - }, - ), - ), - ); - } - - final Animation curvedAnimation = CurvedAnimation( - parent: animation, - curve: Curves.fastOutSlowIn, - reverseCurve: - _transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped, - ); - TweenSequence? colorTween; - TweenSequence? closedOpacityTween, openOpacityTween; - Animatable? scrimTween; - switch (animation.status) { - case AnimationStatus.dismissed: - case AnimationStatus.forward: - closedOpacityTween = _closedOpacityTween; - openOpacityTween = _openOpacityTween; - colorTween = _colorTween; - scrimTween = _scrimFadeInTween; - case AnimationStatus.reverse: - if (_transitionWasInterrupted) { - closedOpacityTween = _closedOpacityTween; - openOpacityTween = _openOpacityTween; - colorTween = _colorTween; - scrimTween = _scrimFadeInTween; - break; - } - closedOpacityTween = _closedOpacityTween.flipped; - openOpacityTween = _openOpacityTween.flipped; - colorTween = _colorTween.flipped; - scrimTween = _scrimFadeOutTween; - case AnimationStatus.completed: - assert(false); // Unreachable. - } - assert(colorTween != null); - assert(closedOpacityTween != null); - assert(openOpacityTween != null); - assert(scrimTween != null); - - final Rect rect = _rectTween.evaluate(curvedAnimation)!; - return SizedBox.expand( - child: Container( - color: scrimTween!.evaluate(curvedAnimation), - child: Align( - alignment: Alignment.topLeft, - child: Transform.translate( - offset: Offset(rect.left, rect.top), - child: SizedBox( - width: rect.width, - height: rect.height, - child: Material( - clipBehavior: Clip.antiAlias, - animationDuration: Duration.zero, - color: colorTween!.evaluate(animation), - shape: _shapeTween.evaluate(curvedAnimation), - elevation: _elevationTween.evaluate(curvedAnimation), - child: Stack( - fit: StackFit.passthrough, - children: [ - // Closed child fading out. - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.topLeft, - child: SizedBox( - width: _rectTween.begin!.width, - height: _rectTween.begin!.height, - child: (hideableKey.currentState?.isInTree ?? - false) - ? null - : FadeTransition( - opacity: closedOpacityTween! - .animate(animation), - child: Builder( - key: closedBuilderKey, - builder: (BuildContext context) { - // Use dummy "open container" callback - // since we are in the process of opening. - return closedBuilder(context, () {}); - }, - ), - ), - ), - ), - - // Open child fading in. - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.topLeft, - child: SizedBox( - width: _rectTween.end!.width, - height: _rectTween.end!.height, - child: FadeTransition( - opacity: openOpacityTween!.animate(animation), - child: Builder( - key: _openBuilderKey, - builder: (BuildContext context) { - return openBuilder(context, closeContainer); - }, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ), - ); - }, - ), - ); - */ } @override From 8d5b11cb040d0070c5f69fb1e586ad7c123b0c6e Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 22 Jan 2025 09:18:08 -0800 Subject: [PATCH 4/4] Cleanup --- packages/animations/lib/src/open_container.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart index 7f85b3bf173a..9789bb779a0c 100644 --- a/packages/animations/lib/src/open_container.dart +++ b/packages/animations/lib/src/open_container.dart @@ -839,10 +839,8 @@ class _OpenContainerRoute extends ModalRoute { ); } - // TODO(justinmc): Make it transition from the pback transition into the opencontainer transition. @override Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { - // TODO(justinmc): This does the normal open container transition. return _PredictiveBackGestureDetector( route: this, builder: (BuildContext context) { @@ -902,8 +900,7 @@ class _OpenContainerRoute extends ModalRoute { String? get barrierLabel => null; } -// TODO(justinmc): This is copied from the framework, but should be made public. -// Definitely think through the API before making it public, though. +// TODO(justinmc): Deduplicate this with the same class in the framework. class _PredictiveBackGestureDetector extends StatefulWidget { const _PredictiveBackGestureDetector({ required this.route,