Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) Predictive back OpenContainer transition #6321

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
334 changes: 334 additions & 0 deletions packages/animations/lib/src/open_container.dart
Expand Up @@ -4,6 +4,7 @@

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

/// Signature for `action` callback function provided to [OpenContainer.openBuilder].
///
Expand Down Expand Up @@ -720,12 +721,187 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
Navigator.of(subtreeContext!).pop(returnValue);
}

Widget _defaultTransition(BuildContext context, Animation<double> animation, Animation<double> 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<double> curvedAnimation = CurvedAnimation(
parent: animation,
curve: Curves.fastOutSlowIn,
reverseCurve:
_transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,
);
TweenSequence<Color?>? colorTween;
TweenSequence<double>? closedOpacityTween, openOpacityTween;
Animatable<Color?>? 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: <Widget>[
// 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<double> animation, Animation<double> 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,
secondaryAnimation: secondaryAnimation,
getIsCurrent: () => isCurrent,
child: child,
);
}

return backlessChild;
},
);
}

@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> 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(
Expand Down Expand Up @@ -857,6 +1033,7 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
},
),
);
*/
}

@override
Expand All @@ -875,6 +1052,163 @@ class _OpenContainerRoute<T> extends ModalRoute<T> {
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.
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,
required this.child,
});

final Animation<double> animation;
final Animation<double> secondaryAnimation;
final ValueGetter<bool> getIsCurrent;
final Widget 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<double> xShiftTween = Tween<double>(begin: xShift, end: 0.0);
final Animatable<double> scaleTween = Tween<double>(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: child,
),
);
}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: _primaryAnimatedBuilder,
child: child,
);
}
}

class _FlippableTweenSequence<T> extends TweenSequence<T> {
_FlippableTweenSequence(this._items) : super(_items);

Expand Down